Custom Indicator Development in Python with backtrader

The origins of backtrader are rooted in a simple idea:

  • Being able to quickly test and prototype new indicators and strategies

Being one of the reasons why Python was chosen as the language and after some iterations the “canonical” way to develop Indicators was to use a declarative approach, i.e.: declare during __init__ the entire set of operations/formulas that make up the indicator (and where needed be, some extra calculations during next although this is avoided whenever possible.

Coupled with the already declarative approach the lines of an indicator (output arrays) and the params which can be passed to it, one can have a complete declarative approach. There is actually no need to declare any input because this is handled automatically with the automagically provided self.datas array (and the aliases self.data0, self.data1, self.dataX) which is already available for the indicator.

Let’s see a very dummy Indicator which will simply divide the the difference of the current data point minus a previous data point

import backtrader as bt
class DummyDifferenceDivider(bt.Indicator):
lines = ('diffdiv',) # output line (array)
params = (
('period', 1), # distance to previous data point
('divfactor', 2.0), # factor to use for the division
)
    def __init__(self):
diff = self.data(0) - self.data(-self.p.period)
diffdiv = diff / self.p.divfactor
        self.lines.diffdiv = diffdiv

It should self-explanatory. The actual operation in __init__ could have been written in a single line, but it has been divided into the 3 basic steps (difference, division and assignment to array) for clarity.

One thing to note here if you haven’t done Indicator development in backtrader ist that the minimum period constraints needed are automatically calculated from the constraints. What are these constraints in this case?

  • self.data(-self.p.period) is pulling the data from period bars ago. As such, the platform knows it has to buffer those many period bars.

This is a declarative Indicator as explained above. There are no step by step calculations. The question here:

  • Can it be done in a different way? Yes

Rather than seeing it with that dummy Indicator, a real life example is going to be used from a discussion in the backtrader Community. The link

In that thread the user is trying to develop (doing it himself rather than asking for someone to write the code for him, which is quite common in our modern days) a custom Stochastic which first calculates the actual stochastic value and then a value derived from it.

Two approaches are going to be examined (and charted to visually see the results are the same)

  1. Declarative as above
  2. Non-declarative or step by step

For anyone interested the definitions of the Stochastic Indicator:

Summarizing:

  • %K = (Current Close — Lowest Low (x periods ago))/(Highest High (x periods ago) — Lowest Low (x periods ago)) * 100
  • %D = y-day SMA of %K

There are Fast, Slow and Full versions of the Stochastic. For simplicity this will focus on the Fast version, which simply calculates %K and %D and doesn’t perform any additional smoothing (“slowing”)

In this case the lowest low and highest high will be taken from the data (ideally the high and low components should be considered)

Declarative Approach

Simple version of a Fast Stochastic which uses a single value data feed and doesn’t handle division by zero errors.

The customized value mystoc will be a very simple operation:

abs(k - k(-1)) / 2

I.e.: the absolute half value of the difference between the current k and the previous k (which is depicted as k(-1) )

import backtrader as bt
class MyStochastic1(bt.Indicator):
lines = ('k', 'd', 'mystoc',) # declare the output lines
    params = (
('k_period', 14), # lookback period for highest/lowest
('d_period', 3), # smoothing period for d with the SMA
)

def __init__(self):
# declare the highest/lowest
highest = bt.ind.Highest(self.data, period=self.p.k_period)
lowest = bt.ind.Lowest(self.data, period=self.p.k_period)
        # calculate and assign lines
self.lines.k = k = (self.data - lowest) / (highest - lowest)
self.lines.d = d = bt.ind.SMA(k, period=self.p.d_period)
self.lines.mystoc = abs(k - k(-1)) / 2.0

Et voilá! It does actually seem very similar to the definition.

Non-declarative Approach

Let’s get dirty

import backtrader as bt
class MyStochastic2(bt.Indicator):
lines = ('k', 'd', 'mystoc',)
# manually counted period
# 14 for the fast moving k
# 3 for the slow moving d
# No extra for the previous k (-1) is needed because
# already buffers more than the 1 period lookback
# If we were doing d - d(-1), there is nothing making
# sure k(-1) is being buffered and an extra 1 would be needed
    params = (
('k_period', 14), # lookback period for highest/lowest
('d_period', 3), # smoothing period for d with the SMA
)

    def __init__(self):
self.addminperiod(self.p.k_period + self.p.d_period)

def next(self):
# Get enough data points to calculate k and do it
d = self.data.get(size=self.p.k_period)
hi = max(d)
lo = min(d)
self.lines.k[0] = k0 = (self.data[0] - lo) / (hi - lo)
        # Get enough ks to calculate the SMA of k. Assign to d
last_ks = self.l.k.get(size=self.p.d_period)
self.lines.d[0] = sum(last_ks) / self.p.d_period
        # Now calculate mystoc
self.lines.mystoc[0] = abs(k0 - self.l.k[-1]) / 2.0

In comparison with the declarative approach the following can be seen

  • Manual calculation of the lookback period and having to understand what contributes to the actual lookback period
  • Manual set-up of the lookback period during __init__ with addminperiod
  • Using get with the different data and lines objects to get the buffers for calculations
  • The simple moving average is calculated manually

In any case it is for sure not clearer and not cleaner and with many things to consider.

The graphical output

Now and using the sample data that is bundled with backtrader, and a script using the standard skeleton most samples use, the two indicators will be put in play to show that they are actually equivalent.

The two custom Stochastic Indicators shown

Conclusion

Both approaches deliver the same results. The declarative approach was the one conceived for the platform, but this doesn’t have to be what everybody likes and a step by step approach is also possible (and mixing both of course)

In any case, the goal was to be able to quickly and easily conceive and develop new indicators … and at least in the opinion of the author, the goal was reached.

Enjoy!