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/

    A Different Approach to Passive Investing - [Help]

    General Code/Help
    2
    10
    199
    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.
    • James Scarr
      James Scarr last edited by

      Good morning all,

      I am new to the Back Trader community and I am attempting to create some simple back tests but I'm struggling. Essentially my idea is a slightly unique take on Dollar-Cost Averaging. I want to first evaluate the effectiveness of simply Dollar-Cost Averaging (Which is done neatly in the article "Buy and Buy More" on the Back Trader site). This means to essentially invest a fixed amount at regular time intervals (For example $500 at the start of each month as I'm sure you're all aware of).

      However, I wish to alter the amount invested depending on recent trends in the market. For example if the S&P500 (The ETF I am backtesting on), has had a correction then I would like to invest more than usual. However if it has risen steeply in the past month invest a little less.

      I am encountering issues as I seem to be able to only buy whole units of the ETF and I really need to be able to test fractional amounts which I have found an article relating to but can't implement it with the "Buy and Buy More" article.

      The strategy would look something like this:
      S&P500 5% Monthly Increase = Invest 0.8 * Standard Monthly Amount
      S&P500 1% Monthly Increase = Invest 1 * Standard Monthly Amount
      S&P500 3% Monthly Decrease = Invest 1.2 * Standard Monthly Amount
      S&P500 5% Monthly Decrease = Invest 1.4 * Standard Monthly Amount

      Is it possible to change the monthly investable amount such as described here and buy fractional units of an ETF based on recent price fluctuations. I am essentially trying to return slightly more than the market by adding a bit of optimisation to Dollar-Cost Averaging.

      If you have any ideas on my strategy, tips or tricks or just general advice or criticism I would love to hear it. Thanks for taking the time to read my post and I eagerly await any comments made :)

      Kind regards,
      James

      James Scarr 1 Reply Last reply Reply Quote 0
      • James Scarr
        James Scarr @James Scarr last edited by

        @james-scarr
        My code so far if anyone wants to play with it:

        import datetime
        import backtrader as bt
        from pandas_datareader import data as pdr

        Get stock data from yahoo

        def get_data(stock, start, end):
        stockData = pdr.get_data_yahoo(stock, start, end)
        return stockData

        Use the S&P500 Symbol ^GSPC on Yahoo Finance

        stock = '^GSPC'

        Choose stock data to retrieve and set a start and end date

        endDate = datetime.datetime(2021,1,1)
        startDate = datetime.datetime(2020,1,1)
        stockData = get_data(stock, startDate, endDate)

        Actual start date is the start of the following day

        actualStart = stockData.index[0]

        Put data in a format that BackTrader understands

        data = bt.feeds.PandasData(dataname=stockData)

        ######################

        class CommInfoFractional(bt.CommissionInfo):
        def getsize(self, price, cash):
        '''Returns fractional size for cash operation @price'''
        return self.p.leverage * (cash / price)

        ######################

        class FixedCommisionScheme(bt.CommInfoBase):
        paras = (
        ('commission', 10),
        ('stocklike', True),
        ('commtype', bt.CommInfoBase.COMM_FIXED)
        )

        def _getcommission(self, size, price, pseudoexec):
            return self.p.commission
        

        #####################

        class PoundCostAveraging(bt.Strategy):
        params = dict(
        monthly_cash=500,
        monthly_range=[1]
        )

        def __init__(self):
            # Keep track of variables
            self.order = None
            self.totalcost = 0
            self.cost_wo_bro = 0
            self.units = 0
            self.times = 0
        
        def log(self, txt, dt=None):
            # Print the trades as they occur
            dt = dt or self.datas[0].datetime.date(0)
            print(dt.isoformat() + ', ' + txt)
        
        def start(self):
            self.broker.set_fundmode(fundmode=True, fundstartval=100.0)
            self.cash_start = self.broker.get_cash()
            self.val_start = 100.0
        
            # ADD A TIMER
            self.add_timer(
                when=bt.timer.SESSION_START,
                monthdays=[i for i in self.p.monthly_range],
                monthcarry=True
                # timername='buytimer',
            )
        
        def notify_timer(self, timer, when, *args):
            self.broker.add_cash(self.p.monthly_cash)
            target_value = self.broker.get_value() + self.p.monthly_cash - 10
            self.order_target_value(target=target_value)
        
        def notify_order(self, order):
            if order.status in [order.Submitted, order.Accepted]:
                return
            
            if order.status in [order.Completed]:
                if order.isbuy():
                    self.log(
                        'BUY EXECUTED, Price %.2f, Cost %.2f, Comm %.2f, Size %.0f' %
                        (order.executed.price,
                        order.executed.value,
                        order.executed.comm,
                        order.executed.size)
                    )
        
                    self.units += order.executed.size
                    self.totalcost += order.executed.value + order.executed.comm
                    self.cost_wo_bro += order.executed.value
                    self.times += 1
        
            elif order.status in [order.Canceled, order.Margin, order.Rejected]:
                self.log('Order Canceled/Margin/Rejected')
                print(order.status, [order.Canceled, order.Margin, order.Rejected])
        
            self.order = None
        
        def stop(self):
            # calculate actual returns
            self.roi = (self.broker.get_value() / self.cash_start) - 1
            self.froi = (self.broker.get_fundvalue() - self.val_start)
            value = self.datas[0].close * self.units + self.broker.get_cash()
            print('-'*50)
            print('PoundCostAveraging')
            print('For year ending in ' + str(endDate))
            print('Time in Market: {:.1f} years'.format((endDate - actualStart).days/365))
            print('#Times:         {:.0f}'.format(self.times))
            print('Value:         ${:,.2f}'.format(value))
            print('Cost:          ${:,.2f}'.format(self.totalcost))
            print('Gross Return:  ${:,.2f}'.format(value - self.totalcost))
            print('Gross %:        {:.2f}%'.format((value/self.totalcost - 1) * 100))
            print('ROI:            {:.2f}%'.format(100.0 * self.roi))
            print('Fund Value:     {:.2f}%'.format(self.froi))
            print('Annualised:     {:.2f}%'.format(100*((1+self.froi/100)**(365/(endDate - actualStart).days) - 1)))
            print('-'*50)
        

        ########################

        def run(data):
        # Pound Cost Averaging
        cerebro = bt.Cerebro()
        cerebro.adddata(data)
        cerebro.addstrategy(PoundCostAveraging)

        # Broker Information
        broker_args = dict(coc=True)
        cerebro.broker = bt.brokers.BackBroker(**broker_args)
        cerebro.broker.addcommissioninfo(CommInfoFractional()) 
        #comminfo = FixedCommisionScheme()
        #cerebro.broker.addcommissioninfo(comminfo)
        
        cerebro.broker.set_cash(500)
        cerebro.run()
        cerebro.plot(iplot=False, style='candlestick')
        

        if name == 'main':
        run(data)

        run-out 1 Reply Last reply Reply Quote 0
        • run-out
          run-out @James Scarr last edited by

          @james-scarr
          I would like to look at this but could you format the code by putting it between triple quotes as per the instructions at the top of the page? Thanks.

          For code/output blocks: Use ``` (aka backtick or grave accent) in a single line before and after the block. See: http://commonmark.org/help/

          RunBacktest.com

          James Scarr 1 Reply Last reply Reply Quote 0
          • James Scarr
            James Scarr @run-out last edited by

            @run-out sorry about that!

            import datetime
            import backtrader as bt
            from pandas_datareader import data as pdr
            
            # Get stock data from yahoo
            stock = '^GSPC'
            def get_data(stock, start, end):
                stockData = pdr.get_data_yahoo(stock, start, end)
                return stockData
            endDate = datetime.datetime(2021,1,1)
            startDate = datetime.datetime(2020,1,1)
            stockData = get_data(stock, startDate, endDate)
            # Actual start date is the start of the following day 
            actualStart = stockData.index[0]
            # Put data in a format that BackTrader understands
            data = bt.feeds.PandasData(dataname=stockData)
            
            ######################
            
            class CommInfoFractional(bt.CommissionInfo):
                def getsize(self, price, cash):
                    '''Returns fractional size for cash operation @price'''
                    return self.p.leverage * (cash / price)
            
            ######################
            
            class FixedCommisionScheme(bt.CommInfoBase):
                paras = (
                    ('commission', 10),
                    ('stocklike', True),
                    ('commtype', bt.CommInfoBase.COMM_FIXED)
                )
                def _getcommission(self, size, price, pseudoexec):
                    return self.p.commission
            
            #####################
            
            class PoundCostAveraging(bt.Strategy):
                params = dict(
                    monthly_cash=500,
                    monthly_range=[1]
                )
            
                def __init__(self):
                    # Keep track of variables
                    self.order = None
                    self.totalcost = 0
                    self.cost_wo_bro = 0
                    self.units = 0
                    self.times = 0
                    self.prices = []
            
                def log(self, txt, dt=None):
                    # Print the trades as they occur
                    dt = dt or self.datas[0].datetime.date(0)
                    print(dt.isoformat() + ', ' + txt)
            
                def start(self):
                    self.broker.set_fundmode(fundmode=True, fundstartval=100.0)
                    self.cash_start = self.broker.get_cash()
                    self.val_start = 100.0
            
                    # ADD A TIMER
                    self.add_timer(
                        when=bt.timer.SESSION_START,
                        monthdays=[i for i in self.p.monthly_range],
                        monthcarry=True
                        # timername='buytimer',
                    )
            
                def notify_timer(self, timer, when, *args):
                    if len(self.prices) > 2:
                        if self.prices[-1] > self.prices[-2]:
                            highFigure = self.p.monthly_cash * 1.2
                            self.broker.add_cash(highFigure)
                            target_value = (self.broker.get_value() + highFigure) - 10
                            self.order_target_value(target=target_value)
                        else:
                            lowFigure = self.p.monthly_cash * 0.8
                            self.broker.add_cash(lowFigure)
                            target_value = (self.broker.get_value() + lowFigure) - 10
                            self.order_target_value(target=target_value)
                    else:
                        self.broker.add_cash(self.p.monthly_cash)
                        target_value = (self.broker.get_value() + self.p.monthly_cash) - 10
                        self.order_target_value(target=target_value)
            
                def notify_order(self, order):
                    if order.status in [order.Submitted, order.Accepted]:
                        return
                    
                    if order.status in [order.Completed]:
                        if order.isbuy():
                            self.log(
                                'BUY EXECUTED, Price %.2f, Cost %.2f, Comm %.2f, Size %.0f' %
                                (order.executed.price,
                                order.executed.value,
                                order.executed.comm,
                                order.executed.size)
                            )
            
                            self.units += order.executed.size
                            self.totalcost += order.executed.value + order.executed.comm
                            self.cost_wo_bro += order.executed.value
                            self.times += 1
                            self.prices.append(order.executed.price)
            
                    elif order.status in [order.Canceled, order.Margin, order.Rejected]:
                        self.log('Order Canceled/Margin/Rejected')
                        print(order.status, [order.Canceled, order.Margin, order.Rejected])
            
                    self.order = None
            
                def stop(self):
                    # calculate actual returns
                    self.roi = (self.broker.get_value() / self.cash_start) - 1
                    self.froi = (self.broker.get_fundvalue() - self.val_start)
                    value = self.datas[0].close * self.units + self.broker.get_cash()
                    print('-'*50)
                    print('PoundCostAveraging')
                    print('For year ending in ' + str(endDate))
                    print('Time in Market: {:.1f} years'.format((endDate - actualStart).days/365))
                    print('#Times:         {:.0f}'.format(self.times))
                    print('Value:         ${:,.2f}'.format(value))
                    print('Cost:          ${:,.2f}'.format(self.totalcost))
                    print('Gross Return:  ${:,.2f}'.format(value - self.totalcost))
                    print('Gross %:        {:.2f}%'.format((value/self.totalcost - 1) * 100))
                    print('ROI:            {:.2f}%'.format(100.0 * self.roi))
                    print('Fund Value:     {:.2f}%'.format(self.froi))
                    print('Annualised:     {:.2f}%'.format(100*((1+self.froi/100)**(365/(endDate - actualStart).days) - 1)))
                    print('-'*50)
            
            
            ########################
            
            def run(data):
                # Pound Cost Averaging
                cerebro = bt.Cerebro()
                cerebro.adddata(data)
                cerebro.addstrategy(PoundCostAveraging)
            
                # Broker Information
                broker_args = dict(coc=True)
                cerebro.broker = bt.brokers.BackBroker(**broker_args)
                #Choose between comission schemes first is for fractional amounts second charges you $10 per buy
                cerebro.broker.addcommissioninfo(CommInfoFractional()) 
                #cerebro.broker.addcommissioninfo(FixedCommisionScheme())
            
                cerebro.broker.set_cash(500)
                cerebro.run()
                cerebro.plot(iplot=False, style='candlestick')
            
            if __name__ == '__main__':
                run(data)
            
            run-out 1 Reply Last reply Reply Quote 0
            • run-out
              run-out @James Scarr last edited by run-out

              @james-scarr You code seems to be working to me. The only minor error I saw was that you are printing to console the units formatting to 0 decimals, so you only see zeros since the stock price is higher than $500. Change like this:

               self.log(
                      'BUY EXECUTED, Price %.2f, Cost %.2f, Comm %.2f, Size %.4f' %
                      (order.executed.price,
                      order.executed.value,
                      order.executed.comm,
                      order.executed.size)
              )
              

              RunBacktest.com

              James Scarr 1 Reply Last reply Reply Quote 0
              • James Scarr
                James Scarr @run-out last edited by

                @run-out
                Thank you very much I'll make that adjustment!

                I don't suppose you know how to alter the indicators in Cerebro.plot() so that I could perhaps change the colour of the buy signal on the graph depending on the value invested (e.g. blue for £800, green for £1000, etc)

                run-out 1 Reply Last reply Reply Quote 0
                • run-out
                  run-out @James Scarr last edited by

                  @james-scarr Sorry I don't use the built in plotting.

                  RunBacktest.com

                  James Scarr 2 Replies Last reply Reply Quote 0
                  • James Scarr
                    James Scarr @run-out last edited by

                    @run-out No worries.

                    Just wanted to let you know that if you did end up using my code it has a major flaw in it.
                    Under the function notify_timer() I compare the two most recent orders with self.prices[-1] > self.prices[-2]. This logic is completely wrong as I am comparing orders that have already happened and then using that data to decide on the next trade.

                    For example on March 1st I am comparing whether February was lower than January and then deciding whether to buy more in March. Whereas I should be checking whether March is lower than February.
                    Sorry!

                    1 Reply Last reply Reply Quote 0
                    • James Scarr
                      James Scarr @run-out last edited by

                      @run-out
                      This can be fixed by changing to:

                       def notify_timer(self, timer, when, *args):
                              # If we bought last month then compare last month to this month
                              # determine how much more or less to invest depending on this difference
                              if len(self.prices) > 1:
                                  percentageChange = round(((self.data[0]/self.prices[-1])-1)*100,2)
                                  #print(f"The percentage change was {percentageChange}%")
                                  print(f'These are the prices: {percentageChange}')
                                  if percentageChange < -1:
                                      lowFigure = self.p.monthly_cash * 1.4
                                      self.broker.add_cash(lowFigure)
                                      target_value = (self.broker.get_value() + lowFigure) - 10
                                      self.order_target_value(target=target_value)
                                  else:
                                      highFigure = self.p.monthly_cash * 1
                                      self.broker.add_cash(highFigure)
                                      target_value = (self.broker.get_value() + highFigure) - 10
                                      self.order_target_value(target=target_value)
                              else:
                                  self.broker.add_cash(self.p.monthly_cash)
                                  target_value = (self.broker.get_value() + self.p.monthly_cash) - 10
                                  self.order_target_value(target=target_value)
                      

                      Unfortunately I seem to be finding that this strategy does not perform much better at all compared to dollar-cost averaging. :(

                      1 Reply Last reply Reply Quote 0
                      • James Scarr
                        James Scarr last edited by

                        @run-out
                        Would you be able to remove this thread from Backtrader as well as the other one I posted? I am trying to delete them but I don't think I can and I would rather my code wasn't publicly available :)
                        Kind regards,
                        James

                        1 Reply Last reply Reply Quote 0
                        • 1 / 1
                        • First post
                          Last post
                        Copyright © 2016, 2017, 2018, 2019, 2020, 2021 NodeBB Forums | Contributors