Skip to content

Momentum Strategy

In another great post, Teddy Koker, has shown again a path for the development of algotrading strategies:

  • Research first applying pandas
  • Backtesting then using backtrader

Kudos!!!

The post can be found at:

Teddy Koker dropped me a message, asking if I could comment on the usage of backtrader. And my opinion can be seen below. It is only my personal humble opinion, because as the author of backtrader I am biased as to how the platform could be best used.

And my personal taste about how to formulate certain constructs, does not have to match how other people prefer to use the platform.

Note

Actually, letting the platform open to plug almost anything and with different ways to do the same thing, was a conscious decision, to let people use it however they see fit (within the constraints of what the platform aims to do, the language possibilities and the failed design decisions I made)

Here, we will just focus on things which could have been done in a different manner. Whether "different" is better or not is always a matter of opinion. And the author of backtrader does not always have to be right on what it is actually "better" for developing with "backtrader" (because the actual development has to suit the developer and not the author of "backtrader")

Params: dict vs tuple of tuples

Many of the samples provided with backtrader and also available in the documentation and/or blog, use the tuple of tuples pattern for the parameters. For example from the code:

class Momentum(bt.Indicator):
    lines = ('trend',)
    params = (('period', 90),)

Together with this paradigm, one has always had the chance to use a dict.

class Momentum(bt.Indicator):
    lines = ('trend',)
    params = dict(period=90)  # or params = {'period': 90}

Over time this has turned to be lighter to use and become the preferred pattern for the author.

Note

The author prefers the dict(period=90), being easier to type, not needing quotes. But the curly braces notation, {'period': 90}, is preferred by many others.

The underlying difference between the dict and tuple approaches:

  • With a tuple of tuples parameters retain the order of declaration, which can be of importance when enumerating them.

    Tip

    The declaration order should be no problem with default ordered dictionaries in Python 3.7 (and 3.6 if using CPython even if it is an implementation detail)

In the examples modified by the author below, the dict notation will be used.

The Momentum indicator

In the article, this is how the indicator is defined

class Momentum(bt.Indicator):
    lines = ('trend',)
    params = (('period', 90),)

    def __init__(self):
        self.addminperiod(self.params.period)

    def next(self):
        returns = np.log(self.data.get(size=self.p.period))
        x = np.arange(len(returns))
        slope, _, rvalue, _, _ = linregress(x, returns)
        annualized = (1 + slope) ** 252
        self.lines.trend[0] = annualized * (rvalue ** 2)

Use the force, i.e.: use something which is already there like the PeriodN indicator, which:

  • Already defines a period parameter and knows how to pass it to the system

As such, this could be better

class Momentum(bt.ind.PeriodN):
    lines = ('trend',)
    params = dict(period=50)

    def next(self):
        ...

We are already skipping the need to define __init__ for the only purpose of using addminperiod, which should only be used in exceptional cases.

To carry on, backtrader defines an OperationN indicator which must have an attribute func defined, which will get period bars passed as an argument and which will put the return value into the defined line.

With that in mind, one can imagine the following as the potential code

def momentum_func(the_array):
    r = np.log(the_array)
    slope, _, rvalue, _, _ = linregress(np.arange(len(r)), r)
    annualized = (1 + slope) ** 252
    return annualized * (rvalue ** 2)


class Momentum(bt.ind.OperationN):
    lines = ('trend',)
    params = dict(period=50)
    func = momentum_func

Which means that we have taken the complexity of the indicator outside of the indicator. We could even be importing momentum_func from a external library and the indicator would need no change to reflect a new behavior if the underlying function changes. As a bonus we have purely declarative indicator. No __init__, no addminperiod and no next

The Strategy

Let's look at the __init__ part.

class Strategy(bt.Strategy):
    def __init__(self):
        self.i = 0
        self.inds = {}
        self.spy = self.datas[0]
        self.stocks = self.datas[1:]

        self.spy_sma200 = bt.indicators.SimpleMovingAverage(self.spy.close,
                                                            period=200)
        for d in self.stocks:
            self.inds[d] = {}
            self.inds[d]["momentum"] = Momentum(d.close,
                                                period=90)
            self.inds[d]["sma100"] = bt.indicators.SimpleMovingAverage(d.close,
                                                                       period=100)
            self.inds[d]["atr20"] = bt.indicators.ATR(d,
                                                      period=20)

Some things about the style:

  • Use parameters where possible rather than fixed values

  • Use shorter and the shorter names (for imports for example), it will in most cases increase readability

  • Use Python to its full extent

  • Don't use close for a data feed. Pass the data feed generically and it will use close. This may not seem relevant but it does help when trying to keep the code generic everywhere (like in indicators)

The first thing that one would/should consider: keep everything as a parameter if possible. Hence

class Strategy(bt.Strategy):
    params = dict(
        momentum=Momentum,  # parametrize the momentum and its period
        momentum_period=90,

        movav=bt.ind.SMA,  # parametrize the moving average and its periods
        idx_period=200,
        stock_period=100,

        volatr=bt.ind.ATR,  # parametrize the volatility and its period
        vol_period=20,
    )


    def __init__(self):
        # self.i = 0  # See below as to why the counter is commented out
        self.inds = collections.defaultdict(dict)  # avoid per data dct in for

        # Use "self.data0" (or self.data) in the script to make the naming not
        # fixed on this being a "spy" strategy. Keep things generic
        # self.spy = self.datas[0]
        self.stocks = self.datas[1:]

        # Again ... remove the name "spy"
        self.idx_mav = self.p.movav(self.data0, period=self.p.idx_period)
        for d in self.stocks:
            self.inds[d]['mom'] = self.p.momentum(d, period=self.momentum_period)
            self.inds[d]['mav'] = self.p.movav(d, period=self.p.stock_period)
            self.inds[d]['vol'] = self.p.volatr(d, period=self.p.vol_period)

By using params and changing a couple of the naming conventions, we have made the __init__ (and with it the strategy) fully customizable and generic (no spy references anyhwere)

next and its len

backtrader tries to use the Python paradigms where possible. It does for sure sometimes fail, but it tries.

Let us see what happens in next

    def next(self):
        if self.i % 5 == 0:
            self.rebalance_portfolio()
        if self.i % 10 == 0:
            self.rebalance_positions()
        self.i += 1

Here is where the Python len paradigm helps. Let's use it

    def next(self):
        l = len(self)
        if l % 5 == 0:
            self.rebalance_portfolio()
        if l % 10 == 0:
            self.rebalance_positions()

As you may see, there is no need to keep the self.i counter. The length of the strategy and of most objects is provided, calculated and updated by the system all along the way.

next and prenext

The code contains this forwarding

    def prenext(self):
        # call next() even when data is not available for all tickers
        self.next()

And there IS NO safeguard when entering next

    def next(self):
        if self.i % 5 == 0:
            self.rebalance_portfolio()
        ...

Ok, we know that a survivorship bias-free data set is in use, but in general not safeguarding the prenext => next forwarding is not a good idea.

  • backtrader calls next when all buffers (indicators, data feeds) can deliver at least data point. A 100-bar moving average will obviously only deliver when it has 100 data points from the data feed.

    This means that when entering next, the data feed will have 100 data points to be examined and the moving average just 1 data point

  • backtrader offers prenext as hook to let the developer access things before the aforementioned guarantee can be met. This is useful for example when several data feeds are in play and they start date is different. The developer may want some examination or action be taken, before all guarantees for all data feeds (and associated indicators) are met and next is called for the first time.

In a general case the prenext => next forwarding should have a guard such as this:

    def prenext(self):
        # call next() even when data is not available for all tickers
        self.next()

    def next(self):
        d_with_len = [d for d in self.datas if len(d)]
        ...

Which means that only the subset d_with_len from self.datas can be used with guarantees.

Note

A similar guard has to used for indicators.

Because it would seem pointless to do this calculation for the entire life of a strategy, an optimization is possible such as this

    def __init__(self):
        ...
        self.d_with_len = []


    def prenext(self):
        # Populate d_with_len
        self.d_with_len = [d for d in self.datas if len(d)]
        # call next() even when data is not available for all tickers
        self.next()

    def nextstart(self):
        # This is called exactly ONCE, when next is 1st called and defaults to
        # call `next`
        self.d_with_len = self.datas  # all data sets fulfill the guarantees now

        self.next()  # delegate the work to next

    def next(self):
        # we can now always work with self.d_with_len with no calculation
        ...

The guard calculation is moved to prenext which will stopped being called when the guarantees are met. nextstart will be called then and by overriding it we can reset the list which holds the data set to work with, to be the full data set, i.e.: self.datas

And with this, all guards have been removed from next.

next with timers

Although the intention of the author here is to rebalance (portfolio/positions) each 5/10 days, this is probably meant as a weekly/bi-weekly rebalancing.

The len(self) % period approach will fail if:

  • The data set did not start on a Monday

  • During trading holidays, which will make the rebalancing move out of alignment

To overcome this, one can use the built-in functionalities in backtrader

Using them will ensure that rebalancing happens when it is meant to happen. Let us imagine that the intention is to rebalance on Fridays

Let's add a bit of magic to the params and __init__ in our strategy

class Strategy(bt.Strategy):
    params = dict(
       ...
       rebal_weekday=5,  # rebalance 5 is Friday
    )

    def __init__(self):
        ...
        self.add_timer(
            when=bt.Timer.SESSION_START,
            weekdays=[self.p.rebal_weekday],
            weekcarry=True,  # if a day isn't there, execute on the next
        )
        ...

And now we are ready to know when it is Friday. Even if a Friday happens to be a trading holiday, adding weekcarry=True ensures we will be notified on Monday (or Tuesday if Monday is also a holiday or ...)

The notification of the timer is taken in notify_timer

def notify_timer(self, timer, when, *args, **kwargs):
    self.rebalance_portfolio()

Because there is also a rebalance_positions which happens every 10 bars in the original code, one could:

  • Add a 2nd timer, also for Fridays

  • Use a counter to only act on each 2nd call, which can even be in the timer itself using the allow=callable argument

Note

Timers could even be better used to achieve patterns like:

  • rebalance_portfolio every on the 2nd and 4th Friday of the month

  • rebalance_positions only on the 4th Friday of each month

Some Extras

Some other things are probably and purely a matter of personal taste.

Personal Taste 1

Use always a pre-built comparison rather than compare things during next. For example from the code (used more than once)

        if self.spy < self.spy_sma200:
            return

We could do the following. First during __init__

    def __init__(self):
        ...
        self.spy_filter = self.spe < self.spy_sma200

And later

        if self.spy_filter:
            return

With this in mind and if we wanted to alter the spy_filter condition, we would only have to do this once in __init__ and not in multiple positions in the code.

The same could apply to this other comparison d < self.inds[d]["sma100"] here:

        # sell stocks based on criteria
        for i, d in enumerate(self.rankings):
            if self.getposition(self.data).size:
                if i > num_stocks * 0.2 or d < self.inds[d]["sma100"]:
                    self.close(d)

Which could also be pre-built during __init__ and therefore changed to something like this

        # sell stocks based on criteria
        for i, d in enumerate(self.rankings):
            if self.getposition(self.data).size:
                if i > num_stocks * 0.2 or self.inds[d]['sma_signal']:
                    self.close(d)

Personal Taste 2

Make everything a parameter. In the lines above we for example see a 0.2 which is used in several parts of the code: make it a parameter. The same with other values like 0.001 and 100 (which was actually already suggested as a parameter for the creation of moving averages)

Having everything as a parameter allows to pack the code and try different things by just changing the instantiation of the strategy and not the strategy itself.