OCO orders
Release1.9.34.116 addsOCO (akaOne Cancel Others) to thebacktesting arsenal.
Note
This is only implemented in backtesting and there isn’t yet animplementation for live brokers
Note
Updated with release1.9.36.116. Interactive Brokers support forStopTrail,StopTrailLimit andOCO.
OCO Specify always the 1st order in a group as parameteroco
StopTrailLimit: the broker simulation and theIB broker have the asme behavior. Specify:price as the initial stop trigger price (specify alsotrailamount) and thenplimi as the initial limit price. The difference between the two will determine thelimitoffset (the distance at which the limit price remains from the stop trigger price)
The usage pattern tries to remain user friendly. As such and if the logic inthe strategy has decided it is the moment to issue orders, usingOCO can bedone like this:
defnext(self):...o1=self.buy(...)...o2=self.buy(...,oco=o1)...o3=self.buy(...,oco=o1)# or even oco=o2, o2 is already in o1 group
Easy. The 1st ordero1 will something like the group leader.o2 ando3 become part of theOCO Group by specifyingo1 with theoconamed argument. See that the comment in the snippet indicates thato3could have also become part of the group by specifyingo2 (which as alreadypart of the group)
With the group formed the following will happen:
- If any order in the group is executed, cancelled or expires, the other orders will be cancelled
The sample below puts theOCO concept in play. A standard execution with a plot:
$ ./oco.py --broker cash=50000 --plot
Note
cash is increased to50000, because the asset reaches values of4000 and 3 orders of1 item would require at least12000monetary units (the default in the broker is10000)
With the following chart.

which actually doesn’t provide much information (it is a standardSMACrossover strategy). The sample does the following:
When the fastSMA crosses the slowSMA to the upside 3 orders are issued
order1 is aLimit order which will expire inlimdays days (parameter to the strategy) with theclose price reduced by a percentage as the limit price
order2 is aLimit order with a much longer period to expire and a much more reduced limit price.
order3 is aLimit order which further reduces the limit price
As such the execution oforder2 andorder3 is not going to happenbecause:
order1 will be executed first and this should trigger the cancellation of the others
or
order1 will expire and this will trigger the the cancellation of the others
The system keeps theref identifier of the 3 orders and will only issue newbuy orders if the threeref identifiers are seen innotify_order aseitherCompleted,Cancelled,Margin orExpired
Exiting is simply done after holding the position for some bars.
To try to keep track of the actual execution, textual output is produced. Someof it:
2005-01-28: Oref 1 / Buy at 2941.110552005-01-28: Oref 2 / Buy at 2896.77222005-01-28: Oref 3 / Buy at 2822.874952005-01-31: Order ref: 1 / Type Buy / Status Submitted2005-01-31: Order ref: 2 / Type Buy / Status Submitted2005-01-31: Order ref: 3 / Type Buy / Status Submitted2005-01-31: Order ref: 1 / Type Buy / Status Accepted2005-01-31: Order ref: 2 / Type Buy / Status Accepted2005-01-31: Order ref: 3 / Type Buy / Status Accepted2005-02-01: Order ref: 1 / Type Buy / Status Expired2005-02-01: Order ref: 3 / Type Buy / Status Canceled2005-02-01: Order ref: 2 / Type Buy / Status Canceled...2006-06-23: Oref 49 / Buy at 3532.399252006-06-23: Oref 50 / Buy at 3479.1472006-06-23: Oref 51 / Buy at 3390.393252006-06-26: Order ref: 49 / Type Buy / Status Submitted2006-06-26: Order ref: 50 / Type Buy / Status Submitted2006-06-26: Order ref: 51 / Type Buy / Status Submitted2006-06-26: Order ref: 49 / Type Buy / Status Accepted2006-06-26: Order ref: 50 / Type Buy / Status Accepted2006-06-26: Order ref: 51 / Type Buy / Status Accepted2006-06-26: Order ref: 49 / Type Buy / Status Completed2006-06-26: Order ref: 51 / Type Buy / Status Canceled2006-06-26: Order ref: 50 / Type Buy / Status Canceled...2006-11-10: Order ref: 61 / Type Buy / Status Canceled2006-12-11: Oref 63 / Buy at 4032.625552006-12-11: Oref 64 / Buy at 3971.83222006-12-11: Oref 65 / Buy at 3870.509952006-12-12: Order ref: 63 / Type Buy / Status Submitted2006-12-12: Order ref: 64 / Type Buy / Status Submitted2006-12-12: Order ref: 65 / Type Buy / Status Submitted2006-12-12: Order ref: 63 / Type Buy / Status Accepted2006-12-12: Order ref: 64 / Type Buy / Status Accepted2006-12-12: Order ref: 65 / Type Buy / Status Accepted2006-12-15: Order ref: 63 / Type Buy / Status Expired2006-12-15: Order ref: 65 / Type Buy / Status Canceled2006-12-15: Order ref: 64 / Type Buy / Status Canceled
With the following happening:
The 1st batch of orders is issued. Order 1 expires and 2 and 3 are cancelled. As expected.
Some months later another batch of 3 orders is issued. In this case Order 49 getsCompleted and 50 and 51 are immediately cancelled
The last batch is just like the 1st
Let’s check now the behavior withoutOCO:
$ ./oco.py --strat do_oco=False --broker cash=500002005-01-28: Oref 1 / Buy at 2941.110552005-01-28: Oref 2 / Buy at 2896.77222005-01-28: Oref 3 / Buy at 2822.874952005-01-31: Order ref: 1 / Type Buy / Status Submitted2005-01-31: Order ref: 2 / Type Buy / Status Submitted2005-01-31: Order ref: 3 / Type Buy / Status Submitted2005-01-31: Order ref: 1 / Type Buy / Status Accepted2005-01-31: Order ref: 2 / Type Buy / Status Accepted2005-01-31: Order ref: 3 / Type Buy / Status Accepted2005-02-01: Order ref: 1 / Type Buy / Status Expired
And that’s it, which isn’t much (no order execution, not much need for a charteither)
The batch of orders is issued
Order 1 expires, but because the strategy has gotten the parameterdo_oco=False, orders 2 and 3 are not made part of theOCO group
Orders 2 and 3 are therefore not cancelled and because the default expiration delta is1000 days later, they never expire with the available data for the sample (2 years of data)
The system never issues a 2nd bath of orders.
Sample usage
$ ./oco.py --helpusage: oco.py [-h] [--data0 DATA0] [--fromdate FROMDATE] [--todate TODATE] [--cerebro kwargs] [--broker kwargs] [--sizer kwargs] [--strat kwargs] [--plot [kwargs]]Sample Skeletonoptional 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)importargparseimportdatetimeimportbacktraderasbtclassSt(bt.Strategy):params=dict(ma=bt.ind.SMA,p1=5,p2=15,limit=0.005,limdays=3,limdays2=1000,hold=10,switchp1p2=False,# switch prices of order1 and order2oco1oco2=False,# False - use order1 as oco for order3, else order2do_oco=True,# use oco or not)defnotify_order(self,order):print('{}: Order ref:{} / Type{} / Status{}'.format(self.data.datetime.date(0),order.ref,'Buy'*order.isbuy()or'Sell',order.getstatusname()))iforder.status==order.Completed:self.holdstart=len(self)ifnotorder.alive()andorder.refinself.orefs:self.orefs.remove(order.ref)def__init__(self):ma1,ma2=self.p.ma(period=self.p.p1),self.p.ma(period=self.p.p2)self.cross=bt.ind.CrossOver(ma1,ma2)self.orefs=list()defnext(self):ifself.orefs:return# pending orders do nothingifnotself.position:ifself.cross>0.0:# crossing upp1=self.data.close[0]*(1.0-self.p.limit)p2=self.data.close[0]*(1.0-2*2*self.p.limit)p3=self.data.close[0]*(1.0-3*3*self.p.limit)ifself.p.switchp1p2:p1,p2=p2,p1o1=self.buy(exectype=bt.Order.Limit,price=p1,valid=datetime.timedelta(self.p.limdays))print('{}: Oref{} / Buy at{}'.format(self.datetime.date(),o1.ref,p1))oco2=o1ifself.p.do_ocoelseNoneo2=self.buy(exectype=bt.Order.Limit,price=p2,valid=datetime.timedelta(self.p.limdays2),oco=oco2)print('{}: Oref{} / Buy at{}'.format(self.datetime.date(),o2.ref,p2))ifself.p.do_oco:oco3=o1ifnotself.p.oco1oco2elseoco2else:oco3=Noneo3=self.buy(exectype=bt.Order.Limit,price=p3,valid=datetime.timedelta(self.p.limdays2),oco=oco3)print('{}: Oref{} / Buy at{}'.format(self.datetime.date(),o3.ref,p3))self.orefs=[o1.ref,o2.ref,o3.ref]else:# in the marketif(len(self)-self.holdstart)>=self.p.hold:self.close()defrunstrat(args=None):args=parse_args(args)cerebro=bt.Cerebro()# Data feed kwargskwargs=dict()# Parse from/to-datedtfmt,tmfmt='%Y-%m-%d','T%H:%M:%S'fora,din((getattr(args,x),x)forxin['fromdate','todate']):ifa:strpfmt=dtfmt+tmfmt*('T'ina)kwargs[d]=datetime.datetime.strptime(a,strpfmt)# Data feeddata0=bt.feeds.BacktraderCSVData(dataname=args.data0,**kwargs)cerebro.adddata(data0)# Brokercerebro.broker=bt.brokers.BackBroker(**eval('dict('+args.broker+')'))# Sizercerebro.addsizer(bt.sizers.FixedSize,**eval('dict('+args.sizer+')'))# Strategycerebro.addstrategy(St,**eval('dict('+args.strat+')'))# Executecerebro.run(**eval('dict('+args.cerebro+')'))ifargs.plot:# Plot if requested tocerebro.plot(**eval('dict('+args.plot+')'))defparse_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 datesparser.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')returnparser.parse_args(pargs)if__name__=='__main__':runstrat()