feat(agents): hand-crafted falsification (compile→backtest→DSR)

Pipeline AST -> compile_strategy -> BacktestEngine -> Sharpe/DSR/DD.
Caso zero-trade ritorna report tutto-zero. n_trials_dsr correzione
multiple-testing parametrizzata via init.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 20:05:01 +02:00
parent 654ab7b6d9
commit 15cb6b37aa
2 changed files with 135 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
"""Falsification agent: compila una :class:`Strategy`, la esegue nel backtest
engine e produce un :class:`FalsificationReport` con metriche di robustezza.
Pipeline:
AST -> compile_strategy -> signals -> BacktestEngine.run -> metriche
Il Deflated Sharpe Ratio (DSR) corregge per multiple-testing: un agente che
prova ``n_trials`` strategie deve battere un baseline atteso piu' alto del
semplice Sharpe nullo. ``n_trials_dsr`` rappresenta il numero di strategie
provate dal swarm; e' un parametro di configurazione, non viene desunto a
runtime.
Caso degenere: se la strategia non genera trades (es. condizione mai vera),
ritorniamo un report tutto-zero. Questo e' diverso dal caso in cui la
strategia opera ma perde: in tal caso le metriche riflettono la perdita.
"""
from __future__ import annotations
from dataclasses import dataclass
import pandas as pd # type: ignore[import-untyped]
from ..backtest.engine import BacktestEngine
from ..metrics.basic import max_drawdown, sharpe_ratio, total_return
from ..metrics.dsr import deflated_sharpe_ratio
from ..protocol.compiler import compile_strategy
from ..protocol.parser import Strategy
@dataclass(frozen=True)
class FalsificationReport:
"""Metriche prodotte dall'agente di falsificazione su una strategia."""
sharpe: float
dsr: float
dsr_pvalue: float
max_drawdown: float
total_return: float
n_trades: int
n_bars: int
class FalsificationAgent:
"""Agente hand-crafted che valuta una strategia tramite backtest + DSR."""
def __init__(self, fees_bp: float = 5.0, n_trials_dsr: int = 50) -> None:
self._engine = BacktestEngine(fees_bp=fees_bp)
self._n_trials_dsr = n_trials_dsr
def evaluate(self, strategy: Strategy, ohlcv: pd.DataFrame) -> FalsificationReport:
signal_fn = compile_strategy(strategy)
signals = signal_fn(ohlcv)
result = self._engine.run(ohlcv, signals)
if len(result.trades) == 0:
return FalsificationReport(
sharpe=0.0,
dsr=0.0,
dsr_pvalue=1.0,
max_drawdown=0.0,
total_return=0.0,
n_trades=0,
n_bars=len(ohlcv),
)
sr = sharpe_ratio(result.returns, periods_per_year=8760)
dsr, p = deflated_sharpe_ratio(
result.returns,
n_trials=self._n_trials_dsr,
periods_per_year=8760,
sharpe_var=1.0,
)
# +1.0 sull'equity curve evita divisione per zero in max_drawdown /
# total_return: l'engine produce equity in valore assoluto partendo da
# 0, ma le metriche sono definite su serie strettamente positive.
equity_pos = result.equity_curve + 1.0
return FalsificationReport(
sharpe=sr,
dsr=dsr,
dsr_pvalue=p,
max_drawdown=max_drawdown(equity_pos),
total_return=total_return(equity_pos),
n_trades=len(result.trades),
n_bars=len(ohlcv),
)