引擎解剖:向量化 vs 事件驱动
我预期约束会让回测变差,实测推翻了它——而这个失败比结论更有用
P4 章开始造东西。第一个问题就是选型:向量化,还是事件驱动?
向量化三行写完,快 371 倍。事件驱动要写事件、撮合、账户三层,慢得多。那为什么还要写?
标准答案是:"因为向量化表达不了 T+1、涨跌停、整手、资金约束——这些都会让回测比现实更好看。"
我带着这个预期做了实测,然后被推翻了。
8 只标的里,向量化只有 3 只是高估,平均反而低估 2.64pp。三条交易约束的影响,合计 -0.05pp。
那真正吃掉收益的是什么?成本,+30.94pp。
这一节讲清楚:两种引擎到底差在哪,以及那三条约束什么时候才会绑定。
汽车碰撞测试里有一堆安全装置:气囊、溃缩区、防抱死。
你在市区通勤测一年,它们一次都没触发。这不能证明它们没用——只能证明你的测试场景没覆盖到它们的作用域。
回测引擎的约束也是这样。问题不是"要不要实现",而是"你知不知道它有没有绑定"。
一、速度:向量化确实快 371 倍
单标的:向量化 1.7ms | 事件驱动 616.3ms | 慢 371 倍
这个差距是决定性的:参数网格搜索时,你一天能试几次,取决于它。
所以本课的分工是:P5 用向量化做粗筛,用事件驱动做终验。先用快的把候选缩小到十几个,再用慢的严格验证。
二、两种引擎差多少:结果与预期相反
8 只个股,同一条 5/20 均线策略:
| 标的 | 向量化累计% | 事件驱动累计% | 差(高估) |
|---|---|---|---|
| sh.600000 | 26.32 | 58.59 | -32.27 |
| sh.600009 | -15.11 | -14.70 | -0.41 |
| sh.600010 | 32.99 | 19.55 | +13.43 |
| sh.600011 | 21.06 | 46.37 | -25.32 |
| sh.600015 | 2.25 | -8.90 | +11.15 |
| sh.600016 | 33.39 | 18.31 | +15.09 |
| sh.600018 | 12.37 | 14.09 | -1.72 |
| sh.600019 | -33.51 | -32.45 | -1.07 |
平均 -2.64pp,8 只里只有 3 只是高估。
我预期的是"向量化必然高估",实测方向不一致。这个失败值得单独讲。
原因:两种引擎最大的差异不是约束,而是成交价假设。
- 向量化:默认按收盘价换仓(信号 shift 一位,乘以次日收益)
- 事件驱动:按次日开盘价成交
开盘价高于还是低于昨收,是随机的。所以这个差异双向抵消,而且量级可以很大(单只标的差 32pp)。
教训:不要把"实现更精细"直接等同于"结果更保守"。你必须逐条拆开,才知道哪一条真的在起作用。
🔧 动手做:推翻"约束一定让回测变差"的直觉(6 分钟)
跑 python vec_vs_event.py,盯【二】那张表最后一列「高估pp」。
你以为加了约束(资金/整手/T+1/涨跌停)的事件驱动,一定比向量化更悲观、更接近现实?
你会看到(实测):8 只标的里 5 只反而是事件驱动更高,方向根本不一致(从 -32pp 到 +15pp 都有)。
想明白:两个引擎最大的差异不是约束,是成交价假设(向量化用当日收盘、事件驱动用次日开盘),而这是双向的——有时帮你、有时坑你。真正单向吃你收益的是成本(下一节 p4.2 会量出来)。所以别默认"更严格的引擎 = 更保守的结果",先分清哪些差异是双向的、哪些是单向的。
三、逐条拆:哪条约束真的吃掉了收益
| 配置 | 平均累计% | 相对基准 | 平均成交 | 平均被拒 |
|---|---|---|---|---|
| 全部约束(基准) | 12.61% | — | 427.2 | 0.0 |
| 关掉涨跌停 | 12.61% | +0.00pp | 427.2 | 0.0 |
| 关掉 T+1 | 12.61% | +0.00pp | 427.2 | 0.0 |
| 关掉整手 | 12.56% | -0.05pp | 430.8 | 0.0 |
| 关掉三项约束 | 12.56% | -0.05pp | 430.8 | 0.0 |
| 再去掉成本 | 43.55% | +30.94pp | 442.8 | 0.0 |
结论很干脆:三条交易约束合计 -0.05pp,而成本一项就是 +30.94pp。
这与 p0.2 的死亡线完全一致:对大盘股 + 低频策略,成本是唯一真正吃掉收益的东西。
四、那三条约束什么时候才会绑定
被拒 0 笔——这不是引擎没实现约束,是这个设定下约束不绑定。我们逐条证明它确实实现了:
(a) 一字板:大盘蓝筹几乎没有
8 只标的共 31847 个交易日:一字涨停 8 天、一字跌停 4 天(合计占 0.038%)
万分之四。大盘蓝筹极少出现全天封死。但小盘股、ST 股、极端行情段完全是另一回事——2015 年千股跌停时,"想卖卖不掉"是压倒性的。
(b) T+1:日频引擎里结构上不可能绑定
我构造了一个信号天天翻转的策略来触发它:
开启 T+1:成交 3974 笔,被拒 0 笔
关闭 T+1:成交 3974 笔,被拒 0 笔
完全相同。
原因是架构性的:本引擎每天只交易一次,今天买、最早也是明天卖——而 T+1 恰恰允许次日卖。
T+1 只在日内多次交易时才绑定(早盘买入、尾盘想卖)。
所以在日频单次调仓的引擎里,
enforce_t1是一段正确但用不上的代码。它的价值出现在你把引擎扩展到分钟频、或一天内多次调仓的那一刻。知道这一点,比让它默默躺在那里更重要——否则你会以为自己"已经处理了 T+1"。
(c) 资金/整手:小资金下立刻绑定
把本金从 100 万降到 5000 元:
成交 1 笔,被拒 131 笔
被拒原因:资金不足一手,无法建仓 × 131
这 131 笔被拒,是我在写这一节时才补进引擎的。
原来的代码里有一条"目标差额不足一手就不动"的规则。它会静默吞掉一种真实情形:想建仓,但连一手都买不起。
不记录的话,回测会显示"策略选择空仓"——而真相是"你没钱建仓"。两者在净值曲线上完全一样,在结论上天差地别:前者说明策略在规避风险,后者说明你的本金不够跑这个策略。
引擎的价值,一半在于它算得对,另一半在于它把"没做成的事"也记下来。
五、选型结论
| 向量化 | 事件驱动 | |
|---|---|---|
| 速度 | 快 371 倍 | 慢 |
| 资金/整手约束 | 表达不了 | ✓ |
| 涨跌停/停牌 | 表达不了 | ✓ |
| 被拒委托记录 | 没有这个概念 | ✓ |
| 适用 | 参数粗筛 | 终验、极端场景、小资金 |
本课的做法:两个都要,分工明确。P5 的参数搜索用向量化(否则搜不完),最终候选用事件驱动跑一遍(否则不知道约束绑没绑)。
📌 免责:本课为技术教学,标的为沪深300 成分股按代码序取样(非精选),所有回测为历史模拟,不构成投资建议。
这一节的结论,有一半和我预期的相反:
- 速度:向量化快 371 倍 → 分工是"粗筛用向量化,终验用事件驱动"
- 两种引擎的差距方向不一致(8 只里 3 只高估,平均 -2.64pp)——因为最大差异是成交价假设(收盘价 vs 次日开盘价),那是双向的
- 三条交易约束合计 -0.05pp,成本一项 +30.94pp —— 与 p0.2 死亡线一致
- 约束的绑定条件:一字板占 0.038%;T+1 在日频单次调仓的引擎里结构上不可能绑定;资金/整手在小资金下立刻绑定(5000 元本金 → 131 笔被拒)
外加一条工程纪律:引擎不仅要算得对,还要把"没做成的事"记下来。那 131 笔被拒是我写这一节时才补进引擎的——在此之前,"没钱建仓"会被伪装成"策略选择空仓"。
下一节把参数摆到明处:capital、commission、slippage、benchmark。每一个默认值都是一个假设,而假设不写出来,回测就没法被别人复核。
🔎 来源与核验· 6 条,点开核对
