DEV Community

Gueta Quant
Gueta Quant

Posted on

Time-Series Validation in Python: Keep Future Data Out of Training

A timeline with separate training and test periods

A random split can mix later observations into training and produce a misleading evaluation. For ordered data, evaluate on later observations and fit transformations using only each training partition.

Educational and simulation content only. This is not financial advice, intermediation or an investment recommendation. Historical results do not guarantee future results.

Where leakage appears

Suppose a feature is standardized using the mean and standard deviation of the entire dataset, including the test period. Even if a model never receives a future date, the transformation has incorporated information from that period. Fit the transformation on training data, then apply the fitted transformation to the test data.

A chronological split reduces leakage, but does not eliminate every form. A label that depends on prices over the next five days can cross the split boundary. Exclude observations with overlapping label horizons according to the label design. A fixed gap alone does not solve every overlap.

A dependency-free split you can inspect

The following example generates index ranges only. It does not use market data or generate trading signals. Four expanding training sets precede four test sets; two observations are excluded between each pair.

def chronological_splits(n, n_splits, test_size, gap):
    if min(n, n_splits, test_size) <= 0 or gap < 0:
        raise ValueError("Invalid split parameters")
    first_test = n - n_splits * test_size
    if first_test - gap <= 0:
        raise ValueError("Not enough training observations")
    for start in range(first_test, n, test_size):
        train = range(0, start - gap)
        test = range(start, min(start + test_size, n))
        yield train, test

splits = list(chronological_splits(120, 4, 15, 2))
assert len(splits) == 4
for train, test in splits:
    assert train[-1] + 2 < test[0]
    print(f"train {train[0]}–{train[-1]}; test {test[0]}–{test[-1]}")
Enter fullscreen mode Exit fullscreen mode

The two-observation gap is illustrative, not a universal parameter. It must reflect how features and labels are constructed. When using scikit-learn, TimeSeriesSplit provides a gap parameter. Equally spaced observations are needed for its test folds to cover comparable durations. Put learned preprocessing in a pipeline fitted separately on each training fold.

What a split still cannot establish

  • Repeated selection: testing many configurations and reporting only the winner biases evaluation. Keep an experiment log.
  • Costs and execution: prediction metrics do not automatically include spread, commission or slippage.
  • Regime changes: historical periods may not represent the future.
  • Final holdout: keep a final period untouched while designing and selecting the procedure.
  • Data availability: check release dates and revisions; historical values may not have been known at the simulated time.

Bailey and coauthors discuss the effect of selecting among many trials in The Probability of Backtest Overfitting. Their method does not turn historical tests into guarantees.

Gueta Quant's Spanish guide to backtest validation covers additional biases and costs. This example is an independent synthetic demonstration, not an executable trading strategy.

Disclosure: AI assisted in drafting this article; the examples and source references were reviewed before publication.

Top comments (0)