Fix 1 (CRITICAL): FleetOrchestrator.hunt now passes n_trials = store.total_trials() + local_eff to evaluate_by_regime so the deflated- Sharpe bar scales with the full cumulative search across all prior hunts, not just the current territory's candidate count. The immune system is no longer decorative. add_trials() was already called once after the loop, so no double-counting is introduced. Fix 2 (IMPORTANT): ForwardValidationService._review no longer auto- promotes a FORWARD sleeve to DEPLOYED when forward_mean() > 0. That path bypassed the factory_loop's diversification gate entirely and could route capital around governance. _review now only retires (CUSUM decay or negative forward mean after min_days); promotion to DEPLOYED is the factory_loop's sole responsibility. test_forward.py updated accordingly. Fix 3 (MINOR): hrp._ivp wraps both 1.0/diag(cov) and iv/iv.sum() inside np.errstate(divide="ignore", invalid="ignore") so a zero-variance asset no longer emits a RuntimeWarning before the uniform fallback catches it. New test: tests/integration/test_global_n_flows.py asserts the n_trials expression (store.total + local_eff) and that hunt() records n_trials_at_discovery >= 5000 after seeding 5000 prior trials. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
52 lines
2.4 KiB
Python
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.FORWARD # edge held -> stays FORWARD (factory_loop promotes via diversification gate)
|
|
assert store.get("YOUNG").status is StrategyStatus.FORWARD # still tracking
|