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()