σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft Academy学习路线
Ctrl K
AO
+1.2k 位学习者
登录加入 ↗
学习路线
AO
+1.2k 位学习者
登录
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python 学习路线›模块 8 · 随机模型与机器学习›主题 8.10·~1小时高级

滚动前向验证

路线进度模块 8/11 · 主题 10/10
8.10 滚动前向验证
—实验得分— / 100总 XP—连续天数—

按你本会交易的方式检验

像普通机器学习那样把训练行和测试行随机划分,在时间序列上会出问题:训练集里有晚于测试日的日期,相邻日期又彼此相关,于是测试在一定程度上是训练的副本。滚动前向验证尊重时间:用过去的一段训练,在紧随其后的一段上测试,然后窗口前移并重复。每个预测都来自从未见过未来的模型,这恰好就是实盘交易的情形。

import numpy as np

n, train_size, test_size = 10, 4, 2
start = 0
while start + train_size + test_size <= n:
    train = np.arange(start, start + train_size)
    test = np.arange(start + train_size, start + train_size + test_size)
    print("train", train.tolist(), "test", test.tolist())
    start += test_size                                   # slide forward by one test block

滚动还是扩展,以及禁运期

有两种形式。滚动窗口保持训练长度固定,丢掉最老的行:能适应市场状态变化,成本恒定。扩展窗口总是从第 0 行开始,训练集越来越大:用到每个观测,但旧数据可能已过时。第三个要素是间隔(禁运期):如果某个特征或目标向前或向后看 h 天,那么紧挨测试期之前的行就与它共享信息,所以训练与测试之间要跳过几行。

def walk_forward_splits(n, train_size, test_size, gap=0):
    splits, start = [], 0
    while start + train_size + gap + test_size <= n:
        train = list(range(start, start + train_size))
        test = list(range(start + train_size + gap, start + train_size + gap + test_size))
        splits.append((train, test))
        start += test_size
    return splits

for train, test in walk_forward_splits(12, 5, 2, gap=1):
    print(train, "->", test)                         # the row between them is skipped

给每一折打分

对每一折,用训练行拟合、在测试行上打分,于是得到每折一个误差,而不是单一数字:它们的离散程度说明模型随时间的稳定性。一个有用的基准是平凡预测,这里是对每个测试日都用训练均值。逐折打印误差:平均表现好但在某个阶段糟透的模型,与始终尚可的模型并不相同。

import numpy as np

def walk_forward_splits(n, train_size, test_size):
    out, start = [], 0
    while start + train_size + test_size <= n:
        out.append((np.arange(start, start + train_size), np.arange(start + train_size, start + train_size + test_size)))
        start += test_size
    return out

rng = np.random.default_rng(2)
returns = rng.normal(0.0004, 0.01, size=300)
errors = []
for train, test in walk_forward_splits(len(returns), 100, 50):
    errors.append(np.mean((returns[test] - returns[train].mean()) ** 2))
print(np.round(np.array(errors) * 1e4, 3))                      # squared error in basis points squared, fold by fold

发现泄露

只有当每个训练行都严格早于每个测试行时,划分才是干净的。检查 train.max() < test.min() 只要一行,就能抓住最常见的错误:未排序或被打乱的索引、错位一位的切片、取自默认会打乱数据的库函数的划分。把这样的检查放进构建各折的代码里,让泄露直接中止运行,而不是产出好看的结果。

import numpy as np

def leaks_future(train_idx, test_idx):
    return bool(np.max(train_idx) >= np.min(test_idx))

train = np.arange(0, 100)
print(leaks_future(train, np.arange(100, 120)))                     # False: clean
shuffled = np.random.default_rng(1).permutation(120)                # what a shuffling splitter does
print(leaks_future(shuffled[:100], shuffled[100:]))                 # True: training rows after test rows
折数少不等于证据多重叠的测试块会重复使用同样的日期,而且每一折仍然来自同一个市场。一个在 8 折中有 7 折表现好的策略,可能只是因为同一个共同事件。除均值外,还要报告各折结果的离散程度,并把滚动前向的结果当作你最诚实的最佳估计,实盘中它还会缩水。绝不能先在各折上调参,再把同样的各折当作证据。
  • 滚动前向:用过去一段训练、在下一段测试、窗口前移;每个测试行都在其全部训练行之后。
  • 滚动窗口保持训练规模固定;扩展窗口从第 0 行开始;当特征跨越多日时,训练与测试之间留间隔(禁运期)。
  • 分别给每折打分并看离散程度;检查 train.max() < test.min();绝不在你要报告的折上调参。

下一节:限价订单簿,价格真正形成的数据结构。

实验:评分内容

  • 用 walk_forward_splits 让窗口沿时间滑动25 分
  • 用 expanding_splits 保留全部历史25 分
  • 用 forecast_mse 逐折检验预测25 分
  • 用 leaks_future 发现泄露25 分
资源管理器 · lab-111-1
▾ lab-111-1
…
bash · 浏览器内
learner@quantcraft:~/lab-111-1$
Python 尚未加载PythonLn 1, Col 1Spaces: 4UTF-8

要重置此主题吗?

lab-111-1 中的所有内容将恢复为初始文件,终端也会被清空。你当前的解答将丢失,且无法撤销。

将被重置
  • main.py → 初始代码
  • 终端历史与测试输出
  • 未提交的更改
将被保留
  • 实验最高得分
  • 你的 XP 与连续天数
  • 讲解进度与笔记

登录后即可运行你的代码。

所有讲解均可免费阅读。创建账户或登录后,即可运行代码、运行测试并提交此实验。

  • 浏览器内 IDE 与 bash 终端,无需安装
  • 即时评分、XP 以及你在学习路线中的位置
创建账户登录
或
使用 GitHub 继续