Files
strategy32/scripts/run_backtest.py

140 lines
5.5 KiB
Python

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()