Skip to main content
On this page

Build Custom Backtrader Indicators: Lines, Timing and Tests

· 7 min read
Pineify Team
Pine Script and AI trading workflow research team

A custom Backtrader indicator is a Python class that turns input data into one or more output lines. The useful first test is whether its values match a calculation you can check by hand. A profitable backtest cannot compensate for an inverted signal or a feature that uses information unavailable at the decision time.

Build Custom Backtrader Indicators: Complete Guide to Advanced Trading Tools

Define the output before writing the strategy​

For a moving-average comparison, define the intended sign explicitly:

  • +1: the current close is above its moving average.
  • 0: they are equal.
  • -1: the close is below its moving average.

Backtrader's comparison operator evaluates its first argument relative to its second. Its implementation makes operand order significant. The older version of this guide used the opposite order while describing these signs.

import backtrader as bt

class PriceVsAverage(bt.Indicator):
lines = ('signal',)
params = (('period', 3),)

def __init__(self):
if self.p.period < 1:
raise ValueError('period must be positive')
average = bt.indicators.SMA(self.data.close, period=self.p.period)
self.l.signal = bt.Cmp(self.data.close, average)

lines declares the output; params declares a configurable input. The SMA dependency supplies its minimum history requirement. The value 3 is chosen to make the example easy to inspect, not because it is a recommended trading setting.

Understand calculation timing​

In __init__, arithmetic between Backtrader lines constructs dependencies. It does not evaluate a single final price and then hold that result forever. When a calculation cannot be expressed with those dependencies, a next() implementation can assign the current output using index [0].

The indicator development guide describes both approaches, automatic minimum periods and addminperiod() for calculations whose history requirement is otherwise unknown. Do not add a period blindly to an already composed indicator: its dependencies may already communicate the required warm-up.

Live or replayed data can update the same bar more than once. Repeated calls must not accidentally double-count state. That is why an assumption such as "every next() call means a new completed candle" is unsafe.

The platform lifecycle includes prenext, nextstart and next. Waiting for normal next() does not stop code you explicitly put in prenext from placing an order. Multi-feed strategies also need to inspect timestamps; the latest value of each feed need not represent the same newly completed interval.

Run a small calculation test​

The following test runner uses the class above and synthetic daily prices. It places no orders and makes no performance claim. Save both code blocks in one Python file in an environment with backtrader==1.9.78.123 installed.

import csv
import datetime
import os
import tempfile

class CheckIndicator(bt.Strategy):
params = (('period', 3),)

def __init__(self):
self.ind = PriceVsAverage(self.data, period=self.p.period)
self.observed = []

def next(self):
self.observed.append(self.ind.signal[0])

prices = [1, 2, 3, 2, 1, 2, 3]
with tempfile.TemporaryDirectory() as folder:
path = os.path.join(folder, 'synthetic.csv')
with open(path, 'w', newline='') as handle:
writer = csv.writer(handle)
writer.writerow(['date', 'open', 'high', 'low', 'close', 'volume', 'oi'])
for index, price in enumerate(prices):
day = datetime.date(2020, 1, 1) + datetime.timedelta(days=index)
writer.writerow([day.isoformat(), price, price, price, price, 1, 0])

for batch_mode in (True, False):
engine = bt.Cerebro(stdstats=False)
engine.adddata(bt.feeds.GenericCSVData(
dataname=path, dtformat='%Y-%m-%d',
))
engine.addstrategy(CheckIndicator, period=3)
result = engine.run(runonce=batch_mode)[0]
assert result.observed == [1.0, -1.0, -1.0, 1.0, 1.0]
print(batch_mode, result.observed)

The first output occurs on the third observation. At that point the mean of 1, 2, 3 is 2, so the signal is positive. On the next observation the mean of 2, 3, 2 exceeds the current price 2, so the signal is negative. Run additional cases with flat prices, a longer warm-up and invalid input values before using the indicator in a strategy.

Add multiple lines deliberately​

A custom class can expose several related calculations. For example, the following class publishes a moving average and symmetric standard-deviation bands:

class CustomVolatilityBands(bt.Indicator):
lines = ('upperband', 'lowerband', 'midline')
params = (('period', 20), ('deviation', 2.0))

def __init__(self):
sma = bt.indicators.SimpleMovingAverage(self.data, period=self.p.period)
stddev = bt.indicators.StandardDeviation(self.data, period=self.p.period)
self.l.midline = sma
self.l.upperband = sma + (self.p.deviation * stddev)
self.l.lowerband = sma - (self.p.deviation * stddev)

These bands do not establish a probability that price will remain inside them. Test constant-price data, where the standard deviation and band width should be zero, and compare nonconstant data against the same standard-deviation convention used by the library.

Plot behavior is configurable. Backtrader distinguishes object-level plotinfo from per-line plotlines; line styling and suppression belong to the relevant setting rather than a blanket claim that every output always appears. See the plotting reference.

Pass parameters through the strategy​

An indicator parameter is not automatically a strategy parameter. The test runner declares period on CheckIndicator and passes it to PriceVsAverage. Follow the same pattern in a trading strategy before using optstrategy.

For a fresh engine with a suitable data feed, engine.optstrategy(CheckIndicator, period=range(10, 31, 5)) requests periods 10, 15, 20, 25 and 30. This particular strategy only records values; it does not evaluate returns. Do not mix addstrategy and optstrategy in the test runner and assume the original assertion applies to different periods.

Optimization evaluates candidates, not future reliability. Keep a separate evaluation period and record the full set of settings tried. If you repeatedly select settings after seeing that evaluation period, it becomes part of development data.

External features, execution and speed​

Extra feed columns require a feed definition and column mapping. An economic release or machine-learning prediction must be timestamped when it became available, not assigned to earlier bars retrospectively. A model trained on the entire backtest includes future information even if its prediction is accessed through next().

For an actual order strategy, add order-status handling, explicit position sizing and a defined exit. Calling buy() requests an order; it does not prove a fill at the current close. Include commission and slippage, inspect rejected or unfilled orders, and compare the recorded execution time with the signal time. The order execution documentation describes the simulation rules.

Backtrader supports batch runonce processing and per-step calculation. Composed indicators can reuse optimized operations, but this is not a universal speed guarantee or the same thing as a NumPy vectorization benchmark. Measure the same data and outputs in both modes before making a performance claim. Memory use also depends on buffering settings and data volume, not only the number of declared lines.

Frequently asked questions​

How do I read the previous indicator value?

Index zero is the current value; index minus one is the previous value in that line. Check available history and the feed's update timing before treating it as a completed interval.

Does addminperiod make an indicator correct?

It communicates a history requirement. It does not verify the formula, prevent future-data leakage or prove profitable trading behavior.

Can I reuse another custom indicator?

Yes. Instantiate it with the intended data and parameters, then use its output lines. Test the composed warm-up and output alignment as well as each component.

Can this Python indicator run directly on TradingView?

No. TradingView uses Pine Script. A translation needs separate checks of initialization, missing values, bar timing and order execution assumptions.

Apply the same calculation discipline in Pine Script​

Pineify provides Pine Script authoring workflows for TradingView. Its output is a separate implementation from this Python example. Compare indicator values on matching inputs, compile the script and test strategy behavior in TradingView before assuming equivalence. For a separate Python framework, see the Backtesting.py workflow guide.

Pineify Website