165 lines
6.4 KiB
Python
165 lines
6.4 KiB
Python
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()
|