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 no datetime payload itself to synchronize to. And synchronizing to the general system wide datetime 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 a Study. A Study 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 at 15

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 notified trade as a flag to know if we have to record the highest point of the input data
  • When the trade closes, the value of isopen will be False 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!!!

Comments

Fork me on GitHub