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
datetimeline payload is carried. This is so, because the input may have nodatetimepayload itself to synchronize to. And synchronizing to the general system widedatetimecould 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. AStudywill 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
periodis 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
nextalways 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
isopenof the notifiedtradeas a flag to know if we have to record the highest point of the input data -
When the
tradecloses, the value ofisopenwill beFalseand 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)
DynamicFnindicator 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!!!