Skip to main content
Indicators in backtesting.py are declared inside Strategy.init() by wrapping a function with self.I(). This ensures they are computed once up-front in vectorized form, automatically plotted, and gradually revealed to next() in a look-ahead-bias-free manner.

The self.I() method

self.I(func, *args, name=None, plot=True, overlay=None, color=None, scatter=False, **kwargs)
self.I() calls func(*args, **kwargs) immediately and wraps the resulting array as a managed indicator. The return value is a NumPy array that is sliced progressively during next(), so indicator[-1] is always the current bar’s value.

Parameters

ParameterDefaultDescription
funcrequiredCallable that returns an array (or tuple of arrays) of the same length as data
*argsPositional arguments forwarded to func
nameNoneLegend label. Auto-generated from func name and args if omitted. For multi-array indicators, pass a list of strings.
plotTrueWhether to include this indicator in Backtest.plot() output
overlayNoneTrue = plot on top of price candles; False = plot in a separate panel below. Auto-detected by default.
colorNoneHex RGB string (e.g. '#FF0000') or X11 color name. Next available color assigned by default.
scatterFalseTrue = render as circle markers instead of a continuous line
**kwargsAdditional keyword arguments forwarded to func

Simple SMA example

The most straightforward indicator declaration uses a plain function or lambda:
import pandas as pd
from backtesting import Strategy

def SMA(values, n):
    """Simple moving average of `values` over `n` bars."""
    return pd.Series(values).rolling(n).mean()

class SmaCross(Strategy):
    n1 = 10
    n2 = 20

    def init(self):
        # Wrap SMA in self.I — result is a NumPy array
        self.sma1 = self.I(SMA, self.data.Close, self.n1)
        self.sma2 = self.I(SMA, self.data.Close, self.n2)

    def next(self):
        if self.sma1[-1] > self.sma2[-1]:
            self.buy()
You can also use a lambda for one-liners:
def init(self):
    self.sma = self.I(lambda x, n: pd.Series(x).rolling(n).mean(),
                      self.data.Close, 20, name='SMA(20)')
Always declare indicators in init(), not in next(). Declaring them in init() computes them once over the full dataset before the simulation starts, which is both faster and correct.

Using TA-Lib and other libraries

backtesting.py imposes no restrictions on which indicator library you use. Any function that accepts array-like inputs and returns an array works:
import talib
from backtesting import Strategy

class MyStrategy(Strategy):
    def init(self):
        # talib.SMA returns a NumPy array — works directly
        self.sma = self.I(talib.SMA, self.data.Close, timeperiod=20)
        self.rsi = self.I(talib.RSI, self.data.Close, timeperiod=14)

Multi-value indicators (e.g. MACD, Bollinger Bands)

Some indicators return multiple arrays — for example Bollinger Bands (upper, middle, lower) or MACD (macd, signal, hist). Return them as a tuple of arrays and provide a matching name list:
import numpy as np
from backtesting import Strategy

def BBANDS(data, n_lookback, n_std):
    """Returns (upper, lower) Bollinger Band arrays."""
    hlc3 = (data.High + data.Low + data.Close) / 3
    mean = hlc3.rolling(n_lookback).mean()
    std  = hlc3.rolling(n_lookback).std()
    upper = mean + n_std * std
    lower = mean - n_std * std
    return upper, lower

class BollingerStrategy(Strategy):
    def init(self):
        # self.I() returns a 2D array; unpack with standard tuple assignment
        self.bb_upper, self.bb_lower = self.I(
            BBANDS, self.data,
            20, 2,
            name=['BB upper', 'BB lower'],
            overlay=True
        )

    def next(self):
        price = self.data.Close[-1]
        if price < self.bb_lower[-1]:
            self.buy()
        elif price > self.bb_upper[-1]:
            self.position.close()
When func returns a tuple of arrays, self.I() returns a 2D NumPy array whose first dimension indexes each sub-array. You can unpack it normally with a, b = self.I(...).

The .s accessor for pandas Series

Data arrays and indicators exposed by backtesting.py are NumPy arrays for performance. If you need a pandas Series (e.g. to call .rolling() or .shift() on the result), use the .s accessor:
class MyStrategy(Strategy):
    def init(self):
        close_series = self.data.Close.s     # pd.Series with DatetimeIndex
        df           = self.data.df          # full OHLCV pd.DataFrame

        # Build an indicator from a pandas operation
        self.signal = self.I(
            lambda s: s.rolling(5).mean() - s.rolling(20).mean(),
            close_series, name='momentum'
        )

NaN warm-up behavior

Many rolling indicators produce NaN values during their initial warm-up period (e.g. an SMA(200) will have 199 leading NaNs). backtesting.py automatically skips the first bars until all declared indicators have non-NaN values before starting to call next().
The simulation does not start on bar 0. It begins on the first bar where every indicator has a valid (non-NaN) value. For a strategy using a 200-bar moving average, next() is first called on bar 201.This means longer lookback periods reduce the number of bars available for trading, which can affect statistics — particularly on shorter datasets.
class HeavyMaStrategy(Strategy):
    def init(self):
        # next() won't be called until bar 201
        self.sma200 = self.I(SMA, self.data.Close, 200)
        # If also using SMA(10), still starts at bar 201 (the slower one dictates)
        self.sma10  = self.I(SMA, self.data.Close, 10)

Plotting options

Overlay vs separate panel

def init(self):
    # Plotted on price chart (auto-detected as close to price range)
    self.sma = self.I(SMA, self.data.Close, 20, overlay=True)

    # Plotted in its own panel below the price chart
    self.rsi = self.I(RSI, self.data.Close, 14, overlay=False)
The overlay heuristic checks whether more than 60% of indicator values fall within 30% of the closing price. If so, it defaults to overlay=True.

Custom colors

def init(self):
    self.fast = self.I(SMA, self.data.Close, 10,
                       color='red', overlay=True)
    self.slow = self.I(SMA, self.data.Close, 50,
                       color='#1E88E5', overlay=True)

Scatter markers

Use scatter=True to render the indicator as discrete circle markers rather than a continuous line — useful for signals or trade entry points:
def init(self):
    # Mark bars where volume spikes above its 20-bar average
    def volume_spike(vol, n):
        avg = pd.Series(vol).rolling(n).mean()
        return np.where(vol > 2 * avg, vol, np.nan)

    self.vol_spike = self.I(
        volume_spike, self.data.Volume, 20,
        name='Volume spike',
        overlay=False,
        scatter=True,
        color='orange'
    )

Hiding indicators from the plot

Set plot=False to compute an indicator for internal use without rendering it:
def init(self):
    # Used in next() but not plotted
    self.trend = self.I(SMA, self.data.Close, 100, plot=False)

Build docs developers (and LLMs) love