Fractional order sizes not floating point
-
I'm doing backtesting for a bitcoin strategy. As we al know, contrary to stock market, we can buy 1.5 BTC. In my strategy, I followed the steps in the official article on fractional order sizes, yet my output shows that the order size is rounded:
I aim to buy with a position size of 99%, but it keeps rounding it. E.g. instead of 5.00 I would like to see 5.12 BTC etc.
To summarize, I did add:
class CommInfoFractional(bt.CommissionInfo): def getsize(self, price, cash): '''Returns fractional size for cash operation @price''' return self.p.leverage * (cash / price)
Output:
2019-11-02 BUY EXECUTED, 9231.40 of size 5.00 2019-11-08 SELL CREATE 8773.730000 2019-11-09 SELL EXECUTED, 8773.74 of size -16.00 2019-11-10 BUY EXECUTED, 8809.18 of size 16.00 2019-11-25 BUY EXECUTED, 6900.23 of size 1.00 2020-02-25 SELL CREATE 9315.840000 2020-02-26 SELL EXECUTED, 9316.48 of size -16.00 2020-02-27 BUY EXECUTED, 8786.00 of size 15.00 2020-03-01 BUY EXECUTED, 8523.61 of size 1.00 2020-03-13 BUY EXECUTED, 4800.01 of size 2.00 2020-04-07 SELL EXECUTED, 7329.90 of size -1.00 2020-05-08 SELL EXECUTED, 9986.30 of size -1.00 2020-05-10 SELL CREATE 8722.770000 ...
My full code. With csv file available here.
import datetime import backtrader as bt import backtrader.feeds as btfeeds from backtrader.feeds import GenericCSVData import quantstats import pandas as pd import argparse import logging import sys class myGenericCSV(GenericCSVData): # Add a line to the inherited ones from the base class lines = ('buy','sell') # add the parameter to the parameters inherited from the base class params = (('buy', 10),('sell', 11),) class CommInfoFractional(bt.CommissionInfo): def getsize(self, price, cash): '''Returns fractional size for cash operation @price''' return self.p.leverage * (cash / price) class firstStrategy(bt.Strategy): params = dict( target=0.99, # percentage of value to use ) # Get cash and balance # New broker method that will let you get the cash and balance for # any wallet. It also means we can disable the getcash() and getvalue() # rest calls before and after next which slows things down. # NOTE: If you try to get the wallet balance from a wallet you have # never funded, a KeyError will be raised! Change LTC below as approriate # if self.live_data: # cash, value = self.broker.get_wallet_balance('USDT') # else: # # Avoid checking the balance during a backfill. Otherwise, it will # # Slow things down. # cash = 'NA' def log(self, txt, dt=None): dt = dt or self.datas[0].datetime.date(0) print(f'{dt.isoformat()} {txt}') #Print date and close def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # An active Buy/Sell order has been submitted/accepted - Nothing to do return # Check if an order has been completed # Attention: broker could reject order if not enough cash if order.status in [order.Completed]: if order.isbuy(): self.log(f'BUY EXECUTED, {order.executed.price:.2f} of size {order.executed.size: .2f}') elif order.issell(): self.log(f'SELL EXECUTED, {order.executed.price:.2f} of size {order.executed.size: .2f}') self.bar_executed = len(self) # self.log(f'{order.executed.size: .2f}') # {order.executed.size, order.executed.price, # order.executed.value, order.executed.comm) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Canceled/Margin/Rejected') # Reset orders self.order = None def loginfo(self, txt, *args): out = [self.datetime.date().isoformat(), txt.format(*args)] logging.info(','.join(out)) def notify_trade(self, trade): if trade.justopened: self.loginfo('Trade Opened - Size {} @Price {}', trade.size, trade.price) elif trade.isclosed: self.loginfo('Trade Closed - Profit {}', trade.pnlcomm) else: # trade updated self.loginfo('Trade Updated - Size {} @Price {}', trade.size, trade.price) def next(self): self.order_target_percent(target=self.p.target) if not self.position: if self.data.buy == 1: self.order = self.order_target_percent(target=self.p.target) # self.order = self.buy(data_min, size = 0.99, , price = self.target_exit_price[stock], exectype = bt.Order.Limit) self.log(f'BUY CREATE {self.data.close[0]:2f}') else: if self.data.sell == 1: self.order = self.order_target_percent(target=-self.p.target) self.log(f'SELL CREATE {self.data.close[0]:2f}') def run(args=None): cerebro = bt.Cerebro() # use the fractional scheme: cerebro.broker.addcommissioninfo(CommInfoFractional()) cerebro.addstrategy(firstStrategy) data = myGenericCSV( dataname='btc_data1d.csv', # timeframe=bt.TimeFrame., # fromdate=datetime.datetime(2022, 2, 9), fromdate=datetime.datetime(2019, 11, 1), todate=datetime.datetime(2022, 7, 25), nullvalue=0.0, dtformat=('%Y-%m-%d %H:%M:%S'), # lines = ('Time','Open','High','Low','Close','Volume','ATR','CC','Top','Btm','Buy','Sell','ODR','Trend','WM','Band','last_pivot' # ), datetime=0, high=2, low=3, open=1, close=4, buy=10, volume=5, sell=11, openinterest=-1, ) cerebro.adddata(data) startcash = 100000 cerebro.broker.setcash(startcash) cerebro.addanalyzer(bt.analyzers.PyFolio, _name='PyFolio') cerebro.broker.setcommission(commission=0.003) # cerebro.add_order_history(orders, notify=True) results = cerebro.run() strat = results[0] portfolio_stats = strat.analyzers.getbyname('PyFolio') returns, positions, transactions, gross_lev = portfolio_stats.get_pf_items() returns.index = returns.index.tz_convert(None) cerebro.plot() quantstats.reports.html(returns, output='stats.html', title='Strat') if __name__ == '__main__': run()
-
One step in the write direction. Found this custom sizer and figured out how to use it. Now order sizes are flowing point based on the prop percentage. However, it works long only.
Surely it must be easy to adapt this to properly reverse the orders (double size) in case of a sell when a long is open.
class Antoine_sizer(bt.Sizer): params = (('prop', 0.99),) def _getsizing(self, comminfo, cash, data, isbuy): """Returns the proper sizing""" if isbuy: # Buying target = self.broker.getvalue() * self.params.prop # Ideal total value of the position price = data.close[0] size_net = target / price # How many shares are needed to get target pos = self.broker.getposition(data).size size = size_net * self.params.prop if size * price > cash: return 0 # Not enough money for this trade else: return size else: # Selling return self.broker.getposition(data).size # Clear the position
Then add
cerebro.addsizer(Antoine_sizer)
and use
self.order = self.buy()
self.order = self.sell()to give orders.
Issue: no shorting.
-
Update. I wrote this sizer that will do fractional position sizing for crypto and that will reverse buy / sell orders. No pyramiding included atm, should not be too hard to add though:
class Dorien_sizer(bt.Sizer): params = (('prop', 0.95),) def _getsizing(self, comminfo, cash, data, isbuy): """Returns the proper sizing""" pos = self.broker.getposition(data).size if isbuy: # Buying if pos == 0: target = cash * self.params.prop price = data.close[0] size = target / price return size elif pos > 0: # don't allow pyramiding for now size = 0 if size * price > cash: return 0 # Not enough money for this trade else: return size # short open elif pos < 0: target = self.broker.getvalue() * self.params.prop # Ideal total value of the position price = data.close[0] size = target / price # How many shares are needed to get target size = size - pos return size else: # Selling if pos > 0: size = pos * 2 return size # Clear the position elif pos <= 0: size = pos return size