向量化回测:信号、仓位、用位移避免前视偏差、交易成本与资金曲线
在 pandas 中为一条移动平均线规则构建完整的向量化回测,包括仓位位移、每笔交易成本、资金曲线以及与基准的比较。
你将学到什么
- 向量化回测的五列结构:信号、仓位、资产收益率、成本和策略收益率。
- 为什么对仓位列执行
shift(1)这一行代码,就是区分诚实测试与前视幻想的关键。 - 如何根据仓位变化收取按比例计算的交易成本,并构建以欧元计的资金曲线。
- 向量化测试能模拟什么、不能模拟什么,以及何时需要第 8 课的事件驱动引擎。
讲解
向量化回测用列运算一次性计算整段历史,而不是逐根 K 线循环。课程《系统化交易与回测》中的电子表格回测就是这种结构;pandas 只需几毫秒就能跑完几十年的数据。它适用于基于收盘价操作的“做多或空仓”或“始终持仓”类规则;K 线内部的止损和特定的成交价格则交给第 8 课处理。
第 1 步,信号。signal = (close > sma(close, 50)).astype(int) 在收盘价高于其 50 日均线时为 1,否则为 0。它描述的是你在第 t 根 K 线收盘时已知的信息。
第 2 步,仓位。你不能依据一个尚未看到的收盘价进行交易;最早能持有第 t 根 K 线信号的时间是第 t + 1 根 K 线,因此 position = signal.shift(1).fillna(0)。现在仓位列的第 t 行表示“在第 t 根 K 线期间持有的敞口”,它是在第 t − 1 根 K 线结束时决定的。省略这次位移,策略就会用第 t 根 K 线的收盘价赚取第 t 根 K 线的收益,这就是前视偏差:使用了决策时并不具备的信息。
第 3 步,资产收益率。ret = close.pct_change().fillna(0)(第 5 课)。由于第 t 根 K 线期间的仓位是在第 t − 1 根 K 线收盘时确定的,position * ret 就是第 t 根 K 线的策略毛收益率。隐含的假设是在第 t − 1 根 K 线的收盘价成交;第 8 课将处理在下一根 K 线开盘价成交的情形。
第 4 步,成本。佣金、点差和滑点被建模为每单位仓位变化的一个比例费率:turnover = position.diff().abs().fillna(0) 在从空仓转为做多或反向操作时为 1,在多空反转时为 2。cost = turnover * c,其中 c 是以小数表示的单边成本(0.1% 即 0.001)。成本计入仓位发生变化的那根 K 线的收益率列中。
第 5 步,策略收益率与资金。strat = position * ret - cost,以及 equity = capital * (1 + strat).cumprod()。买入并持有基准为 capital * (1 + ret).cumprod()。务必用相同的指标比较两者(第 9 课);仅仅用现金稀释买入并持有,会让回撤和收益同等降低,这并不是能力。
写明假设条件:敞口是按日再投资的资金比例,在收盘价成交,成本按比例计算,现金不计利息,股息仅通过复权数据体现,不计税费。这样,测试回答的只是一个很窄的问题:扣除成本后,这条择时规则相对于持有该资产是否带来了任何增益?
相信输出结果之前的检查。(1) 把成本设为零、把信号设为常数 1;资金必须与买入并持有完全相等。(2) 用 turnover.sum() / 2 统计往返交易次数,并检查是否合理。(3) 确保指标预热期的 NaN 行对应的仓位为零,而不是产生一笔虚假交易。
实例演算
假设示例:八个以欧元计的收盘价,100、101、103、102、104、103、101、100;收盘价严格高于其 3 日 SMA 时做多,否则空仓;单边成本 0.1%;资金 €10,000;不计利息、股息和税费。
| K 线 | 收盘价 | SMA3 | 信号 | 仓位 | 收益率 | 换手 | 策略 | 资金 € |
|---|---|---|---|---|---|---|---|---|
| 0 | 100 | – | 0 | 0 | 0 | 0 | 0 | 10,000.00 |
| 1 | 101 | – | 0 | 0 | +1.000% | 0 | 0 | 10,000.00 |
| 2 | 103 | 101.33 | 1 | 0 | +1.980% | 0 | 0 | 10,000.00 |
| 3 | 102 | 102.00 | 0 | 1 | −0.971% | 1 | −1.071% | 9,892.91 |
| 4 | 104 | 103.00 | 1 | 0 | +1.961% | 1 | −0.100% | 9,883.02 |
| 5 | 103 | 103.00 | 0 | 1 | −0.962% | 1 | −1.062% | 9,778.11 |
| 6 | 101 | 102.67 | 0 | 0 | −1.942% | 1 | −0.100% | 9,768.33 |
| 7 | 100 | 101.33 | 0 | 0 | −0.990% | 0 | 0 | 9,768.33 |
第 3 根 K 线:第 2 根 K 线的信号为 1(103 > 101.33),因此第 3 根 K 线期间的仓位为 1。资产从 103 跌到 102(−0.971%),换手 1 产生 0.1% 的成本,策略收益率为 −1.071%,资金为 10,000 × (1 − 0.01071) = €9,892.91。第 3 根 K 线的收盘价等于 SMA,因此严格规则给出 0,仓位在第 4 根 K 线期间平掉(成本 0.1%,无敞口)。最终资金 €9,768.33(−2.32%),而买入并持有恰好为 €10,000。四次单边交易的成本合计 €39.36。
再看前视版本,用 signal 代替经过位移的 position:策略在第 2 和第 4 根 K 线期间“持仓”,而这两个上涨日只有在收盘之后才能识别出来,最终资金为 €10,356.65(+3.57%)。少了一个 shift(1),−2.32% 就变成了 +3.57%。
常见错误
position = signal.shift(1);收益率就是普通的 pct_change()。练习活动
在纸上使用同样的八个收盘价和规则完成:(a) 在单边成本为 0.2% 的情况下重新计算最终资金;(b) 说明往返交易的次数;(c) 一位同事提议用收盘价填充 SMA 最前面的 NaN 值,以便“从第一天就能开始交易”;简要解释为什么这是个问题。
显示答案
(a) 只有四根有换手的 K 线发生变化。第 3 根:−1.171% → €9,882.91;第 4 根:−0.2% → €9,863.15;第 5 根:−1.162% → €9,748.58;第 6 根:−0.2% → €9,729.09。最终亏损 2.71%,而成本为 0.1% 时为 2.32%;成本费率翻倍额外增加了 €39.24 的拖累。
(b) 换手合计为 4 次单边变化,因此是 2 次往返交易(第 3 根 K 线期间入场、第 4 根离场;第 5 根入场、第 6 根离场)。
(c) 用收盘价填充 SMA,会根据一个尚不存在的指标产生交易,因此这些交易的逻辑与被测试的规则不同;请在预热期保持零仓位,并注明排除了多少根 K 线。
局限性:八根 K 线只能证明运作机制,而不能证明规则有效;按比例计算的成本和按收盘价成交会低估流动性差的品种的滑点。
要点回顾
- 向量化回测由五列组成:信号、仓位(经过位移)、资产收益率、基于换手的成本、策略收益率;资金是累积乘积。
position = signal.shift(1)没有商量余地;可以通过去掉它并观察结果如何膨胀来进行检验。- 根据
position.diff().abs()收取成本,并始终在相同假设下与买入并持有进行比较。 - 向量化测试适用于收盘到收盘的规则;止损、仓位规模和真实成交需要第 8 课的事件驱动引擎。
下一步
第 8 课构建一个事件驱动回测:一个逐根 K 线运行的循环,包含 ATR 止损、仓位规模以及以 R 倍数衡量的交易清单。
自测
回答问题后核对答案。你可以随意重试多次。
供您自行复习的自动自测。您的答案在浏览器中核对,我们不会查看或评估。这不是考试,也不是资格证书。
您的进度
“已完成”仅表示您将本课标记为已学完,并不是对您理解程度的测试。 学习进度和笔记仅保存在此设备的此浏览器中,而非账户中。