from __future__ import annotations import argparse import json import sys from pathlib import Path import pandas as pd PACKAGE_PARENT = Path(__file__).resolve().parents[2] if str(PACKAGE_PARENT) not in sys.path: sys.path.insert(0, str(PACKAGE_PARENT)) from strategy29.backtest.window_analysis import evaluate_window_result, slice_bundle from strategy32.backtest.simulator import Strategy32Backtester from strategy32.config import PROFILE_V5_BASELINE, PROFILE_V7_DEFAULT, build_strategy32_config from strategy32.data import ( build_strategy32_market_bundle_from_specs, build_strategy32_price_frames_from_specs, resolve_strategy32_pair_specs, ) DEFAULT_WINDOWS = [365, 1095, 1825] def _slice_price_frames( prices: dict[str, pd.DataFrame], start: pd.Timestamp, end: pd.Timestamp, ) -> dict[str, pd.DataFrame]: sliced: dict[str, pd.DataFrame] = {} for symbol, df in prices.items(): frame = df.loc[(df["timestamp"] >= start) & (df["timestamp"] <= end)].copy() if not frame.empty: sliced[symbol] = frame.reset_index(drop=True) return sliced def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Run Strategy32 backtest on Binance data") parser.add_argument("--profile", default=PROFILE_V7_DEFAULT, choices=[PROFILE_V5_BASELINE, PROFILE_V7_DEFAULT]) parser.add_argument("--symbols", default="") parser.add_argument("--windows", default=",".join(str(days) for days in DEFAULT_WINDOWS)) parser.add_argument("--warmup-days", type=int, default=90) parser.add_argument("--timeframe", default="4h") parser.add_argument("--out", default="/tmp/strategy32_backtest_v0.json") return parser.parse_args() def main() -> None: args = parse_args() strategy_config = build_strategy32_config(args.profile) if args.symbols: strategy_config.symbols = [symbol.strip().upper() for symbol in args.symbols.split(",") if symbol.strip()] strategy_config.auto_discover_symbols = False strategy_config.timeframe = args.timeframe strategy_config.warmup_days = args.warmup_days windows = [int(token.strip()) for token in args.windows.split(",") if token.strip()] end = pd.Timestamp.utcnow() if end.tzinfo is None: end = end.tz_localize("UTC") else: end = end.tz_convert("UTC") start = end - pd.Timedelta(days=max(windows) + strategy_config.warmup_days + 14) specs = resolve_strategy32_pair_specs( symbols=strategy_config.symbols, auto_discover_symbols=strategy_config.auto_discover_symbols, quote_assets=strategy_config.quote_assets, excluded_base_assets=strategy_config.excluded_base_assets, min_quote_volume_24h=strategy_config.discovery_min_quote_volume_24h, ) bundle, latest_completed_bar, accepted_symbols, rejected_symbols, quote_by_symbol = build_strategy32_market_bundle_from_specs( specs=specs, start=start, end=end, timeframe=strategy_config.timeframe, max_staleness_days=strategy_config.max_symbol_staleness_days, ) accepted_specs = [spec for spec in specs if spec.base_symbol in set(accepted_symbols)] execution_prices, _, execution_accepted, execution_rejected, _ = build_strategy32_price_frames_from_specs( specs=accepted_specs, start=start, end=end, timeframe=strategy_config.execution_refinement_timeframe, max_staleness_days=strategy_config.max_symbol_staleness_days, ) results = {} for days in windows: label = "1y" if days == 365 else "3y" if days == 1095 else "5y" if days == 1825 else f"{days}d" eval_end = latest_completed_bar eval_start = eval_end - pd.Timedelta(days=days) raw_start = eval_start - pd.Timedelta(days=strategy_config.warmup_days) sliced = slice_bundle(bundle, raw_start, eval_end) execution_slice = _slice_price_frames( execution_prices, raw_start - pd.Timedelta(hours=24), eval_end, ) result = Strategy32Backtester( strategy_config, sliced, trade_start=eval_start, execution_prices=execution_slice, ).run() metrics = evaluate_window_result(result, eval_start=eval_start, bars_per_day=6) metrics["engine_pnl"] = result.engine_pnl metrics["total_trades"] = result.total_trades metrics["rejection_summary"] = result.metadata.get("rejection_summary", {}) results[label] = metrics payload = { "strategy": "strategy32", "profile": args.profile, "auto_discover_symbols": strategy_config.auto_discover_symbols, "latest_completed_bar": str(latest_completed_bar), "warmup_days": strategy_config.warmup_days, "requested_symbols": [] if strategy_config.auto_discover_symbols else strategy_config.symbols, "accepted_symbols": accepted_symbols, "rejected_symbols": rejected_symbols, "execution_refinement_timeframe": strategy_config.execution_refinement_timeframe, "execution_refinement_symbols": execution_accepted, "execution_refinement_rejected": execution_rejected, "quote_by_symbol": quote_by_symbol, "timeframe": strategy_config.timeframe, "results": results, } target = Path(args.out) target.parent.mkdir(parents=True, exist_ok=True) target.write_text(json.dumps(payload, indent=2), encoding="utf-8") print(json.dumps(payload, indent=2)) print(f"Wrote {target}") if __name__ == "__main__": main()