feat(phase-2.6): Walk-Forward Validation + min-trades filter parametrico

Due fondamenta scientifiche per filtrare overfit e lucky-shot:

1) undertrading_threshold parametrico (era hardcoded 10):
   - AdversarialAgent.__init__(undertrading_threshold=10)
   - CLI flag --undertrading-threshold
   - Aggiunto a hard_kill_findings v2 default
     {"no_trades", "degenerate", "undertrading"}: ora un genome con 1 trade
     fortunato (es. genome 80be6bcc-1trade-fit-0.21 di fitness-v2-combo) viene
     killato anche sotto fitness v2 soft-kill.
   - Test parametric: undertrading_threshold=25 → 15 trade triggerano HIGH.

2) Walk-Forward Validation (WFA):
   - RunConfig.wfa_train_split (None=off, 0<x<1=on) + wfa_top_k=5
   - run_phase1: split ohlcv in train/test; GA usa solo train; a fine GA
     i top_k genomi (by fitness in-sample, fitness>0) vengono rivalutati
     sul test_ohlcv via falsification+adversarial+compute_fitness.
   - Schema migration: evaluations + fitness_oos, sharpe_oos, return_oos,
     max_dd_oos, n_trades_oos (ALTER TABLE con try/except per DB pre-2.6).
   - Repository.update_evaluation_oos helper per popolare colonne OOS.
   - CLI flags --wfa-train-split, --wfa-top-k.
   - Test integration: train_split=0.7 → fitness_oos popolato per top_k.

Motivazione: la fase 2.5 ha generato 17 run con fitness fino a 0.36 + DSR
positivo, ma OOS test su 7 anni mostra che flat-ablation top crolla -37%
mentre fitness-v2 top regge (+143%). WFA in-run permette ora di vedere
direttamente il degradation train→test senza eseguire backtest separati,
rendendo possibile filtrare overfit early durante l'ottimizzazione.

Tests (+2 → 193 totale):
- test_undertrading_threshold_parametric
- test_e2e_wfa_populates_fitness_oos

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 17:31:22 +02:00
parent 4c184bb5f7
commit 242724ba05
7 changed files with 198 additions and 7 deletions
+7 -2
View File
@@ -64,10 +64,12 @@ class AdversarialAgent:
fees_bp: float = 5.0,
fees_eat_alpha_threshold: float = 0.5,
flat_too_long_threshold: float = 0.95,
undertrading_threshold: int = 10,
) -> None:
self._engine = BacktestEngine(fees_bp=fees_bp)
self._fees_eat_alpha_threshold = fees_eat_alpha_threshold
self._flat_too_long_threshold = flat_too_long_threshold
self._undertrading_threshold = undertrading_threshold
def review(self, strategy: Strategy, ohlcv: pd.DataFrame) -> AdversarialReport:
signal_fn = compile_strategy(strategy)
@@ -118,12 +120,15 @@ class AdversarialAgent:
# Undertrading: < 10 trade -> HIGH (Phase 1.5: era < 5 MEDIUM).
# Sample size troppo piccolo per distinguere edge da rumore: e'
# un "lucky shot" non riproducibile out-of-sample.
if n_trades < 10:
if n_trades < self._undertrading_threshold:
report.findings.append(
Finding(
name="undertrading",
severity=Severity.HIGH,
detail=f"only {n_trades} trades — likely lucky shot (<10 over training)",
detail=(
f"only {n_trades} trades — likely lucky shot "
f"(<{self._undertrading_threshold} over training)"
),
)
)