"""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.FORWARD # edge held -> stays FORWARD (factory_loop promotes via diversification gate) assert store.get("YOUNG").status is StrategyStatus.FORWARD # still tracking