In backtrader, line objects are indexed relative to “now”: [0] is the current bar, [-1] is the previous bar, and a positive index like [1] reaches into the FUTURE. That inversion of normal Python list semantics is the most common source of look-ahead bias in backtrader strategies. This guide shows the bug, the fix, and why next-bar-open fills matter. It is an educational engineering guide, not financial or trading advice, and implies nothing about profitability.
In backtrader, indexing a line is relative to the current bar, not absolute from the start of the array. self.data.close[0] is the close of the bar being processed right now. self.data.close[-1] is the previous bar. And self.data.close[1] is the next bar — a value that, during a live run, would not exist yet. Reading it in a backtest is textbook look-ahead bias.
This trips people up because it inverts normal Python. In a plain list, x[-1] is the last element and x[1] is near the front. In a backtrader line, the indexing is anchored at the present moment and counts backward with negatives, forward with positives. A single positive index is enough to leak the future into a decision.
# ❌ WRONG: [1] reads the NEXT bar's close — the future
import backtrader as bt
class Leaky(bt.Strategy):
def next(self):
# comparing today's close to TOMORROW's close
if self.data.close[0] < self.data.close[1]:
self.buy()That strategy buys today whenever tomorrow closes higher. It will look extraordinary in a backtest and is completely untradeable. The rule is blunt: inside next(), never use a positive line index. Only [0] (current) and negative indices ([-1], [-2], … past) are legitimate. For the concept behind the mechanics, see [look-ahead bias](/learn/look-ahead-bias) and [why most backtests fail](/learn/why-most-backtests-fail).
The corrected version of a momentum-style condition compares the current bar to a past bar. If you want "price is rising," you check that today's close is above yesterday's — [0] versus [-1] — not today's versus tomorrow's.
# ✅ CORRECT: [0] is now, [-1] is the previous bar (the past)
import backtrader as bt
class Clean(bt.Strategy):
def __init__(self):
self.sma = bt.indicators.SMA(self.data.close, period=20)
def next(self):
# today's close above its 20-bar SMA, and rising vs. yesterday
if not self.position:
if self.data.close[0] > self.sma[0] and self.data.close[0] > self.data.close[-1]:
self.buy()
else:
if self.data.close[0] < self.sma[0]:
self.close()Every index here is [0] or negative, so every value was knowable at decision time. backtrader's built-in indicators (SMA, RSI, ATR, and the rest) are themselves computed only from current and past bars, so self.sma[0] is safe — it does not peek. The discipline you enforce by hand is: keep your own line accesses at [0] and below.
A fast audit for any backtrader strategy: search the file for a line index that is a positive integer literal — [1], [2], close[3] — inside next() or an indicator's next(). Each one is a future read. There is essentially no correct reason to use a positive line index in strategy logic.
Even with all indices at [0] or below, backtrader can still hand you an optimistic result through fill timing. When you call self.buy() inside next(), the decision is made using bar t's close. The question is which price you get filled at. If your broker is configured to fill at the current bar's close, you are being filled at the very price your signal just used — a same-bar execution leak, the backtrader equivalent of multiplying a signal by its own bar's return.
backtrader's default and safest behavior is to fill a market order at the next bar's open, which correctly models the gap between deciding and acting. The trap is overriding that with close-on-same-bar behavior via cheat-on-close or cheat-on-open:
# ❌ WRONG: cheat-on-close fills at the same bar's close you decided on cerebro.broker.set_coc(True) # coc = cheat-on-close
# ✅ CORRECT: leave cheat-on-close OFF so market orders fill next-bar-open
cerebro.broker.set_coc(False) # this is also the default
# optional: be explicit that you want the next bar's open as the fill
class NextOpen(bt.Strategy):
def next(self):
if not self.position and self.buy_signal():
# a plain market order will fill at the NEXT bar's open
self.buy()Unless you are deliberately modeling market-on-close orders and are certain your data timestamps support it, leave set_coc at its default False. Next-bar-open fills are the honest default: you decide on the close you can see, and you pay the open you could actually reach.
Removing look-ahead is necessary but not sufficient — a backtest with correct timing and zero costs still flatters itself. backtrader lets you attach commission and slippage on the broker so fills reflect friction, and a stop keeps the loss side of the model honest.
# ✅ CORRECT: next-bar-open fills, commission, slippage, and a protective stop
import backtrader as bt
class Framed(bt.Strategy):
params = dict(stop_atr=2.0)
def __init__(self):
self.sma = bt.indicators.SMA(self.data.close, period=20)
self.atr = bt.indicators.ATR(self.data, period=14)
def next(self):
if not self.position:
if self.data.close[0] > self.sma[0]:
self.buy() # market order -> fills next bar's open
else:
# protective stop referenced off current & past values only
stop_price = self.data.close[0] - self.p.stop_atr * self.atr[0]
if self.data.low[0] < stop_price:
self.close()
cerebro = bt.Cerebro()
cerebro.broker.set_coc(False) # next-bar-open fills
cerebro.broker.setcommission(commission=0.0005) # 5 bps per side, illustrative
cerebro.broker.set_slippage_perc(perc=0.0005) # 5 bps slippage, illustrativeNotice the stop uses self.atr[0] and self.data.low[0] — current bar only, no positive index. To confirm nothing leaked, run the strategy across separate periods with [walk-forward analysis](/learn/walk-forward-analysis) and compare [in-sample vs out-of-sample](/learn/in-sample-vs-out-of-sample) behavior; a hindsight edge collapses out of sample. You can also paste your next() logic into the [Python Backtest Validator](/python) to catch positive-index and same-bar-fill patterns statically, and use the [overfitting tool](/backtest/overfitting) to see how much of what remains is curve-fit. Fixing the [1] trap makes the numbers real; it says nothing about whether the strategy makes money.
Educational only — not financial advice. Trading involves substantial risk of loss.