第 5 章 · 01 向量化回测 本节摘要:本节讲策略评估的「快刀」——向量化回测。它是验证交易信号收益最快的方式:把整条收益率序列当成 NumPy/pandas 向量,用矩阵运算一次性算出持仓、收益和累计净值,几行代码就能评估一个多空组合。本节先讲清它的原理(信号→持仓→前向收益→组合收益),再写一个完整的均值回退/ML 信号的多空组合回测,并和 S&P500 基准对比。重点强调它「快但失真」的边界——不含手续费、滑点、容量、订单撮合,因此只能用于信号初筛,不能当作实盘依据。 内容来源:原项目 、 ,汉化并套用体系化模板。 ⚠️ 风险提示:向量化回测忽略了交易成本、滑点、资金占用和订单拒单,容易高估收益。任何「向量化夏普很高」的策略在加入成本后都可能变负,务必用事件驱动回测复核。
本节摘要:本节讲策略评估的「快刀」——向量化回测。它是验证交易信号收益最快的方式:把整条收益率序列当成 NumPy/pandas 向量,用矩阵运算一次性算出持仓、收益和累计净值,几行代码就能评估一个多空组合。本节先讲清它的原理(信号→持仓→前向收益→组合收益),再写一个完整的均值回退/ML 信号的多空组合回测,并和 S&P500 基准对比。重点强调它「快但失真」的边界——不含手续费、滑点、容量、订单撮合,因此只能用于信号初筛,不能当作实盘依据。
内容来源:原项目
ch08/02_vectorized_backtest.ipynb、ch05/01_backtest_with_trades.ipynb,汉化并套用体系化模板。
⚠️ 风险提示:向量化回测忽略了交易成本、滑点、资金占用和订单拒单,容易高估收益。任何「向量化夏普很高」的策略在加入成本后都可能变负,务必用事件驱动回测复核。
阅读完本节,你应当能够:
.shift(-1) 构造前向收益避免未来函数。在第 4 章我们造了一堆 alpha 因子,在第 7 章会用 ML 模型预测收益。预测值出来了,第一个该问的问题是:「这个预测到底赚不赚钱?」这时如果直接上事件驱动回测引擎(像 backtrader、zipline 那样逐根 K 线撮合),配置繁琐、跑得慢,改一次参数要等半天。
向量化回测就是为了快速回答这个问题:把收益率当成一个矩阵,持仓当成另一个矩阵,两者点乘就是策略收益,一次算完所有时间点。它牺牲了真实性(没有撮合、没有成本),换来了毫秒级的迭代速度,非常适合在因子/模型筛选阶段做初判。
| 维度 | 向量化回测 | 事件驱动回测 |
|---|---|---|
| 执行方式 | 矩阵/向量一次性算完 | 逐根 K 线模拟撮合 |
| 速度 | 极快(毫秒级) | 慢(秒到分钟级) |
| 撮合细节 | 无(假设信号即成交) | 有(限价/市价/滑点) |
| 交易成本 | 难以精确建模 | 可逐笔建模 |
| 资金/仓位 | 简化为等权或目标权重 | 完整账户、保证金、拒单 |
| 适用阶段 | 信号初筛、因子排序 | 接近实盘的最终验证 |
💡 核心心法:把向量化回测当成「信号质量的快速体检」,而不是「实盘成绩单」。它回答的是「这个因子有没有 Alpha」,而非「这套交易成本下能不能赚钱」。
向量化回测的输入很轻:predictions(模型/因子在每个日期每个资产的预测值)和 sp500(基准收益)。
import numpy as np import pandas as pd import pandas_datareader.data as web # 模型预测:MultiIndex(date, ticker) -> predicted data = pd.read_hdf('00_data/backtest.h5', 'data') sp500 = web.DataReader('SP500', 'fred', '2014', '2018').pct_change()
⚠️ 数据形状:predictions 必须是
(date, ticker)的长表或经过unstack('ticker')的宽表,才能用rank(axis=1)做横截面排序。
这是向量化回测最关键、最容易出错的一步。我们要测的是「今天根据信号买卖,明天收益如何」,所以要用前向收益 fwd_returns = daily_returns.shift(-1)。
daily_returns = data.open.unstack('ticker').sort_index().pct_change() fwd_returns = daily_returns.shift(-1) # 关键:用明天的开盘对今天开盘的涨幅
为什么是 .shift(-1)?因为 daily_returns[t] 是「t-1 到 t」的涨幅,而我们在 t 日收盘才看到信号,实际能吃到的是「t 到 t+1」的涨幅,即 daily_returns.shift(-1)[t]。如果忘了 shift,等于用未来收益算今天信号,这是经典的未来函数错误,会把亏钱策略算成赚钱。
有了预测值,用 rank(axis=1) 在每个时间截面上对所有股票排序,取预测最高的 N 只做多、最低的 N 只做空:
predictions = data.predicted.unstack('ticker') N_LONG = N_SHORT = 15 # 预测值最大的前 N 只 -> 做多信号 1 long_signals = ((predictions .where(predictions > 0) .rank(axis=1, ascending=False) <= N_LONG) .astype(int)) # 预测值最小(最负)的前 N 只 -> 做空信号 1 short_signals = ((predictions .where(predictions < 0) .rank(axis=1) <= N_SHORT) .astype(int))
rank(axis=1, ascending=False) 表示「按行从大到小排名」,排名 ≤ 15 就是预测最高的 15 只。做空那行 ascending=True 是默认值,排名 ≤ 15 是预测最负的 15 只。where(predictions > 0) 先把非正预测滤掉,避免把接近零的也排进来。
最后一步,把信号和前向收益按元素相乘,再跨股票取均值(等权),多空相加就是策略每日收益:
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') # 累计净值 cum = strategy.join(sp500).add(1).cumprod().sub(1)
注意做空那行乘了 -fwd_returns:做空盈利等于收益的负数。mean(axis=1) 是等权平均(把组合每日收益归一),也可以换成按信号强度加权。
res = strategy.join(sp500).dropna() print(res.std()) # 日波动率,策略 vs 基准 print(res.corr()) # 与基准的相关性,越低越独立
fwd_returns = daily_returns.shift(-1),忘了 shift 就是未来函数。predictions.rank(axis=1) 给每个时间截面的股票排序,取 top/bottom N 形成多空信号。信号 × 前向收益 后 mean(axis=1) 等权聚合,多空相加得策略日收益。下一节,我们看事件驱动回测引擎 backtrader——它逐根 K 线模拟撮合、加成本加滑点,是向量化结果的「实盘校正器」。