Indicator Development
If anything (besides one or more winning Strategies) must ever be developed,this something is a custom Indicator.
Such development within the platform is, according to the author, easy.
The following is needed:
A class derived from Indicator (either directly or from an already existing subclass)
Define thelines it will hold
An indicator must at least have 1 line. If deriving from an existing one,the line(s) may have already be defined
Optionally define parameters which can alter the behavior
Optionally provided/customize some of the elements which enable sensible plotting of the indicators
Provide a fully defined operation in__init__ with a binding (assignment) to the line(s) of the indicator or else providenext and (optionally)once methods
If an indicator can be fully defined with logic/arithmetic operations duringinitialization and the result is assigned to the line: done
Be it not the case, at least anext has to be provided where the indicatormust assign a value to the line(s) at index 0
Optimization of the calculation for therunonce mode (batch operation) canbe achieved by providing aonce method.
Important note: Idempotence
Indicators produce an output for each bar they receive. No assumption has to bemade about how many times the same bar will be sent. Operations have to beidempotent.
The rationale behind this:
- The same bar (index-wise) can be sent many times with changing values (namely the changing value is the closing price)
This enables, for example, “replaying” a daily session but using intraday datawhich could be made of 5 minutes bars.
It could also allow the platform to get values from a live feed.
A dummy (but functional) indicator
So can it be:
classDummyInd(bt.Indicator):lines=('dummyline',)params=(('value',5),)def__init__(self):self.lines.dummyline=bt.Max(0.0,self.params.value)
Done! The indicator will output always the same value: either 0.0 orself.params.value if it happens to be greater than 0.0.
The same indicator but using the next method:
classDummyInd(bt.Indicator):lines=('dummyline',)params=(('value',5),)defnext(self):self.lines.dummyline[0]=max(0.0,self.params.value)
Done! Same behavior.
Note
Notice how in the__init__ versionbt.Max is used to assign tothe Line objectself.lines.dummyline.
bt.Max returns anlines object that is automatically iterated foreach bar passed to the indicator.
Hadmax been used instead, the assigment would have beenpointless, because instead of a line, the indicator would have amember variable with a fixed value.
Duringnext the work is done directly with floating point valuesand the standardmax built-in can be used
Let’s recall thatself.lines.dummyline is the long notation and that it canbe shortened to:
and even to:
The latter being only possible if the code has not obscured this with a memberattribute.
The 3rd and last version provides an additionalonce method to optimize thecalculation:
classDummyInd(bt.Indicator):lines=('dummyline',)params=(('value',5),)defnext(self):self.lines.dummyline[0]=max(0.0,self.params.value)defonce(self,start,end):dummy_array=self.lines.dummyline.arrayforiinxrange(start,end):dummy_array[i]=max(0.0,self.params.value)
A lot more effective but developing theonce method has forced to scratch beyondthe surface. Actually the guts have been looked into.
The__init__ version is in any case the best:
Everything is confined to the initialization
next andonce (both optimized, becausebt.Max already has them) are provided automatically with no need to play with indices and/or formulas
Be it needed for development, the indicator can also override the methodsassociated tonext andonce:
prenext andnexstart
preonce andoncestart
Manual/Automatic Minimum Period
If possible the platform will calculate it, but manual action may be needed.
Here is a potential implementation of aSimple Moving Average:
classSimpleMovingAverage1(Indicator):lines=('sma',)params=(('period',20),)defnext(self):datasum=math.fsum(self.data.get(size=self.p.period))self.lines.sma[0]=datasum/self.p.period
Although it seems sound, the platform doesn’t know what the minimum period is,even if the parameter is named “period” (the name could be misleading and someindicators receive several “period”s which have different usages)
In this casenext would be called already for the 1st bar and everthingwould explode because get cannot return the neededself.p.period.
Before solving the situation something has to be taken into account:
- The data feeds passed to the indicators may already carry aminimum period
The sampleSimpleMovingAverage may be done on for example:
A regular data feed
This has a default mininum period of 1 (just wait for the 1st bar thatenters the system)
Another Moving Average … and this in turn already has aperiod
If this is 20 and again our sample moving average has also 20, we end upwith a minimum period of 40 bars
Actually the internal calculation says 39 … because as soon as the firstmoving average has produced a bar this counts for the next moving average,which creates an overlapping bar, thus 39 are needed.
Other indicators/objects which also carry periods
Alleviating the situation is done as follows:
classSimpleMovingAverage1(Indicator):lines=('sma',)params=(('period',20),)def__init__(self):self.addminperiod(self.params.period)defnext(self):datasum=math.fsum(self.data.get(size=self.p.period))self.lines.sma[0]=datasum/self.p.period
Theaddminperiod method is telling the system to take into account the extraperiod bars needed by this indicator to whatever minimum period there may bein existence.
Sometimes this is absolutely not needed, if all calculations are done withobjects which already communicate its period needs to the system.
A quickMACD implementation with Histogram:
frombacktrader.indicatorsimportEMAclassMACD(Indicator):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-me2self.l.signal=EMA(self.l.macd,period=self.p.period_signal)self.l.histo=self.l.macd-self.l.signal
Done! No need to think about mininum periods.
EMA stands forExponential Moving Average (a platform built-in alias)
And this one (already in the platform) already states what it needs
The named lines of the indicator “macd” and “signal” are being assigned objects which already carry declared (behind the scenes) periods
macd takes the period from the operation “me1 - me2” which has in turn take the maximum from the periods of me1 and me2 (which are both exponential moving averages with different periods)
signal takes directly the period of the Exponential Moving Average over macd. This EMA also takes into account the already existing macd period and the needed amount of samples (period_signal) to calculate itself
histo takes the maximum of the two operands “signal - macd”. Once both are ready can histo also produce a value
A full custom indicator
Let’s develop a simple custom indicator which “indicates” if a moving average(which can be modified with a parameter) is above the given data:
importbacktraderasbtimportbacktrader.indicatorsasbtindclassOverUnderMovAv(bt.Indicator):lines=('overunder',)params=dict(period=20,movav=btind.MovAv.Simple)def__init__(self):movav=self.p.movav(self.data,period=self.p.period)self.l.overunder=bt.Cmp(movav,self.data)
Done! The indicator will have a value of “1” if the average is above the dataand “-1” if below.
Be the data a regular data feed the 1s and -1s would be produced comparing withthe close price.
Although more can be seen in thePlotting section and to have a behaved andnice citizen in the plotting world, a couple of things can be added:
importbacktraderasbtimportbacktrader.indicatorsasbtindclassOverUnderMovAv(bt.Indicator):lines=('overunder',)params=dict(period=20,movav=bt.ind.MovAv.Simple)plotinfo=dict(# Add extra margins above and below the 1s and -1splotymargin=0.15,# Plot a reference horizontal line at 1.0 and -1.0plothlines=[1.0,-1.0],# Simplify the y scale to 1.0 and -1.0plotyticks=[1.0,-1.0])# Plot the line "overunder" (the only one) with dash style# ls stands for linestyle and is directly passed to matplotlibplotlines=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 thereplabels=[self.p.period]# Put only the moving average if it's not the default oneplabels+=[self.p.movav]*self.p.notdefault('movav')returnplabelsdef__init__(self):movav=self.p.movav(self.data,period=self.p.period)self.l.overunder=bt.Cmp(movav,self.data)