跳到主要内容

引擎验证:从数据到资金曲线

这页在做什么

M3 里程碑打通了全书的主干:真实样式的数据 → 回测引擎 → 绩效指标 → 可视化。 本页用一个最简单的「双均线交叉」策略,端到端跑通这条链路,并验证引擎的 两个关键约定——防前视偏差交易成本扣减

后续所有章节(经典策略、因子、组合优化……)都会复用同一个 quant 引擎, 所以先在这里把它跑通、看明白。

:::caution 合成数据 下方 spy_daily.csv合成数据(由随机过程生成,非真实行情,仅教学)。 这里出现的任何收益、夏普、回撤都不代表真实市场表现。详见数据集说明。 :::

全链路演示

下面这段代码做了四件事:读取数据 → 生成双均线信号并 ffill 成持续仓位 → 调用 quant.vector_backtest 回测 → 打印绩效指标并画资金曲线与回撤图。

import quant
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# 1. 读取合成数据(/data/ 为虚拟文件系统绝对路径)
df = pd.read_csv('/data/spy_daily.csv', parse_dates=['date']).set_index('date')
close = df['close']

# 2. 双均线交叉:20 日上穿 60 日 → 多头,否则空仓
short = close.rolling(20).mean()
long  = close.rolling(60).mean()
raw = (short > long).astype(float)               # 1=多头, 0=空仓
# 用 ffill 把「持仓」状态延续到下一次信号,得到持续仓位
signal = raw.replace(0, np.nan).ffill().fillna(0)

# 3. 回测:引擎内部已 shift(1) 防前视、按换手扣成本
res = quant.vector_backtest(close, signal, cost_bps=2.0, freq=252)
print(quant.performance_summary(res['equity'], res['returns'], freq=252))
print('总换手:', round(res['turnover'], 1))

# 4. 可视化:策略 vs 买入持有 + 回撤
bh = close / close.iloc[0]
dd = res['equity'] / res['equity'].cummax() - 1
fig, ax = plt.subplots(2, 1, figsize=(10, 6), sharex=True)
ax[0].plot(res['equity'], label='MA strategy', lw=1.4)
ax[0].plot(bh, label='Buy & Hold', lw=1.0, alpha=0.7)
ax[0].set_title('Equity Curve'); ax[0].legend()
ax[1].fill_between(dd.index, dd, 0, color='tab:red', alpha=0.4)
ax[1].set_title('Drawdown')
plt.tight_layout(); plt.show()

引擎自检:防前视偏差

向量化回测最容易踩的坑就是前视偏差(look-ahead bias)——用了「未来才能知道」 的信号去交易当日的收益。quant.vector_backtest 内部用 position = signals.shift(1) 来规避:信号在 tt 日收盘产生,t+1t+1 日才转为持仓。

下面用一个 4 天的迷你例子验证:全程多头、零成本时,策略权益末值应当精确等于 「价格 / 首日价格」——因为整段都在持有,只是首日不建仓(没有当日收益可吃)。 若引擎忘记 shift,结果就会偏离。

import quant
import pandas as pd

# 价格 100 → 101 → 102 → 101,全程多头信号,零成本
p = pd.Series([100.0, 101.0, 102.0, 101.0])
s = pd.Series([1.0, 1.0, 1.0, 1.0])

res = quant.vector_backtest(p, s, cost_bps=0.0)
expected = (p / p.iloc[0]).iloc[-1]          # 买入持有归一化末值
got = res['equity'].iloc[-1]
print('持仓序列:', list(res['position']))    # 期望 [0,1,1,1] —— 首日空仓=防前视
print(f'权益末值={got:.4f}  期望={expected:.4f}  =>',
    'OK' if abs(got - expected) < 1e-9 else 'FAIL')

动手改一改

回到第一个 playground,试试:

  • 把均线参数 20 / 60 改成更敏感的 5 / 20,观察换手和夏普如何变化(更频繁的交易通常被成本吃掉收益);
  • cost_bps=2.0 调到 20.0(昂贵市场),看资金曲线如何塌缩;
  • 把信号改成三态(多头 / 空头 / 空仓)——例如 signal = np.where(short>long, 1, -1),体验多空策略。

小结

  • quant.vector_backtest(price, signals, cost_bps):信号 → 持仓 → 收益 → 权益,内部已防前视并扣成本,调用方只管传「持续仓位」。
  • quant.performance_summary(equity, returns):一行拿到年化收益/波动/夏普/索提诺/最大回撤/卡玛/胜率。
  • 后续章节会把各种策略的信号生成器接进这个引擎,指标计算因此全站一致、可比较。