Files
easy_tdx_max/src/easy_tdx/backtest/walkforward.py
T
Justin Gu e374a0da28 release: v1.32.6 — 两周改动深度审查全面修复(回测口径三件套/LLM 安全加固/涨停价舍入/时区统一/缓存与竞态等 58 处)
对 v1.21→v1.32.5 的 249 文件 4.2 万行改动做六路专项审查,本轮落地全部发现:

回测正确性:组合收益 fillna(0) 虚增、轮动停牌日过期价成交、单标的 WF 逐窗指标
被预热区稀释(三件套均带先红后绿回归);worst_drawdown 方向、grading 容错、
组合体检品种费率、寻优端点费率透传。

安全:LLM api_url 仅 http/https 且禁 userinfo(封死 file:// 读取与 Key 外送链)、
错误响应不回显原始 body、响应体 2MB 上限、配置原子写、坏配置字段级防御。

数据:涨跌停价整数分币舍入(67/318/90 个价位错 1 分漏判清零)、交易时段/采样/
provisional 统一沪时区、warehouse 增量缺口自动全量重拉、provisional 定点转正、
baostock 真故障抛错 + W/M 去 tradestatus(实测服务端报错,周月兜底此前从未工作)
+ 指数 vol 股→手(实测锚定)、ccpm 结构变更抛错。

Web API:缓存键补 count/vipdoc、NaN 清洗先于缓存、count>800 分页取全量、
submit 透传真实状态、pending 不再被淘汰成幽灵、watchlist/server 入参约束。

公式:FILTER 去副作用、0-1 值域误判收严、递归深度上限、REF 负移位显式禁止。

前端:4 处请求竞态序号守卫、Sparkline viewBox、北交所 market=2 映射、
空数据缓存死角、AI 弹窗卸载中止轮询、量能/资金日历口径修正。

CLI/CI:warehouse sync 失败 exit 1、参数校验干净报错、release 真实发布 SHA256、
CI 超时与缓存、spec 补 baostock 前提。

约 60 条回归测试先红后绿;pytest 1820 全过,ruff/mypy/vue-tsc/node --test 全绿。
2026-09-06 22:16:48 +08:00

659 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Walk-Forward 样本外验证引擎(v1.25 新增)。
回测全样本收益好 ≠ 策略好——参数可能只是拟合了某一段行情。Walk-Forward
把时间轴切成多个连续窗口,逐窗独立回测,检验策略在**不同时段**是否稳定
盈利(时间维度的样本外验证)。
切窗与执行语义(借鉴 backtest-system 踩坑后的严格定义):
1. **切窗**:前 ``warmup_ratio``(默认 30%)作为初始预热区不参与评估,
其余样本均分为 ``n_windows``(默认 7)个连续测试窗。
2. **每窗独立开仓**:每个窗口从**空仓**开始、窗口结束强制了结评估——
持仓不跨窗结转。若把窗口首尾直接拼起来,跨窗持仓会被「期初买入期末
卖出」重复计收益(backtest-system v1.2.1 修复的经典坑)。
3. **指标预热不污染**:窗口开始前带 ``context_bars``(默认 60)根上下文
K 线供指标计算,用引擎的 ``warmup_bars`` 压制该区间的信号生成——指标
有历史、信号只属于窗口内。
聚合口径:
- ``window_returns``:各窗收益率列表(时间升序);
- ``consistency``:盈利窗占比(0~1,WF 稳定性的核心指标);
- ``chained_return``:各窗收益连乘(每窗独立、窗口间现金复利的近似);
- ``worst_window`` / ``best_window``:最差/最好窗收益(尾部风险直觉)。
不做什么:本引擎**不做逐窗重寻参**(经典 anchored/rolling 优化式 WF),
只做「同参数跨时段稳定性」检验——参数寻优由
:class:`~easy_tdx.backtest.optimizer.ParamGridOptimizer` 负责,两者组合
(每窗内寻参、窗外评估)留待后续版本。
"""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from typing import Any
import numpy as np
import pandas as pd
from easy_tdx.backtest.engine import BacktestEngine
from easy_tdx.backtest.performance import PerformanceAnalyzer
from easy_tdx.backtest.strategy import Strategy
from easy_tdx.backtest.types import to_json_native
logger = logging.getLogger(__name__)
# 单个测试窗最少 bar 数(低于此的窗口跳过,不参与评估)
MIN_WINDOW_BARS = 20
__all__ = [
"WalkForwardWindow",
"WalkForwardResult",
"WalkForwardEngine",
"PortfolioWalkForwardEngine",
"MultiStrategyWalkForwardEngine",
]
@dataclass
class WalkForwardWindow:
"""单个测试窗的独立回测结果。"""
index: int # 窗序号(0 起,时间升序)
start: str # 窗口首根 K 线日期(YYYY-MM-DD
end: str # 窗口末根 K 线日期
bars: int # 窗口 K 线数
total_return: float
sharpe: float
max_drawdown: float
total_trades: int
win_rate: float
performance: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return dict(
to_json_native(
{
"index": self.index,
"start": self.start,
"end": self.end,
"bars": self.bars,
"total_return": self.total_return,
"sharpe": self.sharpe,
"max_drawdown": self.max_drawdown,
"total_trades": self.total_trades,
"win_rate": self.win_rate,
"performance": self.performance,
}
)
)
@dataclass
class WalkForwardResult:
"""Walk-Forward 验证汇总。"""
n_windows: int
warmup_ratio: float
windows: list[WalkForwardWindow] = field(default_factory=list)
# 聚合指标(windows 为空时为 0/NaN 安全值)
consistency: float = 0.0 # 盈利窗占比
chained_return: float = 0.0 # 各窗收益连乘 - 1
mean_window_return: float = 0.0
median_window_return: float = 0.0
worst_window: float = 0.0
best_window: float = 0.0
mean_sharpe: float = 0.0
worst_drawdown: float = 0.0
total_trades: int = 0
@property
def window_returns(self) -> list[float]:
"""各窗收益率(时间升序)。"""
return [w.total_return for w in self.windows]
def to_dict(self) -> dict[str, Any]:
return {
"n_windows": self.n_windows,
"warmup_ratio": self.warmup_ratio,
"windows": [w.to_dict() for w in self.windows],
"consistency": self.consistency,
"chained_return": self.chained_return,
"mean_window_return": self.mean_window_return,
"median_window_return": self.median_window_return,
"worst_window": self.worst_window,
"best_window": self.best_window,
"mean_sharpe": self.mean_sharpe,
"worst_drawdown": self.worst_drawdown,
"total_trades": self.total_trades,
}
class WalkForwardEngine:
"""Walk-Forward 样本外验证:切窗、逐窗独立回测、聚合稳定性指标。
Example:
>>> wf = WalkForwardEngine(strategy=MyStrategy, n_windows=7)
>>> result = wf.run(df)
>>> result.consistency # 盈利窗占比
0.71
"""
def __init__(
self,
strategy: type[Strategy] | Strategy,
n_windows: int = 7,
warmup_ratio: float = 0.3,
context_bars: int = 60,
cash: float = 100000.0,
commission: float = 0.0003,
min_commission: float = 5.0,
stamp_tax: float = 0.001,
slippage: float = 0.0,
execution: str = "next_open",
symbol: str | None = None,
auto_fees: bool = False,
) -> None:
"""Initialize.
Args:
strategy: 策略类或实例(各窗共用同一策略与参数)。
n_windows: 测试窗数量(默认 7)。
warmup_ratio: 初始预热区占比(默认 0.3,不参与评估)。
context_bars: 每窗前置上下文 K 线数(指标预热,默认 60)。
cash / commission / min_commission / stamp_tax / slippage /
execution: 透传给各窗的 :class:`BacktestEngine`。
symbol / auto_fees: 品种感知费率(同 ``BacktestEngine``)。
"""
self._strategy = strategy
self._n_windows = max(int(n_windows), 2)
self._warmup_ratio = min(max(float(warmup_ratio), 0.0), 0.8)
self._context_bars = max(int(context_bars), 0)
self._engine_kwargs: dict[str, Any] = {
"cash": cash,
"commission": commission,
"min_commission": min_commission,
"stamp_tax": stamp_tax,
"slippage": slippage,
"execution": execution,
"symbol": symbol,
"auto_fees": auto_fees,
}
def run(self, df: pd.DataFrame) -> WalkForwardResult:
"""执行 Walk-Forward 验证。
Args:
df: 完整 K 线(datetime/open/high/low/close,时间升序)。
Returns:
:class:`WalkForwardResult`。数据不足以切窗时返回空结果
``windows`` 为空,聚合指标为 0)。
"""
result = WalkForwardResult(n_windows=self._n_windows, warmup_ratio=self._warmup_ratio)
n = len(df)
# 最少数据:每窗 ≥ 20 根 + 预热区 ≥ 20 根
min_bars = 20 * (1 + self._n_windows)
if n < min_bars:
return result
eval_start = int(n * self._warmup_ratio)
eval_len = n - eval_start
window_len = eval_len // self._n_windows
for i in range(self._n_windows):
s = eval_start + i * window_len
e = s + window_len if i < self._n_windows - 1 else n # 末窗吃到尾部
if e - s < MIN_WINDOW_BARS:
continue
win = self._run_window(df, s, e, i)
if win is not None:
result.windows.append(win)
self._aggregate(result)
return result
def _run_window(self, df: pd.DataFrame, s: int, e: int, index: int) -> WalkForwardWindow | None:
"""独立回测单个窗口 [s, e)。
带前置上下文(指标预热),用 warmup_bars 压制上下文区间的信号;
窗口起点空仓(每窗独立开仓语义)。绩效只用窗内净值与成交计算,
上下文预热区不稀释 sharpe/年化/波动等时间口径指标(与组合级
``_ComboWalkForwardBase._run_window`` 的 ``ec.iloc[lead:]`` 同口径)。
"""
ctx_s = max(0, s - self._context_bars)
lead = s - ctx_s # 上下文 bar 数 = 需压制的信号数
sub = df.iloc[ctx_s:e].reset_index(drop=True)
if len(sub) < lead + 5:
return None
engine = BacktestEngine(
strategy=self._strategy,
warmup_bars=lead,
**self._engine_kwargs,
)
try:
bt = engine.run(sub)
except Exception as exc: # noqa: BLE001 — 单窗失败不拖垮整组,跳过该窗
logger.warning("WF 第 %s 窗回测失败,跳过该窗:%s: %s", index, type(exc).__name__, exc)
return None
perf = self._window_performance(bt, lead)
dt = self._dates(sub, lead)
return WalkForwardWindow(
index=index,
start=dt[0],
end=dt[1],
bars=int(e - s),
total_return=float(perf.get("total_return", 0.0)),
sharpe=float(perf.get("sharpe", 0.0)),
max_drawdown=float(perf.get("max_drawdown", 0.0)),
total_trades=int(perf.get("total_trades", 0)),
win_rate=float(perf.get("win_rate", 0.0)),
performance=perf,
)
@staticmethod
def _window_performance(bt: Any, lead: int) -> dict[str, Any]:
"""只用窗内净值 + 成交重算绩效(上下文预热区不参与窗指标)。
上下文区净值恒为初始现金(warmup 压制信号),窗口内回撤/收益不变,
但 sharpe/年化/波动等按全序列(含上下文)计算会被零收益段稀释。
"""
equity = bt.equity_curve
if len(equity) <= lead:
return dict(bt.performance)
window_equity = equity.iloc[lead:].reset_index(drop=True)
return dict(PerformanceAnalyzer(equity_curve=window_equity, trades=bt.trades).compute())
@staticmethod
def _dates(sub: pd.DataFrame, lead: int) -> tuple[str, str]:
"""取窗口起止日期(跳过 lead 根上下文)。
int/np 整数(YYYYMMDDTDX 日线原样)先 str 再按 %Y%m%d 解析——
直接 ``pd.Timestamp(int)`` 会被当纳秒换算成 1970 年。
"""
col = "datetime" if "datetime" in sub.columns else "date"
vals = sub[col].iloc[lead:]
if len(vals) == 0:
return "", ""
return (
WalkForwardEngine._fmt_date(vals.iloc[0]),
WalkForwardEngine._fmt_date(vals.iloc[-1]),
)
@staticmethod
def _fmt_date(v: Any) -> str:
"""单个日期值 → YYYY-MM-DDint/float YYYYMMDD 与 Timestamp/datetime64 兼容)。"""
ts: str
if isinstance(v, int | float | np.integer | np.floating) and not isinstance(v, bool):
ts = str(pd.to_datetime(str(int(v)), format="%Y%m%d").strftime("%Y-%m-%d"))
else:
ts = str(pd.Timestamp(v).strftime("%Y-%m-%d"))
return ts
@staticmethod
def _aggregate(result: WalkForwardResult) -> None:
"""聚合各窗指标(空列表安全)。"""
ws = result.windows
if not ws:
return
rets = np.array([w.total_return for w in ws], dtype=float)
result.consistency = float(np.mean(rets > 0))
result.chained_return = float(np.prod(1.0 + rets) - 1.0)
result.mean_window_return = float(np.mean(rets))
result.median_window_return = float(np.median(rets))
result.worst_window = float(np.min(rets))
result.best_window = float(np.max(rets))
result.mean_sharpe = float(np.mean([w.sharpe for w in ws]))
# max_drawdown 为正数幅度((peak-total)/peak),“最差窗回撤”应取最大值
result.worst_drawdown = float(max(w.max_drawdown for w in ws))
result.total_trades = int(sum(w.total_trades for w in ws))
class _ComboSlot:
"""组合 WF 的一个回测槽位(内部结构,由各公开引擎组装)。
Attributes:
key: 槽位标识(组合成交表的 symbol 列值)。
strategy: 策略类或实例。
df: 该槽位的 K 线。
cash: 等权分配到的资金。
symbol: 品种感知费率标识(auto_fees 用;不感知则 None)。
auto_fees: 是否按品种解析费率。
"""
__slots__ = ("key", "strategy", "df", "cash", "symbol", "auto_fees")
def __init__(
self,
key: str,
strategy: type[Strategy] | Strategy,
df: pd.DataFrame,
cash: float,
symbol: str | None = None,
auto_fees: bool = False,
) -> None:
self.key = key
self.strategy = strategy
self.df = df
self.cash = cash
self.symbol = symbol
self.auto_fees = auto_fees
class _ComboWalkForwardBase:
"""组合级 WF 共用实现:按参考时间轴切窗,逐槽位独立回测后合成组合净值。
切窗语义与 :class:`WalkForwardEngine`(单标的)一致,差异仅在「回测单元」
从单只标的换成 N 个槽位(标的或策略×标的)。
"""
def __init__(
self,
slots: list[_ComboSlot],
n_windows: int,
warmup_ratio: float,
context_bars: int,
engine_kwargs: dict[str, Any],
) -> None:
self._slots = list(slots)
self._n_windows = max(int(n_windows), 2)
self._warmup_ratio = min(max(float(warmup_ratio), 0.0), 0.8)
self._context_bars = max(int(context_bars), 0)
self._engine_kwargs = dict(engine_kwargs)
def run(self) -> WalkForwardResult:
"""执行组合 Walk-Forward 验证。
Returns:
:class:`WalkForwardResult`。数据不足以切窗时返回空结果
``windows`` 为空,聚合指标为 0)。
"""
result = WalkForwardResult(n_windows=self._n_windows, warmup_ratio=self._warmup_ratio)
if not self._slots:
return result
# 参考时间轴:全部标的 datetime 的并集(升序)
timeline = self._reference_timeline()
n = len(timeline)
# 最少数据:每窗 ≥ 20 根 + 预热区 ≥ 20 根
min_bars = 20 * (1 + self._n_windows)
if n < min_bars:
return result
eval_start = int(n * self._warmup_ratio)
eval_len = n - eval_start
window_len = eval_len // self._n_windows
for i in range(self._n_windows):
s = eval_start + i * window_len
e = s + window_len if i < self._n_windows - 1 else n # 末窗吃到尾部
if e - s < MIN_WINDOW_BARS:
continue
win = self._run_window(timeline, s, e, i)
if win is not None:
result.windows.append(win)
WalkForwardEngine._aggregate(result)
return result
def _reference_timeline(self) -> pd.DatetimeIndex:
"""全部槽位 datetime 的并集(升序,Timestamp 化)。"""
all_dt: list[pd.Timestamp] = []
for slot in self._slots:
s = self._dt_series(slot.df)
if len(s) > 0:
all_dt.append(s)
if not all_dt:
return pd.DatetimeIndex([])
return pd.DatetimeIndex(sorted(pd.unique(pd.concat(all_dt))))
@staticmethod
def _dt_series(df: pd.DataFrame) -> pd.Series:
"""标的 K 线的 datetime 列统一转 Timestampint YYYYMMDD 兼容)。"""
col = "datetime" if "datetime" in df.columns else "date"
dt = df[col]
if dt.dtype.kind in "iu":
return pd.to_datetime(dt.astype(str), format="%Y%m%d")
if not pd.api.types.is_datetime64_any_dtype(dt):
return pd.to_datetime(dt)
return pd.Series(pd.to_datetime(dt), index=df.index)
def _run_window(
self, timeline: pd.DatetimeIndex, s: int, e: int, index: int
) -> WalkForwardWindow | None:
"""独立回测单个窗口 [s, e)(参考时间轴下标),合成组合窗内净值。"""
window_start = timeline[s]
window_end = timeline[e - 1]
ctx_start = timeline[max(0, s - self._context_bars)]
equity_series: list[pd.Series] = []
trade_frames: list[pd.DataFrame] = []
for slot in self._slots:
dt = self._dt_series(slot.df)
mask = (dt >= ctx_start) & (dt <= window_end)
sub = slot.df.loc[mask].reset_index(drop=True)
dt_sub = dt.loc[mask].reset_index(drop=True)
# 上下文 bar 数 = 窗口起点之前保留的 bar 数(warmup 压制其信号)
lead = int((dt_sub < window_start).sum())
if len(sub) < lead + 5:
continue # 该槽位数据不足(晚上市/停牌过多),本窗跳过
engine = BacktestEngine(
strategy=slot.strategy,
cash=slot.cash,
warmup_bars=lead,
symbol=slot.symbol,
auto_fees=slot.auto_fees,
**self._engine_kwargs,
)
try:
bt = engine.run(sub)
except Exception as exc: # noqa: BLE001 — 单槽位失败不拖垮整窗
logger.warning(
"WF 组合第 %s 窗槽位 %s 回测失败,跳过:%s: %s",
index,
slot.key,
type(exc).__name__,
exc,
)
continue
# 只取窗内净值点(上下文区恒为现金,不参与窗指标,避免稀释波动率)
ec = bt.equity_curve
if len(ec) > lead:
eq = ec.iloc[lead:]
equity_series.append(
pd.Series(eq["total"].to_numpy(), index=self._dt_series(eq), name=slot.key)
)
if len(bt.trades) > 0:
t = bt.trades.copy()
t["symbol"] = slot.key
trade_frames.append(t)
if not equity_series:
return None # 所有槽位都跑不了,跳过该窗
# 合成组合窗内净值:日期并集对齐,ffill 持有不动,上市晚于窗口起点的
# 标的其前导缺口用首值回填(首值即其初始资金——还没开仓,持有现金)
aligned = pd.concat(equity_series, axis=1).sort_index()
aligned = aligned.ffill().bfill()
total = aligned.sum(axis=1)
peak = total.cummax()
drawdown = peak - total
peak_safe = peak.where(peak != 0, 1.0)
window_equity = pd.DataFrame(
{
"datetime": total.index,
"total": total.to_numpy(),
"drawdown": drawdown.to_numpy(),
"drawdown_pct": (drawdown / peak_safe).to_numpy(),
}
)
all_trades = (
pd.concat(trade_frames, ignore_index=True)
if trade_frames
else pd.DataFrame(columns=["symbol", "direction", "pnl", "rejected"])
)
perf = PerformanceAnalyzer(equity_curve=window_equity, trades=all_trades).compute()
return WalkForwardWindow(
index=index,
start=window_start.strftime("%Y-%m-%d"),
end=window_end.strftime("%Y-%m-%d"),
bars=int(e - s),
total_return=float(perf.get("total_return", 0.0)),
sharpe=float(perf.get("sharpe", 0.0)),
max_drawdown=float(perf.get("max_drawdown", 0.0)),
total_trades=int(perf.get("total_trades", 0)),
win_rate=float(perf.get("win_rate", 0.0)),
performance={k: v for k, v in perf.items()},
)
class PortfolioWalkForwardEngine(_ComboWalkForwardBase):
"""组合级 Walk-Forward:一个策略 × 多只标的,逐窗独立回测并合成组合净值。
与 :class:`WalkForwardEngine`(单标的)共用切窗语义与
:class:`WalkForwardWindow` / :class:`WalkForwardResult` 结构——前端
WalkForwardPanel 无需改动即可渲染组合 WF:
1. **参考时间轴**:取全部标的 datetime 的并集(升序),按单标的同样的
规则切预热区 + ``n_windows`` 个连续测试窗;
2. **每窗独立开仓**:窗内每只标的带 ``context_bars`` 前置上下文
``warmup_bars`` 压制上下文信号),从空仓开始、窗口结束强制了结,
持仓不跨窗;
3. **组合净值合成**:各标的窗内净值按等权资金(``total_cash / N``
对齐求合成组合窗内净值,再喂 :class:`~easy_tdx.backtest.performance.PerformanceAnalyzer`
(汇总成交附 symbol 列)得到与单标的同口径的窗指标;
4. **容错**:某标的数据不足(如晚上市)则该窗跳过该标的;某窗所有
标的都跑不了则跳过该窗。
Example:
>>> wf = PortfolioWalkForwardEngine(strategy=MyStrategy, stocks=stocks, n_windows=7)
>>> result = wf.run()
>>> result.consistency # 组合盈利窗占比
0.71
"""
def __init__(
self,
strategy: type[Strategy] | Strategy,
stocks: list[Any],
n_windows: int = 7,
warmup_ratio: float = 0.3,
context_bars: int = 60,
total_cash: float = 1_000_000.0,
commission: float = 0.0003,
min_commission: float = 5.0,
stamp_tax: float = 0.001,
slippage: float = 0.0,
execution: str = "next_open",
chanlun_level: str | None = None,
auto_fees: bool = False,
) -> None:
"""Initialize.
Args:
strategy: 策略类或实例(各窗各标的共用同一策略与参数)。
stocks: :class:`~easy_tdx.backtest.portfolio_engine.StockData` 列表。
n_windows / warmup_ratio / context_bars: 切窗参数(同单标的 WF)。
total_cash: 组合总资金(各标的等权分 1/N)。
其余参数: 透传给各窗各标的的 :class:`BacktestEngine`。
"""
n = max(len(list(stocks)), 1)
slots = [
_ComboSlot(
key=f"{s.market}{s.code}",
strategy=strategy,
df=s.df,
cash=total_cash / n,
symbol=f"{s.market}{s.code}",
auto_fees=auto_fees,
)
for s in stocks
]
super().__init__(
slots,
n_windows=n_windows,
warmup_ratio=warmup_ratio,
context_bars=context_bars,
engine_kwargs={
"commission": commission,
"min_commission": min_commission,
"stamp_tax": stamp_tax,
"slippage": slippage,
"execution": execution,
"chanlun_level": chanlun_level,
},
)
class MultiStrategyWalkForwardEngine(_ComboWalkForwardBase):
"""多策略组合级 Walk-Forward:N 个策略各跑各自的原标的,逐窗独立回测。
与 :class:`PortfolioWalkForwardEngine` 共用切窗语义、组合窗内净值合成与
:class:`WalkForwardResult` 输出结构(前端 WalkForwardPanel 直接复用),
唯一差异是槽位划分:每个槽位是「一个策略 × 它自己的标的」
key 形如 ``"{label}@{symbol}"``,与
:class:`~easy_tdx.backtest.multi_strategy_engine.MultiStrategyEngine` 的
``individual_results`` key 一致)。
Example:
>>> wf = MultiStrategyWalkForwardEngine(strategies=slots, n_windows=5)
>>> result = wf.run()
>>> result.consistency # 组合盈利窗占比
0.6
"""
def __init__(
self,
strategies: list[Any],
n_windows: int = 7,
warmup_ratio: float = 0.3,
context_bars: int = 60,
total_cash: float = 1_000_000.0,
commission: float = 0.0003,
min_commission: float = 5.0,
stamp_tax: float = 0.001,
slippage: float = 0.0,
execution: str = "next_open",
) -> None:
"""Initialize.
Args:
strategies: :class:`~easy_tdx.backtest.multi_strategy_engine.StrategySlot`
列表(每个槽位已绑定策略实例与 K 线)。
n_windows / warmup_ratio / context_bars: 切窗参数(同单标的 WF)。
total_cash: 组合总资金(各槽位等权分 1/N)。
其余参数: 透传给各窗各槽位的 :class:`BacktestEngine`
(与 MultiStrategyEngine 同口径,不含 auto_fees/chanlun_level)。
"""
n = max(len(list(strategies)), 1)
slots = [
_ComboSlot(
key=f"{s.label}@{s.symbol}",
strategy=s.strategy,
df=s.df,
cash=total_cash / n,
)
for s in strategies
]
super().__init__(
slots,
n_windows=n_windows,
warmup_ratio=warmup_ratio,
context_bars=context_bars,
engine_kwargs={
"commission": commission,
"min_commission": min_commission,
"stamp_tax": stamp_tax,
"slippage": slippage,
"execution": execution,
},
)