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:
@@ -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),
|
||||||
|
)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from multi_swarm.agents.falsification import FalsificationAgent, FalsificationReport
|
||||||
|
from multi_swarm.protocol.parser import parse_strategy
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def trending_ohlcv() -> pd.DataFrame:
|
||||||
|
idx = pd.date_range("2024-01-01", periods=500, freq="1h", tz="UTC")
|
||||||
|
close = 100 + np.cumsum(np.random.RandomState(0).normal(0.01, 1.0, 500))
|
||||||
|
return pd.DataFrame(
|
||||||
|
{
|
||||||
|
"open": close,
|
||||||
|
"high": close + 0.5,
|
||||||
|
"low": close - 0.5,
|
||||||
|
"close": close,
|
||||||
|
"volume": 1.0,
|
||||||
|
},
|
||||||
|
index=idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_falsification_returns_report(trending_ohlcv: pd.DataFrame) -> None:
|
||||||
|
src = (
|
||||||
|
"(strategy "
|
||||||
|
"(when (gt (indicator rsi 14) 70.0) (entry-short)) "
|
||||||
|
"(when (lt (indicator rsi 14) 30.0) (entry-long)))"
|
||||||
|
)
|
||||||
|
ast = parse_strategy(src)
|
||||||
|
agent = FalsificationAgent(fees_bp=5.0, n_trials_dsr=20)
|
||||||
|
report = agent.evaluate(ast, trending_ohlcv)
|
||||||
|
assert isinstance(report, FalsificationReport)
|
||||||
|
assert isinstance(report.sharpe, float)
|
||||||
|
assert isinstance(report.dsr, float)
|
||||||
|
assert 0.0 <= report.dsr <= 1.0
|
||||||
|
assert isinstance(report.max_drawdown, float)
|
||||||
|
assert isinstance(report.n_trades, int)
|
||||||
|
|
||||||
|
|
||||||
|
def test_falsification_zero_trades_returns_zero_metrics(trending_ohlcv: pd.DataFrame) -> None:
|
||||||
|
src = "(strategy (when (gt (feature close) 1e9) (entry-long)))"
|
||||||
|
ast = parse_strategy(src)
|
||||||
|
agent = FalsificationAgent(fees_bp=5.0, n_trials_dsr=20)
|
||||||
|
report = agent.evaluate(ast, trending_ohlcv)
|
||||||
|
assert report.n_trades == 0
|
||||||
|
assert report.sharpe == 0.0
|
||||||
Reference in New Issue
Block a user