Count the outcomes that neighbouring labels share
The labels are strongly related even when daily returns themselves are independent. A random split frequently places such labels on opposite sides of a fold.
With 252 sessions, use successive 21-session test blocks. At each boundary, fit on earlier observations whose five-day labels are known. If labels correspond to the next five closes and the first test decision is after close 106, a fit frozen by close 105 may include row 100, whose label ends at close 105. It may not include row 101, whose label ends at close 106. In a real calendar, the comparison should use timestamps and the intended cutoff, not an estimate of the number of rows.
It helps to write the dates next to the actual label sums. If the test target contains Thursday's and Friday's returns, a training row whose outcome also uses those returns is sharing observations with the test set even when the feature dates differ. A forward split with an appropriate gap excludes training labels that have not fully matured at the first test decision. A gap defined in row counts can be misleading for an irregular panel with multiple assets or missing dates. Check actual start and end timestamps there.
Use time-ordered folds with a gap
Here the latest included training index plus five is strictly earlier than the first test index.
import numpy as np
from sklearn.model_selection import TimeSeriesSplit
sessions = np.arange(252)
splitter = TimeSeriesSplit(n_splits=5, test_size=21, gap=5)
for train, test in splitter.split(sessions):
assert train[-1] + 5 < test[0]
print(f"train {train[0]}–{train[-1]}, test {test[0]}–{test[-1]}")References and next steps
- Scikit-learn TimeSeriesSplit documents chronological splits and the gap parameter.
- How to Split Market Data Into Training and Test Periods