← Back to Home
Custom Indicator Development in Backtrader

Custom Indicator Development in Backtrader

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.

Core Components of an Indicator

To create a custom indicator in backtrader, certain elements are required:

Defining Indicator Logic: __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.

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).

2. Logic in 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):
    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).

3. Logic in 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.

Important Note: Idempotence

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:

Manual vs. Automatic Minimum Period

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.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:

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.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.

Example: Automatic minperiod with MACD

A 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 lines

In this MACD example, no addminperiod call is needed.

A Full Custom Indicator Example

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.

Plotting Enhancements:

Conclusion

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.