A 20-day Donchian looks harmless in a spec. The agent writes a 20-bar rolling max, the backtest starts on row one of the parquet file, and for the first two weeks the "20-day high" is just the running max of whatever bars exist. On day four it is a four-day high. The strategy you thought you were testing is not the one that produced the first dozen fills.
I have a notebook on ETHUSDT daily bars where the full-sample Sharpe is 1.4. Cut the first 20 sessions and it falls to 0.6. Keep those 20 and drop the rest of year one, and it prints 3.1. Nobody leaked tomorrow. The strategy used the past, just not enough of it, and the report treated those half-formed trades as evidence.
Let the window trade while it is still filling
The usual wreckage is min_periods=1. Pandas will compute a rolling mean on a single bar if you let it. Agents do this constantly, because a NaN at the top of the frame feels like a bug, and filling it makes the unit test go green.
A 20-day realized-vol target with two observations of return is a size explosion. The variance of two numbers is a coin flip, you invert it, and the first position is several times the intended risk. A z-score against a near-zero standard deviation does the same trick with a sign: the strategy appears to have a razor entry that never shows up again once the window is honest.
Breakouts are worse, because they look like the real rule. A 20-bar channel on bar six is a six-bar channel. You get a cluster of trades right at the start, often in a quiet stretch that would not have triggered the finished indicator. Quiet markets drift a little. A tight channel fires. The equity curve gets a head start that no later year repeats.
Zero-fill and forward-fill invent a different lie. RSI at 0 reads as oversold. A missing ATR of 0 blows up the unit size, and the backtest dutifully buys a warehouse. I would rather a NaN kill the order than a 0 invent one.
I still catch myself plotting from row 0 because a blank month at the left of the chart looks unfinished. The blank month is the honest part.
Crop the equity curve, keep the position
This one survives code review. You compute the 20-day features properly, drop the first 20 rows from the plotted curve, and publish from January 21. The position on January 21 was opened on January 14. The trailing stop's high-water, the pyramid count, the cash after those burns: all of it walked into the "clean" sample.
The wreckage is a paper strategy that cannot be started on the date in the write-up. Restart from flat at the reported start and the first week's orders disagree. Sometimes the backtest is already in a trade the paper system has no reason to enter. Sometimes the paper system enters because its lookback is cold, and the backtest is sitting out because its hidden month already took the breakout. Two opening books, one published start date.
Walk-forward folds do this quietly. Each test window has a lookback tail you do not plot. Skip simulating that tail and the first test trades are unfinished-window trades again. Simulate it and report only the test days, and the test day's position is conditional on fills you are not showing. That second version can be a legitimate research choice. Calling the fold's first morning a cold start, while a trailing stop is already live from the tail, is how two researchers get two Sharpes out of one config.
Warm up in calendar days when the feature isn't a day
"Give it 30 days" leaves the feature underspecified. Thirty calendar days on a 5-minute Donchian is thousands of bars past a 20-period lookback. Thirty days on a daily feature that needs 20 sessions is either fat or thin, depending on weekends and holes.
A funding-rate z-score on one of our perp venues got a 30-day warmup because the spec said 30 periods and someone translated periods into calendar. Funding there prints every eight hours. Thirty days is about 90 settlements. Thirty periods is ten days. The extra two months of burn-in happened to include a depeg week. Dropping it made the live window look calmer than the strategy's real history. People blamed start-date sensitivity. The warmup had eaten the regime.
And crypto versus equity makes the same number change meaning without anyone editing a parameter. Twenty "days" of ETH includes Saturdays. Twenty equity sessions is almost a calendar month. Copy a 20-day lookback from a stocks notebook onto hourly perpetual candles and you have a 20-hour channel with a 20-day name. The first valid bar is then either 20 hours in, or 20 days in if some later function resamples, and I have seen both in the same agent diff.
Pin the first tradable timestamp
The warmup is a data requirement, not a bonus sample. Every feature names the number of finished observations it needs, in the units it is computed in. The first tradable timestamp is the first bar where all of those are true and a flat account would have been allowed to send an order.
| Feature | What the spec usually says | What actually has to exist | Silent substitute |
|---|---|---|---|
| 20-day Donchian | 20 daily highs | 20 completed sessions | Running max with min_periods=1 |
| 30-day realized vol | 30-day window | 30 close-to-close returns | Two-day variance, inverted into size |
| 20-print funding z-score | 20 days | 20 funding settlements | 20 calendar days (~90 prints), or 20 bars of the wrong timeframe |
| 14-period RSI on 5m | 14 periods | 14 closed five-minute bars | 14 days, or 14 bars with gaps forward-filled |
P&L before that timestamp stays out of the track record. State before that timestamp may still need to be simulated, so a trailing stop or a cooldown is real when the clock hits the first tradable bar, but those fills do not go in the Sharpe, the trade count, or the capacity math. Then run the ugly check: restart from flat at that timestamp, with the lookback available as data only. If the orders change, you were shipping hidden trades.
Paper trading starts on the same contract. So does each walk-forward test window. If the chart looks like it is missing a month, leave it missing.
← All posts


