MultiTrades
One can now add a unique identifier to each trade, even if running on the samedata.
Following a request atTick Data and Resamplingrelease 1.1.12.88 ofbacktrader support “MultiTrades”, ie: the ability toassign atradeid to orders. This id is passed on toTrades which makesit possible to have different categories of trades and have them simultaneouslyopen.
Thetradeid can be specified when:
Calling Strategy.buy/sell/close with kwarg
tradeidCalling Broker.buy/sell with kwarg
tradeidCreating an Order instance with kwarg
tradeid
If not specified the default value is:
tradeid = 0
To test a small script has been implemented, visualizing the result with theimplementation of a customMTradeObserver which assigns different markers onthe plot accordingtradeid (for the test values 0, 1 and 2 are used)
The script supports using the three ids (0, 1, 2) or simply use 0 (default)
An execution without enabling multiple ids:
$ ./multitrades.py --plotWith the resulting chart showing all Trades carry id0 and therefore cannotbe diferentiated.
A second execution enables multitrades by cycling amongs 0, 1 and 2:
$ ./multitrades.py --plot --mtradeAnd now 3 different markers alternate showing each Trade can be distinguishedusing thetradeid member.
Note
backtrader tries to use models which mimic reality. Therefore “trades”are not calculated by theBroker instance which only takes care ofoders.
Trades are calculated by the Strategy.
And hencetradeid (or something similar) may not be supported by a reallife broker in which case manually keeping track of the unique orde idassigned by the broker would be needed.
Now, the code for the custom observer
from __future__ import (absolute_import, division, print_function, unicode_literals)import mathimport backtrader as btclass MTradeObserver(bt.observer.Observer): lines = ('Id_0', 'Id_1', 'Id_2') plotinfo = dict(plot=True, subplot=True, plotlinelabels=True) plotlines = dict( Id_0=dict(marker='*', markersize=8.0, color='lime', fillstyle='full'), Id_1=dict(marker='o', markersize=8.0, color='red', fillstyle='full'), Id_2=dict(marker='s', markersize=8.0, color='blue', fillstyle='full') ) def next(self): for trade in self._owner._tradespending: if trade.data is not self.data: continue if not trade.isclosed: continue self.lines[trade.tradeid][0] = trade.pnlcommThe main script usage:
$ ./multitrades.py --helpusage: multitrades.py [-h] [--data DATA] [--fromdate FROMDATE] [--todate TODATE] [--mtrade] [--period PERIOD] [--onlylong] [--cash CASH] [--comm COMM] [--mult MULT] [--margin MARGIN] [--stake STAKE] [--plot] [--numfigs NUMFIGS]MultiTradesoptional arguments: -h, --help show this help message and exit --data DATA, -d DATA data to add to the system --fromdate FROMDATE, -f FROMDATE Starting date in YYYY-MM-DD format --todate TODATE, -t TODATE Starting date in YYYY-MM-DD format --mtrade Activate MultiTrade Ids --period PERIOD Period to apply to the Simple Moving Average --onlylong, -ol Do only long operations --cash CASH Starting Cash --comm COMM Commission for operation --mult MULT Multiplier for futures --margin MARGIN Margin for each future --stake STAKE Stake to apply in each operation --plot, -p Plot the read data --numfigs NUMFIGS, -n NUMFIGS Plot using numfigs figuresThe code for the script.
from __future__ import (absolute_import, division, print_function, unicode_literals)import argparseimport datetimeimport itertools# The above could be sent to an independent moduleimport backtrader as btimport backtrader.feeds as btfeedsimport backtrader.indicators as btindimport mtradeobserverclass MultiTradeStrategy(bt.Strategy): '''This strategy buys/sells upong the close price crossing upwards/downwards a Simple Moving Average. It can be a long-only strategy by setting the param "onlylong" to True ''' params = dict( period=15, stake=1, printout=False, onlylong=False, mtrade=False, ) def log(self, txt, dt=None): if self.p.printout: dt = dt or self.data.datetime[0] dt = bt.num2date(dt) print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): # To control operation entries self.order = None # Create SMA on 2nd data sma = btind.MovAv.SMA(self.data, period=self.p.period) # Create a CrossOver Signal from close an moving average self.signal = btind.CrossOver(self.data.close, sma) # To alternate amongst different tradeids if self.p.mtrade: self.tradeid = itertools.cycle([0, 1, 2]) else: self.tradeid = itertools.cycle([0]) def next(self): if self.order: return # if an order is active, no new orders are allowed if self.signal > 0.0: # cross upwards if self.position: self.log('CLOSE SHORT , %.2f' % self.data.close[0]) self.close(tradeid=self.curtradeid) self.log('BUY CREATE , %.2f' % self.data.close[0]) self.curtradeid = next(self.tradeid) self.buy(size=self.p.stake, tradeid=self.curtradeid) elif self.signal < 0.0: if self.position: self.log('CLOSE LONG , %.2f' % self.data.close[0]) self.close(tradeid=self.curtradeid) if not self.p.onlylong: self.log('SELL CREATE , %.2f' % self.data.close[0]) self.curtradeid = next(self.tradeid) self.sell(size=self.p.stake, tradeid=self.curtradeid) def notify_order(self, order): if order.status in [bt.Order.Submitted, bt.Order.Accepted]: return # Await further notifications if order.status == order.Completed: if order.isbuy(): buytxt = 'BUY COMPLETE, %.2f' % order.executed.price self.log(buytxt, order.executed.dt) else: selltxt = 'SELL COMPLETE, %.2f' % order.executed.price self.log(selltxt, order.executed.dt) elif order.status in [order.Expired, order.Canceled, order.Margin]: self.log('%s ,' % order.Status[order.status]) pass # Simply log # Allow new orders self.order = None def notify_trade(self, trade): if trade.isclosed: self.log('TRADE PROFIT, GROSS %.2f, NET %.2f' % (trade.pnl, trade.pnlcomm)) elif trade.justopened: self.log('TRADE OPENED, SIZE %2d' % trade.size)def runstrategy(): args = parse_args() # Create a cerebro cerebro = bt.Cerebro() # Get the dates from the args fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') # Create the 1st data data = btfeeds.BacktraderCSVData( dataname=args.data, fromdate=fromdate, todate=todate) # Add the 1st data to cerebro cerebro.adddata(data) # Add the strategy cerebro.addstrategy(MultiTradeStrategy, period=args.period, onlylong=args.onlylong, stake=args.stake, mtrade=args.mtrade) # Add the commission - only stocks like a for each operation cerebro.broker.setcash(args.cash) # Add the commission - only stocks like a for each operation cerebro.broker.setcommission(commission=args.comm, mult=args.mult, margin=args.margin) # Add the MultiTradeObserver cerebro.addobserver(mtradeobserver.MTradeObserver) # And run it cerebro.run() # Plot if requested if args.plot: cerebro.plot(numfigs=args.numfigs, volume=False, zdown=False)def parse_args(): parser = argparse.ArgumentParser(description='MultiTrades') parser.add_argument('--data', '-d', default='../../datas/2006-day-001.txt', help='data to add to the system') parser.add_argument('--fromdate', '-f', default='2006-01-01', help='Starting date in YYYY-MM-DD format') parser.add_argument('--todate', '-t', default='2006-12-31', help='Starting date in YYYY-MM-DD format') parser.add_argument('--mtrade', action='store_true', help='Activate MultiTrade Ids') parser.add_argument('--period', default=15, type=int, help='Period to apply to the Simple Moving Average') parser.add_argument('--onlylong', '-ol', action='store_true', help='Do only long operations') parser.add_argument('--cash', default=100000, type=int, help='Starting Cash') parser.add_argument('--comm', default=2, type=float, help='Commission for operation') parser.add_argument('--mult', default=10, type=int, help='Multiplier for futures') parser.add_argument('--margin', default=2000.0, type=float, help='Margin for each future') parser.add_argument('--stake', default=1, type=int, help='Stake to apply in each operation') parser.add_argument('--plot', '-p', action='store_true', help='Plot the read data') parser.add_argument('--numfigs', '-n', default=1, help='Plot using numfigs figures') return parser.parse_args()if __name__ == '__main__': runstrategy()
