Files
easy_tdx_max/src/easy_tdx/backtest/benchmark.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

584 lines
22 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.
"""一条龙策略评估(evaluate_strategyv1.25 新增)。
把「全样本回测 + Walk-Forward 样本外 + 适配性体检 + 综合评分 + S-D 评级 +
基准对比(买入持有)」打包成一次调用、一份报告(借鉴 backtest-system 的
``evaluate_engine()``「拉数 / 对齐 / 选模式 / 对比参考引擎」一条龙思路,
落在 easy-tdx 的三通道输出上)。
报告结构(全部 JSON 兼容,直接喂 REST / CLI / AI Agent::
{
"performance": {...19 项绩效...},
"score": {"total": 78.3, "components": {...}}, # 综合评分(含 WF)
"grade": {"grade": "B", "score": 71.2, ...}, # S-D 评级(不看收益)
"walkforward": {"consistency": 0.71, "windows": [...]},
"fitness": {"pass_ratio": 0.875, "high_fitness": true, "checks": [...]},
"benchmark": {
"buy_hold": {"total_return": 0.32, ...},
"excess_return": 0.18, # 策略 - 买入持有
"alpha": 0.09, "beta": 0.72, # v1.28CAPM 对比
"information_ratio": 0.85, "tracking_error": 0.12 # v1.28:主动管理指标
},
"config": {...}
}
基准对比的语义:**同区间、同费率、同初始资金**下,「首根 K 线全仓买入、
持有到末根」的买入持有收益。策略连买入持有都跑不赢时,报告的
``excess_return`` 为负——这是一票否决级别的研发信号。
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import numpy as np
import pandas as pd
from easy_tdx.backtest.engine import BacktestEngine
from easy_tdx.backtest.fitness import FitnessCheck, FitnessEngine, FitnessReport, FitnessSegment
from easy_tdx.backtest.grading import grade_performance, grade_portfolio_equity
from easy_tdx.backtest.scoring import score_strategy
from easy_tdx.backtest.strategy import Strategy
from easy_tdx.backtest.types import to_json_native
from easy_tdx.backtest.walkforward import (
MultiStrategyWalkForwardEngine,
PortfolioWalkForwardEngine,
WalkForwardEngine,
)
if TYPE_CHECKING:
import numpy.typing as npt
from easy_tdx.backtest.types import BacktestResult
NDArray = npt.NDArray[np.float64]
else:
NDArray = np.ndarray
__all__ = [
"evaluate_strategy",
"evaluate_portfolio",
"evaluate_multi",
"run_buy_hold_benchmark",
"compute_benchmark_comparison",
]
class _BuyAndHold(Strategy):
"""买入持有基准:首根 K 线全仓买入,持有到末根。"""
def init(self) -> None:
self._bought = False
def next(self) -> None:
if not self._bought:
self.buy()
self._bought = True
def _run_buy_hold_result(
df: pd.DataFrame,
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,
) -> BacktestResult:
"""买入持有基准完整回测(内部用,返回 BacktestResult 以取资金曲线)。"""
engine = BacktestEngine(
strategy=_BuyAndHold,
cash=cash,
commission=commission,
min_commission=min_commission,
stamp_tax=stamp_tax,
slippage=slippage,
execution=execution,
symbol=symbol,
auto_fees=auto_fees,
)
return engine.run(df)
def run_buy_hold_benchmark(
df: pd.DataFrame,
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,
) -> dict[str, Any]:
"""买入持有基准回测(与策略回测同区间、同费率、同资金)。"""
result = _run_buy_hold_result(
df, cash, commission, min_commission, stamp_tax, slippage, execution, symbol, auto_fees
)
keys = (
"total_return",
"annual_return",
"max_drawdown",
"sharpe",
"calmar",
"volatility",
)
return dict(to_json_native({k: result.performance.get(k, 0.0) for k in keys}))
def compute_benchmark_comparison(
strategy_curve: pd.DataFrame,
benchmark_curve: pd.DataFrame,
annual_days: int = 252,
) -> dict[str, float]:
"""策略 vs 基准的 CAPM / 主动管理对比指标(v1.28 新增)。
从两条资金曲线的日收益率序列计算:
- ``beta``: 协方差/基准方差,策略对基准的敏感度(1 = 与基准同涨跌)
- ``alpha``: 年化 CAPM α ≈ (策略日均收益 − β×基准日均收益) × 年化天数,
简化版(无风险利率并入截距),>0 说明剔除基准影响后仍有超额
- ``information_ratio``: 年化信息比率 = mean(策略−基准)/std(策略−基准)×√N,
每 1 单位跟踪误差换来多少超额收益
- ``tracking_error``: 年化跟踪误差 = std(策略−基准)×√N
两条曲线按 bar 对齐(截取较短长度);基准方差为 0(曲线恒定)时
beta/alpha 记 0IR 在差值恒正且无波动时沿用 999 上限约定。
Args:
strategy_curve: 策略资金曲线(含 total 列)
benchmark_curve: 基准资金曲线(含 total 列)
annual_days: 年化交易日数
Returns:
{alpha, beta, information_ratio, tracking_error}
"""
s_total = strategy_curve["total"].to_numpy(dtype=np.float64)
b_total = benchmark_curve["total"].to_numpy(dtype=np.float64)
n = min(len(s_total), len(b_total))
if n < 3:
return {"alpha": 0.0, "beta": 0.0, "information_ratio": 0.0, "tracking_error": 0.0}
def _daily_ret(total: NDArray) -> NDArray:
safe_prev = np.where(total[:-1] != 0, total[:-1], np.nan)
ret = np.diff(total) / safe_prev
return ret[np.isfinite(ret)]
s_ret = _daily_ret(s_total[:n])
b_ret = _daily_ret(b_total[:n])
m = min(len(s_ret), len(b_ret))
if m < 2:
return {"alpha": 0.0, "beta": 0.0, "information_ratio": 0.0, "tracking_error": 0.0}
s_ret, b_ret = s_ret[:m], b_ret[:m]
b_var = float(np.var(b_ret))
if b_var > 1e-18:
beta = float(np.cov(s_ret, b_ret)[0, 1] / b_var)
alpha = float((np.mean(s_ret) - beta * np.mean(b_ret)) * annual_days)
else:
beta = 0.0
alpha = float(np.mean(s_ret) * annual_days)
diff = s_ret - b_ret
diff_std = float(np.std(diff))
if diff_std > 1e-12:
information_ratio = float(np.mean(diff) / diff_std * np.sqrt(annual_days))
elif np.mean(diff) > 0:
information_ratio = 999.0
else:
information_ratio = 0.0
tracking_error = diff_std * np.sqrt(annual_days)
return {
"alpha": alpha,
"beta": beta,
"information_ratio": information_ratio,
"tracking_error": tracking_error,
}
def evaluate_strategy(
strategy: type[Strategy] | Strategy,
df: pd.DataFrame,
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,
n_windows: int = 7,
warmup_ratio: float = 0.3,
context_bars: int = 60,
split: tuple[float, float, float] = (0.6, 0.2, 0.2),
) -> dict[str, Any]:
"""一条龙策略评估:回测 + WF + 适配性 + 评分 + 评级 + 基准对比。
Args:
strategy: 策略类或实例。
df: K 线(datetime/open/high/low/close,时间升序)。
其余参数: 透传给各子引擎(回测 / WF / 适配性共用同口径费率与执行)。
n_windows / warmup_ratio: Walk-Forward 切窗参数。
split: 适配性三段占比。
Returns:
完整评估报告字典(结构见模块 docstring)。
"""
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,
}
# 1. 全样本回测
bt = BacktestEngine(strategy=strategy, **engine_kwargs).run(df)
perf = bt.performance
# 2. Walk-Forward 样本外
wf = WalkForwardEngine(
strategy=strategy,
n_windows=n_windows,
warmup_ratio=warmup_ratio,
context_bars=context_bars,
**engine_kwargs,
).run(df)
# 3. 适配性体检
fitness = FitnessEngine(
strategy=strategy,
split=split,
context_bars=context_bars,
**engine_kwargs,
).evaluate(df)
# 4. 综合评分(叠加 WF 一致性)+ S-D 评级
score = score_strategy(perf, wf=wf)
grade = grade_performance(perf)
# 5. 基准对比(买入持有,同区间同费率):超额收益 + Alpha/Beta/IR/TE
bh_result = _run_buy_hold_result(df, **engine_kwargs)
bh_keys = ("total_return", "annual_return", "max_drawdown", "sharpe", "calmar", "volatility")
bh = dict(to_json_native({k: bh_result.performance.get(k, 0.0) for k in bh_keys}))
comparison = compute_benchmark_comparison(bt.equity_curve, bh_result.equity_curve)
return {
"performance": to_json_native(dict(perf)),
"score": score.to_dict(),
"grade": grade.to_dict(),
"walkforward": wf.to_dict(),
"fitness": fitness.to_dict(),
"benchmark": {
"buy_hold": bh,
"excess_return": float(perf.get("total_return", 0.0))
- float(bh.get("total_return", 0.0)),
**comparison,
},
"config": {
"symbol": symbol,
"auto_fees": auto_fees,
"execution": execution,
"n_windows": n_windows,
"warmup_ratio": warmup_ratio,
"split": list(split),
},
}
def evaluate_portfolio(
strategy: type[Strategy] | Strategy,
stocks: list[Any],
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,
n_windows: int = 7,
warmup_ratio: float = 0.3,
context_bars: int = 60,
split: tuple[float, float, float] = (0.6, 0.2, 0.2),
) -> dict[str, Any]:
"""一条龙组合评估:组合回测 + 组合 WF + 适配性体检 + 综合评分 + 组合评级
+ 等权买入持有基准对比。
与 :func:`evaluate_strategy`(单标的)同构的报告结构,前端 EvaluatePanel
可直接复用;差异点:
- ``performance`` 来自组合引擎(完整 25 项指标,含 SQN/最大连胜连亏);
- ``walkforward`` 来自 :class:`~easy_tdx.backtest.walkforward.PortfolioWalkForwardEngine`
- ``fitness`` 为**跨标的聚合**:逐标的跑三段体检,检查项按「≥60% 标的
通过」的多数口径合成,段指标取截面均值——诚实反映组合整体适配性;
- ``grade`` 用组合净值口径 :func:`~easy_tdx.backtest.grading.grade_portfolio_equity`
- ``benchmark`` 为**等权买入持有组合**(每只标的分 1/N 资金首根买入持有
到末根,同费率同区间),α/β/信息比率/跟踪误差基于两条组合净值曲线。
Args:
strategy: 策略类或实例。
stocks: :class:`~easy_tdx.backtest.portfolio_engine.StockData` 列表。
其余参数: 透传给组合回测 / 组合 WF / 适配性(同口径费率与执行)。
Returns:
完整评估报告字典(结构同 evaluate_strategyconfig 记录标的清单)。
"""
from easy_tdx.backtest.portfolio_engine import PortfolioBacktestEngine
engine_kwargs: dict[str, Any] = {
"total_cash": total_cash,
"commission": commission,
"min_commission": min_commission,
"stamp_tax": stamp_tax,
"slippage": slippage,
"execution": execution,
"chanlun_level": chanlun_level,
"auto_fees": auto_fees,
}
# 1. 全样本组合回测(完整 25 项指标 + 合并净值曲线)
bt = PortfolioBacktestEngine(strategy=strategy, stocks=stocks, **engine_kwargs).run()
perf = bt.total_performance
# 2. 组合 Walk-Forward 样本外
wf = PortfolioWalkForwardEngine(
strategy=strategy,
stocks=stocks,
n_windows=n_windows,
warmup_ratio=warmup_ratio,
context_bars=context_bars,
**engine_kwargs,
).run()
# 3. 适配性体检:逐标的跑三段体检,跨标的多数口径聚合。
# 每标的传入各自 symbol,使 auto_fees 按品种解析费率——与组合回测主路径
# PortfolioBacktestEngine 逐标的 resolve_fee_model)同口径。此前漏传
# symbol:ETF/可转债组合的三段体检被按股票口径错收印花税。
fitness_kwargs: dict[str, Any] = {
k: v for k, v in engine_kwargs.items() if k not in ("total_cash", "chanlun_level")
}
per_stock_fitness = [
FitnessEngine(
strategy=strategy,
split=split,
context_bars=context_bars,
symbol=f"{stock.market}{stock.code}",
**fitness_kwargs,
).evaluate(stock.df)
for stock in stocks
]
fitness = _aggregate_fitness(per_stock_fitness, split)
# 4. 综合评分(叠加组合 WF 一致性)+ 组合评级(净值曲线口径)
score = score_strategy(dict(perf), wf=wf)
grade = grade_portfolio_equity(bt.combined_equity.to_dict(orient="records"))
# 5. 基准对比:等权买入持有组合(每只标的 1/N 首根买入持有到末根,同费率)
bh_bt = PortfolioBacktestEngine(strategy=_BuyAndHold, stocks=stocks, **engine_kwargs).run()
bh_keys = ("total_return", "annual_return", "max_drawdown", "sharpe", "calmar", "volatility")
bh = dict(to_json_native({k: bh_bt.total_performance.get(k, 0.0) for k in bh_keys}))
comparison = compute_benchmark_comparison(bt.combined_equity, bh_bt.combined_equity)
return {
"performance": to_json_native(dict(perf)),
"score": score.to_dict(),
"grade": grade.to_dict(),
"walkforward": wf.to_dict(),
"fitness": fitness.to_dict(),
"benchmark": {
"buy_hold": bh,
"excess_return": float(perf.get("total_return", 0.0))
- float(bh.get("total_return", 0.0)),
**comparison,
},
"config": {
"stocks": [f"{s.market}{s.code}" for s in stocks],
"total_cash": total_cash,
"auto_fees": auto_fees,
"execution": execution,
"n_windows": n_windows,
"warmup_ratio": warmup_ratio,
"split": list(split),
},
}
def _aggregate_fitness(
reports: list[FitnessReport],
split: tuple[float, float, float],
pass_ratio_threshold: float = 0.6,
) -> FitnessReport:
"""把逐标的的适配性体检报告聚合为组合级报告(多数口径)。
- 检查项:同名检查项跨标的计通过率,≥ ``pass_ratio_threshold``(默认
60%)标的通过则组合级该项通过,detail 记「x/y 只标的通过」;
- 段摘要:段起止取各标的的最早/最晚,收益/夏普/胜率取截面均值,
最大回撤取最深(max),交易数取合计——回答「组合整体在三段的形态」。
"""
aggregated = FitnessReport(split=split)
valid = [r for r in reports if r.checks]
if not valid:
return aggregated
# 检查项:按首份报告的检查顺序(FitnessEngine 的 8 项固定顺序)
n = len(valid)
for check in valid[0].checks:
passed_n = sum(1 for r in valid for c in r.checks if c.name == check.name and c.passed)
aggregated.checks.append(
FitnessCheck(
name=check.name,
passed=passed_n >= max(1, int(np.ceil(pass_ratio_threshold * n))),
detail=f"{passed_n}/{n} 只标的通过(组合多数口径)",
)
)
# 段摘要:train/valid/test 逐段截面聚合
for seg in valid[0].segments:
same = [s for s in (r.segment_by_name(seg.name) for r in valid) if s is not None]
if not same:
continue
aggregated.segments.append(
FitnessSegment(
name=seg.name,
start=min(s.start for s in same),
end=max(s.end for s in same),
bars=int(round(float(np.mean([s.bars for s in same])))),
total_return=float(np.mean([s.total_return for s in same])),
sharpe=float(np.mean([s.sharpe for s in same])),
max_drawdown=float(max(s.max_drawdown for s in same)),
total_trades=int(sum(s.total_trades for s in same)),
win_rate=float(np.mean([s.win_rate for s in same])),
)
)
aggregated.pass_ratio = (
sum(1 for c in aggregated.checks if c.passed) / len(aggregated.checks)
if aggregated.checks
else 0.0
)
aggregated.high_fitness = aggregated.pass_ratio >= 0.75
return aggregated
def evaluate_multi(
strategies: list[Any],
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",
n_windows: int = 7,
warmup_ratio: float = 0.3,
context_bars: int = 60,
split: tuple[float, float, float] = (0.6, 0.2, 0.2),
) -> dict[str, Any]:
"""多策略组合一条龙评估:组合回测 + 组合 WF + 跨槽位适配性体检 + 综合评分
+ 组合评级 + 等权买入持有基准对比。
与 :func:`evaluate_portfolio`(一个策略 × 多标的)同构,报告结构一致;
差异点仅在槽位划分——每个槽位是「一个策略 × 它自己的标的」
:class:`~easy_tdx.backtest.multi_strategy_engine.StrategySlot`):
- 全样本回测走 :class:`~easy_tdx.backtest.multi_strategy_engine.MultiStrategyEngine`
- Walk-Forward 走
:class:`~easy_tdx.backtest.walkforward.MultiStrategyWalkForwardEngine`
- 适配性体检逐槽位(各自策略 × 各自标的)跑三段后按多数口径聚合;
- 买入持有基准 = 各槽位标的的等权买入持有组合(策略换成 _BuyAndHold
其余不变),α/β/信息比率/跟踪误差基于两条组合净值曲线。
Args:
strategies: StrategySlot 列表(每个槽位已绑定策略实例与 K 线)。
其余参数: 透传给组合回测 / 组合 WF / 适配性(同口径费率与执行)。
Returns:
完整评估报告字典(结构同 evaluate_strategyconfig 记录槽位清单)。
"""
from easy_tdx.backtest.multi_strategy_engine import MultiStrategyEngine, StrategySlot
engine_kwargs: dict[str, Any] = {
"total_cash": total_cash,
"commission": commission,
"min_commission": min_commission,
"stamp_tax": stamp_tax,
"slippage": slippage,
"execution": execution,
}
# 1. 全样本组合回测(完整 25 项指标 + 合并净值曲线)
bt = MultiStrategyEngine(strategies=list(strategies), **engine_kwargs).run()
perf = bt.total_performance
# 2. 组合 Walk-Forward 样本外
wf = MultiStrategyWalkForwardEngine(
strategies=list(strategies),
n_windows=n_windows,
warmup_ratio=warmup_ratio,
context_bars=context_bars,
**engine_kwargs,
).run()
# 3. 适配性体检:逐槽位(各自策略 × 各自标的)跑三段体检,多数口径聚合
per_cash = total_cash / max(len(strategies), 1)
fitness_kwargs: dict[str, Any] = {
"commission": commission,
"min_commission": min_commission,
"stamp_tax": stamp_tax,
"slippage": slippage,
"execution": execution,
}
per_slot_fitness = [
FitnessEngine(
strategy=slot.strategy,
split=split,
context_bars=context_bars,
cash=per_cash,
**fitness_kwargs,
).evaluate(slot.df)
for slot in strategies
]
fitness = _aggregate_fitness(per_slot_fitness, split)
# 4. 综合评分(叠加组合 WF 一致性)+ 组合评级(净值曲线口径)
score = score_strategy(dict(perf), wf=wf)
grade = grade_portfolio_equity(bt.combined_equity.to_dict(orient="records"))
# 5. 基准对比:各槽位标的的等权买入持有组合(策略换成 _BuyAndHold,其余不变)
bh_slots = [
StrategySlot(label=s.label, symbol=s.symbol, strategy=_BuyAndHold(), df=s.df)
for s in strategies
]
bh_bt = MultiStrategyEngine(strategies=bh_slots, **engine_kwargs).run()
bh_keys = ("total_return", "annual_return", "max_drawdown", "sharpe", "calmar", "volatility")
bh = dict(to_json_native({k: bh_bt.total_performance.get(k, 0.0) for k in bh_keys}))
comparison = compute_benchmark_comparison(bt.combined_equity, bh_bt.combined_equity)
return {
"performance": to_json_native(dict(perf)),
"score": score.to_dict(),
"grade": grade.to_dict(),
"walkforward": wf.to_dict(),
"fitness": fitness.to_dict(),
"benchmark": {
"buy_hold": bh,
"excess_return": float(perf.get("total_return", 0.0))
- float(bh.get("total_return", 0.0)),
**comparison,
},
"config": {
"slots": [f"{s.label}@{s.symbol}" for s in strategies],
"total_cash": total_cash,
"execution": execution,
"n_windows": n_windows,
"warmup_ratio": warmup_ratio,
"split": list(split),
},
}