"""单一回测引擎(fast/strict 两档,同一代码路径、同一撮合逻辑)。 不做"快层 vectorbt + 真层自研"双引擎——用户必然信任更快的那层, 一旦快层省略 T+1/费用,两套结论背离即反复扯皮"以谁为准"。 这里用开关控制规则是否注入,引擎只有一份。 数据流: bars -> DataFrame -> strategy.compute(指标) -> 逐 bar 喂 strategy.on_bar(broker) -> broker 产生 fills -> 每根 bar 记录 equity -> metrics """ from __future__ import annotations from dataclasses import dataclass import pandas as pd from ..domain import Bar from .broker import PaperBroker from .metrics import compute_metrics @dataclass class BacktestConfig: initial_cash: float = 1000000.0 fast_mode: bool = False # True: 关 T+1/费用,交互试探 bars_per_year: int = 252 # 日线 252;分钟级另算 def run_backtest(bars: list[Bar], strategy, cfg: BacktestConfig | None = None) -> dict: cfg = cfg or BacktestConfig() if not bars: return _empty_result() df = pd.DataFrame( [{"ts": b.ts, "open": b.open, "high": b.high, "low": b.low, "close": b.close, "volume": b.volume} for b in bars] ).sort_values("ts").reset_index(drop=True) # 指标预计算(由策略持有,单一事实源) ind = strategy.compute(df["close"], df["high"], df["low"]) indicator_cols = list(ind.columns) df = pd.concat([df, ind], axis=1) broker = PaperBroker( initial_cash=cfg.initial_cash, enable_costs=not cfg.fast_mode, enable_t_plus_1=not cfg.fast_mode, ) equity_values = [] for i in range(len(df)): row = df.iloc[i] strategy.on_bar(i, row, broker) equity_values.append(broker.equity(row["close"])) broker.release_t_plus_1() equity = pd.Series(equity_values, index=df["ts"], name="equity") metrics = compute_metrics(equity, cfg.bars_per_year) metrics["num_trades"] = len(broker.fills) metrics["win_rate"] = _win_rate(broker.fills) return { "df": df, "indicator_cols": indicator_cols, "fills": broker.fills, "equity": equity, "metrics": metrics, "final_cash": broker.cash, "final_position": broker.position, } def _win_rate(fills) -> float: """FIFO 配对计算卖出胜率。""" from collections import deque buys: deque = deque() wins = 0 total_sells = 0 for f in fills: if f.side.value == "buy": buys.append((f.price, f.qty)) else: # sell remaining = f.qty total_sells += 1 profitable = True while remaining > 0 and buys: buy_price, buy_qty = buys[0] if f.price >= buy_price: pass else: profitable = False take = min(remaining, buy_qty) buy_qty -= take remaining -= take if buy_qty <= 0: buys.popleft() else: buys[0] = (buy_price, buy_qty) if profitable and f.qty > 0: wins += 1 return wins / total_sells if total_sells else 0.0 def _empty_result() -> dict: return { "df": pd.DataFrame(), "indicator_cols": [], "fills": [], "equity": pd.Series(dtype=float), "metrics": {"total_return": 0.0, "max_drawdown": 0.0, "sharpe": 0.0, "volatility": 0.0, "num_trades": 0, "win_rate": 0.0}, "final_cash": 0.0, "final_position": 0.0, }