Files
fxhnt/tests/integration/test_forward.py
jgrusewski 8b3214f628 feat: wire CUSUM edge-decay into forward-validation — the missing Kelly<->kill layer
ForwardValidationService now runs a CUSUM over each FORWARD specialist's in-regime forward return
sequence (StrategyRecord.forward_returns, persisted): a structural NEGATIVE break -> RETIRE with minimal
delay, even before min_forward_days. This is the edge-decay/trust layer the foxhunt memory flagged as the
'missing abstraction' between Kelly (long-run sizing) and the circuit breaker (tail kill) — a slow bleed
that passes mean/threshold filters is caught by the changepoint detector. _review order: CUSUM-decay ->
min-days gate -> deploy(mean>0)/retire. 53/53 tests (decayed retired, holding-edge deployed, young keeps tracking).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:09:13 +02:00

52 lines
2.4 KiB
Python

"""Forward-validation review: CUSUM edge-decay retires a decaying specialist; a holding edge deploys;
a young one keeps tracking."""
from __future__ import annotations
import datetime as dt
from fxhnt.adapters.persistence import DuckDbAnalyticalStore, SqlStrategyStore
from fxhnt.application.forward import ForwardValidationService
from fxhnt.config import Settings
from fxhnt.domain.factory import RegimeFit, StrategyRecord, StrategyStatus
from fxhnt.domain.models import AssetClass, Market, PriceSeries, StrategySpec
_NOW = dt.datetime(2026, 6, 13, tzinfo=dt.timezone.utc)
class FakeData:
name = "fake"
def fetch(self, market: Market, start=None, end=None) -> PriceSeries: # not used by _review
import numpy as np
return PriceSeries(market=market, dates=("0",) * 400, close=np.ones(400))
def _fwd(sid: str, returns: list[float]) -> StrategyRecord:
return StrategyRecord(
strategy_id=sid, markets=["NQ"], asset_class=AssetClass.FUTURE,
spec=StrategySpec(kind="trend", params={"window": 200.0}),
regime_fits=[RegimeFit(regime="trend_up|low_vol", dsr=0.97, is_sharpe=1.3, oos_sharpe=0.9, passed=True)],
status=StrategyStatus.FORWARD, discovered_at=_NOW, updated_at=_NOW,
forward_days=len(returns), forward_sum=sum(returns), forward_sumsq=sum(r * r for r in returns),
forward_returns=returns,
)
def test_forward_review_cusum_gate(tmp_path) -> None:
settings = Settings(operational_dsn=f"sqlite:///{tmp_path / 's.db'}", analytical_path=str(tmp_path / "a.duckdb"))
store = SqlStrategyStore(settings.operational_dsn)
svc = ForwardValidationService(FakeData(), DuckDbAnalyticalStore(settings.analytical_path), store, settings,
min_forward_days=15)
decayed = _fwd("DEC", [0.01] * 10 + [-0.05] * 20) # clear structural down-shift -> CUSUM negative break
good = _fwd("GOOD", [0.01] * 30) # positive, no negative break
young = _fwd("YOUNG", [0.002] * 5) # < warmup and < min_days
for rec in (decayed, good, young):
store.upsert(rec)
svc._review() # noqa: SLF001
assert store.get("DEC").status is StrategyStatus.RETIRED # edge decayed -> retired by CUSUM
assert store.get("GOOD").status is StrategyStatus.DEPLOYED # edge held -> deployed
assert store.get("YOUNG").status is StrategyStatus.FORWARD # still tracking