diff --git a/src/fxhnt/adapters/orchestration/assets.py b/src/fxhnt/adapters/orchestration/assets.py index 7485beb..daa202c 100644 --- a/src/fxhnt/adapters/orchestration/assets.py +++ b/src/fxhnt/adapters/orchestration/assets.py @@ -169,48 +169,11 @@ def sixtyforty_nav(context: AssetExecutionContext) -> dict: # type: ignore[type # and the 60/40 benchmark — signal, not noise. -@asset -def eqfactor_scores(context: AssetExecutionContext) -> dict: # type: ignore[type-arg] - """Shared PRICE-ONLY factor cross-section fetched ONCE (Tiingo universe + daily bars, batched-concurrent). - - The sleeve is price-only (momentum + low-vol): one adj_closes fetch per ticker yields both signals, so - prices alone — which Tiingo serves broadly — score the full universe. Fundamentals are NOT fetched (the - Tiingo plan covers DOW-30 only, which silently dropped ~270/300 names). The three equity-factor sleeves - consume this SAME precomputed scores dict. A transient network/API error logs a warning and yields an - EMPTY cross-section (sleeves then book nothing) rather than crashing the nightly run.""" - from fxhnt.adapters.data.tiingo_daily import TiingoDailyClient - from fxhnt.adapters.data.tiingo_universe import TiingoUniverseSource - from fxhnt.application.equity_factor_strategy import compute_factor_scores - - try: - s = compute_factor_scores(TiingoUniverseSource(), TiingoDailyClient(), n=150) - except (OSError, TimeoutError, ConnectionError) as e: # transient: don't crash the run - context.log.warning(f"eqfactor_scores fetch failed: {e}") - s = {"date": "", "syms": [], "scores": [], "prices": {}} - context.log.info(f"eqfactor_scores: {len(s['syms'])} names scored") - return s - - -@asset -def eqfactor_long_nav(context: AssetExecutionContext, eqfactor_scores: dict) -> dict: # type: ignore[type-arg] - """Paper-forward long-only equity-factor sleeve over the shared precomputed scores (no I/O of its own).""" - from fxhnt.application.equity_factor_strategy import EquityFactorLong - from fxhnt.application.forward_tracker import ForwardTracker - - st = ForwardTracker(EquityFactorLong(eqfactor_scores), f"{_data_dir()}/eqfactor_long_state.json").step() - context.log.info(f"eqfactor_long_nav: {st.forward_days}d through {st.last_date}") - return {"forward_days": st.forward_days, "last_date": st.last_date} - - -@asset -def eqfactor_tilt_nav(context: AssetExecutionContext, eqfactor_scores: dict) -> dict: # type: ignore[type-arg] - """Paper-forward long-only rank-weighted (tilt) equity-factor sleeve over the shared precomputed scores.""" - from fxhnt.application.equity_factor_strategy import EquityFactorTilt - from fxhnt.application.forward_tracker import ForwardTracker - - st = ForwardTracker(EquityFactorTilt(eqfactor_scores), f"{_data_dir()}/eqfactor_tilt_state.json").step() - context.log.info(f"eqfactor_tilt_nav: {st.forward_days}d through {st.last_date}") - return {"forward_days": st.forward_days, "last_date": st.last_date} +# RETIRED 2026-06-20: eqfactor_scores + eqfactor_long_nav + eqfactor_tilt_nav (US-equity price-factor +# sleeves — weakly-held, edge never established; kept briefly as a non-crypto experiment, now retired as +# unproven). Strategy classes (compute_factor_scores / EquityFactorLong / EquityFactorTilt) stay in +# equity_factor_strategy.py for the record. Cockpit fleet now = the 2 validated crypto edges + the +# crypto-momentum book + the 60/40 benchmark. @asset @@ -241,8 +204,7 @@ def unlock_nav(context: AssetExecutionContext) -> dict: # type: ignore[type-arg return _run_paper_tracker(context, "unlock_nav", lambda: UnlockShortForward(cal), "unlock_state") -@asset(deps=[combined_forward_nav, sixtyforty_nav, - eqfactor_long_nav, eqfactor_tilt_nav, xsfunding_nav, unlock_nav]) +@asset(deps=[combined_forward_nav, sixtyforty_nav, xsfunding_nav, unlock_nav]) def cockpit_forward(context: AssetExecutionContext) -> None: """Normalize tracker state files and upsert rows + summary into the operational cockpit DB.""" from fxhnt.application.forward_ingest import ingest_forward_state diff --git a/src/fxhnt/adapters/orchestration/definitions.py b/src/fxhnt/adapters/orchestration/definitions.py index 156907b..0a143bd 100644 --- a/src/fxhnt/adapters/orchestration/definitions.py +++ b/src/fxhnt/adapters/orchestration/definitions.py @@ -8,9 +8,6 @@ from fxhnt.adapters.orchestration.assets import ( cockpit_forward, combined_forward_nav, crypto_bars, - eqfactor_long_nav, - eqfactor_scores, - eqfactor_tilt_nav, futures_bars, sixtyforty_nav, unlock_nav, @@ -21,7 +18,6 @@ combined_book_job = define_asset_job( name="combined_book_forward_job", selection=[ crypto_bars, futures_bars, combined_forward_nav, sixtyforty_nav, - eqfactor_scores, eqfactor_long_nav, eqfactor_tilt_nav, xsfunding_nav, unlock_nav, cockpit_forward, ], @@ -38,7 +34,6 @@ daily_combined_book_schedule = ScheduleDefinition( defs = Definitions( assets=[ crypto_bars, futures_bars, combined_forward_nav, sixtyforty_nav, - eqfactor_scores, eqfactor_long_nav, eqfactor_tilt_nav, xsfunding_nav, unlock_nav, cockpit_forward, ], diff --git a/src/fxhnt/registry.py b/src/fxhnt/registry.py index a87e392..67f29c9 100644 --- a/src/fxhnt/registry.py +++ b/src/fxhnt/registry.py @@ -8,31 +8,26 @@ from typing import Any # gate_spec keys: min_days, min_total_return (fraction), min_sharpe. STRATEGY_REGISTRY: dict[str, dict[str, Any]] = { "combined": { - "display_name": "Combined book (crypto-mom + futures-trend)", "sleeve": "multi", + # Relabeled 2026-06-20: the engine is crypto momentum (validated ~1.2 Sharpe); the futures-trend + # leg riding the crypto spine has no edge (falsified) and never gates — vestigial overlay, not a + # co-driver. Name reflects the real driver. + "display_name": "Crypto momentum book (vestigial trend overlay)", "sleeve": "crypto-momentum", "venue": "binance+glbx", "state_file": "fxhnt_combined_forward", "gate_spec": {"min_days": 20, "min_total_return": 0.0, "min_sharpe": 0.5}, }, # NOTE: the absolute-hurdle "funding" track was RETIRED 2026-06-20 (idle ~60% of days, flat 2022); # the canonical funding deploy is the cross-sectional "xsfunding" below (harvests dispersion, always # active, +71% in the 2022 backtest). The faithful-port harness stays in paper_strategies.py. - # RETIRED 2026-06-20 (cleanup): multistrat/gd (futures premia — falsified, scale-gated) + poc (old - # crypto PoC, superseded). Strategy classes stay in paper_strategies.py. DB rows pruned + _seed_registry - # now prunes stale rows. Kept: the 3 crypto edges, 2 equity-factor sleeves, and the 60/40 benchmark. + # RETIRED 2026-06-20: multistrat/gd (futures premia — falsified, scale-gated) + poc (old crypto PoC, + # superseded) + eqfactor_long/eqfactor_tilt (US-equity factors — weakly-held, edge never established; + # kept only as a non-crypto experiment, now retired as unproven). Strategy classes stay in + # equity_factor_strategy.py / paper_strategies.py. _seed_registry prunes stale rows automatically. + # Cockpit now = 2 validated crypto edges (xsfunding, unlock) + crypto-momentum book + 60/40 benchmark. "sixtyforty": { "display_name": "60/40 baseline", "sleeve": "beta", "venue": "glbx", "state_file": "sixtyforty_state", "gate_spec": {"min_days": 20, "min_total_return": 0.0, "min_sharpe": 0.0}, }, - "eqfactor_long": { - "display_name": "Equity factor — long-only", "sleeve": "equity-factor", - "venue": "us-equity", "state_file": "eqfactor_long_state", - "gate_spec": {"min_days": 60, "min_total_return": 0.0, "min_sharpe": 0.3}, - }, - "eqfactor_tilt": { - "display_name": "Equity factor — long-tilt", "sleeve": "equity-factor", - "venue": "us-equity", "state_file": "eqfactor_tilt_state", - "gate_spec": {"min_days": 60, "min_total_return": 0.0, "min_sharpe": 0.3}, - }, "xsfunding": { "display_name": "Crypto funding harvest (cross-sectional, delta-neutral)", "sleeve": "crypto-funding", "venue": "binance-perp", "state_file": "xsfunding_state", diff --git a/tests/integration/test_dashboard_service.py b/tests/integration/test_dashboard_service.py index 0930f41..2a7a2b1 100644 --- a/tests/integration/test_dashboard_service.py +++ b/tests/integration/test_dashboard_service.py @@ -19,12 +19,13 @@ def _seeded() -> ForwardNavRepo: Row("combined", "2026-06-06", 0.0099, 1.0201)], at) repo.upsert_summary(ForwardSummary("combined", "2026-06-06", 2, 1.0201, 0.0201, 1.2, -0.01), "WAIT", "2/20 forward days", at) - # backtest verdicts exist only for the equity-factor constructions + # a surviving track (xsfunding) carries a backtest verdict; a RETIRED one (eqfactor_long) has a + # leftover backtest-summary DB row but must NOT surface (fleet is registry-driven). repo.upsert_backtest_summary(BacktestSummary( - "eqfactor_long", "2026-05-30", cagr=0.12, ann_vol=0.18, sharpe=0.85, max_drawdown=-0.22, + "xsfunding", "2026-05-30", cagr=0.12, ann_vol=0.18, sharpe=0.85, max_drawdown=-0.22, passed=True, dsr=0.61, is_sharpe=0.9, oos_sharpe=0.7, pvalue=0.03), at) repo.upsert_backtest_summary(BacktestSummary( - "eqfactor_ls", "2026-05-30", cagr=-0.01, ann_vol=0.11, sharpe=-0.05, max_drawdown=-0.31, + "eqfactor_long", "2026-05-30", cagr=-0.01, ann_vol=0.11, sharpe=-0.05, max_drawdown=-0.31, passed=False, dsr=-0.2, is_sharpe=0.1, oos_sharpe=-0.3, pvalue=0.6), at) return repo @@ -33,7 +34,7 @@ def test_fleet_joins_registry_to_summary() -> None: svc = DashboardService(_seeded()) fleet = svc.fleet() combined = next(f for f in fleet if f.strategy_id == "combined") - assert combined.display_name.startswith("Combined book") + assert combined.display_name.startswith("Crypto momentum book") assert combined.days == 2 and combined.gate_status == "WAIT" assert round(combined.total_return_pct, 2) == 2.01 @@ -49,7 +50,7 @@ def test_detail_returns_summary_and_history() -> None: svc = DashboardService(_seeded()) detail = svc.detail("combined") assert detail is not None - assert detail.display_name.startswith("Combined book") + assert detail.display_name.startswith("Crypto momentum book") assert [p.date for p in detail.history] == ["2026-06-05", "2026-06-06"] @@ -69,7 +70,7 @@ def test_detail_unknown_strategy_is_none() -> None: def test_fleet_surfaces_backtest_verdict() -> None: fleet = DashboardService(_seeded()).fleet() - el = next(f for f in fleet if f.strategy_id == "eqfactor_long") + el = next(f for f in fleet if f.strategy_id == "xsfunding") assert el.bt_status == "PASS" assert abs(el.bt_sharpe - 0.85) < 1e-9 assert abs(el.bt_dsr - 0.61) < 1e-9 @@ -77,10 +78,9 @@ def test_fleet_surfaces_backtest_verdict() -> None: assert abs(el.bt_maxdd - (-0.22)) < 1e-9 assert el.bt_as_of == "2026-05-30" - # eqfactor_ls is retired from the registry (B3b — no edge); even though a backtest summary row - # still exists in the DB (the backtest reports all three constructions), the fleet is registry-driven - # so the retired LS track is no longer surfaced. - assert all(f.strategy_id != "eqfactor_ls" for f in fleet) + # eqfactor_long is retired from the registry (unproven); even though a backtest-summary row still + # exists in the DB, the fleet is registry-driven so the retired track is no longer surfaced. + assert all(f.strategy_id != "eqfactor_long" for f in fleet) def test_fleet_no_backtest_renders_blank() -> None: diff --git a/tests/integration/test_orchestration_definitions.py b/tests/integration/test_orchestration_definitions.py index ef324c3..b9821af 100644 --- a/tests/integration/test_orchestration_definitions.py +++ b/tests/integration/test_orchestration_definitions.py @@ -17,20 +17,15 @@ def test_definitions_load_with_assets_and_schedule() -> None: assert "sixtyforty_nav" in asset_keys, f"missing sixtyforty benchmark: {asset_keys}" for retired in ("crossvenue_nav", "funding_nav", "multistrat_nav", "gd_nav", "poc_nav"): assert retired not in asset_keys, f"{retired} must be retired" - # B2: the equity-factor forward sleeves are wired into the graph alongside the B1 paper tracks - # (eqfactor_ls retired per B3b — no edge in backtest; only long + tilt forward-track now) - eqfactor = {"eqfactor_long_nav", "eqfactor_tilt_nav"} - assert eqfactor <= asset_keys, f"missing equity-factor assets: {eqfactor - asset_keys}" - assert "eqfactor_ls_nav" not in asset_keys, "eqfactor_ls_nav must be retired (B3b)" - # XSFP T3: the cross-sectional crypto funding sleeve is wired into the graph + # equity-factor sleeves RETIRED 2026-06-20 (weakly-held, edge never established) + for retired_eq in ("eqfactor_long_nav", "eqfactor_tilt_nav", "eqfactor_scores", "eqfactor_ls_nav"): + assert retired_eq not in asset_keys, f"{retired_eq} must be retired" + # the 2 validated crypto edges are wired in assert "xsfunding_nav" in asset_keys, f"missing xsfunding_nav asset: {asset_keys}" - # 2nd uncorrelated edge: token-unlock dilution shorts wired in assert "unlock_nav" in asset_keys, f"missing unlock_nav asset: {asset_keys}" - # B2.1: the shared scores asset is wired in (fetched once, consumed by the sleeves) - assert "eqfactor_scores" in asset_keys, f"missing eqfactor_scores asset: {asset_keys}" - # 10 assets: crypto_bars, futures_bars, combined_forward_nav, sixtyforty_nav, eqfactor_scores, - # eqfactor_long_nav, eqfactor_tilt_nav, xsfunding_nav, unlock_nav, cockpit_forward - assert len(asset_keys) == 10, f"expected 10 assets, got {len(asset_keys)}: {asset_keys}" + # 7 assets: crypto_bars, futures_bars, combined_forward_nav, sixtyforty_nav, + # xsfunding_nav, unlock_nav, cockpit_forward + assert len(asset_keys) == 7, f"expected 7 assets, got {len(asset_keys)}: {asset_keys}" # --- schedule present with the right cron --- # defs.schedules is a list[ScheduleDefinition] (or None when empty) @@ -48,11 +43,5 @@ def test_definitions_load_with_assets_and_schedule() -> None: cockpit_def = repo.assets_defs_by_key[AssetKey("cockpit_forward")] deps = cockpit_def.asset_deps[AssetKey("cockpit_forward")] upstream = {k.to_user_string() for k in deps} - expected_upstream = {"combined_forward_nav", "xsfunding_nav", "unlock_nav", "sixtyforty_nav"} | eqfactor + expected_upstream = {"combined_forward_nav", "xsfunding_nav", "unlock_nav", "sixtyforty_nav"} assert expected_upstream <= upstream, f"cockpit_forward missing upstream: {expected_upstream - upstream}" - - # --- each equity-factor nav sleeve depends on the shared eqfactor_scores asset (fetched once) --- - for nav in ("eqfactor_long_nav", "eqfactor_tilt_nav"): - nav_def = repo.assets_defs_by_key[AssetKey(nav)] - nav_up = {k.to_user_string() for k in nav_def.asset_deps[AssetKey(nav)]} - assert "eqfactor_scores" in nav_up, f"{nav} missing eqfactor_scores upstream dep: {nav_up}" diff --git a/tests/integration/test_web.py b/tests/integration/test_web.py index f2e61d9..a010686 100644 --- a/tests/integration/test_web.py +++ b/tests/integration/test_web.py @@ -21,7 +21,7 @@ def _client() -> TestClient: repo.upsert_summary(ForwardSummary("combined", "2026-06-06", 2, 1.0201, 0.0201, 1.2, -0.01), "WAIT", "2/20 forward days", at) repo.upsert_backtest_summary(BacktestSummary( - "eqfactor_long", "2026-05-30", cagr=0.12, ann_vol=0.18, sharpe=0.85, max_drawdown=-0.22, + "xsfunding", "2026-05-30", cagr=0.12, ann_vol=0.18, sharpe=0.85, max_drawdown=-0.22, passed=True, dsr=0.61, is_sharpe=0.9, oos_sharpe=0.7, pvalue=0.03), at) return TestClient(create_app(repo)) @@ -33,7 +33,7 @@ def test_healthz() -> None: def test_cockpit_lists_fleet() -> None: r = _client().get("/") assert r.status_code == 200 - assert "Combined book" in r.text and "WAIT" in r.text + assert "Crypto momentum book" in r.text and "WAIT" in r.text def test_cockpit_shows_backtest_verdict_column() -> None: @@ -52,7 +52,7 @@ def test_cockpit_has_responsive_markup() -> None: def test_strategy_detail_renders_backtest_verdict() -> None: - r = _client().get("/strategy/eqfactor_long") + r = _client().get("/strategy/xsfunding") assert r.status_code == 200 assert "backtest verdict" in r.text # verdict block header present assert "badge PASS" in r.text # PASS badge rendered