From 04305a1c72ced7af2c548e6f492f737e6fd6ac01 Mon Sep 17 00:00:00 2001 From: Jeroen Grusewski Date: Sun, 19 Jul 2026 09:36:05 +0000 Subject: [PATCH] feat(orchestration): fold bybit precompute into a Dagster asset (K8sRunLauncher-ready) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move _persist_bybit_book + _BYBIT_BOOK_SIDS to application/bybit_book_persist.py (single source of truth; breaks the cli<->assets coupling). Add a new nightly asset bybit_book_precompute (deps=[bybit_warehouse_refresh]) that wraps the same helper backtest-refs/bybit-persist-sleeve-ret run — writing bybit_sleeve_ret + the report-kind gate refs in one compute-once pass. Tagged dagster-k8s/config 6Gi req / 8Gi limit so K8sRunLauncher runs it in its own right-sized Job (never the 4Gi daemon). Wired into combined_book_job + defs; tests updated (13->14 assets + presence + wiring). CLI commands left intact (deleted at the cron layer later). 60 tests pass. Co-Authored-By: Claude Opus 4.8 --- src/fxhnt/adapters/orchestration/assets.py | 28 ++++ .../adapters/orchestration/definitions.py | 7 +- src/fxhnt/application/bybit_book_persist.py | 121 ++++++++++++++++++ src/fxhnt/cli.py | 104 +-------------- .../test_orchestration_definitions.py | 33 ++++- 5 files changed, 183 insertions(+), 110 deletions(-) create mode 100644 src/fxhnt/application/bybit_book_persist.py diff --git a/src/fxhnt/adapters/orchestration/assets.py b/src/fxhnt/adapters/orchestration/assets.py index 3a47dfc..7179641 100644 --- a/src/fxhnt/adapters/orchestration/assets.py +++ b/src/fxhnt/adapters/orchestration/assets.py @@ -781,6 +781,34 @@ def bybit_warehouse_refresh(context: AssetExecutionContext) -> dict: # type: ig store.close() +@asset( + deps=[bybit_warehouse_refresh], + op_tags={"dagster-k8s/config": {"container_config": {"resources": { + "requests": {"memory": "6Gi"}, "limits": {"memory": "8Gi"}}}}}, +) +def bybit_book_precompute(context: AssetExecutionContext) -> dict: # type: ignore[type-arg] + """NIGHTLY heavy precompute of the Bybit 4-edge book's scheduled data — the SINGLE asset that folds in the + retired `compare-measured-precompute` + `backtest-refs` CronJobs. Runs AFTER `bybit_warehouse_refresh` on + the LIQUID `bybit_features` universe and, in ONE compute-once pass over the 4 deploy sleeves, writes BOTH + the `bybit_sleeve_ret` cockpit-sim precompute AND the `_BYBIT_BOOK_SIDS` report-kind reconciliation-gate + refs (bybit_4edge / bybit_4edge_levered / positioning / xsfunding / unlock). Thin wrapper over the shared + `_persist_bybit_book` helper — the SAME logic `backtest-refs --all` and `bybit-persist-sleeve-ret` run — + so there is exactly one source of the persist logic. + + HEAVY compute — must NOT run in the 4Gi dagster daemon (inline in `bybit_4edge_nav` it OOM-killed the + daemon, exit 137, and — OOM being uncatchable — skipped the forward-nav step). It is tagged for a + high-memory per-run K8s Job (K8sRunLauncher isolates it in its own pod, `op_tags` requests 6Gi/limits + 8Gi), so the daemon never carries the compute. `_persist_bybit_book` is imported lazily inside the body + (assets.py stays import-light, matching the other assets).""" + from fxhnt.application.bybit_book_persist import _persist_bybit_book + from fxhnt.config import get_settings + + n, scope = _persist_bybit_book(get_settings()) + context.log.info(f"bybit_book_precompute: persisted {n} bybit_sleeve_ret row(s) + the report-kind " + f"reconciliation-gate refs from {scope}") + return {"bybit_sleeve_ret_rows": n, "scope": scope} + + @asset def deribit_funding_bars(context: AssetExecutionContext) -> dict: # type: ignore[type-arg] """NIGHTLY incremental ingest of Deribit perp DAILY funding into `bybit_features` (feature diff --git a/src/fxhnt/adapters/orchestration/definitions.py b/src/fxhnt/adapters/orchestration/definitions.py index f4fc66b..0de71a4 100644 --- a/src/fxhnt/adapters/orchestration/definitions.py +++ b/src/fxhnt/adapters/orchestration/definitions.py @@ -17,6 +17,7 @@ from dagster import DefaultScheduleStatus, Definitions, ScheduleDefinition, defi from fxhnt.adapters.orchestration.assets import ( bybit_4edge_levered_nav, bybit_4edge_nav, + bybit_book_precompute, bybit_paper_book, bybit_warehouse_refresh, cockpit_forward, @@ -35,7 +36,8 @@ combined_book_job = define_asset_job( selection=[ futures_bars, sixtyforty_nav, xsfunding_nav, unlock_nav, multistrat_nav, multistrat_levered_nav, - deribit_funding_bars, bybit_warehouse_refresh, bybit_4edge_nav, bybit_4edge_levered_nav, + deribit_funding_bars, bybit_warehouse_refresh, bybit_book_precompute, + bybit_4edge_nav, bybit_4edge_levered_nav, positioning_nav, bybit_paper_book, cockpit_forward, ], ) @@ -53,7 +55,8 @@ defs = Definitions( assets=[ futures_bars, sixtyforty_nav, xsfunding_nav, unlock_nav, multistrat_nav, multistrat_levered_nav, - deribit_funding_bars, bybit_warehouse_refresh, bybit_4edge_nav, bybit_4edge_levered_nav, + deribit_funding_bars, bybit_warehouse_refresh, bybit_book_precompute, + bybit_4edge_nav, bybit_4edge_levered_nav, positioning_nav, bybit_paper_book, cockpit_forward, ], jobs=[combined_book_job], diff --git a/src/fxhnt/application/bybit_book_persist.py b/src/fxhnt/application/bybit_book_persist.py new file mode 100644 index 0000000..c38ca39 --- /dev/null +++ b/src/fxhnt/application/bybit_book_persist.py @@ -0,0 +1,121 @@ +"""Single source of the Bybit 4-edge book's scheduled-data persist logic. + +`_persist_bybit_book` precomputes the deploy sleeves' full daily return series ONCE (liquid +`bybit_features` universe) and, from that SAME series, writes BOTH the `bybit_sleeve_ret` cockpit-sim +precompute AND the `_BYBIT_BOOK_SIDS` report-kind reconciliation-gate refs. It lives here (a neutral +application module) — NOT in the CLI composition root — so its three callers can share it without any of +them dragging in the other's import graph: + + * `fxhnt bybit-persist-sleeve-ret` / `fxhnt backtest-refs --all` (the CLI commands); + * the nightly `bybit_book_precompute` Dagster asset (runs in an own-memory per-run K8s Job). + +Keeping it out of `cli.py` matters: `cli.py` is the composition root (`from __future__ import annotations` ++ heavy top-level typer/broker/databento imports) AND `_persist_bybit_book` imports back INTO +`fxhnt.adapters.orchestration.assets` — so an asset importing it from `cli.py` would both pull the whole +CLI chain into the import-light daemon graph and route through that awkward cli↔assets coupling. This +module has neither problem. +""" +from __future__ import annotations + +# The report-kind sids covering the bybit 4-edge book (+ its standalone single-sleeve tracks) — handled as a +# compute-once batch by `_persist_bybit_book`, NOT by the generic per-sid `backtest_ref` loop in +# `backtest_refs_cmd`. `xsfunding`/`unlock` joined 2026-07 (Phase 0b venue consolidation: their own Bybit +# forward tracks, like `positioning`) — all three sleeves are already computed as part of +# `_DEFAULT_BYBIT_SLEEVES` below, so this is a routing change only, no new compute. +_BYBIT_BOOK_SIDS = ("bybit_4edge", "bybit_4edge_levered", "positioning", "xsfunding", "unlock") + + +def _persist_bybit_book(settings, *, min_dollar_vol: float = 10_000_000.0, + cost_bps: float | None = None) -> tuple[int, str]: + """Compute-once writer of the bybit 4-edge book's scheduled data. Precomputes the 4 sleeve return + series ONCE (liquid `bybit_features` universe), then (a) persists them to `bybit_sleeve_ret` (the + cockpit sim's precompute) and (b) upserts the `_BYBIT_BOOK_SIDS` report-kind reconciliation-gate refs + (bybit_4edge / bybit_4edge_levered / positioning / xsfunding / unlock) from the SAME series. Single source of the + bybit-book persist logic, shared by the on-demand `bybit-persist-sleeve-ret` command, the scheduled + `backtest-refs --all`, and the nightly `bybit_book_precompute` asset (own-memory per-run K8s Job). + + Why an application helper, not the nightly daemon asset body: this is the heavy 4-sleeve compute. Wired + INLINE into the nightly `bybit_4edge_nav` asset it OOM-killed the 4Gi dagster daemon (exit 137) — and + because OOM is uncatchable the try/except guard could not save the forward-nav step either, breaking the + forward track. So this writer runs in its OWN memory: the `bybit_book_precompute` asset is tagged for a + high-memory per-run K8s Job (K8sRunLauncher), and it is ALSO reachable via `backtest-refs --all` / + `bybit-persist-sleeve-ret` for an out-of-band run. + + MEMORY-BOUNDED: only the `--min-dollar-vol` liquid subset is READ (never the full ~794-coin table). + IDEMPOTENT: (run_date, sleeve) primary-key overwrite — re-running on the same data rewrites the same rows. + unlock needs the DefiLlama calendar (DB-first, JSON fallback); absent → the unlock sleeve is simply + omitted, never fabricated. + + cost_bps defaults to `settings.bybit_cost_bps` (None sentinel) — the SAME single-sourced cost the + forward tracks charge, so this ref can never silently diverge from what the tracks actually pay.""" + import datetime as dt + import logging + + from fxhnt.adapters.orchestration.assets import _data_dir, persist_bybit_sleeve_returns + from fxhnt.adapters.persistence.paper_repo import PaperRepo + from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore + from fxhnt.application.bybit_liquidity import liquid_universe + from fxhnt.application.unlock_calendar_loader import load_unlock_events, operational_unlock_repo + + log = logging.getLogger(__name__) + cost_bps = settings.bybit_cost_bps if cost_bps is None else cost_bps + store = TimescaleFeatureStore(settings.operational_dsn, table="bybit_features") + try: + universe = liquid_universe(store, min_dollar_vol=min_dollar_vol) or None + try: + unlock_events = load_unlock_events(operational_unlock_repo(settings), + f"{_data_dir()}/unlock_calendar.json") + except Exception as exc: # noqa: BLE001 — absent calendar → unlock sleeve omitted, not a crash + log.warning("_persist_bybit_book: could not load unlock calendar: %s", exc) + unlock_events = [] + + # COMPUTE-ONCE: the 4 deploy sleeves' full backtest series are the SAME input the row-persist and BOTH + # backtest references (naive + levered) need. Compute them ONE time here and reuse for all three, instead + # of recomputing the heavy multi-symbol walk-forward 3x (the old cost). The positioning capturability + # haircuts (capacity + tail-cap) are baked into the positioning series HERE so every downstream + # reference (bybit_4edge, levered, standalone positioning) reconciles against the capturable book. + from fxhnt.application.bybit_book_eval import _DEFAULT_BYBIT_SLEEVES, sleeve_returns_from_store + from fxhnt.application.bybit_forward_track import POSITIONING_TAIL_CAP_K + sleeve_rets = { + s: sleeve_returns_from_store(store, s, universe=universe, cost_bps=cost_bps, + unlock_events=unlock_events, + capacity_capital=settings.paper_capital, + tail_cap_k=POSITIONING_TAIL_CAP_K) + for s in _DEFAULT_BYBIT_SLEEVES} + + repo = PaperRepo(settings.operational_dsn) + repo.migrate() + n = persist_bybit_sleeve_returns( + repo, store, at=dt.datetime.now(dt.UTC), universe=universe, + cost_bps=cost_bps, unlock_events=unlock_events, sleeve_returns=sleeve_rets) + + # Persist the `_BYBIT_BOOK_SIDS` report-kind reconciliation-gate refs (bybit_4edge naive-eqwt / levered + # / standalone positioning / standalone xsfunding / standalone unlock) from the SAME precomputed sleeve + # series — so the reconciliation gate has an `expected_window_return` to reconcile the forward against. + # Without it the gate's divergence band is silently skipped and it degrades to a bare min-days + + # clean-exec timer — a false-PASS risk on the real-capital book. `bybit_report_sid_kwargs` is the SINGLE + # source of the per-sid identity kwargs; for bybit_4edge/bybit_4edge_levered/positioning it is ALSO + # shared with `report_backtest_summary` (backtest_refs_report.py) so the two can never drift apart — + # xsfunding/unlock are NOT in that module's `_REPORT_SIDS` (their refs are only ever computed here, + # gated out of the generic per-sid loop below by `sid in _BYBIT_BOOK_SIDS`). + # positioning's/xsfunding's/unlock's OWN forward tracks use BybitFourEdgeStrategy(sleeves=[]) — + # the SAME single-sleeve construction (no vol overlay), so their reconciliation-gate ref is that book's + # summary keyed as their own sid. crypto_tstrend / stablecoin have DIFFERENT forward constructions than + # their raw sleeve series, so their refs are persisted from their own strategies in + # `_persist_track_backtest_ref` (their nav assets), NOT here. + from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo + from fxhnt.application.bybit_forward_track import bybit_4edge_backtest_summary, bybit_report_sid_kwargs + fwd_repo = ForwardNavRepo(settings.operational_dsn) + fwd_repo.migrate() + for sid in _BYBIT_BOOK_SIDS: + bt = bybit_4edge_backtest_summary( + store, universe=universe, cost_bps=cost_bps, unlock_events=unlock_events, + sleeve_returns=sleeve_rets, **bybit_report_sid_kwargs(sid)) + fwd_repo.upsert_backtest_summary(bt, at=dt.datetime.now(dt.UTC).replace(tzinfo=None)) + log.info("_persist_bybit_book: %s backtest ref cagr=%.1f%% sharpe=%.2f -> backtest_summary", + sid, bt.cagr * 100, bt.sharpe) + finally: + store.close() + + scope = f"{len(universe)} liquid symbols" if universe else "full universe (no liquid subset)" + return n, scope diff --git a/src/fxhnt/cli.py b/src/fxhnt/cli.py index 85efec2..40efcf0 100644 --- a/src/fxhnt/cli.py +++ b/src/fxhnt/cli.py @@ -18,6 +18,7 @@ from fxhnt.application import ( PortfolioEvaluationService, ResearchService, ) +from fxhnt.application.bybit_book_persist import _BYBIT_BOOK_SIDS, _persist_bybit_book from fxhnt.config import Settings, get_settings from fxhnt.domain.models import AssetClass, Market, StrategySpec from fxhnt.domain.portfolio import Book, StrategyAllocation, book_from_runs @@ -2958,109 +2959,6 @@ def bybit_book_eval( "universe is GLOBAL; live Bybit-EU (MiCA) trades a SMALLER subset, so this is an upper bound.") -# The report-kind sids covering the bybit 4-edge book (+ its standalone single-sleeve tracks) — handled as a -# compute-once batch by `_persist_bybit_book`, NOT by the generic per-sid `backtest_ref` loop in -# `backtest_refs_cmd`. `xsfunding`/`unlock` joined 2026-07 (Phase 0b venue consolidation: their own Bybit -# forward tracks, like `positioning`) — all three sleeves are already computed as part of -# `_DEFAULT_BYBIT_SLEEVES` below, so this is a routing change only, no new compute. -_BYBIT_BOOK_SIDS = ("bybit_4edge", "bybit_4edge_levered", "positioning", "xsfunding", "unlock") - - -def _persist_bybit_book(settings, *, min_dollar_vol: float = 10_000_000.0, - cost_bps: float | None = None) -> tuple[int, str]: - """Compute-once writer of the bybit 4-edge book's scheduled data. Precomputes the 4 sleeve return - series ONCE (liquid `bybit_features` universe), then (a) persists them to `bybit_sleeve_ret` (the - cockpit sim's precompute) and (b) upserts the `_BYBIT_BOOK_SIDS` report-kind reconciliation-gate refs - (bybit_4edge / bybit_4edge_levered / positioning / xsfunding / unlock) from the SAME series. Single source of the - bybit-book persist logic, shared by the on-demand `bybit-persist-sleeve-ret` command and the scheduled - `backtest-refs --all`. Returns (rows_written, scope_desc). - - Why a CLI helper, not the nightly daemon asset: this is the heavy 4-sleeve compute. Wired into the - nightly `bybit_4edge_nav` asset it OOM-killed the 4Gi dagster daemon (exit 137) — and because OOM is - uncatchable the try/except guard could not save the forward-nav step either, breaking the forward track. - So this writer runs in its OWN-MEMORY CronJob (`infra/k8s/jobs/fxhnt-backtest-refs.yaml`, scheduled - weekly) or on-demand via `bybit-persist-sleeve-ret`. - - MEMORY-BOUNDED: only the `--min-dollar-vol` liquid subset is READ (never the full ~794-coin table). - IDEMPOTENT: (run_date, sleeve) primary-key overwrite — re-running on the same data rewrites the same rows. - unlock needs the DefiLlama calendar (DB-first, JSON fallback); absent → the unlock sleeve is simply - omitted, never fabricated. - - cost_bps defaults to `settings.bybit_cost_bps` (None sentinel) — the SAME single-sourced cost the - forward tracks charge, so this ref can never silently diverge from what the tracks actually pay.""" - import datetime as dt - import logging - - from fxhnt.adapters.orchestration.assets import _data_dir, persist_bybit_sleeve_returns - from fxhnt.adapters.persistence.paper_repo import PaperRepo - from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore - from fxhnt.application.bybit_liquidity import liquid_universe - from fxhnt.application.unlock_calendar_loader import load_unlock_events, operational_unlock_repo - - log = logging.getLogger(__name__) - cost_bps = settings.bybit_cost_bps if cost_bps is None else cost_bps - store = TimescaleFeatureStore(settings.operational_dsn, table="bybit_features") - try: - universe = liquid_universe(store, min_dollar_vol=min_dollar_vol) or None - try: - unlock_events = load_unlock_events(operational_unlock_repo(settings), - f"{_data_dir()}/unlock_calendar.json") - except Exception as exc: # noqa: BLE001 — absent calendar → unlock sleeve omitted, not a crash - log.warning("_persist_bybit_book: could not load unlock calendar: %s", exc) - unlock_events = [] - - # COMPUTE-ONCE: the 4 deploy sleeves' full backtest series are the SAME input the row-persist and BOTH - # backtest references (naive + levered) need. Compute them ONE time here and reuse for all three, instead - # of recomputing the heavy multi-symbol walk-forward 3x (the old cost). The positioning capturability - # haircuts (capacity + tail-cap) are baked into the positioning series HERE so every downstream - # reference (bybit_4edge, levered, standalone positioning) reconciles against the capturable book. - from fxhnt.application.bybit_book_eval import _DEFAULT_BYBIT_SLEEVES, sleeve_returns_from_store - from fxhnt.application.bybit_forward_track import POSITIONING_TAIL_CAP_K - sleeve_rets = { - s: sleeve_returns_from_store(store, s, universe=universe, cost_bps=cost_bps, - unlock_events=unlock_events, - capacity_capital=settings.paper_capital, - tail_cap_k=POSITIONING_TAIL_CAP_K) - for s in _DEFAULT_BYBIT_SLEEVES} - - repo = PaperRepo(settings.operational_dsn) - repo.migrate() - n = persist_bybit_sleeve_returns( - repo, store, at=dt.datetime.now(dt.UTC), universe=universe, - cost_bps=cost_bps, unlock_events=unlock_events, sleeve_returns=sleeve_rets) - - # Persist the `_BYBIT_BOOK_SIDS` report-kind reconciliation-gate refs (bybit_4edge naive-eqwt / levered - # / standalone positioning / standalone xsfunding / standalone unlock) from the SAME precomputed sleeve - # series — so the reconciliation gate has an `expected_window_return` to reconcile the forward against. - # Without it the gate's divergence band is silently skipped and it degrades to a bare min-days + - # clean-exec timer — a false-PASS risk on the real-capital book. `bybit_report_sid_kwargs` is the SINGLE - # source of the per-sid identity kwargs; for bybit_4edge/bybit_4edge_levered/positioning it is ALSO - # shared with `report_backtest_summary` (backtest_refs_report.py) so the two can never drift apart — - # xsfunding/unlock are NOT in that module's `_REPORT_SIDS` (their refs are only ever computed here, - # gated out of the generic per-sid loop below by `sid in _BYBIT_BOOK_SIDS`). - # positioning's/xsfunding's/unlock's OWN forward tracks use BybitFourEdgeStrategy(sleeves=[]) — - # the SAME single-sleeve construction (no vol overlay), so their reconciliation-gate ref is that book's - # summary keyed as their own sid. crypto_tstrend / stablecoin have DIFFERENT forward constructions than - # their raw sleeve series, so their refs are persisted from their own strategies in - # `_persist_track_backtest_ref` (their nav assets), NOT here. - from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo - from fxhnt.application.bybit_forward_track import bybit_4edge_backtest_summary, bybit_report_sid_kwargs - fwd_repo = ForwardNavRepo(settings.operational_dsn) - fwd_repo.migrate() - for sid in _BYBIT_BOOK_SIDS: - bt = bybit_4edge_backtest_summary( - store, universe=universe, cost_bps=cost_bps, unlock_events=unlock_events, - sleeve_returns=sleeve_rets, **bybit_report_sid_kwargs(sid)) - fwd_repo.upsert_backtest_summary(bt, at=dt.datetime.now(dt.UTC).replace(tzinfo=None)) - log.info("_persist_bybit_book: %s backtest ref cagr=%.1f%% sharpe=%.2f -> backtest_summary", - sid, bt.cagr * 100, bt.sharpe) - finally: - store.close() - - scope = f"{len(universe)} liquid symbols" if universe else "full universe (no liquid subset)" - return n, scope - - @app.command("honest-reference-report") def honest_reference_report( min_dollar_vol: float = typer.Option( diff --git a/tests/integration/test_orchestration_definitions.py b/tests/integration/test_orchestration_definitions.py index 8a7fa7c..29e31f2 100644 --- a/tests/integration/test_orchestration_definitions.py +++ b/tests/integration/test_orchestration_definitions.py @@ -44,6 +44,11 @@ def test_definitions_load_with_assets_and_schedule() -> None: assert "paper_book_snapshot" not in asset_keys, "paper_book_snapshot must be retired" # Bybit forward paper track (out-of-sample record): nightly warehouse refresh + naive eq-wt 4-edge NAV. assert "bybit_warehouse_refresh" in asset_keys, f"missing bybit_warehouse_refresh: {asset_keys}" + # bybit_book_precompute (2026-07-19): the heavy 4-sleeve precompute folded in from the retired + # compare-measured-precompute + backtest-refs CronJobs — writes bybit_sleeve_ret (cockpit-sim precompute) + # + the report-kind reconciliation-gate refs in one compute-once pass. Runs in an own-memory per-run K8s + # Job (tagged 6Gi/8Gi), depends on bybit_warehouse_refresh. + assert "bybit_book_precompute" in asset_keys, f"missing bybit_book_precompute: {asset_keys}" assert "bybit_4edge_nav" in asset_keys, f"missing bybit_4edge_nav: {asset_keys}" # observe-only levered shadow of the 4-edge book — independently recomputes from the warehouse, own state file assert "bybit_4edge_levered_nav" in asset_keys, f"missing bybit_4edge_levered_nav: {asset_keys}" @@ -60,10 +65,10 @@ def test_definitions_load_with_assets_and_schedule() -> None: # the leg is now CLI-driven via `run_bybit_testnet` / `execute-bybit`, not a Dagster asset. assert "bybit_testnet_reconcile" not in asset_keys, "bybit_testnet_reconcile must be removed" assert "bybit_testnet_record" not in asset_keys, "bybit_testnet_record must be removed" - # 13 assets: futures_bars, sixtyforty_nav, + # 14 assets: futures_bars, sixtyforty_nav, # xsfunding_nav, unlock_nav, multistrat_nav, multistrat_levered_nav, - # deribit_funding_bars, bybit_warehouse_refresh, bybit_4edge_nav, bybit_4edge_levered_nav, positioning_nav, - # bybit_paper_book, cockpit_forward + # deribit_funding_bars, bybit_warehouse_refresh, bybit_book_precompute, bybit_4edge_nav, + # bybit_4edge_levered_nav, positioning_nav, bybit_paper_book, cockpit_forward # (xvenue_carry_nav retired 2026-07-07; multistrat_nav re-added 2026-07-08; # bybit_testnet_reconcile/bybit_testnet_record removed 2026-07-11, Task 7; # combined_forward_nav retired 2026-07-11 — vestigial crypto-momentum book; @@ -72,8 +77,10 @@ def test_definitions_load_with_assets_and_schedule() -> None: # crypto_bars/crypto_funding/crypto_spot Binance ingest retired 2026-07-13, Phase 0b Task 7b; # paper_book_snapshot retired 2026-07-14, Phase 0b Task 7d — the dead combined-crypto book; # vrp_nav ARCHIVED/de-wired 2026-07-15 — VRP falsified/shelved, asset fn kept but unregistered; - # multistrat_levered_nav added 2026-07-18 — observe-only levered shadow of the ETF book) - assert len(asset_keys) == 13, f"expected 13 assets, got {len(asset_keys)}: {asset_keys}" + # multistrat_levered_nav added 2026-07-18 — observe-only levered shadow of the ETF book; + # bybit_book_precompute added 2026-07-19 — heavy 4-sleeve precompute folded in from the retired + # compare-measured-precompute + backtest-refs CronJobs) + assert len(asset_keys) == 14, f"expected 14 assets, got {len(asset_keys)}: {asset_keys}" # bybit_paper_book is READ-ONLY against the warehouse: its ONLY upstream is bybit_warehouse_refresh. from dagster import AssetKey as _AK @@ -106,6 +113,22 @@ def test_definitions_load_with_assets_and_schedule() -> None: assert "vrp_nav" not in upstream, f"vrp_nav must be de-wired from cockpit_forward: {upstream}" +def test_bybit_book_precompute_wired_into_job_and_defs(): + """The heavy 4-sleeve precompute (folded in from the retired compare-measured-precompute + backtest-refs + CronJobs) must be BOTH a registered asset AND in the nightly `combined_book_forward_job` selection — else + it would exist but never materialize on the schedule.""" + from fxhnt.adapters.orchestration import definitions as d + + # in the Definitions asset set + asset_keys = {k.to_user_string() for k in d.defs.resolve_all_asset_keys()} + assert "bybit_book_precompute" in asset_keys, f"missing from defs.assets: {asset_keys}" + + # in the combined_book_forward_job selection (so the daily schedule materializes it) + job_keys = {k.to_user_string() for k in d.combined_book_job.selection.resolve(list(d.defs.assets))} + assert "bybit_book_precompute" in job_keys, ( + f"bybit_book_precompute must be in combined_book_forward_job selection: {job_keys}") + + def test_bybit_testnet_exec_removed_from_definitions(): from fxhnt.adapters.orchestration.definitions import defs job_names = {j.name for j in defs.jobs}