Hidden Powers of Python (1)
It’s only when meeting real users of backtrader when one can realize if the abstractions and Python powers used in the platform make sense.
Without leaving the pythonic motto aside, backtrader tries to give the users as much control as possible, whilst at the same time simplifying the usage by putting into action the hidden powers that Python offers.
The first example in this the first post of a series.
Is it an array or what is it?
A very quick example:
import backtrader as bt class MyStrategy(bt.Strategy): def __init__(self): self.hi_lo_avg = (self.data.high + self.data.low) / 2.0 def next(self): if self.hi_lo_avg[0] > another_value: print('we have a winner!') ... ... cerebro.addstrategy(MyStrategy) cerebro.run()
One of the questions that very quickly pops up is:
- Couldn’t one also use the
[]
during__init__
?.
The question being asked because the user has already tried and Python has stopped running with an exception.
The answer:
- No. Using
[]
is not meant during initialization.
With the next question being then:
- So what’s actually stored in
self.hi_lo_avg
during__init__
if it’s not an array?
And the answer is not puzzling for programmers but it may be for algo traders who went for Python
- It’s a lazily evaluated object, which will calculate and deliver the values
via the
[]
operator during thecerebro.run
phase, i.e.: in thenext
method of the strategy.
Bottomline: in the next
method the array indexing operator []
will give
you access to the calculated values for past and current time moments.
The secret is in the sauce
And operator overriding is the real sauce. Let’s break down the calculation of the high-low-average:
self.hi_lo_avg = (self.data.high + self.data.low) / 2.0
The components:
self.data.high
andself.data.low
are themselves objects (lines in the backtrader naming scheme)
They are in many cases mistakenly taken for pure arrays but they are not. The reasons for them being objects:
-
Implementation of the
0
and-1
indexing scheme in place in backtrader -
Control of the buffer sizing and linking to other objects
And the most important aspect in this case:
- Overriding operators to return objects
And that why the operation below returns a lines object. Let’s start:
temp = self.data.high - self.data.low
The temporary object is then divided by 2.0
and assigned to the member
variable:
self.hi_lo_avg = temp / 2.0
This agains returns another lines object. Because operator overriding does not only apply to operations executed directly amongst lines objects, but also to, for example, arithmetic operations like this division.
Which means that self.hi_lo_avg
has a reference to a lines object. This
object is useful in the next
method of the strategy or as input to
indicators or other calculations.
A logic operator example
The example above used an arithmetic operator during __init__
and later the
combination of [0]
and a logic opertor, >
in next
.
Because operator overriding is not limited to arithmetic, let’s put another example in place, adding an indicator to the mix. A first attempt would be:
import backtrader as bt class MyStrategy(bt.Strategy): def __init__(self): self.hi_lo_avg = (self.data.high + self.data.low) / 2.0 self.sma = bt.indicators.SMA(period=30) def next(self): if self.hi_lo_avg[0] > self.sma[0]: print('we have a winner!') ... ... cerebro.addstrategy(MyStrategy) cerebro.run()
But in this case there is simply change from another_value
to
self.sma[0]
. Let’s improve it:
import backtrader as bt class MyStrategy(bt.Strategy): def __init__(self): self.hi_lo_avg = (self.data.high + self.data.low) / 2.0 self.sma = bt.indicators.SMA(period=30) def next(self): if self.hi_lo_avg > self.sma: print('we have a winner!') ... ... cerebro.addstrategy(MyStrategy) cerebro.run()
One for the good guys. Operator overriding does also work in next
and the
users can actually drop the [0]
and directly compare the objects.
If all that were what’s actually possible it would actually seem an overkill. But the good thing is that there is more. See this example:
import backtrader as bt class MyStrategy(bt.Strategy): def __init__(self): hi_lo_avg = (self.data.high + self.data.low) / 2.0 sma = bt.indicators.SMA(period=30) self.signal = hi_lo_avg > sma def next(self): if self.signal: print('we have a winner!') ... ... cerebro.addstrategy(MyStrategy) cerebro.run()
We have done 2 things:
-
Create a lines object named
self.signal
which compares the high-low-average against the value of a Simple Moving AverageAs explained above this object is useful in
next
, when it has been calculated -
Remove the usage of
[0]
innext
when checking ifsignal
isTrue
. This is possible because operators have also been overriden for boolean operations
Conclusion
Hopefully this adds some light to what actually happens when operations are
executed in __init__
and how operator overriding actually happens.