Dollar Cost Averaging Index Funds & ETFs

This tutorial series is based off a article written by Backtrader

Here we will evaluate the difference between dollar cost averaging strategy and lump-sum investing. First, let’s import the dependencies and get the data in a format backtrader can use. 

import datetime
import math
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pandas_datareader import data as pdr
import backtrader as bt

# import data
def get_data(stocks, start, end):
    stockData = pdr.get_data_yahoo(stocks, start, end)
    return stockData

stockList = ['VTS.AX']
endDate =
startDate = endDate - datetime.timedelta(days=1800)

stockData = get_data(stockList[0], startDate, endDate)

actualStart = stockData.index[0]

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

Next we have to define a valid strategy tha backtrader can interpret. This requires the following functions; start, nextstart and end.

class BuyAndHold(bt.Strategy):
    def start(self):
        self.val_start =

    def nextstart(self):
        size = math.floor( ( - 10) /[0] )

    def stop(self):
        # calculate actual returns
        self.roi = ( / self.val_start) - 1
        print('BUY & HOLD')
        print('Starting Value:  ${:,.2f}'.format(self.val_start))
        print('ROI:              {:.2f}%'.format(self.roi * 100.0))
        print('Annualised:       {:.2f}%'.format(100*((1+self.roi)**(365/(endDate-actualStart).days) -1)))
        print('Gross Return:    ${:,.2f}'.format( - self.val_start))

BackTrader also requires a class to be defined in order to evaluate commission of each trade. This can be customised, example below for Fixed Commission Scheme.

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

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

Next we define a strategy to periodically (twice a month) buy $1,000 worth of the ETF. Here we have added logging functionality and are diving within the orders to ensure information is being calculated correctly by BackTrader.

class BuyAndHold_More_Fund(bt.Strategy):
    params = dict(

    def __init__(self):
        self.order = None
        self.totalcost = 0
        self.cost_wo_bro = 0
        self.units = 0
        self.times = 0

    def log(self, txt, dt=None):
        dt = dt or self.datas[0]
        print('%s, %s' % (dt.isoformat(), txt))

    def start(self):, fundstartval=100.0)

        self.cash_start =
        self.val_start = 100.0

        # ADD A TIMER
            monthdays=[i for i in self.p.monthly_range],
            # timername='buytimer',

    def notify_timer(self, timer, when, *args):

        target_value = + self.p.monthly_cash - 10

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
        if order.status in [order.Completed]:
            if order.isbuy():
                    'BUY EXECUTED, Price %.2f, Cost %.2f, Comm %.2f, Size %.0f' %

                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.cash_start) - 1
        self.froi = ( - self.val_start)
        value = self.datas[0].close * self.units +
        print('BUY & BUY MORE')
        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)))

The final step to the puzzle is to define a run method that establishes BackTrader class Cerebro and adds in strategy, brokerage considerations and cash amount at the start of trading. Then you can run and plot results.

def run(data):
    # BUY and HOLD
    cerebro = bt.Cerebro()

    # Broker Information
    broker_args = dict(coc=True) = bt.brokers.BackBroker(**broker_args)
    comminfo = FixedCommisionScheme()

    # BUY and BUY MORE
    cerebro1 = bt.Cerebro()

    # Broker Information
    broker_args = dict(coc=True) = bt.brokers.BackBroker(**broker_args)
    comminfo = FixedCommisionScheme()
    cerebro.plot(iplot=False, style='candlestick')
    cerebro1.plot(iplot=False, style='candlestick')

if __name__ == '__main__':