Purging removes training rows whose outcome intervals overlap a held-out outcome interval. An embargo removes some additional training decisions shortly after a held-out block when nearby inputs may still depend on the same observations. Both depend on the information window for each row. If your goal is to simulate live retraining, also enforce that training uses only past information at each fit date.
Compare the intervals behind the labels
Store start and end timestamps for every realised outcome. With closed intervals, remove a training row when it overlaps any test row :
Use a strict inequality where a shared endpoint carries no shared outcome under your convention. After purging, define exactly which timestamp anchors the embargo and how long it lasts. One conservative rule removes later training starts in a buffer after the last test outcome ends. In a strictly forward split there are no post-test training rows to embargo. The equivalent boundary check is that every included training label is resolved by the fit cutoff.
Use closed intervals and suppose a test block contains labels covering sessions 10–13 and 12–15. Training labels covering 6–9 or 19–22 are disjoint. A row spanning 8–11 overlaps the first outcome, and a row spanning 15–18 touches the end of the second. Purge both. With a two-session embargo after session 15, also remove later training decisions on sessions 16 and 17. The boundary conditions change if your labels measure intervals between closes and you choose a different endpoint convention.
Purging and embargo protect different boundaries. Purging tests the actual time span used to realise an outcome. Embargo removes an additional buffer after the test block when the chosen validation design also trains on later rows. If features use trailing windows, examine their reach too. A five-day embargo does not repair a feature accidentally constructed from tomorrow's close. For a strict simulation of a production forecast, train only on earlier observations with labels already known by the cutoff.
Choose an interval convention based on the data used to realise each return. For example, if labels represent half-open time intervals, two labels touching at an endpoint may share no return interval. Their strict overlap condition is and . If both labels explicitly include the same endpoint observation and that creates information overlap in your design, a closed interval rule is safer. Document the convention and test a touching pair, an entirely contained pair and a fully disjoint pair. Those cases catch different boundary errors.
Filter overlapping intervals in Python
def eligible_rows(intervals, test_indices, embargo_sessions):
test_set = set(test_indices)
test_windows = [intervals[j] for j in test_indices]
test_end = max(end for _, end in test_windows)
retained = []
for i, (start, end) in enumerate(intervals):
overlaps = any(start <= b and end >= a for a, b in test_windows)
embargoed = test_end < start <= test_end + embargo_sessions
if i not in test_set and not overlaps and not embargoed:
retained.append(i)
return retained
windows = [(6, 9), (8, 11), (10, 13), (12, 15),
(15, 18), (17, 20), (19, 22)]
print(eligible_rows(windows, [2, 3], 2)) # [0, 6]References and next steps
- López de Prado, Advances in Financial Machine Learning, chapter 7, introduces purging and embargoes for financial cross-validation.
- How to Run Walk-Forward Optimisation Without Peeking