A Dynamic Indicator
Indicators are difficult beasts. Not because they are difficult to code in general, but mostly because the name is misleading and people have different expectations as to what an indicator is.
Let’s try to at least define what an Indicator is inside the backtrader ecosystem.
It is an object which defines at least one output line, may define parameters that influence its behavior and takes one or more data feeds as input.
In order to keep indicators as general as possible the following design principles were chosen:
-
The input data feeds can be anything that looks like a data feed, which brings an immediate advantage: because other indicators look like data feeds, one can pass indicators as the input to other indicators
-
No
datetime
line payload is carried. This is so, because the input may have nodatetime
payload itself to synchronize to. And synchronizing to the general system widedatetime
could be incorrect, because the indicator could be working with data from a weekly timeframe whereas the system time may be ticking in seconds, because that’s the lowest resolution one of several data feeds bears. -
Operations have to be idempotent, i.e.: if called twice with the same input and without change in the parameters, the output has to be the same.
Take into account that an indicator can be asked to perform an operation several times at the same point in time with the same input. Although this would seem not needed, it is if the system support data replaying (i.e.: building a larger timeframe in real-time from a smaller timeframe)
-
And finally: an Indicator writes its output value to the current moment of time, i.e.: index
0
. If not it will named aStudy
. AStudy
will look for patterns and write output values in the past.See for example the Backtrader Community - ZigZag
Once the definitions (in the backtrader ecosystem) are clear, let’s try to see how we can actually code a dynamic indicator. It would seem we cannot, because looking at the aforementioned design principles, the operational procedure of an indicator is more or less … non-mutable.
The Highest High … since …
One indicator which is usually put in motion is the Highest
(alias
MaxN
), to get the highest something in a given period. As in
import backtrader as bt class MyStrategy(bt.Strategy) def __init__(self): self.the_highest_high_15 = bt.ind.Highest(self.data.high, period=15) def next(self): if self.the_highest_high_15 > X: print('ABOUT TO DO SOMETHING')
In this snippet we instantiate Highest
to keep track of the highest high
along the last 15 periods. Were the highest high greater than X
something
would be done.
The catch here:
- The
period
is fixed at15
Making it dynamic
Sometimes, we would need the indicator to be dynamic and change its behavior to react to real-time conditions. See for example this question in the backtrader community: Highest high since position was opened
We of course don’t know when a position is going to be opened/closed and
setting the period
to a fixed value like 15
would make no sense. Let’s
see how we can do it, packing everything in an indicator
Dynamic params
We’ll first be using parameters that we’ll be changing during the life of the indicator, achieving dynamism with it.
import backtrader as bt class DynamicHighest(bt.Indicator): lines = ('dyn_highest',) params = dict(tradeopen=False) def next(self): if self.p.tradeopen: self.lines.dyn_highest[0] = max(self.data[0], self.dyn_highest[-1]) class MyStrategy(bt.Strategy) def __init__(self): self.dyn_highest = DynamicHighest(self.data.high) def notify_trade(self, trade): self.dyn_highest.p.tradeopen = trade.isopen def next(self): if self.dyn_highest > X: print('ABOUT TO DO SOMETHING')
Et voilá! We have it and we have so far not broken the rules laid out for our indicators. Let’s look at the indicator
-
It defines an output line named
dyn_highest
-
It has one parameter
tradeopen=False
-
(Yes, it takes data feeds, simply because it subclasses
Indicator
) -
And if we were to call
next
always with the same input, it would always return the same value
The only thing:
- If the value of the parameter changes, the output changes (the rules above said the output remains constant as long as the parameters don’t change)
We use this in notify_trade
to influence our DynamicHighest
-
We use the value
isopen
of the notifiedtrade
as a flag to know if we have to record the highest point of the input data -
When the
trade
closes, the value ofisopen
will beFalse
and we will stop recording the highest value
For reference see: Backtrader Documentation Trade
Easy!!!
Using a method
Some people would argue against the modification of a param
which is part
of the declaration of the Indicator and should only be set during the
instantiation.
Ok, let’s go for a method.
import backtrader as bt class DynamicHighest(bt.Indicator): lines = ('dyn_highest',) def __init__(self): self._tradeopen = False def tradeopen(self, yesno): self._tradeopen = yesno def next(self): if self._tradeopen: self.lines.dyn_highest[0] = max(self.data[0], self.dyn_highest[-1]) class MyStrategy(bt.Strategy) def __init__(self): self.dyn_highest = DynamicHighest(self.data.high) def notify_trade(self, trade): self.dyn_highest.tradeopen(trade.isopen) def next(self): if self.dyn_highest > X: print('ABOUT TO DO SOMETHING')
Not a huge difference, but now the indicator has some extra boilerplate with
__init__
and the method tradeopen(self, yesno)
. But the dynamics of our
DynamicHighest
are the same.
Bonus: let’s make it general purpose
Let’s recover the params
and make the Indicator one that can apply
different functions and not only max
import backtrader as bt class DynamicFn(bt.Indicator): lines = ('dyn_highest',) params = dict(fn=None) def __init__(self): self._tradeopen = False # Safeguard for not set function self._fn = self.p.fn or lambda x, y: x def tradeopen(self, yesno): self._tradeopen = yesno def next(self): if self._tradeopen: self.lines.dyn_highest[0] = self._fn(self.data[0], self.dyn_highest[-1]) class MyStrategy(bt.Strategy) def __init__(self): self.dyn_highest = DynamicHighest(self.data.high, fn=max) def notify_trade(self, trade): self.dyn_highest.tradeopen(trade.isopen) def next(self): if self.dyn_highest > X: print('ABOUT TO DO SOMETHING')
Said and done! We have added:
-
params=dict(fn=None)
To collect the function the end user would like to use
-
A safeguard to use a placeholder function if the user passes no specific function:
# Safeguard for not set function self._fn = self.p.fn or lambda x, y: x
-
And we use the function (or the placeholder) for the calculation:
self.lines.dyn_highest[0] = self._fn(self.data[0], self.dyn_highest[-1])
-
Stating in the invocation of our (now named)
DynamicFn
indicator which function we want to use …max
(no surprises here):self.dyn_highest = DynamicHighest(self.data.high, fn=max)
Not much more left today … Enjoy it!!!