There is a line I go looking for before anything else when someone shows me a backtest with a Sharpe ratio above 3. I skip the signal logic and the indicator stack and go straight to the part where the fill happens, because more often than not the signal is computed from the close of a candle and the trade is filled at the close of that same candle. I once watched a strategy drop from a Sharpe over 3 to well under 1 after fixing that single line, with nothing else about the code changed.
Look-ahead bias is the general name for the bug class: any moment where a simulated trade touches information that did not exist yet at the simulated time. Machine learning people call it data leakage. It is responsible for most of the too-good backtests I have ever been shown, and it almost never comes from one dramatic mistake. The usual sources are a handful of specific code patterns that all look innocent on their own. Worth walking through them one by one, because once you know their shapes you will start finding them in your own old projects, which is a humbling afternoon but a cheap one.
Trading a price you could not have known
The same-candle fill is the classic. You compute an indicator on the current bar, which requires the bar's close, then you record a fill at that same close. In live trading that sequence is impossible. The close of a candle only exists once the candle is over, and once the candle is over you can no longer trade at that price. The backtest is effectively getting a free peek at where the bar ends before deciding whether to be in it.
The vectorized version of the same bug is even easier to write. In pandas it looks like pnl = signal * price.pct_change(), where the signal on day t is multiplied by the return of day t. But the signal on day t was built from day t's close, and the return of day t ends at that close, so the position earns the very move that created it. The fix is one call: signal.shift(1) * price.pct_change(), so that today's position comes from yesterday's information. In an event-driven backtester the equivalent fix is filling at the next bar's open, with slippage, instead of at the current bar's close.
Mean reversion is where this bug flatters results the most. A dip-buying signal that gets to trade at the same close it was computed from is buying the exact tick that defined the dip. Shift it one bar and much of the edge evaporates, because the market has often snapped back before you could realistically get filled. That sensitivity is itself a diagnostic. If a one-bar lag destroys the strategy, historically that has meant the edge lived inside the leak rather than in the market.
Statistics that saw the whole movie
The second pattern is subtler because every individual line is mathematically correct. You normalize a feature with a z-score: subtract the mean, divide by the standard deviation. Except the mean and standard deviation were computed over the entire dataset, including years of data that come after the bar being scored. Your early signals know the volatility regimes of later years. No single future price ever appears in the formula, which is why it does not feel like cheating, but distributional knowledge of the future is still knowledge of the future.
The same leak hides anywhere a summary statistic gets computed once over the full sample: min-max scaling, percentile ranks against full history, detrending with a regression fitted to the whole series, PCA fitted on all the data, or a machine learning scaler fit before the train-test split instead of after. Full-sample normalization flatters results in a particular way, too. Extreme readings look reliably extreme because the scaler already knows the true range of the data. Live, your scaler only knows the past, and yesterday's all-time high keeps getting replaced.
The fix is to make every statistic point-in-time. Rolling or expanding windows, so the mean at bar t only uses data through bar t. For ML pipelines, fit the scaler on the training window only, apply it frozen to the test window, and refit as the walk-forward advances. The code gets uglier and slower, and that is roughly the price of a number you can trust.
Timestamps that quietly lie
The third pattern lives in the data rather than the strategy code, which is why code review alone will not catch it. A daily series arrives stamped at midnight, so the backtest assumes the value was knowable at midnight. But a lot of daily data describes something that was published hours later. An on-chain metric for Monday cannot be computed until Monday's last block, and most providers publish it sometime Tuesday. An economic release dated to the first of the month actually hit the wire mid-morning on some later day. Quarterly fundamentals are the worst offenders. A company's first-quarter numbers get stamped to the end of March in many datasets, while the filing that contained them might not have appeared until May.
Trading on a midnight timestamp for a noon publication gives the strategy a head start of hours on every data point, forever, and a daily strategy can build an entire fake edge out of those hours. Funding rate snapshots, sentiment aggregates, exchange volume rollups, and anything scraped from a provider's daily summary all deserve suspicion. The question to ask about every input is concrete: at what wall-clock moment could a live process have had this exact number in memory? Then lag the series to that moment, or better, find a point-in-time version of the dataset that stores publication timestamps instead of reference dates. Revisions compound the problem as well. The macro figure sitting in a historical dataset today is often the second or third revision, and the number a live trader saw on release day was different.
The review checklist
When I review a backtest now, mine or anyone else's, I run the same list before looking at a single performance number.
- Find the execution line. Fills should happen at the next bar's open or later, never at the close the signal was computed from.
- Grep for shift. In a vectorized backtest, positions need to be shifted at least one bar against returns. If there is no shift anywhere in the file, that is usually the whole story.
- Check every rolling window for
center=Trueand every resample for its label convention. Centered windows average future bars into the present, and some smoothing utilities do it by default. - Hunt for full-sample statistics. Any mean, std, min, max, percentile, or fitted scaler computed once over the whole series needs to become expanding, rolling, or train-only.
- Interrogate every timestamp. For each data source, write down when the value became publishable and lag it to that moment. Fundamentals and macro series usually need weeks of lag rather than hours.
- Run the lag test. Delay all signals by one extra bar and rerun. A real edge tends to degrade gradually, while a leak tends to collapse outright.
- Test for repainting. Truncate the data at some past date and confirm the signals before that date match the full-history run exactly. Indicators that redraw themselves, like zigzag and some pivot logic, fail this instantly.
None of this is exotic. Every bug here is a line or two of code and so is every fix. When we built the backtesting engine at Blockcircle, next-bar execution and point-in-time indicator windows became the defaults for exactly this reason, and I still run the one-bar lag test on anything that looks better than it should. The habit underneath the checklist is the useful part. Walk through the backtest as if you were the process running live, bar by bar, and ask at each step whether the number in front of you existed yet. Most strategies do not survive that walk with their Sharpe intact, and the few that do are the ones worth spending real slippage to verify.