order_target_percent calculation problems.
-
Hi All,
I am writing a simple test code to try out the order_target functionality of backtrader as described here. For some reason the order_target_percent is not being placed/filled properly for backtesting purposes. I suspect the problem arises when
market
order size derived from theclose
of the day bar is larger than theopen
of the next trading day, causing the order to be placed at more than the portfolio liquidation value and thus not placed/filled.What happens in a portfolio asset allocation process is, at end of the rebalancing period, recalculate the statistics and the new weightings. The rebalancing orders will be placed at the open of the next rebalancing period and filled during that day.
But instead, backtrader is placing the orders at the close of the day of a rebalance period but executing those orders on the next day. Those orders are not filled if the
open
of the next day is higher than theclose
of the previous day because the sizing is calculated off the wrong basis and triggers an order which buys more than the portfolio's cash.What can be done to alleviate this problem?
Here's my code. Basically owns 100% S&P between October and April and hold only cash between May and September.
class SellInMay(bt.Strategy): def next(self): dt = self.data.datetime.date() portfolio_value = self.broker.get_value() print('%04d - %s - Position Size: %02d - Value %.2f' % (len(self), dt.isoformat(), self.position.size, portfolio_value)) data_value = self.broker.get_value([self.data]) print('data is', self.data[0]) port_perc = data_value / portfolio_value print('%04d - %s - data percent %.2f' % (len(self), dt.isoformat(), port_perc)) if dt.month >=10 or dt.month <=4: percent = 1 print('%04d - %s - Order Target Percent: %.2f' % (len(self), dt.isoformat(), percent)) self.order = self.order_target_percent(target=percent) else: percent = 0 self.order = self.order_target_percent(target=percent) print('%04d - %s - Order Target Percent: %.2f' % (len(self), dt.isoformat(), percent))
And here is where I found the feature:
Starting Portfolio Value: 10000.00 0001 - 1950-01-03 - Position Size: 00 - Value 10000.00 data is 16.66 0001 - 1950-01-03 - data percent 0.00 0001 - 1950-01-03 - Order Target Percent: 1.00 <= Order placed 0002 - 1950-01-04 - Position Size: 00 - Value 10000.00 data is 16.85 0002 - 1950-01-04 - data percent 0.00 <= uptick, order_target_percent not filled. 0002 - 1950-01-04 - Order Target Percent: 1.00 <= Order placed 0003 - 1950-01-05 - Position Size: 00 - Value 10000.00 data is 16.93 0003 - 1950-01-05 - data percent 0.00 <= uptick, order_target_percent not filled. 0003 - 1950-01-05 - Order Target Percent: 1.00 <= Order placed 0004 - 1950-01-06 - Position Size: 00 - Value 10000.00 data is 16.98 0004 - 1950-01-06 - data percent 0.00 <= uptick, order_target_percent not filled. 0004 - 1950-01-06 - Order Target Percent: 1.00 <= Order placed 0005 - 1950-01-09 - Position Size: 00 - Value 10000.00 data is 17.08 0005 - 1950-01-09 - data percent 0.00 <= uptick, order_target_percent not filled. 0005 - 1950-01-09 - Order Target Percent: 1.00 <= Order placed 0006 - 1950-01-10 - Position Size: 585 - Value 10000.00 data is 17.030001000000002 0006 - 1950-01-10 - data percent 1.00 <= downtick, order_target_percent filled!!~ 0006 - 1950-01-10 - Order Target Percent: 1.00 0007 - 1950-01-11 - Position Size: 587 - Value 10035.10
-
I've raised this as an issue on his
github
repo. Also had skipped trades due to price differences. -
Thanks for the confirmation. I was reading through the documentations trying to figure out if I've placed the order wrong...
Also, for chart outputs, anyway we can plot multiple lines of our choices? aka, plotting strategy values over time and a calculated benchmark over the same plot?
-
The issue has been seen and was about to get the same answer. It's not an issue. It works exactly as designed.
If no price is specified, then the
close
price is used for the calculations. If no order execution type is specified, thenMarket
is used. An order forX
shares withMarket
execution type. The matching price is the next incoming price.If the opening price is higher the order is as expected rejected because the account doesn't have enough cash. The platform will NOT look into the future (even if backtesting, the data is not always preloaded) to modify the calculation when the incoming price is there.
Changing the execution type to
Limit
makes sure that the order is not rejected and waits for the price. -
Yes, its according to the design and outlined in your blog post. But the design is, in my honest opinion, flawed for asset allocation algorithm backtesting.
As I've stated previously, in an asset allocation scenario, the weighting is determined after market close at the end of the day. The rebalancing orders are created next day at the open or sometimes during market, and filled by the end of the day.
Using
limit
order will make sure the order is not rejected, but it will still cause rebalancing orders to be skipped for several days until we see a down-tick.Even if the
backtrader
usesopen
to calculate sizing, it still might not be filled ifclose
is higher than open.Is there a way to create rebalancing orders to make sure that they get 100% filled during a daily bar?
-
Looks like we have two options:
- some look up into the future
self.order_target_percent(target=1.0, exectype=bt.Order.Limit, price=self.data.open[1])
- split daily bar in two bars: OOOO only and OHLC. Then issue orders on the 1st new bar with
set_coc
parameter. I am not sure if this will work, but some similar things were shown here -
https://www.backtrader.com/blog/posts/2016-07-29-pinkfish-challenge/pinkfish-challenge.html
-
Splitting the bar is possible. The blog post was the first time it was done. Standard filters were added to the platform to do it.
- Docu - Filters Reference - Look for
BarReplayer_Open
It has really not been tested in a while and some of the changes to the resampling/replaying may have had an impact.
- Docu - Filters Reference - Look for
-
@ab_trader said in order_target_percent calculation problems.:
- some look up into the future
self.order_target_percent(target=1.0, exectype=bt.Order.Limit, price=self.data.open[1])
The existence of something at index
[1]
cannot be guaranteed. It will for sure be there if the data has been preloaded, but this guarantee is not possible. -
@backtrader Thank you for direction. I'll check it.
-
@ab_trader Lookup into next day open gets the job done with a
try
except
wraparound to catchIndexError
but its a quick/dirty workaround with lookhead bias and won't be able to carry into live rebalancing.@backtrader Not sure how using DaySplitter_Close filter will carry the code to actual live rebalancing, any suggestions?
Also, in testing DaySplitter_Close, I've added the
DaySplitter_Close
filter to my code, and it generates this errorNameError: name 'datetime' is not defined
when I tried either import datetimeor
from datetime impiort datetime. I've also tried both
results = cerebro.run()and just
cerebro.run()` and neither method worked.I find it perplexing because in either import methods,
feed.py
always didimport datetime
so I am not sure what is wrong. Any help will be greatly appreciated. By the waypandas_datareader
is required for future backtests that incorporate other macro data for portfolio rebalancing.Here's my code:
import backtrader as bt import backtrader.filters as btfilters #from datetime import datetime import datetime class SellInMay(bt.Strategy): def next(self): dt = self.data.datetime.date() portfolio_value = self.broker.get_value() print('%04d - %s - Position Size: %02d - Value %.2f' % (len(self), dt.isoformat(), self.position.size, portfolio_value)) data_value = self.broker.get_value([self.data]) print('data is', self.data[0]) port_perc = data_value / portfolio_value print('%04d - %s - data percent %.2f' % (len(self), dt.isoformat(), port_perc)) if dt.month >=10 or dt.month <=4: percent = 1 print('%04d - %s - Order Target Percent: %.2f' % (len(self), dt.isoformat(), percent)) self.order = self.order_target_percent(target=percent) else: percent = 0 self.order = self.order_target_percent(target=percent) print('%04d - %s - Order Target Percent: %.2f' % (len(self), dt.isoformat(), percent)) def runstrat(): start = datetime.datetime(1949,12,31) end = datetime.datetime(1951,1,3) spx = web.DataReader("^GSPC", 'yahoo', start, end) data = bt.feeds.PandasData(dataname=spx) data.addfilter(btfilters.DaySplitter_Close) # <= added filter. cerebro = bt.Cerebro() cerebro.addstrategy(SellInMay) cerebro.adddata(data) cerebro.broker.setcash(10000.0) print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) results = cerebro.run() print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue()) cerebro.plot() if __name__ == '__main__': runstrat()
And here's the error:
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-29-496d5974d836> in <module>() 54 55 if __name__ == '__main__': ---> 56 runstrat() <ipython-input-29-496d5974d836> in runstrat() 48 49 print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) ---> 50 results = cerebro.run() 51 print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue()) 52 C:\Anaconda3\envs\backtrader\lib\site-packages\backtrader\cerebro.py in run(self, **kwargs) 808 # let's skip process "spawning" 809 for iterstrat in iterstrats: --> 810 runstrat = self.runstrategies(iterstrat) 811 self.runstrats.append(runstrat) 812 else: C:\Anaconda3\envs\backtrader\lib\site-packages\backtrader\cerebro.py in runstrategies(self, iterstrat, predata) 871 data._start() 872 if self._dopreload: --> 873 data.preload() 874 875 for stratcls, sargs, skwargs in iterstrat: C:\Anaconda3\envs\backtrader\lib\site-packages\backtrader\feed.py in preload(self) 390 391 def preload(self): --> 392 while self.load(): 393 pass 394 C:\Anaconda3\envs\backtrader\lib\site-packages\backtrader\feed.py in load(self) 475 retff = ff(self, *fargs, **fkwargs) 476 else: --> 477 retff = ff(self, *fargs, **fkwargs) 478 479 if retff: # bar removed from systemn C:\Anaconda3\envs\backtrader\lib\site-packages\backtrader\filters\bsplitter.py in __call__(self, data) 87 88 # Adjust times ---> 89 dt = datetime.datetime.combine(datadt, data.p.sessionstart) 90 ohlbar[data.DateTime] = data.date2num(dt) 91 NameError: name 'datetime' is not defined
-
@cnimativ said in order_target_percent calculation problems.:
@backtrader Not sure how using DaySplitter_Close filter will carry the code to actual live rebalancing, any suggestions?
Not sure because the recommendation was to use
BarReplayer_Open
and the filter is applied for backtesting. In live trading one would most probably issue the order during market open with the actual opening price. In any case the final order going to a broker must have a number of items tobuy/sell
and this has to be calculated using an actual price.Even in live circumstances and with the actual last price, the order could be rejected if the price moves against the order and the total cash is exceeded.
Note:
DaySplitter_Close
was backported from a sample and unfortunately thedatetime
import was missed. Will be corrected. -
if I use
BarReplayer_Open
filter, then I'll need to have two feeds added:- one with the filter applied to simulate order processing, and
- one for indicator calculations
Am I correct?
-
It is a replayer. The length of the stream will not be changed by the splitting of
O
andHLC
. It simply gives you the chance to seeO
and act before you see the rest. The indicator will be calculated twice, but within the same length (like if you were receiving ticks in real-time for a daily bar) -
@backtrader I've used
Daysteps
sample as a start point forBarReplayer_Open
implementation. The sample itself works well, until I added orders (here is the updatednext()
from the sample):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.order = self.buy(size=1) #<------ this line was added self.lcontrol = len(self.data)
This addition causes the following error message right on the 1st bar:
Calls,Len Strat,Len Data,Datetime,Open,High,Low,Close,Volume,OpenInterest 0001,0001,0001,2016-12-09T23:59:59.999989,225.41,225.41,225.41,225.41,0.00,0.00 - I could issue a buy order during the Opening Traceback (most recent call last): File "bt_sample_daysteps.py", line 143, in <module> runstrat() File "bt_sample_daysteps.py", line 118, in runstrat cerebro.run(**(eval('dict(' + args.cerebro + ')'))) File "D:\Python27\lib\site-packages\backtrader\cerebro.py", line 810, in run runstrat = self.runstrategies(iterstrat) File "D:\Python27\lib\site-packages\backtrader\cerebro.py", line 940, in runstrategies self._runnext(runstrats) File "D:\Python27\lib\site-packages\backtrader\cerebro.py", line 1250, in _runnext self._brokernotify() File "D:\Python27\lib\site-packages\backtrader\cerebro.py", line 1002, in _brokernotify self._broker.next() File "D:\Python27\lib\site-packages\backtrader\brokers\bbroker.py", line 863, in next self._try_exec(order) File "D:\Python27\lib\site-packages\backtrader\brokers\bbroker.py", line 809, in _try_exec popen = data.tick_open or data.open[0] File "D:\Python27\lib\site-packages\backtrader\lineseries.py", line 429, in __getattr__ return getattr(self.lines, name) AttributeError: 'Lines_LineSeries_DataSeries_OHLC_OHLCDateTime_Abst' object has no attribute 'tick_open'
Same error I obtained using
BarReplayer_Open
with little bit different definition of theOpen
virtual bar. Could you please advice me how to fix this? -
This is one of the many interactions of the many things introduced over time which have accumulated. Luckily it can be easily taken care of away from where the problem happens, by making it safe in the broker.
This commit in the
development
branch should help: https://github.com/mementum/backtrader/commit/0c84b342a4155da27804c38f172955b733b0f581 -
Thank you!
I'll wait for release. -
@backtrader with new release this approach works great!. In order to have the order execution on the current bar
Open
, I setcoc
parameter toTrue
. Otherwise execution happened on the next barOpen
. Probably this replayer will increase run time, but now gaps can be also considered.Thank you!
PS In the
Daysteps
sample it is a linecerebro._doreplay = True
. This line doesn't change anything in the results. What is this line for? -
Something that later was detected automatically. To make some provisions internally like automatically disabling
runonce=True
(i.e.: set it toFalse
) -
See: Community - Release 1.9.44.116
For Cheat-On-Open see Blog - Cheat On Open
-
@backtrader Very nice! One step closer to the quality of R's Quantstrat package. Will test it soon