chore(surfer): relabel combined as crypto-momentum book + retire equity-factor sleeves

- combined relabeled 'Crypto momentum book (vestigial trend overlay)': the engine is validated
  crypto momentum; the futures-trend leg riding the crypto spine has no edge and never gates.
- Retired eqfactor_long/eqfactor_tilt (US-equity price-factors — weakly-held, edge never
  established) + their orphaned shared eqfactor_scores asset. Strategy classes stay in
  equity_factor_strategy.py. _seed_registry prunes stale rows automatically.

Cockpit fleet now = 4 tracks: 2 validated crypto edges (xsfunding, unlock) + the crypto-momentum
book + the 60/40 benchmark. 7 Dagster assets. Tests updated (labels + retarget to surviving
tracks); full suite 511 passed, mypy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-06-20 22:53:03 +02:00
parent d0d84497c7
commit 33b36f8eb8
6 changed files with 36 additions and 95 deletions

View File

@@ -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

View File

@@ -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,
],

View File

@@ -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",

View File

@@ -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:

View File

@@ -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}"

View File

@@ -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