Developing custom indicators is a fundamental aspect of creating
tailored trading strategies in backtrader. The platform is
designed to make this process intuitive and straightforward. This guide
outlines the essential components and best practices for building your
own indicators, from simple calculations to sophisticated
visualizations.
To create a custom indicator in backtrader, certain
elements are required:
bt.Indicator (or an existing subclass).lines attribute, which declares the output line(s) your
indicator will produce. An indicator must have at least one line. If
deriving from an existing indicator, lines may already be defined.params): Optionally,
define parameters to allow customization of the indicator’s
behavior.__init__, next, and
onceThe core logic of an indicator can be implemented in one of three
primary ways: entirely within __init__, or using
next, or by optimizing with once.
__init__
(Recommended)This is often the cleanest and most efficient approach for indicators
that can be fully defined using backtrader’s line objects
and operations during initialization.
class DummyInd(bt.Indicator):
lines = ('dummyline',)
params = (('value', 5),)
def __init__(self):
self.lines.dummyline = bt.Max(0.0, self.params.value)In this example, bt.Max is a backtrader
operation that returns a line object. When assigned to
self.lines.dummyline, backtrader automatically
handles the iteration for each bar, effectively providing optimized
next and once methods in the background. The
indicator will always output 0.0 or
self.params.value if it’s greater than
0.0.
Note: It is crucial to use backtrader’s line operations
(like bt.Max, bt.indicators.SMA, arithmetic
operations on lines) when assigning to
self.lines.<your_line> within __init__.
Using standard Python built-ins (e.g., max) would assign a
fixed value, not a dynamic line object.
Shorthand notations for accessing lines are also available:
self.lines.dummyline can be shortened to
self.l.dummyline or even self.dummyline
(provided no other member attribute obscures it).
nextIf an indicator’s calculation requires bar-by-bar processing that
cannot be easily expressed with backtrader’s line
operations in __init__, the logic must be placed in the
next method. This method is executed for each incoming
bar.
class DummyInd(bt.Indicator):
lines = ('dummyline',)
params = (('value', 5),)
def next(self):
self.lines.dummyline[0] = max(0.0, self.params.value)Here, self.lines.dummyline[0] is used to assign the
current value to the line at index 0 (the current bar). This method
directly uses floating-point values and standard Python built-ins (like
max).
once
(for Optimization)For indicators with next method logic, performance can
be optimized for “runonce” mode (batch processing of historical data) by
implementing a once method. This method processes a range
of bars in a single go, often leveraging NumPy for speed.
class DummyInd(bt.Indicator):
lines = ('dummyline',)
params = (('value', 5),)
def next(self):
self.lines.dummyline[0] = max(0.0, self.params.value)
def once(self, start, end):
dummy_array = self.lines.dummyline.array
for i in xrange(start, end): # xrange for Python 2, use range for Python 3
dummy_array[i] = max(0.0, self.params.value)Developing the once method requires a deeper
understanding of backtrader’s internal data structures
(e.g., lines.array), making it more complex than the
__init__ approach.
Indicators must produce the same output for a given bar, regardless of how many times that bar is sent. This property, known as idempotence, is crucial because:
backtrader to process data from live
feeds.Indicators often require a certain number of past bars to calculate
their current value (e.g., a 20-period moving average needs 20 bars).
This is the “minimum period” (minperiod).
Consider a naive Simple Moving Average implementation:
import math
# from backtrader.indicators import Indicator # Assuming Indicator is imported
class SimpleMovingAverage1(bt.Indicator): # Using bt.Indicator assuming it's imported as bt
lines = ('sma',)
params = (('period', 20),)
def next(self):
datasum = math.fsum(self.data.get(size=self.p.period))
self.lines.sma[0] = datasum / self.p.periodThis code would likely fail when executed because next
would be called for the very first bar, but
self.data.get(size=self.p.period) would not have enough
data.
The system often calculates minperiod automatically, but
manual intervention might be needed. The minperiod can be
influenced by:
minperiod of 1).minperiod depends on the minperiod of the
indicators it consumes (e.g., an SMA of another SMA).To manually ensure the indicator waits for enough data, use
self.addminperiod in __init__:
# import math # Assuming math is imported
# from backtrader.indicators import Indicator # Assuming Indicator is imported
class SimpleMovingAverage1(bt.Indicator): # Using bt.Indicator assuming it's imported as bt
lines = ('sma',)
params = (('period', 20),)
def __init__(self):
self.addminperiod(self.params.period)
def next(self):
datasum = math.fsum(self.data.get(size=self.p.period))
self.lines.sma[0] = datasum / self.p.periodself.addminperiod(self.params.period) tells the system
to factor in these additional required bars.
However, manual addminperiod is often not
needed if calculations are done primarily with
backtrader’s built-in line objects and operations, as they
automatically communicate their minperiod requirements.
minperiod with MACDA quick implementation of MACD with a Histogram demonstrates how
minperiod is handled automatically when chaining
backtrader’s built-in indicators:
from backtrader.indicators import EMA # EMA is a platform built-in alias
class MACD(bt.Indicator): # Using bt.Indicator assuming it's imported as bt
lines = ('macd', 'signal', 'histo',)
params = (('period_me1', 12), ('period_me2', 26), ('period_signal', 9),)
def __init__(self):
me1 = EMA(self.data, period=self.p.period_me1)
me2 = EMA(self.data, period=self.p.period_me2)
self.l.macd = me1 - me2 # 'macd' line implicitly inherits minperiod from me1 and me2
self.l.signal = EMA(self.l.macd, period=self.p.period_signal) # 'signal' line inherits minperiod from macd line and its own period
self.l.histo = self.l.macd - self.l.signal # 'histo' line inherits minperiod from macd and signal linesIn this MACD example, no addminperiod call
is needed.
EMA (Exponential Moving Average) automatically declares
its period needs.me1 and me2 are created, they
internally register their minperiod.macd line, being me1 - me2,
automatically derives its minperiod as the maximum of
me1 and me2’s minperiod.signal line, being an EMA of the
macd line, correctly accounts for both the
macd line’s minperiod and its own
period_signal.histo line similarly considers the
minperiod of both macd and signal
lines.Let’s develop a simple custom indicator that indicates if a moving average is above or below the given data’s close price. We’ll also enhance it for better plotting.
import backtrader as bt
import backtrader.indicators as btind
class OverUnderMovAv(bt.Indicator):
lines = ('overunder',)
params = dict(period=20, movav=btind.MovAv.Simple)
plotinfo = dict(
# Add extra margins above and below the 1s and -1s
plotymargin=0.15,
# Plot a reference horizontal line at 1.0 and -1.0
plothlines=[1.0, -1.0],
# Simplify the y scale to 1.0 and -1.0
plotyticks=[1.0, -1.0])
# Plot the line "overunder" (the only one) with dash style
# ls stands for linestyle and is directly passed to matplotlib
plotlines = dict(overunder=dict(ls='--'))
def _plotlabel(self):
# This method returns a list of labels that will be displayed
# behind the name of the indicator on the plot
# The period must always be there
plabels = [self.p.period]
# Put only the moving average if it's not the default one
plabels += [self.p.movav] * self.p.notdefault('movav')
return plabels
def __init__(self):
movav = self.p.movav(self.data, period=self.p.period)
self.l.overunder = bt.Cmp(movav, self.data)This OverUnderMovAv indicator will have a value of
1 if the moving average is above the data (close price) and
-1 if below.
plotinfo: A dictionary that provides
general plotting hints to backtrader.
plotymargin: Adds extra vertical margin above and below
the plotted line.plothlines: Draws horizontal reference lines at
specified values (here, 1.0 and -1.0).plotyticks: Simplifies the y-axis ticks to only show
1.0 and -1.0.plotlines: A dictionary defining
specific plotting attributes for individual lines.
overunder=dict(ls='--'): Specifies that the
overunder line should be plotted with a dashed linestyle
(ls='--'), which is directly passed to
matplotlib._plotlabel(): This method returns a
list of labels that will appear next to the indicator’s name on the
plot. It’s dynamically generated to include the period and
the movav type if it’s not the default.backtrader simplifies the development of custom
indicators by providing a clear class structure and powerful built-in
functionalities. By understanding how to define lines, parameters, and
implement logic (preferably in __init__ for automatic
optimization), along with managing minimum periods and customizing
plotting, developers can create sophisticated and visually informative
technical indicators to power their trading strategies.