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
once
The 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):
= ('dummyline',)
lines
= (('value', 5),)
params
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).
next
If 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):
= ('dummyline',)
lines
= (('value', 5),)
params
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):
= ('dummyline',)
lines
= (('value', 5),)
params
def next(self):
self.lines.dummyline[0] = max(0.0, self.params.value)
def once(self, start, end):
= self.lines.dummyline.array
dummy_array
for i in xrange(start, end): # xrange for Python 2, use range for Python 3
= max(0.0, self.params.value) dummy_array[i]
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
= ('sma',)
lines = (('period', 20),)
params
def next(self):
= math.fsum(self.data.get(size=self.p.period))
datasum self.lines.sma[0] = datasum / self.p.period
This 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
= ('sma',)
lines = (('period', 20),)
params
def __init__(self):
self.addminperiod(self.params.period)
def next(self):
= math.fsum(self.data.get(size=self.p.period))
datasum self.lines.sma[0] = datasum / self.p.period
self.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
= ('macd', 'signal', 'histo',)
lines = (('period_me1', 12), ('period_me2', 26), ('period_signal', 9),)
params
def __init__(self):
= EMA(self.data, period=self.p.period_me1)
me1 = EMA(self.data, period=self.p.period_me2)
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 lines
In 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):
= ('overunder',)
lines = dict(period=20, movav=btind.MovAv.Simple)
params
= dict(
plotinfo # Add extra margins above and below the 1s and -1s
=0.15,
plotymargin
# Plot a reference horizontal line at 1.0 and -1.0
=[1.0, -1.0],
plothlines
# Simplify the y scale to 1.0 and -1.0
=[1.0, -1.0])
plotyticks
# Plot the line "overunder" (the only one) with dash style
# ls stands for linestyle and is directly passed to matplotlib
= dict(overunder=dict(ls='--'))
plotlines
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
= [self.p.period]
plabels
# Put only the moving average if it's not the default one
+= [self.p.movav] * self.p.notdefault('movav')
plabels
return plabels
def __init__(self):
= self.p.movav(self.data, period=self.p.period)
movav 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.