Initial strategy32 research and live runtime
This commit is contained in:
139
scripts/run_backtest.py
Normal file
139
scripts/run_backtest.py
Normal file
@@ -0,0 +1,139 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user