freqtrade’s two most common look-ahead leaks come from higher-timeframe data: merging an informative pair that references a still-forming HTF candle, and populate_indicators computing values that quietly depend on future rows. This guide shows the correct merge_informative_pair usage, why the merge lags the HTF candle by design, and how startup_candle_count keeps indicators consistent between backtest and live. It is an educational engineering guide, not financial or trading advice, and implies nothing about profitability.
In freqtrade, look-ahead bias almost always enters through time: your strategy references a candle that, at the moment of the decision, had not finished forming. The two hotspots are informative (higher-timeframe) pairs and populate_indicators. Get the merge or the indicator window wrong and your backtest sees a completed 1h candle while live trading would only have had a partial one — the Python equivalent of Pine's request.security repaint problem.
The reason it is easy to miss: freqtrade's backtester replays candles, and if you naively join a 1h series onto 5m rows by timestamp, each 5m row at 10:05 can end up carrying the 1h candle labeled 10:00 — which does not close until 10:59. During the 10:05–10:55 window that candle is still forming; using its final values is reading the future.
freqtrade ships a helper, merge_informative_pair, specifically to prevent this by lagging the higher-timeframe candle so each base-timeframe row only ever sees the last fully closed HTF candle. Use it, and use it correctly. For the underlying concept, see [look-ahead bias](/learn/look-ahead-bias); for why leaks like this wreck results, [why most backtests fail](/learn/why-most-backtests-fail).
Here is the leak in its most common form — pulling a 1h dataframe and merging it onto the 5m base without the helper, so the current (unfinished) 1h candle bleeds into 5m rows.
# ❌ WRONG: naive merge lets a still-forming 1h candle leak into 5m rows
def populate_indicators(self, dataframe, metadata):
inf = self.dp.get_pair_dataframe(metadata['pair'], timeframe='1h')
inf['ema50_1h'] = ta.EMA(inf, timeperiod=50)
# merge_asof on raw timestamps aligns 5m 10:05 with the 1h candle
# dated 10:00 — which does NOT close until 10:59.
dataframe = pd.merge_asof(
dataframe, inf[['date', 'ema50_1h']], on='date'
)
return dataframeThe fix is merge_informative_pair, which shifts the higher-timeframe candle forward by one HTF period so a base-timeframe row only ever joins to a candle that has already closed. You also let freqtrade name the informative timeframe explicitly.
# ✅ CORRECT: merge_informative_pair lags the HTF candle to the last CLOSED one
from freqtrade.strategy import merge_informative_pair
def populate_indicators(self, dataframe, metadata):
inf_tf = '1h'
informative = self.dp.get_pair_dataframe(
pair=metadata['pair'], timeframe=inf_tf
)
# indicators on the informative frame use only its own past bars
informative['ema50'] = ta.EMA(informative, timeperiod=50)
dataframe = merge_informative_pair(
dataframe, informative,
self.timeframe, inf_tf,
ffill=True
)
# column is auto-suffixed: 'ema50_1h'
return dataframemerge_informative_pair renames informative columns with a _1h suffix, forward-fills between HTF closes, and — critically — aligns each base row to the most recent completed HTF candle. That single helper is the difference between a repainting backtest and one that matches live. Don't roll your own merge_asof for this.
Even on a single timeframe, populate_indicators runs once over the entire backtest dataframe. That is efficient, but it means any operation touching future rows silently leaks, because in live trading the dataframe ends at the current candle. Two offenders show up repeatedly: a negative .shift() and a centered rolling window — the same pandas traps that appear everywhere, now inside a freqtrade hook.
# ❌ WRONG: both lines read future rows during backtest
def populate_indicators(self, dataframe, metadata):
# negative shift pulls the NEXT candle's close backward
dataframe['next_close'] = dataframe['close'].shift(-1)
# centered window straddles future candles
dataframe['mid'] = dataframe['close'].rolling(20, center=True).mean()
return dataframeEvery indicator must be causal — computed only from the current row and earlier ones. TA-Lib functions (EMA, RSI, ATR) are causal by construction. Your own columns must be too: positive shifts only, trailing windows only.
# ✅ CORRECT: causal indicators — current and past rows only
def populate_indicators(self, dataframe, metadata):
dataframe['ema20'] = ta.EMA(dataframe, timeperiod=20) # causal
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) # causal
dataframe['mid'] = dataframe['close'].rolling(20).mean() # trailing
# if you need a prior value, shift the PAST forward (+1), never -1
dataframe['close_prev'] = dataframe['close'].shift(1)
return dataframeA useful mental test: for any column, ask "at the candle where this value sits, could I have computed it live from data that had already closed?" If the answer needs a row to its right, it is a leak. Grep freqtrade strategies for .shift(- and center=True the same way you would any pandas backtest — see the four traps in the [Python hub article](/learn/python-backtest-look-ahead-bias).
The last piece is consistency between backtest and live, and it hinges on startup_candle_count. Indicators with a lookback (an EMA-50, an RSI-14) need warm-up candles before their output is valid. In live trading freqtrade only holds a rolling buffer of recent candles; if your indicators need more history than the buffer provides, their early values differ from the backtest — which can masquerade as, or hide, a leak. You declare the requirement so freqtrade feeds enough warm-up data and then drops the warm-up region so signals are computed on fully-formed indicators.
# ✅ CORRECT: declare enough warm-up for your longest lookback
class MyStrategy(IStrategy):
timeframe = '5m'
# longest lookback here is the 1h EMA50; give ample warm-up buffer
startup_candle_count: int = 200
def populate_entry_trend(self, dataframe, metadata):
dataframe.loc[
(dataframe['close'] > dataframe['ema20']) &
(dataframe['close'] > dataframe['ema50_1h']) & # from merge_informative_pair
(dataframe['volume'] > 0),
'enter_long'
] = 1
return dataframeSet startup_candle_count to comfortably exceed your longest indicator lookback (counted in the base timeframe — a 1h EMA-50 on a 5m base needs far more than 50 base candles). Too low and early signals are computed on immature indicators; the numbers will not reproduce live.
To confirm the whole strategy is clean, freqtrade provides lookahead-analysis, which reruns your backtest on truncated data and flags results that change when future candles are withheld — the direct test for this class of bug. Pair it with [walk-forward analysis](/learn/walk-forward-analysis) and an [in-sample vs out-of-sample](/learn/in-sample-vs-out-of-sample) split, and paste your indicator logic into the [Python Backtest Validator](/python) for the static .shift(-/center=True checks. The [overfitting tool](/backtest/overfitting) tells you how much remaining edge is curve-fit. As always: correct HTF merging and warm-up make results honest, not profitable.
Educational only — not financial advice. Trading involves substantial risk of loss.