σμ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 AcademySkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
Skills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python skill›Module 8 · Stochastic models & machine learning›Topic 8.10·~1 hrAdvanced

Walk-forward validation

Skill positionModule 8 of 11 · Topic 10 of 10
8.10 Walk-forward validation
—Lab score— / 100Total XP—Streak—

Test the way you would have traded

A random split into training and test rows, as in ordinary machine learning, breaks on time series: the training set contains days after test days, and neighbouring days are correlated, so the test is partly a copy of the training. Walk-forward validation respects time. Train on a block of the past, test on the block right after it, slide the window forward and repeat. Every prediction is made by a model that had never seen the future, which is exactly what live trading is.

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

Rolling or expanding, and the embargo

Two flavours. A rolling window keeps the training length fixed, dropping the oldest rows: it adapts to regime changes and uses constant cost. An expanding window always starts at row 0, so the training set grows: it uses every observation, but old data may be stale. A third ingredient is the gap (or embargo): if a feature or the target looks h days back or ahead, rows just before the test period share information with it, so skip a few rows between train and test.

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

Scoring each fold

For each fold fit on the training rows and score on the test rows, so you get one error per fold instead of one number: their spread tells how stable the model is through time. A useful baseline is the trivial forecast, here the training mean for every test day. Plot or print the errors fold by fold: a model that is good on average but terrible in one regime is not the same as a consistently decent one.

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

Catching a leak

A split is clean only if every training row comes strictly before every test row. Checking train.max() < test.min() takes one line and catches the most common bug: an unsorted or shuffled index, a slice off by one, a split taken from a library function that shuffles by default. Put such a check inside the code that builds the folds, so a leak stops the run instead of producing a flattering result.

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
Fewer folds do not mean more evidenceOverlapping test blocks reuse the same days, and each fold still comes from one market. A strategy that is good in 7 of 8 folds may owe it to one shared event. Report the spread of the fold results, not only the mean, and treat the walk-forward result as your best honest estimate, which still shrinks live. Never tune the model on the folds and then quote the same folds as proof.
  • Walk-forward: train on a block of the past, test on the next block, slide forward; every test row comes after all its training rows.
  • Rolling windows keep the training size fixed; expanding windows start at row 0; a gap (embargo) separates train and test when features look over several days.
  • Score each fold separately and look at the spread; check train.max() < test.min(); never tune on the folds you report.

Next up: the limit order book, the data structure where prices are actually made.

Lab · what's graded

  • Slide a window through time with walk_forward_splits25 pts
  • Keep all the history with expanding_splits25 pts
  • Test a forecast fold by fold with forecast_mse25 pts
  • Catch a leak with leaks_future25 pts
Explorer · lab-111-1
▾ lab-111-1
…
bash · in-browser
learner@quantcraft:~/lab-111-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-111-1 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes

Log in to run your code.

Every lecture is free to read. Create an account or log in to run code, run tests and submit this lab.

  • In-browser IDE with a bash terminal — nothing to install
  • Instant grading, XP and your place on the skill
Create an accountLog in
OR
Continue with GitHub