From 15cb6b37aadf5f4f74959d00c325290e2b0ac7f9 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Sat, 9 May 2026 20:05:01 +0200 Subject: [PATCH] =?UTF-8?q?feat(agents):=20hand-crafted=20falsification=20?= =?UTF-8?q?(compile=E2=86=92backtest=E2=86=92DSR)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/multi_swarm/agents/falsification.py | 87 +++++++++++++++++++++++++ tests/unit/test_falsification.py | 48 ++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/multi_swarm/agents/falsification.py create mode 100644 tests/unit/test_falsification.py diff --git a/src/multi_swarm/agents/falsification.py b/src/multi_swarm/agents/falsification.py new file mode 100644 index 0000000..df50e68 --- /dev/null +++ b/src/multi_swarm/agents/falsification.py @@ -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), + ) diff --git a/tests/unit/test_falsification.py b/tests/unit/test_falsification.py new file mode 100644 index 0000000..c7fb256 --- /dev/null +++ b/tests/unit/test_falsification.py @@ -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