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 btclass 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 fromperiod
bars ago. As such, the platform knows it has to buffer those manyperiod
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)
- Declarative as above
- Non-declarative or step by step
For anyone interested the definitions of the Stochastic Indicator:
- Stockcharts: https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:stochastic_oscillator_fast_slow_and_full
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 btclass 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 btclass 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__
withaddminperiod
- 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.
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!