order_target_percent is not working as expected. Please read further; complete code, inputs and outputs are provided
-
Hi,
I am running the Longshort strategy (provided as an example here: https://www.backtrader.com/blog/posts/2015-09-14-write-it-down/write-it-down/) on BTC USD data (using fractional sizing as explained here: https://www.backtrader.com/blog/posts/2019-08-29-fractional-sizes/fractional-sizes/).
I'm using order_target_percent(0.5) in my code. I expect that for each buy operation, 50.0% of my cash should get used up. This does happen most of the time. Except when there is a buy immediately after a sell. The amount of cash that is used to make such buy operations is around 50%, but not exactly 50%.
Check the buy operations on Row Id = 20 and Row Id = 46 in the linked output to see what I'm talking about. For ex, in row 20: I expect my cash value to go from 9908.73 to 4954.36 (which is 50%). Instead it goes from 9908.73 to 4947.38 (which is 49.92%). Buy operations on row 72 and 91 don't have this discrepancy.
I suspect this could be due to rounding, but not sure. Let me know if any more info is required.
Input data: https://drive.google.com/file/d/1rPEfRnf1z4DknzhQGSZuL6esK7CWdGGa/view?usp=sharing
output: https://drive.google.com/file/d/1wjFaNYKJSC_Tez8yCneZlSmFaIxzb-Rd/view?usp=sharingCode:
from __future__ import (absolute_import, division, print_function, unicode_literals) import pandas as pd import numpy as np import datetime import os import seaborn as sns import sys import copy import argparse import logging import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind from backtrader.analyzers import SQN class CommInfoFractional(bt.CommissionInfo): def getsize(self, price, cash): '''Returns fractional size for cash operation @price''' return self.p.leverage * (cash / price) class act_buy_sell(bt.observer.Observer): ' For actual buy and sell prices' alias = ('Act_buy_sell',) lines = ('act_buy', 'act_sell', 'order_size') plotinfo = dict(plot=False, subplot=False) def next(self): for order in self._owner._orderspending: if order.data is not self.data: continue if order.isbuy(): self.lines.act_buy[0] = order.executed.price self.lines.order_size[0] = order.executed.size elif order.issell(): self.lines.act_sell[0] = order.executed.price self.lines.order_size[0] = order.executed.size class LongShortStrategy(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, csvcross=False, ) def start(self): pass def stop(self): pass 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.orderid = 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) self.signal.csv = self.p.csvcross def next(self): if self.orderid: 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() self.log('BUY CREATE , %.2f' % self.data.close[0]) self.order_target_percent(target=self.p.stake) elif self.signal < 0.0: if self.position: self.log('CLOSE LONG , %.2f' % self.data.close[0]) self.close() if not self.p.onlylong: self.log('SELL CREATE , %.2f' % self.data.close[0]) self.order_target_percent(target=-self.p.stake) 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.orderid = 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(period = 15 ,onlylong = True ,csvcross = True ,stake = 1 ,comm = 0 ,plot = True ,fractional = True): # Create a cerebro cerebro = bt.Cerebro() # Create the 1st data data = btfeeds.GenericCSVData( dataname=data #refer to attached data dtformat=('%Y-%m-%d'), fromdate=datetime.datetime(2017, 1, 1), todate=datetime.datetime(2017, 4, 9), openinterest=-1 ) # Add the 1st data to cerebro cerebro.adddata(data) cerebro.addobserver(act_buy_sell) if fractional: cerebro.broker.addcommissioninfo(CommInfoFractional()) else: # Add the commission - only stocks like a for each operation cerebro.broker.setcommission(commission=comm) # Add the strategy cerebro.addstrategy(LongShortStrategy, period=period, onlylong=onlylong, csvcross=csvcross, stake=stake) # Add the commission - only stocks like a for each operation cerebro.broker.setcash(10000) # cerebro.addanalyzer(SQN) cerebro.addwriter(bt.WriterFile, csv=True, rounding=2) # And run it cerebro.run() # Plot if requested if plot: cerebro.plot(volume=True, zdown=False) runstrategy(plot = False, stake = 0.5)
-
@backtrader @ab_trader @run-out please have a look when you get the chance as this could potentially be a bug. Thanks!