From 4c184bb5f70da74639e629ade5b44c900cd25988 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Tue, 12 May 2026 16:56:47 +0200 Subject: [PATCH] =?UTF-8?q?feat(scripts):=20backtest=5Fstrategy.py=20?= =?UTF-8?q?=E2=80=94=20esegue=20una=20strategia=20standalone=20su=20range?= =?UTF-8?q?=20esteso?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Script utility per validare OOS strategie discovered durante run Phase 2.5. Carica un JSON strategia (formato Hypothesis output), fetcha OHLCV via Cerbero, esegue BacktestEngine + FalsificationReport + AdversarialReport, stampa metriche annualizzate (CAGR, Sharpe, max DD, Calmar). Esempio: uv run python scripts/backtest_strategy.py /tmp/strategy.json \ --start 2018-09-01 --end 2026-01-01 --label my-strategy Validato sui top 2 genomi Phase 2.5 (flat-ablation e fitness-v2-combo): flat-ablation top overfit su 7y (-37%), fitness-v2 top regge (+143% in 7y, CAGR +12.8%). Conferma che strategie con time gate temporal feature generalizzano meglio di strategie con SMA crossover hard-tied al regime del training period. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/backtest_strategy.py | 99 ++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 scripts/backtest_strategy.py diff --git a/scripts/backtest_strategy.py b/scripts/backtest_strategy.py new file mode 100644 index 0000000..0475749 --- /dev/null +++ b/scripts/backtest_strategy.py @@ -0,0 +1,99 @@ +"""Backtest standalone di una strategia su range esteso. + +Carica un JSON strategia (formato del Hypothesis Agent output), fetcha OHLCV +via Cerbero, esegue BacktestEngine + FalsificationReport + AdversarialReport, +stampa metriche annualizzate. + +Esempio: + uv run python scripts/backtest_strategy.py /tmp/strategy_e52604ba.json \ + --start 2019-01-01 --end 2026-01-01 --label flat-ablation-top +""" + +from __future__ import annotations + +import argparse +import json +import math +from datetime import datetime +from pathlib import Path + +from multi_swarm.agents.adversarial import AdversarialAgent +from multi_swarm.agents.falsification import FalsificationAgent +from multi_swarm.cerbero.client import CerberoClient +from multi_swarm.config import load_settings +from multi_swarm.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest +from multi_swarm.protocol.parser import parse_strategy +from multi_swarm.protocol.validator import validate_strategy + + +def main() -> None: + p = argparse.ArgumentParser() + p.add_argument("strategy_file", type=Path) + p.add_argument("--start", default="2019-01-01T00:00:00+00:00") + p.add_argument("--end", default="2026-01-01T00:00:00+00:00") + p.add_argument("--exchange", default="deribit") + p.add_argument("--symbol", default="BTC-PERPETUAL") + p.add_argument("--timeframe", default="1h") + p.add_argument("--fees-bp", type=float, default=5.0) + p.add_argument("--n-trials-dsr", type=int, default=50) + p.add_argument("--label", default="strategy") + args = p.parse_args() + + strategy_json = json.loads(args.strategy_file.read_text()) + raw = json.dumps(strategy_json) + parsed = parse_strategy(raw) + validate_strategy(parsed) + print(f"Strategy '{args.label}' parsed OK: {len(parsed.rules)} rules") + + settings = load_settings() + token = ( + settings.cerbero_mainnet_token.get_secret_value() + if settings.cerbero_mainnet_token + else settings.cerbero_testnet_token.get_secret_value() + ) + cerbero = CerberoClient( + base_url=settings.cerbero_base_url, + token=token, + bot_tag=settings.cerbero_bot_tag, + ) + loader = CerberoOHLCVLoader(client=cerbero, cache_dir=settings.series_dir) + req = OHLCVRequest( + symbol=args.symbol, + timeframe=args.timeframe, + start=datetime.fromisoformat(args.start), + end=datetime.fromisoformat(args.end), + exchange=args.exchange, + ) + ohlcv = loader.load(req) + n_bars = len(ohlcv) + years = n_bars / (24 * 365.25) + print( + f"OHLCV loaded: {n_bars} bars " + f"({ohlcv.index[0]} → {ohlcv.index[-1]}, ~{years:.2f} anni)" + ) + + fals_agent = FalsificationAgent(fees_bp=args.fees_bp, n_trials_dsr=args.n_trials_dsr) + adv_agent = AdversarialAgent(fees_bp=args.fees_bp) + fals = fals_agent.evaluate(parsed, ohlcv) + adv = adv_agent.review(parsed, ohlcv) + + cagr = (1.0 + float(fals.total_return)) ** (1.0 / years) - 1.0 if years > 0 else float("nan") + calmar = (cagr / float(fals.max_drawdown)) if fals.max_drawdown > 0 else float("inf") + + print(f"\n=== {args.label} on {args.symbol} {args.timeframe} ({years:.2f} anni) ===") + print(f"n_trades: {fals.n_trades}") + print(f"total_return: {fals.total_return:+.4f} ({fals.total_return * 100:+.2f}%)") + print(f"CAGR: {cagr:+.4f} ({cagr * 100:+.2f}%)") + print(f"Sharpe (ann): {fals.sharpe:+.3f}") + print(f"DSR: {fals.dsr:.4f} (pvalue {fals.dsr_pvalue:.4f})") + print(f"max_drawdown: {fals.max_drawdown:.4f} ({fals.max_drawdown * 100:.2f}%)") + print(f"Calmar: {calmar:+.3f}") + print(f"\nAdversarial findings:") + if not adv.findings: + print(" (none)") + for f in adv.findings: + print(f" [{f.severity.value:6s}] {f.name:30s} {f.detail}") + + +if __name__ == "__main__": + main()