mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 15:44:18 +08:00
策略库(/strategies)多策略组合卡片此前只有主回测:本轮把 v1.31.0 的组合分析链路延伸到多策略组合(N 策略 × 各自原标的): - walkforward:组合 WF 泛化为槽位模型(_ComboSlot/_ComboWalkForwardBase), PortfolioWalkForwardEngine 行为不变;新增 MultiStrategyWalkForwardEngine (N 个策略各跑各自原标的,key 形如 label@symbol),复用切窗语义与 WalkForwardResult 结构(前端 WalkForwardPanel 直接渲染) - benchmark:新增 evaluate_multi 一条龙(MultiStrategyEngine 回测 + 多策略 组合 WF + 逐槽位三段体检多数口径聚合 + 综合评分 + 组合评级 + 各槽位标的 等权买入持有基准对比),报告结构与单标的 evaluate_strategy 同构 - Web:新增 POST /backtest/multi-strategy/wf/run/async 与 /backtest/multi-strategy/evaluate/run/async;多策略组合回测响应附带 grade/score(与单标的/多标的组合响应同构) - 前端 StrategiesView:组合结果区新增「WF 样本外验证 / 一条龙评估 / AI 解读」 按钮与同构面板(按需触发,复用最近一次组合回测的 items/cash); 绩效指标表补齐 v1.28 深度 6 项(SQN/最大连胜连亏/Ulcer/VaR/CVaR); aiPrompt 新增 multi 模式(策略明细语境 + 槽位表现段) - 测试:多策略 WF 引擎 3 例、evaluate_multi 3 例、新端点 Web 级 2 例、 aiPrompt multi 模式 1 例(pytest 1611 绿、node --test 5/5、E2E 9/9)
615 lines
23 KiB
Python
615 lines
23 KiB
Python
"""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
|
||
|
||
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.strategy import Strategy
|
||
from easy_tdx.backtest.types import to_json_native
|
||
|
||
__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 < 5:
|
||
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 压制上下文区间的信号;
|
||
窗口起点空仓(每窗独立开仓语义)。
|
||
"""
|
||
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: # noqa: BLE001 — 单窗失败不拖垮整组,跳过该窗
|
||
return None
|
||
perf = bt.performance
|
||
|
||
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={k: v for k, v in perf.items()},
|
||
)
|
||
|
||
@staticmethod
|
||
def _dates(sub: pd.DataFrame, lead: int) -> tuple[str, str]:
|
||
"""取窗口起止日期(跳过 lead 根上下文)。"""
|
||
col = "datetime" if "datetime" in sub.columns else "date"
|
||
vals = sub[col].iloc[lead:]
|
||
if len(vals) == 0:
|
||
return "", ""
|
||
return (
|
||
pd.Timestamp(vals.iloc[0]).strftime("%Y-%m-%d"),
|
||
pd.Timestamp(vals.iloc[-1]).strftime("%Y-%m-%d"),
|
||
)
|
||
|
||
@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]))
|
||
result.worst_drawdown = float(min(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 < 5:
|
||
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 列统一转 Timestamp(int 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: # noqa: BLE001 — 单槽位失败不拖垮整窗
|
||
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"])
|
||
)
|
||
from easy_tdx.backtest.performance import PerformanceAnalyzer
|
||
|
||
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,
|
||
},
|
||
)
|