diff --git a/src/fxhnt/adapters/orchestration/assets.py b/src/fxhnt/adapters/orchestration/assets.py index b9de5db..3ceada8 100644 --- a/src/fxhnt/adapters/orchestration/assets.py +++ b/src/fxhnt/adapters/orchestration/assets.py @@ -290,7 +290,7 @@ def _persist_track_backtest_ref(context: AssetExecutionContext, name: str, build """Persist the reconciliation-gate backtest reference for an individual forward track, routed through the ONE `backtest_ref` dispatcher (fxhnt.application.backtest_refs) so this inline nightly path, the `backtest-refs --all` CLI (report-kind), and the B3 runner all share the same dispatch. For - `recompute-replay` strategies (multistrat/vrp/crypto_tstrend/stablecoin/unlock) this is bit-identical to + `recompute-replay` strategies (multistrat/crypto_tstrend/stablecoin/unlock) this is bit-identical to the prior inline behaviour: `build_strategy().advance(None, {})` = the full inception series at the EXACT basis the forward books (e.g. crypto_tstrend's forward is 15%-book-vol-targeted, not the raw TrendRunner sleeve). For a `sleeve` strategy it would resolve a feature store instead, since `sleeve_returns_from_store` @@ -302,9 +302,9 @@ def _persist_track_backtest_ref(context: AssetExecutionContext, name: str, build BEST-EFFORT: a ref-persist failure logs and returns — it must NEVER fail the forward step (the ref is a gate input, not the track itself). Without it the gate WITHHOLDS PASS (no ref to reconcile). - For `recompute-replay` strategies (multistrat/vrp/crypto_tstrend/stablecoin/unlock) this ALSO persists + For `recompute-replay` strategies (multistrat/crypto_tstrend/stablecoin/unlock) this ALSO persists the full inception series into `sim_curve_ret` (Task 1/D1) — the precomputed curve the cockpit's - `/paper/sim` reads for its IBKR books (multistrat/vrp), mirroring the Bybit book's `bybit_sleeve_ret` + `/paper/sim` reads for its IBKR books (multistrat), mirroring the Bybit book's `bybit_sleeve_ret` precompute. One replay call feeds BOTH writes (the reconciliation ref and the sim curve), so the sim's curve is always the SAME basis the gate reconciles the forward track against.""" import datetime as _dt @@ -913,7 +913,7 @@ def cockpit_forward(context: AssetExecutionContext) -> None: dsn = get_settings().operational_dsn ingest_forward_state(dsn) # Runtime health axis (SEPARATE from the gate): surface ANY track that stopped updating LOUDLY — a red - # cockpit badge + an ERROR log the FIRST night, so a stale recompute (the 2-week vrp rot) can never sit as + # cockpit badge + an ERROR log the FIRST night, so a stale recompute (a dead strategy's silent rot) can never sit as # a quiet WAIT again. evaluate_health logs at ERROR with the unhealthy list; also surface the count here. unhealthy = [v for v in evaluate_health(dsn) if not v.ok] if unhealthy: diff --git a/src/fxhnt/adapters/orchestration/definitions.py b/src/fxhnt/adapters/orchestration/definitions.py index 0de71a4..a768fb6 100644 --- a/src/fxhnt/adapters/orchestration/definitions.py +++ b/src/fxhnt/adapters/orchestration/definitions.py @@ -7,9 +7,8 @@ retired here — crypto is 100% Bybit now (`bybit_warehouse_refresh` + the Bybit `bybit_4edge` deploy book below) — was RETIRED (Phase 0b Task 7d); sixtyforty_nav/multistrat_nav are their OWN independent live assets below, unaffected by that retirement.) -(vrp_nav ARCHIVED/FALSIFIED 2026-07-15 — the VRP sleeve is a shelved dead-end; its asset function is KEPT -in assets.py but DE-WIRED from the nightly here so it no longer materializes. See -project_fxhnt_diversifier_hunt_2026_07_15.) +(VRP removed entirely 2026-07-20 — see project_fxhnt_diversifier_hunt_2026_07_15 for the +falsification that led to the removal.) NOTE: no `from __future__ import annotations` — Dagster 1.12 runtime type-hint inspection.""" from dagster import DefaultScheduleStatus, Definitions, ScheduleDefinition, define_asset_job diff --git a/src/fxhnt/adapters/persistence/cockpit_models.py b/src/fxhnt/adapters/persistence/cockpit_models.py index d0a2956..cab8850 100644 --- a/src/fxhnt/adapters/persistence/cockpit_models.py +++ b/src/fxhnt/adapters/persistence/cockpit_models.py @@ -43,7 +43,8 @@ class StrategyHealthRow(CockpitBase): """Runtime HEALTH verdict per track — a SEPARATE axis from the gate (a track can be legitimately WAIT/building AND healthy; STALE/NO_REF/BROKEN is orthogonal). Written each nightly run by `forward_health.evaluate_health`; the cockpit reads it to render a LOUD red badge so a live/gated track - that stops updating surfaces on day 1 (the anti-silent-rot backbone that would have caught vrp). One row + that stops updating surfaces on day 1 (the anti-silent-rot backbone that would have caught a dead + strategy's silent staleness). One row per strategy, replaced each run (a track that recovers clears its stale row). Brand-new table → `create_all` (ForwardNavRepo.migrate) creates it; no ALTER, Postgres-safe.""" __tablename__ = "strategy_health" @@ -225,7 +226,7 @@ class BybitSleeveRetRow(CockpitBase): class SimCurveRetRow(CockpitBase): - """One recompute-replay strategy's (multistrat/vrp — the IBKR paper books the Backtest page's sim reads) + """One recompute-replay strategy's (multistrat — the IBKR paper books the Backtest page's sim reads) daily honest-cost return for one run_date. Mirrors `BybitSleeveRetRow`'s shape/purpose for the Bybit-book sim: written nightly by `_persist_track_backtest_ref` (the `fxhnt-backtest-refs` Job) from the SAME inception rows `backtest_ref`'s recompute-replay branch derives the reconciliation-gate ref from, so diff --git a/src/fxhnt/adapters/persistence/forward_nav.py b/src/fxhnt/adapters/persistence/forward_nav.py index 7e6d31f..0831c10 100644 --- a/src/fxhnt/adapters/persistence/forward_nav.py +++ b/src/fxhnt/adapters/persistence/forward_nav.py @@ -143,9 +143,9 @@ class ForwardNavRepo: return list(s.scalars(select(StrategyHealthRow).order_by(StrategyHealthRow.strategy_id))) def xsp_freeze_max_date(self) -> str | None: - """The latest date present in the vrp freeze table (`xsp_option_bars`), ISO yyyy-mm-dd, or None when - the table is empty. The freeze-staleness axis: a vrp recompute is bounded by this max date, so if the - freeze stops advancing the track is STALE even though its forward 'updates'.""" + """The latest date present in the opra-pit-anchored freeze table (`xsp_option_bars`), ISO yyyy-mm-dd, + or None when the table is empty. The freeze-staleness axis: a frozen-input recompute is bounded by + this max date, so if the freeze stops advancing the track is STALE even though its forward 'updates'.""" from sqlalchemy import func with Session(self._engine) as s: d = s.scalar(select(func.max(XspOptionBarRow.date))) @@ -196,7 +196,7 @@ class ForwardNavRepo: def registry(self) -> list[StrategyRegistryRow]: """The cockpit-visible registry rows. SINGLE SOURCE for the hide: an ARCHIVED sleeve (its - `STRATEGY_REGISTRY` entry carries `archived: True` — e.g. the shelved/FALSIFIED vrp/vrp_exec) is + `STRATEGY_REGISTRY` entry carries `archived: True` — e.g. a shelved/FALSIFIED strategy) is excluded here, so EVERY caller (fleet overview, per-strategy gate specs, `detail()`'s reg lookup) drops it automatically. The DB row is still seeded (code + data kept) — it is only hidden from the UI.""" with Session(self._engine) as s: diff --git a/src/fxhnt/adapters/persistence/paper_repo.py b/src/fxhnt/adapters/persistence/paper_repo.py index 6195dce..ff486fe 100644 --- a/src/fxhnt/adapters/persistence/paper_repo.py +++ b/src/fxhnt/adapters/persistence/paper_repo.py @@ -515,8 +515,8 @@ class PaperRepo: # ---- sim_curve_ret: the PRECOMPUTED recompute-replay (IBKR) book curve /paper/sim reads ------------ def upsert_sim_curve_ret(self, strategy_id: str, run_date: str, ret: float, *, at: dt.datetime) -> None: - """Idempotent per (strategy_id, run_date): write/overwrite that recompute-replay book's (multistrat/ - vrp) per-day honest-cost return. `_persist_track_backtest_ref` writes the full inception series here + """Idempotent per (strategy_id, run_date): write/overwrite that recompute-replay book's (multistrat) + per-day honest-cost return. `_persist_track_backtest_ref` writes the full inception series here from the SAME rows it derives the reconciliation-gate ref from, so /paper/sim NEVER re-runs an IBKR replay on a web request — mirrors `upsert_bybit_sleeve_ret`'s pattern for the Bybit-book sim.""" rd = dt.date.fromisoformat(run_date) diff --git a/src/fxhnt/adapters/web/app.py b/src/fxhnt/adapters/web/app.py index 634559e..b1ba647 100644 --- a/src/fxhnt/adapters/web/app.py +++ b/src/fxhnt/adapters/web/app.py @@ -446,12 +446,13 @@ def create_app(repo: ForwardNavRepo, svc: DashboardReadModel | None = None, # The books the sim can configure. bybit_4edge = the precomputed 4-edge Bybit backtest (naive eq-wt — # NOT the overlay, since the A/B proved naive is best OOS), sized from bybit_sleeve_ret. bybit_4edge_levered - # = the observe-only levered shadow. multistrat/vrp (Task 1/D1) = the IBKR paper strategies' precomputed + # = the observe-only levered shadow. multistrat (Task 1/D1) = the IBKR paper strategies' precomputed # recompute-replay curve (sim_curve_ret) — generalizes the sim beyond the Bybit-only venue. Order = render # order of the book-switch pills: the deploy book leads (and is the default). - # Exclude ARCHIVED books (their STRATEGY_REGISTRY entry carries `archived: True` — e.g. the - # shelved/FALSIFIED vrp): they never appear as a Backtest book pill, and a `?book=vrp` request falls - # back to `_DEFAULT_BOOK` (so the folded "Live forward" section can't surface an archived sleeve either). + # Exclude ARCHIVED books (their STRATEGY_REGISTRY entry carries `archived: True` — e.g. a + # shelved/FALSIFIED strategy): they never appear as a Backtest book pill, and a request for an + # archived book falls back to `_DEFAULT_BOOK` (so the folded "Live forward" section can't surface an + # archived sleeve either). _SIM_BOOKS = tuple(b for b in SIM_BOOKS if not STRATEGY_REGISTRY.get(b, {}).get("archived")) _DEFAULT_BOOK = "bybit_4edge" # Plain-language pill labels for the sim's book switch — NEVER the raw registry id. Sourced from the @@ -470,7 +471,7 @@ def create_app(repo: ForwardNavRepo, svc: DashboardReadModel | None = None, _sim_returns_cache: dict[str, tuple[float, dict[str, dict[int, float]]]] = {} def _ibkr_sim_curve(book: str) -> dict[str, dict[int, float]]: - """The precomputed recompute-replay (IBKR) book curve for `book` (multistrat/vrp), as + """The precomputed recompute-replay (IBKR) book curve for `book` (multistrat), as `{book: {epoch_day: ret}}` — read from `sim_curve_ret` (written nightly by `_persist_track_backtest_ref`/the `fxhnt-backtest-refs` Job from the SAME inception rows the reconciliation-gate ref is derived from). NEVER recomputes the replay on this request path.""" @@ -479,7 +480,7 @@ def create_app(repo: ForwardNavRepo, svc: DashboardReadModel | None = None, def _sim_returns(book: str = _DEFAULT_BOOK) -> dict[str, dict[int, float]]: """Cached per-sleeve returns for `book`, dispatched by the registry's `backtest.kind` (Task 1/D1): `report`-kind Bybit books (bybit_4edge / bybit_4edge_levered) read the precomputed bybit_sleeve_ret - table; `recompute-replay`-kind IBKR books (multistrat / vrp) read the precomputed sim_curve_ret + table; `recompute-replay`-kind IBKR books (multistrat) read the precomputed sim_curve_ret table via `_ibkr_sim_curve`. NEVER recomputes any edge on the request path — both sources are nightly-precomputed. Memoized in-process with a 300s TTL, keyed by book (returns change only nightly).""" now = time.monotonic() @@ -548,7 +549,7 @@ def create_app(repo: ForwardNavRepo, svc: DashboardReadModel | None = None, def _measured_pending_context(book: str) -> dict[str, Any]: """Graceful degrade when the precomputed curve is ABSENT: a short 'precomputing — check back after the nightly job' note (NOT a flat slider, never a 500). The live forward track still renders for bybit_4edge - and for the IBKR recompute-replay books (multistrat/vrp) — each has its own real forward track.""" + and for the IBKR recompute-replay books (multistrat) — each has its own real forward track.""" is_ibkr = registry_backtest_kind(book) == "recompute-replay" return { "measured_pending": True, @@ -564,7 +565,7 @@ def create_app(repo: ForwardNavRepo, svc: DashboardReadModel | None = None, def _ibkr_measured_context(book: str, *, capital: float, start: str | None = None, end: str | None = None) -> dict[str, Any] | None: - """The IBKR recompute-replay books' (multistrat/vrp) Backtest view: compound the PRECOMPUTED per-day + """The IBKR recompute-replay books' (multistrat) Backtest view: compound the PRECOMPUTED per-day honest-cost return series (`_sim_returns(book)`, written nightly from the SAME replay basis the reconciliation gate reconciles the forward track against) into an equity curve via the pure `simulate_naive_eqwt` engine — no per-coin cost model here (that is the Bybit-only measured-cost path; @@ -725,7 +726,7 @@ def create_app(repo: ForwardNavRepo, svc: DashboardReadModel | None = None, capital = get_settings().paper_capital # Pre-fill AND bound the period inputs with the book's full precomputed date range, so the date fields # are never blank ("dates not set") and the picker can't go off the data. Bybit books read their range - # from the per-coin measured cache; IBKR recompute-replay books (multistrat/vrp, Task 1/D1) read it + # from the per-coin measured cache; IBKR recompute-replay books (multistrat, Task 1/D1) read it # from their own precomputed sim_curve_ret series (`_sim_returns`). if registry_backtest_kind(book) == "recompute-replay": _days = sorted(_sim_returns(book).get(book) or {}) @@ -749,7 +750,7 @@ def create_app(repo: ForwardNavRepo, svc: DashboardReadModel | None = None, end: str | None = None, book: str = _DEFAULT_BOOK) -> HTMLResponse: """The honest Backtest: book + capital + period -> the precomputed curve. Capital is parsed leniently (a cleared/comma'd/odd capital must not 422). Bybit books (report-kind) render the per-coin MEASURED- - cost cache scaled by capital + windowed by period; IBKR recompute-replay books (multistrat/vrp, Task + cost cache scaled by capital + windowed by period; IBKR recompute-replay books (multistrat, Task 1/D1) compound their own precomputed honest-cost return series the same way. Absent precompute -> a graceful 'precomputing' note (never the flat slider, never a 500).""" book = book if book in _SIM_BOOKS else _DEFAULT_BOOK @@ -807,7 +808,7 @@ def create_app_from_settings() -> FastAPI: repo.migrate() paper_repo = PaperRepo(settings.operational_dsn) paper_repo.migrate() - # The REAL IBKR paper-account section on the multistrat/vrp detail pages (D2.2/2.3) — a light + # The REAL IBKR paper-account section on the multistrat detail pages (D2.2/2.3) — a light # schema-ensure migrate (create_all + a guarded additive ALTER), same shape as repo/paper_repo above. ibkr_account_repo = IbkrAccountRepo(settings.operational_dsn) ibkr_account_repo.migrate() diff --git a/src/fxhnt/adapters/web/templates/_macros.html b/src/fxhnt/adapters/web/templates/_macros.html index bca00b1..f0904aa 100644 --- a/src/fxhnt/adapters/web/templates/_macros.html +++ b/src/fxhnt/adapters/web/templates/_macros.html @@ -57,8 +57,8 @@ {% macro rollup_plan_pill(rollup) %}{% if rollup and rollup.n_executing > 0 %}{{ '%.1f'|format((rollup.agg_divergence|abs) * 100) }}% {{ 'ahead of plan' if rollup.agg_divergence >= 0 else 'behind plan' }}{% endif %}{% endmacro %} {# ---- health_badge: the LOUD runtime-health axis (SEPARATE from the gate). A track that is STALE / NO_REF / - BROKEN shows a red badge — visually impossible to confuse with a healthy WAIT — so a stale recompute (the - 2-week vrp rot) surfaces on day 1. HEALTHY renders nothing. #} + BROKEN shows a red badge — visually impossible to confuse with a healthy WAIT — so a stale recompute + (a strategy silently rotting for weeks) surfaces on day 1. HEALTHY renders nothing. #} {% macro health_badge(f) %}{% if f.health and f.health != 'HEALTHY' %}{% if f.health == 'STALE' %}STALE {{ f.health_stale_days }}d{% elif f.health == 'NO_REF' %}NO REF{% else %}{{ f.health }}{% endif %}{% endif %}{% endmacro %} {# ---- track_row: one compact edge row. `record` is the meaningful number (backfill/backtest ref, else diff --git a/src/fxhnt/adapters/web/templates/cockpit.html b/src/fxhnt/adapters/web/templates/cockpit.html index 7fd066e..464949f 100644 --- a/src/fxhnt/adapters/web/templates/cockpit.html +++ b/src/fxhnt/adapters/web/templates/cockpit.html @@ -75,7 +75,7 @@ {# ---- D3 (Task 4): the REAL IBKR paper-account "real trades" sub-row — reuses the exec-twin's `.exrow`/`.exec-tag` style, but plain-language content (real return, click-through to holdings, the shared `plan_pill`, honest cost-to-trade). Replaces the generic exec-twin row below for the - two IBKR real-account books (multistrat/vrp); the crypto book's own exec-twin row is unaffected. #} + IBKR real-account book (multistrat); the crypto book's own exec-twin row is unaffected. #} {# The sub-row BORROWS the parent table's 6 columns, so each cell is self-labeled inline (like the exec-twin row below) — the real return in particular must read as a return, not sit naked under the "allocated" header. data-label carries the true meaning for the mobile card reflow, not the borrowed diff --git a/src/fxhnt/application/dashboard_service.py b/src/fxhnt/application/dashboard_service.py index 153467a..94fad57 100644 --- a/src/fxhnt/application/dashboard_service.py +++ b/src/fxhnt/application/dashboard_service.py @@ -52,7 +52,7 @@ def _expected_book_symbols() -> frozenset[str]: def _active_ibkr_account_sids() -> list[str]: """The IBKR-account sids that are still cockpit-visible — `_IBKR_ACCOUNT_SIDS` minus any ARCHIVED entry - (`STRATEGY_REGISTRY` `archived: True`, e.g. shelved/FALSIFIED vrp). Sorted for a deterministic render + (`STRATEGY_REGISTRY` `archived: True`, e.g. a shelved/FALSIFIED strategy). Sorted for a deterministic render order. Every account-wide aggregation (the per-strategy breakdown, the recent-fills roll-up) iterates THIS, so an archived book never surfaces a row/fill on the IBKR account page even though its fills are still captured in the repo.""" @@ -86,7 +86,7 @@ class DashboardService: def __init__(self, repo: ForwardNavRepo, ibkr_repo: IbkrAccountRepo | None = None) -> None: self._repo = repo # Optional: absent in most callers/tests (the ordinary crypto/registry tracks never touch it); when - # given, powers the IBKR real-account section on multistrat/vrp's detail pages (D2.2/2.3). + # given, powers the IBKR real-account section on multistrat's detail pages (D2.2/2.3). self._ibkr_repo = ibkr_repo def _safe_allocs(self) -> dict: @@ -387,7 +387,7 @@ class DashboardService: # silently carried in the account NLV — surfaced as a warning in the holdings view. "in_book": sym in expected} for sym, qty in positions.items()] - # GROSS exposure denominator: a defined-risk book (e.g. vrp put-credit-spreads = short put + long + # GROSS exposure denominator: a defined-risk book (e.g. a put-credit-spread book = short put + long # wing) holds mixed-sign legs, so a signed total nets toward the small net credit and makes "% of # book" blow past 100% / go negative. % of GROSS keeps every row's signed value but a sane [0,100] # share that sums to ~100. @@ -413,8 +413,8 @@ class DashboardService: "stray_holdings": stray_holdings} def detail(self, strategy_id: str) -> StrategyDetail | None: - # An ARCHIVED sleeve (STRATEGY_REGISTRY `archived: True` — e.g. shelved/FALSIFIED vrp/vrp_exec) must - # never render its detail page: a direct hit to `/strategy/vrp` returns None so the route 404s. This + # An ARCHIVED sleeve (STRATEGY_REGISTRY `archived: True` — e.g. a shelved/FALSIFIED strategy) must + # never render its detail page: a direct hit to its `/strategy/` returns None so the route 404s. This # is belt-and-suspenders — `registry()` below already excludes archived rows (single source) — but it # keeps the hide explicit and independent of the repo filter. if STRATEGY_REGISTRY.get(strategy_id, {}).get("archived"): diff --git a/src/fxhnt/application/display_names.py b/src/fxhnt/application/display_names.py index ca5ffaf..879cdfc 100644 --- a/src/fxhnt/application/display_names.py +++ b/src/fxhnt/application/display_names.py @@ -1,5 +1,5 @@ """display_names.py — the SINGLE internal-id -> human-readable name map used by EVERY cockpit view (the -plain-language global constraint: no raw registry id — `multistrat`/`vrp`/`bybit_4edge` — ever reaches an +plain-language global constraint: no raw registry id — `multistrat`/`bybit_4edge` — ever reaches an end user). Absorbs the interim `_SIM_BOOK_LABELS` local map that lived in `app.py` (Task 1) for the sim/Backtest book pills — those two overrides now live here as the SSOT, and `app.py` reads through this module instead of its own copy. @@ -48,7 +48,7 @@ def display_name(strategy_id: str, fallback: str | None = None) -> str: def asset_description(symbol: str) -> str: """Plain 'what it is' description for an asset symbol (e.g. SPY -> 'US stocks'). An UNMAPPED symbol (a - future vrp option contract, say) falls back to a plain generic 'other' rather than echoing a raw/odd + future option contract, say) falls back to a plain generic 'other' rather than echoing a raw/odd ticker string — the symbol itself is still shown in its own column, so nothing is lost. Never blank, never raises.""" return ASSET_DESCRIPTIONS.get(symbol, "other") diff --git a/src/fxhnt/application/em_asia_marginal_eval.py b/src/fxhnt/application/em_asia_marginal_eval.py index 5961378..b737c88 100644 --- a/src/fxhnt/application/em_asia_marginal_eval.py +++ b/src/fxhnt/application/em_asia_marginal_eval.py @@ -3,8 +3,8 @@ LOO marginal-Sharpe gate, run BEFORE any forward tracker is wired. This is the g reconciliation gate on two EM-beta ETFs would PASS trivially (the forward NAV just tracks the backtest NAV) and prove NOTHING about edge. The real question is decision-theoretic and answerable now from ~8-10y of history: does adding CNYA.L / NDIA.L to the fund's ETF book RAISE the book's risk-adjusted return after -costs, or is it a co-crashing beta sleeve that fails the marginal gate — the SAME bar that killed VRP and -the token-unlock diversifier (see pearl_fxhnt_diversifier_hunt_2026_07_15). +costs, or is it a co-crashing beta sleeve that fails the marginal gate — the SAME bar that killed prior failed +diversifier candidates, including the token-unlock diversifier (see pearl_fxhnt_diversifier_hunt_2026_07_15). PASS bar (predetermined, not chosen after seeing the numbers): marginal Sharpe > 0 after cost. > 0 -> proceed to Stage 2 (wire the `em_asia` registry sleeve + reconciliation forward gate). @@ -15,8 +15,8 @@ overlapping dates — CNYA.L ~10y, NDIA.L ~8y. Per-name verdicts (not a cross-na windows are acceptable. Two marginal measures are reported per name: - 1. `marginal_sharpe` (SSOT weight-blend, domain/diversification/marginal.py) — DIRECTLY comparable to the - prior VRP / unlock verdicts, which used this exact primitive. + 1. `marginal_sharpe` (SSOT weight-blend, domain/diversification/marginal.py) — DIRECTLY comparable to + prior failed diversifier candidates' verdicts (e.g. unlock), which used this exact primitive. 2. In-book delta-Sharpe — `book_series(FUND + candidate) - book_series(FUND)` through the FULL adaptive machinery (vol-norm -> trust -> correlation de-risk), i.e. the candidate's effect where it would actually live. The truer measure; if the two disagree, that disagreement is itself signal. @@ -68,7 +68,7 @@ class CandidateVerdict: window_start: str window_end: str n_days: int - marginal_sharpe_blend: float # SSOT weight-blend, cost-inclusive (comparable to VRP/unlock) + marginal_sharpe_blend: float # SSOT weight-blend, cost-inclusive (comparable to prior candidates/unlock) delta_sharpe_inbook: float # book_series(FUND+cand) - book_series(FUND), cost-inclusive corr_to_book: float crash_drawdowns: dict[str, float] = field(default_factory=dict) diff --git a/src/fxhnt/application/forward_health.py b/src/fxhnt/application/forward_health.py index 02ad8dc..e3485bb 100644 --- a/src/fxhnt/application/forward_health.py +++ b/src/fxhnt/application/forward_health.py @@ -1,19 +1,21 @@ """Runtime staleness/health axis — the anti-silent-rot backbone. -WHY: vrp was silently broken for 2 weeks because a data failure degraded QUIETLY — it recomputed off a -frozen-but-stale `xsp_option_bars` table, its forward "updated" every night, and only a log line marked the -rot. The gate never noticed (a stale track just sits as a quiet WAIT). This module adds a HEALTH axis that is +WHY: a dead/archived strategy was once silently broken for 2 weeks because a data failure degraded +QUIETLY — it recomputed off a frozen-but-stale `xsp_option_bars` table, its forward "updated" every night, +and only a log line marked the rot. The gate never noticed (a stale track just sits as a quiet WAIT). This +module adds a HEALTH axis that is SEPARATE from the gate: a track can be legitimately WAIT/building AND healthy; STALE/NO_REF/BROKEN is orthogonal. Any live/gated track that stops updating surfaces LOUDLY on day 1 (red cockpit badge + ERROR log), not a footnote two weeks later. The health-check line is `record_mode`, which faithfully encodes "is this track EXPECTED to advance nightly": - - `recompute` tracks (sixtyforty/xsfunding/unlock/multistrat/vrp/bybit_4edge/…): the engine recomputes from + - `recompute` tracks (sixtyforty/xsfunding/unlock/multistrat/bybit_4edge/…): the engine recomputes from fresh source data EVERY run, so a non-advancing `as_of` (or a frozen freeze table) is a real STALE fault. - vrp lives here → the incident is caught. - - `observed` tracks (the executed twins multistrat_exec/vrp_exec/bybit_4edge_exec): advance ONLY when a - human/executor books a fill, so they are legitimately DORMANT when execution is suspended (vrp_exec has no - IBKR options permission yet). Staleness/no-ref do NOT apply — they must not false-alarm. This is the + A `recompute` track lives here → an incident like the one above is caught. + - `observed` tracks (the executed twins multistrat_exec/bybit_4edge_exec): advance ONLY when a + human/executor books a fill, so they are legitimately DORMANT when execution is suspended (an executed + twin can lack IBKR options permission, for example). Staleness/no-ref do NOT apply — they must not + false-alarm. This is the "intentionally-dormant reference_only" carve-out. (BROKEN — a health-compute error — still applies, since a track whose health we cannot even determine is not healthy.) @@ -21,14 +23,14 @@ States (worst wins, precedence BROKEN > STALE > NO_REF > HEALTHY): - HEALTHY — advancing within threshold, or dormant-by-design, or a GENUINELY new recompute track (its active `forward_anchor.t0` is within the threshold, so "no data yet" is expected, not rot). - STALE(n) — a recompute track's source `as_of` OR its freeze table's max date is > threshold business days - behind the run date. The freeze axis catches the exact vrp failure mode: forward "updates" yet the frozen - input table is stale. Also STALE (not the HEALTHY "no data yet" carve-out): a recompute track with ZERO + behind the run date. The freeze axis catches the exact failure mode described above: forward "updates" + yet the frozen input table is stale. Also STALE (not the HEALTHY "no data yet" carve-out): a recompute track with ZERO forward data whose anchor `t0` is OLDER than the threshold (broken-from-inception — never advanced since it was created), or which has no active anchor at all (should have one; a live track with no anchor is a real signal, never silently HEALTHY). - NO_REF — a reconciliation-gated recompute track with NO resolvable backtest reference: the gate can NEVER reconcile (it silently degrades to the min-window/clean-exec checks only). The M6 static invariant that - missed vrp, made a real runtime check. + missed the incident described above, made a real runtime check. - BROKEN — the PER-TRACK health computation raised (e.g. a malformed registry entry for that strategy): surfaced for that track only, not swallowed. This does NOT cover the repo-wide reads `evaluate_health` does once up front (`migrate`/`all_summaries`/`all_backtest_summaries`/`xsp_freeze_max_date`) — those are @@ -144,7 +146,7 @@ def evaluate_health(dsn: str, at: dt.datetime | None = None) -> list[HealthVerdi summaries = {s.strategy_id: s for s in repo.all_summaries()} backtest_sids = {b.strategy_id for b in repo.all_backtest_summaries()} ref_alias = CockpitBacktestRefProvider._REF_ALIAS # the SAME alias the gate uses to resolve its ref - freeze_max = repo.xsp_freeze_max_date() # one query for the vrp freeze axis + freeze_max = repo.xsp_freeze_max_date() # one query for the frozen-input freeze axis verdicts: list[HealthVerdict] = [] for sid, meta in STRATEGY_REGISTRY.items(): diff --git a/src/fxhnt/application/sim_curves.py b/src/fxhnt/application/sim_curves.py index b236405..c10e496 100644 --- a/src/fxhnt/application/sim_curves.py +++ b/src/fxhnt/application/sim_curves.py @@ -1,5 +1,5 @@ """Dispatch a sim book's honest-cost curve by its registry backtest.kind — bybit books read the precomputed -bybit_sleeve_ret table; recompute-replay books (multistrat/vrp, the IBKR paper strategies) read their own +bybit_sleeve_ret table; recompute-replay books (multistrat, the IBKR paper strategies) read their own precomputed curve. The sim NEVER recomputes an edge on the request path (both sources are nightly-precomputed: bybit_sleeve_ret by `_persist_bybit_book`, the IBKR curve by `_persist_track_backtest_ref`).""" from __future__ import annotations @@ -9,7 +9,7 @@ from collections.abc import Callable from fxhnt.application.backtest_refs import registry_backtest_kind # The Backtest page's book pills, in render order. bybit_4edge/bybit_4edge_levered are the existing Bybit -# books (report-kind, precomputed bybit_sleeve_ret). multistrat/vrp are the IBKR paper strategies +# books (report-kind, precomputed bybit_sleeve_ret). multistrat is the IBKR paper strategy # (recompute-replay-kind) added by Task 1/D1 — their curve is a precomputed per-day return series read the # same cheap way (see `sim_returns_for`'s dispatch below). SIM_BOOKS: tuple[str, ...] = ("bybit_4edge", "bybit_4edge_levered", "multistrat", "multistrat_levered") @@ -19,7 +19,7 @@ def sim_returns_for(book: str, *, bybit_returns: Callable[[], dict[str, dict[int ibkr_curve: Callable[[str], dict[str, dict[int, float]]] ) -> dict[str, dict[int, float]]: """`book`'s per-sleeve honest-cost return series — `{sleeve: {epoch_day: ret}}` — dispatched by the - registry's `backtest.kind` for `book`: `recompute-replay` (multistrat/vrp) reads the precomputed IBKR + registry's `backtest.kind` for `book`: `recompute-replay` (multistrat) reads the precomputed IBKR curve via `ibkr_curve(book)`; everything else (the `report`-kind Bybit books) reads the precomputed `bybit_sleeve_ret` table via `bybit_returns()` — the existing path, unchanged.""" kind = registry_backtest_kind(book) diff --git a/src/fxhnt/config.py b/src/fxhnt/config.py index e5d9d71..8ab4f71 100644 --- a/src/fxhnt/config.py +++ b/src/fxhnt/config.py @@ -212,7 +212,8 @@ class Settings(BaseSettings): paper_enabled: bool = True # toggle the daily-rebalance paper-book persistence hook (T7) # Staleness/health axis: a recompute track whose source data (as_of) OR freeze table hasn't advanced in # more than this many BUSINESS days reads as STALE (a red cockpit badge + ERROR log). Default 3 allows - # the normal ~1-business-day ingest lag; the anti-silent-rot threshold that would have caught vrp. + # the normal ~1-business-day ingest lag; the anti-silent-rot threshold that would have caught a + # strategy silently rotting for weeks. stale_after_business_days: int = 3 cost_bps_per_turnover: float = 10.0 # round-trip cost model (bps of traded notional) diff --git a/src/fxhnt/ports/dashboard.py b/src/fxhnt/ports/dashboard.py index 165937f..c22766b 100644 --- a/src/fxhnt/ports/dashboard.py +++ b/src/fxhnt/ports/dashboard.py @@ -68,8 +68,8 @@ class FleetRow: exec_twin_sid: str = "" exec_return_pct: float | None = None exec_divergence_pct: float | None = None - # D3 (Task 4) — the REAL IBKR paper-account "real trades" signal: "EXEC" only for the two IBKR real- - # account books (multistrat/vrp) once real fills have been captured (`exec_vs_sim(...).has_exec`); "" + # D3 (Task 4) — the REAL IBKR paper-account "real trades" signal: "EXEC" only for the IBKR real- + # account book (multistrat) once real fills have been captured (`exec_vs_sim(...).has_exec`); "" # (with `recon=None`) for every other strategy AND for an IBKR book with no captured data yet — the # cockpit's Overview sub-row only ever renders for a genuinely-executing row, never a fabricated one. exec_status: str = "" @@ -187,7 +187,7 @@ class StrategyDetail: exec_venue: str | None = None # D2.2/2.3 (Task 3) — the REAL IBKR paper-account section: what the book is holding right now, its # recent trades, and the account's raw NLV series (for the value-over-time overlay curve). Empty for - # every strategy that isn't one of the IBKR real-account books (multistrat/vrp) or when no account data + # every strategy that isn't the IBKR real-account book (multistrat) or when no account data # has been captured yet — the template renders "no trades recorded yet", never a 500. holdings: list[dict[str, Any]] = field(default_factory=list) # Held symbols outside the book's allowed universe (US + UCITS) — a legacy/stray position (e.g. an IBIT diff --git a/src/fxhnt/vendor/surfer/__init__.py b/src/fxhnt/vendor/surfer/__init__.py index a6f46da..d25f6fb 100644 --- a/src/fxhnt/vendor/surfer/__init__.py +++ b/src/fxhnt/vendor/surfer/__init__.py @@ -8,9 +8,8 @@ ONLY edits applied (no algorithm / math / constant changes): * data root made configurable via `$FXHNT_SURFER_DATA_DIR` (default `/data/surfer`): `pit_sweep.load()` reads `$FXHNT_SURFER_DATA_DIR/crypto_pit/*.npz`. -(The `surfer_poc` module — a value-returning `paper_step` refactor of `paper()` for the fxhnt -ForwardTracker's tail-managed VRP sleeve — was RETIRED with its dormant `MomentumVrpStrategy` caller; -Phase 0b Task 7c.) +(The `surfer_poc` module — a value-returning `paper_step` refactor of `paper()` for a since-retired +ForwardTracker sleeve — was RETIRED along with its dormant caller; Phase 0b Task 7c.) torch is imported at module level (used in `signal_sweep.sharpe_t`/`validate`) and is kept — this is a faithful port, not a numpy reimplementation.