from __future__ import annotations import copy import itertools 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, build_strategy32_config from strategy32.data import build_strategy32_market_bundle WINDOWS = [(30, "1m"), (365, "1y"), (1095, "3y"), (1825, "5y")] FEATURES: list[tuple[str, str, bool]] = [ ("no_sideways", "enable_sideways_engine", False), ("strong_kill_switch", "enable_strong_kill_switch", True), ("daily_trend_filter", "enable_daily_trend_filter", True), ("expanded_hedge", "enable_expanded_hedge", True), ("max_holding_exit", "enable_max_holding_exit", True), ] def variant_name(enabled: list[str]) -> str: return "baseline_v5" if not enabled else "+".join(enabled) def balanced_score(results: dict[str, dict[str, float | int | str]]) -> float: score = 0.0 for label, weight in (("1y", 1.0), ("3y", 1.0), ("5y", 1.2)): annualized = float(results[label]["annualized_return"]) drawdown = abs(float(results[label]["max_drawdown"])) score += weight * (annualized / max(drawdown, 0.01)) score += 0.25 * float(results["1m"]["total_return"]) return score def build_variants() -> list[tuple[str, dict[str, bool], list[str]]]: variants: list[tuple[str, dict[str, bool], list[str]]] = [("baseline_v5", {}, [])] feature_names = [feature[0] for feature in FEATURES] for r in range(1, len(FEATURES) + 1): for combo in itertools.combinations(range(len(FEATURES)), r): overrides: dict[str, bool] = {} enabled: list[str] = [] for idx in combo: label, attr, value = FEATURES[idx] overrides[attr] = value enabled.append(label) variants.append((variant_name(enabled), overrides, enabled)) return variants def main() -> None: base = build_strategy32_config(PROFILE_V5_BASELINE) end = pd.Timestamp("2026-03-15 00:00:00", tz="UTC") start = end - pd.Timedelta(days=max(days for days, _ in WINDOWS) + base.warmup_days + 14) print("fetching bundle...") bundle, latest_completed_bar, accepted_symbols, rejected_symbols, quote_by_symbol = build_strategy32_market_bundle( symbols=base.symbols, auto_discover_symbols=base.auto_discover_symbols, quote_assets=base.quote_assets, excluded_base_assets=base.excluded_base_assets, min_quote_volume_24h=base.discovery_min_quote_volume_24h, start=start, end=end, timeframe=base.timeframe, max_staleness_days=base.max_symbol_staleness_days, ) print("latest", latest_completed_bar) results: dict[str, dict[str, dict[str, float | int | str]]] = {} summary_rows: list[dict[str, float | int | str | list[str]]] = [] for idx, (name, overrides, enabled) in enumerate(build_variants(), start=1): cfg = copy.deepcopy(base) for attr, value in overrides.items(): setattr(cfg, attr, value) variant_results = {} print(f"\n[{idx:02d}/32] {name}") for days, label in WINDOWS: eval_end = latest_completed_bar eval_start = eval_end - pd.Timedelta(days=days) raw_start = eval_start - pd.Timedelta(days=cfg.warmup_days) sliced = slice_bundle(bundle, raw_start, eval_end) backtester = Strategy32Backtester(cfg, sliced, trade_start=eval_start) backtester.engine_config.initial_capital = 1000.0 result = backtester.run() metrics = evaluate_window_result(result, eval_start=eval_start, bars_per_day=backtester.engine_config.bars_per_day) metrics["engine_pnl"] = result.engine_pnl metrics["total_trades"] = result.total_trades variant_results[label] = metrics print( label, "ret", round(float(metrics["total_return"]) * 100, 2), "mdd", round(float(metrics["max_drawdown"]) * 100, 2), "sharpe", round(float(metrics["sharpe"]), 2), "trades", metrics["trade_count"], ) score = balanced_score(variant_results) results[name] = variant_results summary_rows.append( { "name": name, "enabled": enabled, "balanced_score": score, "ret_1m": float(variant_results["1m"]["total_return"]), "ret_1y": float(variant_results["1y"]["total_return"]), "ret_3y": float(variant_results["3y"]["total_return"]), "ret_5y": float(variant_results["5y"]["total_return"]), "mdd_1y": float(variant_results["1y"]["max_drawdown"]), "mdd_3y": float(variant_results["3y"]["max_drawdown"]), "mdd_5y": float(variant_results["5y"]["max_drawdown"]), } ) summary_rows.sort(key=lambda row: float(row["balanced_score"]), reverse=True) payload = { "strategy": "strategy32", "analysis": "v6_exhaustive_combo", "initial_capital": 1000.0, "auto_discover_symbols": base.auto_discover_symbols, "latest_completed_bar": str(latest_completed_bar), "requested_symbols": [] if base.auto_discover_symbols else base.symbols, "accepted_symbols": accepted_symbols, "rejected_symbols": rejected_symbols, "quote_by_symbol": quote_by_symbol, "timeframe": base.timeframe, "results": results, "summary": summary_rows, } out = Path("/tmp/strategy32_v6_exhaustive_combos.json") out.write_text(json.dumps(payload, indent=2), encoding="utf-8") print("\nTop 10 by balanced score") for row in summary_rows[:10]: print( row["name"], "score", round(float(row["balanced_score"]), 3), "1y", round(float(row["ret_1y"]) * 100, 2), "3y", round(float(row["ret_3y"]) * 100, 2), "5y", round(float(row["ret_5y"]) * 100, 2), "mdd5y", round(float(row["mdd_5y"]) * 100, 2), ) print("\nwrote", out) if __name__ == "__main__": main()