Trading a Day in Steps
It seems that somewhere in the world there is an interest that can be summarized as follows:
- Introduce an order using daily bars but using the opening price
This comes from the conversations in tickets #105 Order execution logic with current day data and #101 Dynamic stake calculation
backtrader tries to remain as realistic as possible and the following premise applies when working with daily bars:
- When a daily bar is being evaluated, the bar is already over
It makes sense because all price (open/high/low/close) components are
known. It would actually seem illogical to allow an action on the open
price when the close
price is already known.
The obvious approach to this is to use intraday data and enter when the opening prices is known. But it seems intraday data is not so widespread.
This is where adding a filter to a data feed can help. A filter that:
- Converts daily data into intraday-like data
Blistering barnacles!!! The curious reader will immediately point out that
upsampling for example Minutes
to Days
is logical and works, but that
downsampling Days
to Minutes
cannot be done.
And it is 100% right. The filter presented below will not try that, but a much humble and simpler goal:
-
Break a daily bar in 2 parts
-
A bar with only the opening price and no volume
-
A 2nd bar which is a copy of the regular daily bar
-
This can still be held as a logical approach:
-
Upon seeing the opening price, the trader can act
-
The order is matched during the rest of the day (actually may or may not be matched depending on execution type and price constraints)
The full code is presented below. Let’s see a sample run with a well known data
of 255
daily bars:
$ ./daysteps.py --data ../../datas/2006-day-001.txt
Output:
Calls,Len Strat,Len Data,Datetime,Open,High,Low,Close,Volume,OpenInterest 0001,0001,0001,2006-01-02T23:59:59,3578.73,3578.73,3578.73,3578.73,0.00,0.00 - I could issue a buy order during the Opening 0002,0001,0001,2006-01-02T23:59:59,3578.73,3605.95,3578.73,3604.33,0.00,0.00 0003,0002,0002,2006-01-03T23:59:59,3604.08,3604.08,3604.08,3604.08,0.00,0.00 - I could issue a buy order during the Opening 0004,0002,0002,2006-01-03T23:59:59,3604.08,3638.42,3601.84,3614.34,0.00,0.00 0005,0003,0003,2006-01-04T23:59:59,3615.23,3615.23,3615.23,3615.23,0.00,0.00 - I could issue a buy order during the Opening 0006,0003,0003,2006-01-04T23:59:59,3615.23,3652.46,3615.23,3652.46,0.00,0.00 ... ... 0505,0253,0253,2006-12-27T23:59:59,4079.70,4079.70,4079.70,4079.70,0.00,0.00 - I could issue a buy order during the Opening 0506,0253,0253,2006-12-27T23:59:59,4079.70,4134.86,4079.70,4134.86,0.00,0.00 0507,0254,0254,2006-12-28T23:59:59,4137.44,4137.44,4137.44,4137.44,0.00,0.00 - I could issue a buy order during the Opening 0508,0254,0254,2006-12-28T23:59:59,4137.44,4142.06,4125.14,4130.66,0.00,0.00 0509,0255,0255,2006-12-29T23:59:59,4130.12,4130.12,4130.12,4130.12,0.00,0.00 - I could issue a buy order during the Opening 0510,0255,0255,2006-12-29T23:59:59,4130.12,4142.01,4119.94,4119.94,0.00,0.00
The following happens:
-
next
is called:510 times
which is255 x 2
-
The
len
of the Strategy and that of the data reaches a total of255
, which is the expected: the data has only those many bars -
Every time the
len
of the data increases, the 4 price components have the same value, namely theopen
priceHere a remark is printed out to indicate that during this opening phase action could be taken, like for example buying.
Effectively:
- The daily data feed is being replayed with 2 steps for each day, giving
the option to act in between
open
and the rest of the price components
The filter will be added to the default distribution of backtrader in the next release.
The sample code including the filter.
from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse from datetime import datetime, time import backtrader as bt class DayStepsFilter(object): def __init__(self, data): self.pendingbar = None def __call__(self, data): # Make a copy of the new bar and remove it from stream newbar = [data.lines[i][0] for i in range(data.size())] data.backwards() # remove the copied bar from stream openbar = newbar[:] # Make an open only bar o = newbar[data.Open] for field_idx in [data.High, data.Low, data.Close]: openbar[field_idx] = o # Nullify Volume/OpenInteres at the open openbar[data.Volume] = 0.0 openbar[data.OpenInterest] = 0.0 # Overwrite the new data bar with our pending data - except start point if self.pendingbar is not None: data._updatebar(self.pendingbar) self.pendingbar = newbar # update the pending bar to the new bar data._add2stack(openbar) # Add the openbar to the stack for processing return False # the length of the stream was not changed def last(self, data): '''Called when the data is no longer producing bars Can be called multiple times. It has the chance to (for example) produce extra bars''' if self.pendingbar is not None: data.backwards() # remove delivered open bar data._add2stack(self.pendingbar) # add remaining self.pendingbar = None # No further action return True # something delivered return False # nothing delivered here class St(bt.Strategy): params = () def __init__(self): pass def start(self): self.callcounter = 0 txtfields = list() txtfields.append('Calls') txtfields.append('Len Strat') txtfields.append('Len Data') txtfields.append('Datetime') txtfields.append('Open') txtfields.append('High') txtfields.append('Low') txtfields.append('Close') txtfields.append('Volume') txtfields.append('OpenInterest') print(','.join(txtfields)) self.lcontrol = 0 def next(self): self.callcounter += 1 txtfields = list() txtfields.append('%04d' % self.callcounter) txtfields.append('%04d' % len(self)) txtfields.append('%04d' % len(self.data0)) txtfields.append(self.data.datetime.datetime(0).isoformat()) txtfields.append('%.2f' % self.data0.open[0]) txtfields.append('%.2f' % self.data0.high[0]) txtfields.append('%.2f' % self.data0.low[0]) txtfields.append('%.2f' % self.data0.close[0]) txtfields.append('%.2f' % self.data0.volume[0]) txtfields.append('%.2f' % self.data0.openinterest[0]) print(','.join(txtfields)) if len(self.data) > self.lcontrol: print('- I could issue a buy order during the Opening') self.lcontrol = len(self.data) def runstrat(): args = parse_args() cerebro = bt.Cerebro() data = bt.feeds.BacktraderCSVData(dataname=args.data) data.addfilter(DayStepsFilter) cerebro.adddata(data) cerebro.addstrategy(St) cerebro.run(stdstats=False, runonce=False, preload=False) if args.plot: cerebro.plot(style='bar') def parse_args(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='Sample for pivot point and cross plotting') parser.add_argument('--data', required=False, default='../../datas/2005-2006-day-001.txt', help='Data to be read in') parser.add_argument('--plot', required=False, action='store_true', help=('Plot the result')) return parser.parse_args() if __name__ == '__main__': runstrat()