Crossing over numbers
An oversight has been corrected with Release 1.9.27.105
of backtrader. It
was an oversight because all pieces of the puzzle were in place, but the
activation was not made in all corners.
The mechanism uses an attribute called _mindatas
, so let’s call it:
mindatas
.
The community asked and the answer was not really right on spot. See the conversation here:
Even if the conversation was about something else, the question could have quickly answered with: “Hey, it should actually work!”. But who has time to consider a proper and thoughtful answer these days.
Let’s consider the use case of crossing over a plain old number argument. Something like this
mycrossover = bt.ind.CrossOver(bt.ind.RSI(), 50.0)
Which would break as in
Traceback (most recent call last):
File "./cross-over-num.py", line 114, in <module>
runstrat()
File "./cross-over-num.py", line 70, in runstrat
cerebro.run(**eval('dict(' + args.cerebro + ')'))
File "d:\dro\01-docs\01-home\src\backtrader\backtrader\cerebro.py", line 810, in run
runstrat = self.runstrategies(iterstrat)
File "d:\dro\01-docs\01-home\src\backtrader\backtrader\cerebro.py", line 878, in runstrategies
strat = stratcls(*sargs, **skwargs)
File "d:\dro\01-docs\01-home\src\backtrader\backtrader\metabase.py", line 87, in __call__
_obj, args, kwargs = cls.doinit(_obj, *args, **kwargs)
File "d:\dro\01-docs\01-home\src\backtrader\backtrader\metabase.py", line 77, in doinit
_obj.__init__(*args, **kwargs)
File "./cross-over-num.py", line 35, in __init__
bt.ind.CrossOver(bt.ind.RSI(), 50)
File "d:\dro\01-docs\01-home\src\backtrader\backtrader\indicator.py", line 53, in __call__
return super(MetaIndicator, cls).__call__(*args, **kwargs)
File "d:\dro\01-docs\01-home\src\backtrader\backtrader\metabase.py", line 87, in __call__
_obj, args, kwargs = cls.doinit(_obj, *args, **kwargs)
File "d:\dro\01-docs\01-home\src\backtrader\backtrader\metabase.py", line 77, in doinit
_obj.__init__(*args, **kwargs)
Typeerror: __init__() takes exactly 1 argument (2 given)
Being the last line the most informative, because it is telling us that
something got too many arguments. And this means that the 50.0
is hurting us.
To solve the problem at hand, a number wrapper was given as the answer.
class ConstantValue(bt.Indicator):
lines = ('constant',)
params = (('constant', float('NaN')),)
def next(self):
self.lines.constant[0] = self.p.constant
...
mycrossover = bt.ind.CrossOver(bt.ind.RSI(), ConstantValue(50.0))
Problem solved. But wait, the solution was already on board. There is an
internal helper, to solve the problem and it was completely forgotten:
LineNum
. And it does what the name tries to imply: Takes a num and makes
it a line. The problem solution was there and the solution could have looked
like this:
mycrossover = bt.ind.CrossOver(bt.ind.RSI(), bt.LineNum(50.0))
The usual background thread kept anyhow ticking, telling something was still not 100% clear and the solution should be the obvious, without having the user specifying the wrapper.
And here comes the oversight. Even if the mindatas
mechanism exists and is
applied in some parts of the echosystem, it was not being applied to
CrossOver
. It was tried, but humans miserably fail sometimes, believing
they have done something only to find out, they didn’t scroll further
downwards. And this was the case. A one line addition like this:
class CrossOver(Indicator):
...
_mindatas = 2
...
And now the solution to the question is straightforward:
mycrossover = bt.ind.CrossOver(bt.ind.RSI(), 50.0)
The way it should have always been in the 1st place (see the sample and chart below)
mindatas
at work
This is a handy attribute which is meant to be used for specific situations and
hence the leading _
, to indicate it should be used with real caution. The
default value for indicators is:
-
_mindatas = 1
This tells the system that if NO data source has been passed to an indicator, the system should copy the 1st data source from the parent. Without this, instantiating for example the
RelativeStrengthIndicator
should be done like this:class Strategy(bt.Indicator): def __init__(self): rsi = bt.ind.RSI(self.data0)
But with the default indication given by
_mindatas
, the following is possible:class Strategy(bt.Indicator): def __init__(self): rsi = bt.ind.RSI()
And the result is exactly the same, because the 1st data source in the strategy,
self.data0
is passed to the instantiation ofRSI
An indicator like CrossOver
needs 2 data feeds, because it’s checking that
one thing is crossing over another. In this case and as seen above the default
has been set to:
_mindatas = 2
This tells the system things like:
-
If no datas are passed, then copy 2 data feeds from the parent (if possible)
-
If only 1 data has been passed, try to convert the next incoming argument to a lines object to have 2 data feeds available. Usseful for the use case of a line crossing over a plain old float. Again for reference:
mycrossover = bt.ind.CrossOver(bt.ind.RSI(), 50.0)
-
If 2 or more data feeds are passed to
CrossOver
, do nothing and proceed further
In the community, the mechanism has been lately applied to for example the 1st
sketches to implement the KalmanFilter
for pair trading. And when talking
about pairs, one needs 2 data feeds and with it: _mindatas = 2
A small sample (although with a complete skeleton) to test the complete solution:
$ ./cross-over-num.py --plot
Which outputs this.
Sample usage
$ ./cross-over-num.py --help
usage: cross-over-num.py [-h] [--data0 DATA0] [--fromdate FROMDATE]
[--todate TODATE] [--cerebro kwargs]
[--broker kwargs] [--sizer kwargs] [--strat kwargs]
[--plot [kwargs]]
Sample Skeleton
optional arguments:
-h, --help show this help message and exit
--data0 DATA0 Data to read in (default:
../../datas/2005-2006-day-001.txt)
--fromdate FROMDATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: )
--todate TODATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: )
--cerebro kwargs kwargs in key=value format (default: )
--broker kwargs kwargs in key=value format (default: )
--sizer kwargs kwargs in key=value format (default: )
--strat kwargs kwargs in key=value format (default: )
--plot [kwargs] kwargs in key=value format (default: )
Sample Code
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import argparse
import datetime
import backtrader as bt
class St(bt.Strategy):
params = ()
def __init__(self):
bt.ind.CrossOver(bt.ind.RSI(), 50)
def next(self):
pass
def runstrat(args=None):
args = parse_args(args)
cerebro = bt.Cerebro()
# Data feed kwargs
kwargs = dict()
# Parse from/to-date
dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S'
for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']):
if a:
strpfmt = dtfmt + tmfmt * ('T' in a)
kwargs[d] = datetime.datetime.strptime(a, strpfmt)
# Data feed
data0 = bt.feeds.BacktraderCSVData(dataname=args.data0, **kwargs)
cerebro.adddata(data0)
# Broker
cerebro.broker = bt.brokers.BackBroker(**eval('dict(' + args.broker + ')'))
# Sizer
cerebro.addsizer(bt.sizers.FixedSize, **eval('dict(' + args.sizer + ')'))
# Strategy
cerebro.addstrategy(St, **eval('dict(' + args.strat + ')'))
# Execute
cerebro.run(**eval('dict(' + args.cerebro + ')'))
if args.plot: # Plot if requested to
cerebro.plot(**eval('dict(' + args.plot + ')'))
def parse_args(pargs=None):
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description=(
'Sample Skeleton'
)
)
parser.add_argument('--data0', default='../../datas/2005-2006-day-001.txt',
required=False, help='Data to read in')
# Defaults for dates
parser.add_argument('--fromdate', required=False, default='',
help='Date[time] in YYYY-MM-DD[THH:MM:SS] format')
parser.add_argument('--todate', required=False, default='',
help='Date[time] in YYYY-MM-DD[THH:MM:SS] format')
parser.add_argument('--cerebro', required=False, default='',
metavar='kwargs', help='kwargs in key=value format')
parser.add_argument('--broker', required=False, default='',
metavar='kwargs', help='kwargs in key=value format')
parser.add_argument('--sizer', required=False, default='',
metavar='kwargs', help='kwargs in key=value format')
parser.add_argument('--strat', required=False, default='',
metavar='kwargs', help='kwargs in key=value format')
parser.add_argument('--plot', required=False, default='',
nargs='?', const='{}',
metavar='kwargs', help='kwargs in key=value format')
return parser.parse_args(pargs)
if __name__ == '__main__':
runstrat()