Skip to content

Improving Random Python Internet Study Notes

Every now and then, samples with backtrader code pop up in the Internet. There are several in what it looks to me to be Chinese. The latest one is here:

The title is: backtrader-学习笔记2, which apparently (thanks Google) translates to backtrader- study notes 2. If those are study notes, let’s try to improve the code there where it can really be improved and in my personal opinion there where backtrader shines the most.

In the __init__ method of the strategy inside the study notes we find the following

def __init__(self):
    ...
    self.ma1 = bt.indicators.SMA(self.datas[0],
                                   period=self.p.period
                                  )
    self.ma2 = bt.indicators.SMA(self.datas[1],
                                   period=self.p.period
                                  )

Nothing to argue here (style is something very personal, I won’t touch that)

And in the next method of the strategy, the following are the logic decisions for buying and selling.

...
# Not yet ... we MIGHT BUY if ...
if (self.ma1[0]-self.ma1[-1])/self.ma1[-1]>(self.ma2[0]-self.ma2[-1])/self.ma2[-1]:
...

and

...
# Already in the market ... we might sell
if (self.ma1[0]-self.ma1[-1])/self.ma1[-1]<=(self.ma2[0]-self.ma2[-1])/self.ma2[-1]:
...

These two logic blocks is what one can actually make a lot more better, which will also add to readability, maintainability and tweaking (if needed be)

Instead of having those comparison of moving averages (current point 0 and previous point -1) followed by some divisions, let’s look at how to have it precalculated for us.

Let’s tweak __init__

def __init__(self):
    ...

    # Let's create the moving averages as before
    ma1 = bt.ind.SMA(self.data0, period=self.p.period)
    ma2 = bt.ind.SMA(self.data1, period=self.p.period)

    # Use line delay notation (-x) to get a ref to the -1 point
    ma1_pct = ma1 / ma1(-1) - 1.0  # The ma1 percentage part
    ma2_pct = ma2 / ma2(-1) - 1.0  # The ma2 percentage part

    self.buy_sig = ma1_pct > ma2_pct  # buy signal
    self.sell_sig = ma1_pct <= ma2_pct  # sell signal

And we can now take that to the next method and do the following:

def next(self):
    ...
    # Not yet ... we MIGHT BUY if ...
    if self.buy_sig:
    ...

    ...
    # Already in the market ... we might sell
    if self.sell_sig:
    ...

Notice that we don’t even have to use self.buy_sig[0], because the boolean test make with if self.buy_sig is already translated by the backtrader machinery to a check for [0]

Imho, a much cleaner approach in which defining the logic in __init__ with standard arithmetic and logical operations (and using the line delay notation (-x)) makes the code much better.

In any case and for closing note, one could have also tried to use the built-in PercentChange indicator (aka PctChange)

See: backtrader documentation - Indicator Reference

As the name suggests it does already calculate the percentage change over a given period of bars. The code in __init__ would now look like this

def __init__(self):
    ...

    # Let's create the moving averages as before
    ma1 = bt.ind.SMA(self.data0, period=self.p.period)
    ma2 = bt.ind.SMA(self.data1, period=self.p.period)

    ma1_pct = bt.ind.PctChange(ma1, period=1)  # The ma1 percentage part
    ma2_pct = bt.ind.PctChange(ma2, period=1)  # The ma2 percentage part

    self.buy_sig = ma1_pct > ma2_pct  # buy signal
    self.sell_sig = ma1_pct <= ma2_pct  # sell signal

It doesn’t make much of a difference in this case, but it may for sure save you from lots of trouble if the calculations are larger and more complex.

Happy backtrading!