4. 向量化思维基础
在传统回测框架中,我们习惯用 for 循环逐根 K 线推进:判断信号、计算仓位、更新资金曲线。这种写法直观,但在参数扫描场景下会成为性能瓶颈——当你要测试 10 个参数组合、每个组合跑 5000 根 K 线时,循环次数会迅速膨胀到数千万次。vectorbt 的核心价值,正是把这种"逐行推进"改造成"整段数组同时运算"。理解向量化思维,是真正用好 vectorbt 的前提。
一、核心原理:从"时间步"到"整条序列"
循环回测的本质是状态依赖:当前资金取决于上一根 K 线的资金,因此必须按时间顺序一步步算。向量化的突破口在于:很多回测逻辑其实可以拆成"信号生成"和"收益累加"两个阶段,前者只依赖价格序列,后者可以用累积乘积或累积求和一次性完成。
举个最简单的例子。循环写法如下:
equity = [10000]
for i in range(1, len(close)):
ret = close[i] / close[i-1] - 1
equity.append(equity[-1] * (1 + ret * position[i]))
向量化写法只需一行:
ret = close.pct_change().fillna(0)
equity = 10000 * (1 + ret * position).cumprod()
关键差异在于:循环把"上一期结果"作为输入,而向量化把整条 ret * position 序列先算出来,再用 cumprod() 一次性完成累积。这就是向量化思维的第一层——把递推关系转化为累积运算。
二、广播机制:让不同形状的数组自动对齐
广播(broadcasting)是 NumPy 和 pandas 的底层能力,也是 vectorbt 参数扫描的基石。它的规则是:当两个数组形状不同但满足"从右向左逐维兼容"时,较小数组会自动扩展以匹配较大数组。
假设你有 3 个参数组合的均线周期 [5, 10, 20],想对同一条价格序列分别计算均线。循环写法要跑三次,而广播写法是:
windows = np.array([5, 10, 20])[:, None] # 形状 (3, 1)
prices = close.values[None, :] # 形状 (1, N)
ma = pd.DataFrame(prices).rolling(...) # 实际中常用 rolling 逐列
更典型的广播场景是"信号矩阵 × 收益向量"。当你用 vbt.MA.run(close, window=[5,10,20]) 时,vectorbt 内部会生成形状为 (3, N) 的均线矩阵,再与形状为 (N,) 的收益序列做比较,广播机制自动把收益复制到每一行。理解这一点,你就能预判内存占用:参数组合数 × K 线数,就是矩阵规模。
组合数 × 数据长度 × 8 字节,避免内存溢出。三、布尔索引:用条件数组替代 if 判断
循环回测里最常见的 if signal == 1: buy(),在向量化中变成布尔索引。布尔索引的本质是:用一个同形状的 True/False 数组去"筛选"另一个数组的元素。
signal = ma_fast > ma_slow # 布尔数组
entries = signal & ~signal.shift(1) # 金叉:今天 True 昨天 False
exits = ~signal & signal.shift(1) # 死叉
这里 &、~、| 是逐元素逻辑运算,配合 shift() 就能表达"状态变化"这类时序逻辑,完全不需要循环。vectorbt 的 Portfolio.from_signals() 接收的正是这样的布尔数组(或入场/出场价格数组),内部再统一处理仓位与资金。
下表对比了循环思维与向量化思维的常见映射关系:
| 循环写法 | 向量化写法 | 说明 |
|---|---|---|
for i: if sig[i]: ... | entries = sig | 布尔索引直接筛选 |
equity[-1] * (1+ret) | (1+ret).cumprod() | 递推转累积 |
| 逐参数 for 循环 | 参数数组 + 广播 | 一次算完所有组合 |
if cross_up: | sig & ~sig.shift(1) | 用 shift 表达时序变化 |
| 手动累计最大回撤 | (equity/equity.cummax()-1).min() | 累积极值运算 |
四、案例:双均线参数扫描的向量化改造
假设我们要扫描 fast 周期 [5,10,15] 与 slow 周期 [20,30,40],共 9 个组合。循环思路是双重 for 嵌套,每个组合单独回测。向量化思路则分三步:
- 生成参数网格:用
np.meshgrid或列表推导得到 9 组 (fast, slow)。 - 批量计算指标:
vbt.MA.run(close, window=[5,10,15])得到形状(3, N)的矩阵。 - 批量回测:把 fast/slow 矩阵按组合配对,交给
Portfolio.from_signals一次性得到 9 条资金曲线。
fast_ma = vbt.MA.run(close, window=[5,10,15])
slow_ma = vbt.MA.run(close, window=[20,30,40])
entries = fast_ma.ma_crossed_above(slow_ma)
exits = fast_ma.ma_crossed_below(slow_ma)
pf = vbt.Portfolio.from_signals(close, entries, exits)
pf.total_return() # 返回 9 个组合的收益
注意 ma_crossed_above 会自动处理不同 window 组合的广播,最终 entries 的形状是 (3, 3, N)——前两维对应参数组合。这就是 vectorbt 把"参数扫描"变成"数组维度扩展"的典型手法。
五、误区与复盘:向量化不是万能药
from_signals 内置止损/止盈参数,或退回到 from_order_func 的逐行回调。常见误区还包括:
- 忽视 NaN 传播:均线前 N 根是 NaN,布尔比较后仍是 False,但若直接参与乘法会污染整条曲线,需
fillna(0)。 - 混淆 shift 方向:
shift(1)是把昨天移到今天,用于判断"状态变化";写反会导致未来函数。 - 内存爆炸:参数组合过多时,
(组合数, N)矩阵可能达到 GB 级,应分批扫描或使用vbt.Portfolio.from_signals的 chunk 机制。 - 过度追求一行代码:可读性同样重要,必要时拆成多个中间变量,便于调试和复盘。
复盘时建议养成一个习惯:每写一段循环逻辑,先问自己"这段逻辑依赖上一期的结果吗?"如果不依赖,就一定能向量化;如果依赖,就考虑用 cumprod、cummax、shift、where 等累积或条件函数改写。向量化思维的本质,是把"时间上的递推"翻译成"数组上的整体运算",这正是 vectorbt 高性能参数扫描的根基。