Skip to content

Choose walk-forward windows

Use run_walk_forward to choose parameters on an earlier slice, then evaluate them on a later slice.

Window sizes, step and purge_bars count observations, not days. Pass a chronological, sliceable sequence, usually a list of bars. The only real mode is refit_per_fold: each fold runs a grid on its train slice, then evaluates the winner on the later test slice. There is no leaky slice_diagnostics label and no post-test embargo. Default window="rolling" and purge_bars=0 keep the previous contiguous layout; each result row now also carries mode.

With train 3, test 2 and step 2 on eight observations, rolling trains on [0, 3), tests on [3, 5), then trains on [2, 5) and tests on [5, 7). The trailing eighth observation cannot fit a third complete pair and is omitted. Each training slice precedes its own test slice. Different folds can overlap; step < test_window overlaps test slices, so summing their results would double-count observations.

Choose window when you want a different train shape:

  • rolling (default): a fixed-length train window slides forward by step.
  • expanding: train_start stays at 0 and train_end grows by step from the first train length.
  • anchored: the same ranges as expanding. The anchor is the first observation; there is no separate origin argument.

purge_bars=k drops exactly k observations between train_end and test_start. Negative purge and a positive purge that cannot fit a first fold (train_window + purge_bars + test_window longer than the data) raise ATOM-WF-PURGE, even when a zero-purge fold would also not fit. A window that is not the string rolling, expanding or anchored raises ATOM-WF-WINDOW. A mode other than refit_per_fold raises ATOM-WF-MODE.

This example uses twenty observations, train 6, test 4, step 4 and a one-bar purge. Rolling fold 0 trains [0, 6), drops bar 6, and tests [7, 11); fold 1 trains [4, 10) and tests [11, 15); fold 2 trains [8, 14) and tests [15, 19). Expanding (and anchored) keep train_start at 0, so fold 1 trains [0, 10) instead of [4, 10):

from atom.research import run_walk_forward


def score_window(params, values):
    return {"score": float(sum(values)) * params["scale"]}


def main():
    values = list(range(20))
    rolling = run_walk_forward(
        score_window, {"scale": [1]}, values,
        train_window=6, test_window=4, step=4,
        workers=1, selection_metric="score",
        window="rolling", purge_bars=1,
    )
    assert rolling["train_start"].to_list() == [0, 4, 8]
    assert rolling["train_end"].to_list() == [6, 10, 14]
    assert rolling["test_start"].to_list() == [7, 11, 15]
    assert rolling["test_end"].to_list() == [11, 15, 19]
    assert rolling["mode"].to_list() == ["refit_per_fold"] * 3

    expanding = run_walk_forward(
        score_window, {"scale": [1]}, values,
        train_window=6, test_window=4, step=4,
        workers=1, selection_metric="score",
        window="expanding", purge_bars=1,
    )
    assert expanding["train_start"].to_list() == [0, 0, 0]
    assert expanding["train_end"].to_list() == [6, 10, 14]
    assert expanding["test_start"].to_list() == [7, 11, 15]


if __name__ == "__main__":
    main()

Train/test/step must all be positive integers; booleans and fractional values are rejected before window generation. Empty or insufficient data with zero purge produces an empty DataFrame with no columns. Positive purge on that same empty or short input raises ATOM-WF-PURGE. Invalid worker counts and empty candidate lists still raise even when no complete window fits. The helper does not sort data, validate timestamps or detect information leakage inside your runner.

selection_metric defaults to "sharpe", but your runner must actually supply that metric. Higher is better; negate a loss if you want to minimize it. Every training score must be a finite number. Missing metrics now raise instead of silently selecting the first combination. Ties choose the first combination. The chosen original parameter values are passed to the test call, which runs in the parent process. No training state, positions or earlier bars are carried into it: the test slice starts cold, with no warm-up prefix. A lookback that needs history before the first test bar will not see those bars unless you put them in the test window itself. Include any warmup/history handling deliberately in your runner without using future observations to select parameters.

Rows contain exclusive-end index boundaries, mode as the string refit_per_fold, best_params as JSON text and test metrics prefixed oos_. Use JSON-compatible parameter values when you need to reconstruct them exactly; non-JSON values in best_params use string representations. There is no automatic aggregation or trading-return claim. run_walk_forward has no backend/TTL arguments; ATOM_CACHE=1 enables only its training grids' default cache, not test calls. For isolated experiments leave that environment flag unset. Optuna and seeded random search are not part of this helper.