Files
fxhnt/tests/integration/test_forward.py
jgrusewski 420d74c142 fix(factory): scale DSR bar by global N + remove gate-bypass promote + silence HRP warning
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>
2026-06-14 20:54:16 +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.FORWARD # edge held -> stays FORWARD (factory_loop promotes via diversification gate)
assert store.get("YOUNG").status is StrategyStatus.FORWARD # still tracking