AlgoPro UniversityCourse home
Part 6 · Lesson

Walk-forward analysis

Rolling optimise-then-test windows that mimic how you would actually run a strategy over time — the gold standard for honest parameter selection.

A single train/test split has two weaknesses. It uses only one arbitrary cut point, and it tunes the parameters *once* and assumes they stay optimal forever. But markets drift — the right lookback for 2019 may be wrong for 2023. Walk-forward analysis fixes both by doing what you would actually do in real life: periodically re-tune on recent history, then trade the next stretch, then re-tune, and so on.

The rolling window idea

You slide a pair of windows across history. An in-sample (optimise) window where you pick the best parameters, immediately followed by an out-of-sample (test) window where you trade those parameters *without changing them*. Then you roll both windows forward and repeat. Crucially, you only ever keep the results from the out-of-sample windows — stitched together, they form one continuous equity curve made entirely of decisions taken with no knowledge of their own future.

rollrollrollOPT 1TEST 1OPT 2TEST 2OPT 3TEST 3OPT 4TEST 4
Walk-forward windows rolling through time (left → right). Each violet OPTIMISE window tunes the parameters; the blue TEST window immediately after trades them on unseen data. Only the blue segments count. Roll forward, re-optimise, repeat.

The out-of-sample slices — TEST 1, TEST 2, TEST 3, TEST 4 — are joined end to end into a single walk-forward equity curve. This is the closest thing to a time machine you get: it simulates having actually run and periodically re-tuned the strategy through history, and every trade in it was taken blind to what came next.

Anchored vs rolling

Walk-forward in code

pythonA rolling walk-forward loop
import numpy as np, pandas as pd

df = load_prices()                         class="c"># long, time-ordered price history
opt_len  = class="n">500                             class="c"># in-sample bars to optimise on
test_len = class="n">125                             class="c"># out-of-sample bars to then trade
param_grid = range(class="n">10, class="n">200)                class="c"># candidate lookbacks

def sharpe(data, lookback):
    ma     = data[class="s">'close'].rolling(lookback).mean()
    sig    = (data[class="s">'close'] > ma).astype(int).shift(class="n">1)   class="c"># lagged — no peeking
    strat  = sig * data[class="s">'close'].pct_change()
    return strat.mean() / (strat.std() + 1e-class="n">9) * np.sqrt(class="n">252), strat

oos_returns = []          class="c"># we ONLY keep out-of-sample returns
chosen      = []

start = class="n">0
while start + opt_len + test_len <= len(df):
    in_sample  = df.iloc[start : start + opt_len]
    out_sample = df.iloc[start + opt_len : start + opt_len + test_len]

    class="c"># class="n">1. optimise on the in-sample window
    best_lb = max(param_grid, key=lambda lb: sharpe(in_sample, lb)[class="n">0])
    chosen.append(best_lb)

    class="c"># class="n">2. trade that FROZEN parameter on the next, unseen window
    _, oos = sharpe(out_sample, best_lb)
    oos_returns.append(oos)

    start += test_len          class="c"># roll forward by one test window

class="c"># Stitch the out-of-sample pieces into one honest equity curve.
wf = pd.concat(oos_returns).fillna(class="n">0)
equity   = (class="n">1 + wf).cumprod()
wf_sharpe = wf.mean() / (wf.std() + 1e-class="n">9) * np.sqrt(class="n">252)
print(fclass="s">"Walk-forward OOS sharpe: {wf_sharpe:.2f}")
print(fclass="s">"Parameter chosen each window: {chosen}")

Two things to study in that output. The wf_sharpe is your *real* expectation — it is built only from out-of-sample trades, so it is not inflated by fitting. And chosen — the parameter picked in each window — is quietly one of the most useful diagnostics you have.

Walk-forward tells you whether your parameters generalise through time. The final lesson tackles the other question: given that any single backtest is just one roll of the dice, how good or bad could the outcome realistically have been — and is your performance a stable plateau or a fragile spike?