第 8 章 · 02 向量化回测实战


文档摘要

第 8 章 · 02 向量化回测实战 本节摘要:本节演示 ML4T 工作流里最轻量的一步——向量化回测(vectorized backtest)。它不模拟订单、不撮合、不算手续费,只用 pandas 把「信号向量」和「收益向量」逐元素相乘,几行代码就能看出一个策略雏形是否值得继续投入。本节用第 7 章岭回归产生的日收益预测作为信号,讲清三件事:如何把连续预测转换成「做多 N 只 + 做空 N 只」的离散信号;如何用 算出组合每日收益并与 SP500 基准对比;以及向量化回测的天花板在哪里——为什么它只能当「快速筛子」而不能当「最终裁判」。读完本节,你能用十行 pandas 跑通一个多空策略的雏形评估。 内容来源:原项目 ,汉化并套用体系化模板。

第 8 章 · 02 向量化回测实战

本节摘要:本节演示 ML4T 工作流里最轻量的一步——向量化回测(vectorized backtest)。它不模拟订单、不撮合、不算手续费,只用 pandas 把「信号向量」和「收益向量」逐元素相乘,几行代码就能看出一个策略雏形是否值得继续投入。本节用第 7 章岭回归产生的日收益预测作为信号,讲清三件事:如何把连续预测转换成「做多 N 只 + 做空 N 只」的离散信号;如何用 signal * forward_return 算出组合每日收益并与 SP500 基准对比;以及向量化回测的天花板在哪里——为什么它只能当「快速筛子」而不能当「最终裁判」。读完本节,你能用十行 pandas 跑通一个多空策略的雏形评估。

内容来源:原项目 08_ml4t_workflow/02_vectorized_backtest.ipynb,汉化并套用体系化模板。

⚠️ 学习提示:向量化回测忽略滑点、手续费、冲击成本与订单未成交,结果一定偏乐观。它只回答「这个信号有没有方向性的预测力」,不回答「实盘能不能赚」。

学习目标

阅读完本节,你应当能够:

  1. 说清向量化回测事件驱动回测的本质差别。
  2. rank + 阈值把连续预测转成多空离散信号
  3. signal.mul(forward_return) 算出组合每日收益。
  4. 把策略累计收益与 SP500 基准画在一起做对比。
  5. 判断什么场景下该停用向量化、转向事件驱动。

一、向量化回测的本质:信号 × 收益

向量化回测的核心只有一行数学:

r_{\text{策略},t} = \sum_i w_{i,t} \cdot r_{i,t+1}

即「当天的持仓权重」乘以「次日的资产收益」。之所以叫「向量化」,是因为整段时间的权重矩阵 W 和收益矩阵 R 用 pandas 一次性相乘,没有 for 循环、没有订单对象、没有撮合引擎。

整个 notebook 严格按这条流水线展开。它的价值不在「精确」,而在「快」——你可以在几秒内扫几十个参数组合,把明显没戏的信号先淘汰掉。

💡 核心心法:向量化回测是「初筛」工具,像体检里的快速筛查,而不是确诊检查。它帮你省下「为烂策略写完整撮合逻辑」的时间,但不该用它做最终的上线决策。

二、数据与前瞻收益

notebook 先从 backtest.h5 读取第 7 章岭回归给出的预测,再用开盘价构造次日收益(forward returns):

data = pd.read_hdf('00_data/backtest.h5', 'data') daily_returns = data.open.unstack('ticker').sort_index().pct_change() fwd_returns = daily_returns.shift(-1) # 用「明天的开盘对今天的开盘」

关键细节:shift(-1) 是把「明天的收益」对齐到「今天」这一行。这样信号(今天生成)乘上 fwd_returns(对齐到今天)就表示「今天根据信号下单,明天收获的收益」。

⚠️ 时点陷阱:shift 方向搞反是向量化回测最常见的 bug,会把「未来收益」当「过去收益」乘进信号,造出完美的曲线——但这是前视偏差,实盘立刻原形毕露。务必画一张时序图,逐根 K 线确认「信号在第 t 天,收益在第 t+1 天」。

三、从连续预测到离散多空信号

预测是连续的(每个股票都有一个收益预测值),但下单是离散的(资金有限,只能选若干只)。notebook 用横截面 rank 把每日预测排序,选出最高与最低的各 N_LONG/N_SHORT 只:

N_LONG = N_SHORT = 15 long_signals = ((predictions .where(predictions > 0) .rank(axis=1, ascending=False) > N_LONG) .astype(int)) short_signals = ((predictions .where(predictions < 0) .rank(axis=1) > N_SHORT) .astype(int))

解读这段代码:

  • where(predictions > 0) 把非正预测置为 NaN,只在「预测为正」的股票里挑做多。
  • rank(axis=1, ascending=False) 按行降序排名,值越大排名越靠前(第 1 名 = 预测最高)。
  • > N_LONG 取排名在 15 名之外的(也就是前 15 名之外为 False,前 15 名为 True)——注意这里的方向,实际效果是「挑出预测最高的若干只」。
  • astype(int) 把布尔转成 0/1 信号矩阵。
元素 含义
predictions 每日每股票的收益预测值
long_signals / short_signals 0/1 矩阵,1 表示这天这只股票被选中
N_LONG = N_SHORT = 15 每天做多 15 只、做空 15 只

💡 为什么用 rank 不用绝对值:预测的绝对数值会随模型尺度漂移,「预测 0.005」算大算小没共识;但「今天排第 3 名」永远清晰。rank 把不同日期、不同股票的预测拉到同一尺度,是横截面选股的标准做法。

四、组合收益与基准对比

有了信号和前瞻收益,组合日收益就是逐元素相乘再等权平均:

long_returns = long_signals.mul(fwd_returns).mean(axis=1) short_returns = short_signals.mul(-fwd_returns).mean(axis=1) strategy = long_returns.add(short_returns).to_frame('Strategy')

注意三点:

  1. long_signals.mul(fwd_returns) 是 DataFrame 逐元素乘,效果就是「选中为 1 的股票贡献其收益,未选中为 0 的贡献 0」。
  2. .mean(axis=1) 对当日选中的股票等权平均——因为没选中的是 0,分母仍是总股票数,所以这里隐含「等权多头/空头组合」假设。
  3. 做空那侧乘 -fwd_returns:做空收益 = 负的资产收益。

最后 notebook 把策略累计收益和 SP500 基准画在一起对比:

sp500 = web.DataReader('SP500', 'fred', '2014', '2018').pct_change() strategy.join(sp500).add(1).cumprod().sub(1).plot()

add(1).cumprod().sub(1) 是从日收益还原累计收益的标准写法:每日收益 +1 累乘再 -1。两条曲线一比较,就能直观看出策略相对市场的超额。

五、向量化回测的天花板

notebook 最后还看了标准差、相关性等,但向量化回测到这里就该停。它的假设清单写满了「理想世界」:

假设 真实情况 偏差方向
信号一出立即以开盘价成交 有滑点、有限价单未成交 高估收益
无手续费 每笔都有佣金 高估收益
等权且资金充足 资金有限、有最小手数 高估可执行性
任意标的都可做空 融券难、有利息 高估空头可行性
无市场冲击 大单会推高/压低价格 高估容量

⚠️ 何时必须切换:一旦雏形值得继续,就立刻转向事件驱动回测(下一节的 backtrader),让它把滑点、手续费、订单未成交、做空限制一个个加回去。向量化回测的「漂亮曲线」永远不能作为上线依据,只能作为「是否值得写更多代码」的判断。

本节要点回顾

  1. 本质:策略收益 = 持仓权重 × 资产收益,向量化用 pandas 一次性算完整段时间,极快但极理想化。
  2. 时点对齐:shift(-1) 把「次日收益」对齐到「今天」,方向搞反就是前视偏差——必须画图核对。
  3. rank 选股:用横截面 rank 把连续预测转成「多 N + 空 N」的离散信号,规避预测尺度漂移。
  4. 逐元素乘:signal.mul(forward_return).mean(axis=1) 隐含等权组合假设,做空侧乘负号。
  5. 基准对比:add(1).cumprod().sub(1) 还原累计收益,与 SP500 同图对比看超额。
  6. 天花板:无滑点、无手续费、无冲击、可任意做空——这些都让结果偏乐观,雏形过关即应转事件驱动。

下一节,我们看 backtrader 事件驱动回测——用 Cerebro 架构把订单、撮合、手续费逐根 K 线模拟,看一个策略在更接近实盘的环境里还活不活得下来。


发布者: 作者: 灏天文库 转发
评论区 (0)
U