Strategy Selection
Houston we have a problem:
- cerebro is not meant to be run several times. This is not the 1st time and rather than thinking that users are doing it wrong, it seems it is a use case.
This interesting use case has come up via Ticket 177. In this case cerebro is being used multiple times to evaluate differet strategies which are being fetched from an external data source.
backtrader can still support this use case, but not in the direct way it has been attempted.
Optimizing the selection
The buil-in optimization in backtrader already does the required thing:
- Instantiate several strategy instances and collect the results
Being the only thing that the instances all belong to the same class. This is where Python helps by lettings us control the creation of an object.
First, let’s add to very quick strategies to a script using the Signal technology built in backtrader
class St0(bt.SignalStrategy): def __init__(self): sma1, sma2 = bt.ind.SMA(period=10), bt.ind.SMA(period=30) crossover = bt.ind.CrossOver(sma1, sma2) self.signal_add(bt.SIGNAL_LONG, crossover) class St1(bt.SignalStrategy): def __init__(self): sma1 = bt.ind.SMA(period=10) crossover = bt.ind.CrossOver(self.data.close, sma1) self.signal_add(bt.SIGNAL_LONG, crossover)
It cannot get easier.
And now let’s do the magic of delivering those two strategies.
class StFetcher(object): _STRATS = [St0, St1] def __new__(cls, *args, **kwargs): idx = kwargs.pop('idx') obj = cls._STRATS[idx](*args, **kwargs) return obj
Et voilá! When the class StFetcher
is being instantiated, method
__new__
takes control of instance creation. In this case:
-
Gets the
idx
param which is passed to it -
Uses this param to get a strategy from the
_STRATS
list in which our previous sample strategies have been storedNote
Nothing would prevent using this
idx
value to fetch strategies from a server and/or a database. -
Instantiate and return the fecthed strategy
Running the show
cerebro.addanalyzer(bt.analyzers.Returns) cerebro.optstrategy(StFetcher, idx=[0, 1]) results = cerebro.run(maxcpus=args.maxcpus, optreturn=args.optreturn)
Indeed! Optimiization it is! Rather than addstrategy
we use optstrategy
and pass an array of values for idx
. Those values will be iterated over by
the optimization engine.
Because cerebro
can host several strategies in each optimization pass, the
result will contain a list of lists. Each sublist is the result of each
optimization pass.
In our case and with only 1 strategy per pass, we can quickly flatten the results and extract the values of the analyzer we have added.
strats = [x[0] for x in results] # flatten the result for i, strat in enumerate(strats): rets = strat.analyzers.returns.get_analysis() print('Strat {} Name {}:\n - analyzer: {}\n'.format( i, strat.__class__.__name__, rets))
A sample run
./strategy-selection.py Strat 0 Name St0: - analyzer: OrderedDict([(u'rtot', 0.04847392369449283), (u'ravg', 9.467563221580632e-05), (u'rnorm', 0.02414514457151587), (u'rnorm100', 2.414514457151587)]) Strat 1 Name St1: - analyzer: OrderedDict([(u'rtot', 0.05124714332260593), (u'ravg', 0.00010009207680196471), (u'rnorm', 0.025543999840699633), (u'rnorm100', 2.5543999840699634)])
Our 2 strategies have been run and deliver (as expected) different results.
Note
The sample is minimal but has been run with all available
CPUs. Executing it with --maxpcpus=1
will be faster. For more
complex scenarios using all CPUs will be useful.
Conclusion
The Strategy Selection use case is possible and doesn’t need circumventing any of the built-in facilities in either backtrader or Python itself.
Sample Usage
$ ./strategy-selection.py --help usage: strategy-selection.py [-h] [--data DATA] [--maxcpus MAXCPUS] [--optreturn] Sample for strategy selection optional arguments: -h, --help show this help message and exit --data DATA Data to be read in (default: ../../datas/2005-2006-day-001.txt) --maxcpus MAXCPUS Limit the numer of CPUs to use (default: None) --optreturn Return reduced/mocked strategy object (default: False)
The code
Which has been included in the sources of backtrader
from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import backtrader as bt class St0(bt.SignalStrategy): def __init__(self): sma1, sma2 = bt.ind.SMA(period=10), bt.ind.SMA(period=30) crossover = bt.ind.CrossOver(sma1, sma2) self.signal_add(bt.SIGNAL_LONG, crossover) class St1(bt.SignalStrategy): def __init__(self): sma1 = bt.ind.SMA(period=10) crossover = bt.ind.CrossOver(self.data.close, sma1) self.signal_add(bt.SIGNAL_LONG, crossover) class StFetcher(object): _STRATS = [St0, St1] def __new__(cls, *args, **kwargs): idx = kwargs.pop('idx') obj = cls._STRATS[idx](*args, **kwargs) return obj def runstrat(pargs=None): args = parse_args(pargs) cerebro = bt.Cerebro() data = bt.feeds.BacktraderCSVData(dataname=args.data) cerebro.adddata(data) cerebro.addanalyzer(bt.analyzers.Returns) cerebro.optstrategy(StFetcher, idx=[0, 1]) results = cerebro.run(maxcpus=args.maxcpus, optreturn=args.optreturn) strats = [x[0] for x in results] # flatten the result for i, strat in enumerate(strats): rets = strat.analyzers.returns.get_analysis() print('Strat {} Name {}:\n - analyzer: {}\n'.format( i, strat.__class__.__name__, rets)) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='Sample for strategy selection') parser.add_argument('--data', required=False, default='../../datas/2005-2006-day-001.txt', help='Data to be read in') parser.add_argument('--maxcpus', required=False, action='store', default=None, type=int, help='Limit the numer of CPUs to use') parser.add_argument('--optreturn', required=False, action='store_true', help='Return reduced/mocked strategy object') return parser.parse_args(pargs) if __name__ == '__main__': runstrat()