Navigation

    Backtrader Community

    • Register
    • Login
    • Search
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Search
    For code/output blocks: Use ``` (aka backtick or grave accent) in a single line before and after the block. See: http://commonmark.org/help/

    Fractional order sizes not floating point

    General Code/Help
    1
    3
    51
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • D
      dorien last edited by

      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()
      
      1 Reply Last reply Reply Quote 0
      • D
        dorien last edited by

        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.

        D 1 Reply Last reply Reply Quote 0
        • D
          dorien @dorien last edited by

          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
          
          1 Reply Last reply Reply Quote 0
          • 1 / 1
          • First post
            Last post
          Copyright © 2016, 2017, 2018, 2019, 2020, 2021 NodeBB Forums | Contributors