← All articles

Strategy Graveyard, chapter 1 — methodology traps

Look-ahead bias in a higher-timeframe filter

10 min read · Published 27 Oct 2026

The most dangerous bug in a backtest is the one that improves the result, because nobody goes looking for it. In our channel-breakout test, a filter taken from the daily chart “knew” the day’s close before it happened. It turned a near-zero result into a strategy with a PF of 1.29, survived our first fix, and only came to light when we compared the backtest with signals from a demo account, value by value.

Two abbreviations we use: R is a trade’s result in units of risk (−1 R = a loss equal to the distance to the stop). PF (profit factor) is total gains divided by total losses; a PF of 1.00 means breaking even: gains equal losses.

Hypothesis

The Donchian channel breakout is one of the oldest trend-following rules. If a candle closes above the highest high of the last N candles, you buy; if it closes below the lowest low, you sell. The idea has support in the literature. In a review of 4,128 market-timing strategies for gold (Bartsch, Baur, Dichtl and Drobetz, a 2018 paper), only selected technical strategies held up after correcting for multiple testing. According to the authors, they made their money mainly by getting out of gold after long declines — in other words, by following the trend. Those strategies ran on monthly data, not hourly, so this is a pointer, not proof for our version.

Our version traded the hourly chart (H1) and had an extra timeframe-alignment filter. A buy was allowed only when, on M15, H1, H4 and D1, the last close sat above that timeframe’s moving average; a sell required the opposite. Stated at its best, the hypothesis is this: an hourly breakout that agrees with the daily and four-hour trend is more likely to follow through than a breakout against it. It sounds sensible, and filters like this are widely recommended.

How we tested it

Data and cost. We used nine instruments: gold and eight indices (the German, Japanese, French, European, Spanish, Dutch and Swiss indices, plus the US small-cap index). The H1 candles covered the years from 2016 onwards; for three instruments the history starts in 2017–2020, because only from then on is it reliable. Transaction costs were included. The exit was mechanical: a fixed stop and a time limit on holding the position.

The first alarm, and the first, wrong fix (September 22, 2026). It began with a question: did the multi-timeframe filter in the backtest compute the same thing as the strategy on demo? We found the line that selected the higher-timeframe candle. The code took the last candle that started before the moment of decision. We fixed that one line, and the comment next to it read: “last candle closed before the decision”. PF fell by about 0.09, and we noted with relief that “the edge survived its own bug”. Along the way we measured that without the multi-timeframe filter the result drops to roughly PF 1.10. The conclusion was: the filter provides most of the edge. On the strength of those numbers, the strategy went onto a demo account.

The second alarm (September 25, 2026). We were building a tool that takes every signal from demo, reconstructs it from historical data and compares every value: price, channel bounds, moving averages and distances from the averages on four timeframes. On D1 the values did not match. So we counted how often the backtest had used a candle other than the last closed one:

timeframe backtest used a candle other than the last closed one
M15 0% of decisions
H1 0% of decisions
H4 76.8% of decisions
D1 98.3% of decisions (17,783 of 18,085)

The September 22 fix only worked where the decision falls exactly at the start of a new candle, that is, on M15 and H1. On the daily chart, a decision at 14:00 falls in the middle of the day, so “the last candle that has started” is still today’s candle. In historical data that candle already has its final close — a price nobody knew at 14:00.

The correct rule — and there is only one — is this: a higher-timeframe candle may be used for a decision only if its end falls no later than the moment of decision. The candle’s start does not settle it.

For those who write their own backtests. The bug looks innocent: “take the last D1 candle whose open time is earlier than the moment of decision”. The correct version: “take the last D1 candle whose open time plus the candle’s length is no later than the moment of decision”. On a daily chart, that is the difference between “today” and “yesterday”. Around session breaks and shortened days this rule is conservative; when in doubt, take the older candle. Also watch out for the platform convention in which the candle with index 0 is the current one and index 1 is the last closed one. That convention does not hold in an array of historical data, because every candle in the file is already closed — including the one that was still forming at the moment of decision.

Independent check. We recomputed everything two ways. The first was the corrected backtest. The second was a separate emulation in which the data are physically truncated at the moment of decision, so the future simply does not exist in the program’s memory. Both routes produced the same entries in 99–100% of cases.

Then we built permanent safeguards. The first is an automatic truncation test: 227 signals (a stratified random sample) × 44 features, all identical on full and truncated data, and any difference aborts the dataset build. The second is a comparison with the demo signals: 9 out of 9 reproduced with no difference. The code was also checked by an independent reviewer (another AI model). It found further, smaller bugs, among them a spread counted twice on buys and a fake M5 candle history in the early years. We cover these in the chapters on cost and on data quality.

Result

The mechanics and the data were the same. The only difference was which higher-timeframe candle the filter could see.

9 instruments, from 2016 with look-ahead correct (as live)
number of trades 8,449 7,523
total result +1,720 R +149 R
average per trade +0.204 R +0.020 R
PF 1.29 1.03
PF without the 20 best trades 1.24 0.98

By instrument, PF looked like this: the German index 1.52 → 1.14, the Japanese index 1.39 → 1.11, gold 1.39 → 1.10, and the six other indices combined 1.19 → 0.96. About 91% of the total profit came from information that did not yet exist at the moment of decision. Remove the 20 best trades and the correct version is in the red.

We declared every number for this strategy from September 22–25 invalid, including the earlier “passed” test and the conclusions about how to exit positions. From that point on, the starting point for further research was a PF of 1.03 — practically zero.

Why it doesn’t work

The filter became an oracle. In the buggy version, the condition “buy when the day closes above the average” in practice meant “buy only on days that will close above the average”. An upside breakout on a day you know will end strong obviously has a better chance. So the backtest was not measuring whether the daily trend helps a breakout. It was measuring whether knowing how the day ends helps you make money during it. And yes, it does.

The bug was on the “good” side. Had the future made the result worse, we would have hunted for the cause straight away. An inflated result looks like success, so it gets fewer questions.

We checked the fix against a comment, not against an example. On M15 and H1 the new line worked correctly, so a quick glance confirmed that “all is well”. Nobody took one specific decision at 14:00 and checked by hand which daily candle the filter could see.

The sentence “the filter provides most of the edge” should have worried us. The strongest part of the system was exactly the part where the bug sat. When one element explains almost the entire result, that is the element to check first.

The live version and the backtest computed different things. The demo strategy was written separately, and it correctly took the last closed candle. Without comparing both sides value by value, the live results would simply have been “weaker than the test”. With a hit rate of around 30–35%, which is typical for breakouts, a losing streak can look like ordinary bad luck for a long time.

The lesson for you

If your strategy uses anything from a higher timeframe (the D1 trend, yesterday’s level, the weekly candle, an H4 indicator), that element is the most suspicious part of your backtest. Platforms and libraries define the “current” candle differently, and in historical data every candle already has its close.

How to check this yourself:

  1. Record the moment of decision and the end of every candle used. For every trade in the backtest, the end of the higher-timeframe candle must fall no later than the moment of decision. Checking a few dozen random trades is enough.
  2. Run a truncation test. Cut the data exactly at the moment of decision, recompute the indicators and compare them with the values from the full data. Any difference means the full data are giving something away.
  3. Compare with live signals, value by value. A truncation test checks the code against its own definition of a “closed candle”. A bug in that definition itself is caught only by comparing with what the platform showed live or on demo.
  4. Test the fix on a known example, ideally a decision in the middle of a higher-timeframe candle, not one that falls at its start.
  5. Examine first the element that contributes the most to the result. If the strategy is close to zero without the filter and excellent with it, the filter is the first to go through a truncation test.

In your trading journal, record not only the result but also the indicator values at the moment of entry, because only they let you compare live results with the test. Once you have a run of trades, the “Luck or edge?” calculator will show how much of the result is left after removing the best positions and how wide the uncertainty range is.


Educational content, not investment advice. Past results do not guarantee future results. We describe the results of our own tests, on our data and with our costs.

Check it on your own trades

Free tools, no sign-up, your data stays in your browser.

← All articles