Buy and Hold with backtrader

This is sometimes one of the baselines which is used to test the performance of a given strategy, i.e.: "if the carefully crafted logic cannot beat a simple buy and hold approach, the strategy is probably not worth a dime"

A simple "buy and hold" strategy, would simply buy with the first incoming data point and see what the portfolio value is available with the last data point.

Tip The snippets below forego the imports and set-up boilerplate. A complete script is available at the end.

Cheating On Close

In many cases, an approach like Buy and Hold is not meant to yield an exact reproduction of order execution and price matching. It is about evaluating the large numbers. That is why, the cheat-on-close mode of the default broker in backtrader is going to be activated. This means

As only Market orders will be issued, execution will be done against the current close price.

Take into account that when a price is available for the trading logic (in this case the close ), that price is GONE. It may or may not be available in a while and in reality execution cannot be guaranteed against it.

Buy and Forget

buy method class BuyAndHold_1 ( bt . Strategy ): def start ( self ): self . val_start = self . broker . get_cash () # keep the starting cash def nextstart ( self ): # Buy all the available cash size = int ( self . broker . get_cash () / self . data ) self . buy ( size = size ) def stop ( self ): # calculate the actual returns self . roi = ( self . broker . get_value () / self . val_start ) - 1.0 print ( 'ROI: {:.2f}%' . format ( 100.0 * self . roi )) order_target method class BuyAndHold_1 ( bt . Strategy ): def start ( self ): self . val_start = self . broker . get_cash () # keep the starting cash def nextstart ( self ): # Buy all the available cash self . order_target_value ( target = self . broker . get_cash ()) def stop ( self ): # calculate the actual returns self . roi = ( self . broker . get_value () / self . val_start ) - 1.0 print ( 'ROI: {:.2f}%' . format ( 100.0 * self . roi ))

The following is happening here:

A single go long operation to enter the market is being issued. Either with buy and a manual calculation of the size All the available cash is used to buy a fixed amount of units of the asset. Notice it is being truncated to be an int . This is appropriate for things like stocks, futures. or order_target_value and letting the system know we want to use all the cash. The method will take care of automatically calculating the size.

In the start method, the initial amount of cash is being saved

In the stop method, the returns are calculated, using the current value of the portfolio and the initial amount of cash

Note In backtrader the nextstart method is called exactly once, when the data/indicator buffers can deliver. The default behavior is to delegate the work to next . But because we want to buy exactly once and do it with the first available data, it is the right point to do it.

Tip As only 1 data feed is being considered, there is no need to specify the target data feed. The first (and only) data feed in the system will be used as the target. If more than one data feed is present, the target can be selected by using the named argument data as in self.buy(data=the_desired_data, size=calculated_size)

The sample script below can be executed as follows

buy $ ./buy-and-hold.py --bh-buy --plot ROI: 34.50% order_target $ ./buy-and-hold.py --bh-target --plot ROI: 34.50%

The graphical output is the same for both

Buy and Buy More

But an actual regular person does usually have a day job and can put an amount of money into the stock market each and every month. This person is not bothered with trends, technical analysis and the likes. The only actual concern is to put the money in the market the 1st day of the month.

Given that the Romans left us with a calendar which has months which differ in the number of days ( 28 , 29 , 30 , 31 ) and taking into account non-trading days, one cannot for sure use the following simple approach:

Buy each X days

A method to identify the first trading day of the month needs to be used. This can be done with Timers in backtrader

Note Only the order_target_value method is used in the next examples.

Buy and Buy More - order_target method class BuyAndHold_More ( bt . Strategy ): params = dict ( monthly_cash = 1000.0 , # amount of cash to buy every month ) def start ( self ): self . cash_start = self . broker . get_cash () self . val_start = 100.0 # Add a timer which will be called on the 1st trading day of the month self . add_timer ( bt . timer . SESSION_END , # when it will be called monthdays = [ 1 ], # called on the 1st day of the month monthcarry = True , # called on the 2nd day if the 1st is holiday ) def notify_timer ( self , timer , when , * args , ** kwargs ): # Add the influx of monthly cash to the broker self . broker . add_cash ( self . p . monthly_cash ) # buy available cash target_value = self . broker . get_value () + self . p . monthly_cash self . order_target_value ( target = target_value ) def stop ( self ): # calculate the actual returns self . roi = ( self . broker . get_value () / self . cash_start ) - 1.0 print ( 'ROI: {:.2f}%' . format ( self . roi ))

During the start phase a timer is added

# Add a timer which will be called on the 1st trading day of the month self . add_timer ( bt . timer . SESSION_END , # when it will be called monthdays = [ 1 ], # called on the 1st day of the month monthcarry = True , # called on the 2nd day if the 1st is holiday )

Timer which will be called at the end of the session ( bt.timer.SESSION_END ) Note For daily bars this is obviously not relevant, because the entire bar is delivered in a single shot.

The timer lists only day 1 of the month as the one in which the timer has to be called

In case day 1 happens to be a non-trading day, monthcarry=True ensures that the timer will still be called on the first trading day of the month.

The timer received during the notify_timer method, which is overridden to perform the market operations.

def notify_timer ( self , timer , when , * args , ** kwargs ): # Add the influx of monthly cash to the broker self . broker . add_cash ( self . p . monthly_cash ) # buy available cash target_value = self . broker . get_value () + self . p . monthly_cash self . order_target_value ( target = target_value )

Tip Notice that what is bought is not the monthly cash influx, but the total value of the account, which comprises the current portfolio, plus the money we have added. The reasons There can be some initial cash to be consumed

The monthly operation may not consume all the cash, because a single month may not be enough to buy the stock and because there will be a rest after acquiring the stock In our example it is actually so, because the default monthly cash inflow is 1000 and the asset has a value of over 3000

If the target were to be the available cash, this could be smaller than the actual value

Execution

Execution with default 1000 money units $ ./buy-and-hold.py --bh-more --plot ROI: 320.96% Execution with default 5000 money units $ ./buy-and-hold.py --bh-more --strat monthly_cash=5000.0 ROI: 1460.99%

Blistering Barnacles!!! a ROI of 320.96% for the default 1000 money units and an even greater ROI of 1460.99% for 5000 monetary units. We have probably found a money printing machine ...

The more money we add each month ... the more we win ... regardless of what the market does.

Of course not ...

The calculation stored in self.roi during stop is NO longer valid. The simple monhtly addition of cash to the broker changes the scales (even if the money were not used for anything, it would still count as an increment)

The graphical output with 1000 money units

Notice the interval between actual operations in the market, because the 1000 money units are not enough to buy 1 unit of the asset and money has to be accumulated until an operation can succeed.

The graphical output with 5000 money units

In this case, 5000 monetary units can always buy 1 unit of the asset and the market operations take place each and every month.

Performance Tracking for Buy and Buy More

As pointed out above, hen money is added to (and sometimes taken out of) the system, performance has to measured in a different way. There is no need to invent anything, because it was invented a long time ago and it is what is done for Fund Management.

A perf_value is set as the reference to track the performance. More often than not this will 100

Using that peformance value and the initial amount of cash, a number of shares is calculated, i.e.: shares = cash / perf_value

Whenever cash is added to/subsctracted from the system, the number of shares changes, but the perf_value remains the same.

The cash will be sometimes invested and the daily value will be updated as in perf_value = portfolio_value / shares

With that approach the actual perfomance can be calculated and it is independent of cash additions to/withdrawals from the system.

Luckily enough, backtrader can already do all of that automatically.

Buy and Buy More - order_target method class BuyAndHold_More_Fund ( bt . Strategy ): params = dict ( monthly_cash = 1000.0 , # amount of cash to buy every month ) def start ( self ): # Activate the fund mode and set the default value at 100 self . broker . set_fundmode ( fundmode = True , fundstartval = 100.00 ) self . cash_start = self . broker . get_cash () self . val_start = 100.0 # Add a timer which will be called on the 1st trading day of the month self . add_timer ( bt . timer . SESSION_END , # when it will be called monthdays = [ 1 ], # called on the 1st day of the month monthcarry = True , # called on the 2nd day if the 1st is holiday ) def notify_timer ( self , timer , when , * args , ** kwargs ): # Add the influx of monthly cash to the broker self . broker . add_cash ( self . p . monthly_cash ) # buy available cash target_value = self . broker . get_value () + self . p . monthly_cash self . order_target_value ( target = target_value ) def stop ( self ): # calculate the actual returns self . roi = ( self . broker . get_value () - self . cash_start ) - 1.0 self . froi = self . broker . get_fundvalue () - self . val_start print ( 'ROI: {:.2f}%' . format ( self . roi )) print ( 'Fund Value: {:.2f}%' . format ( self . froi ))

During start

The fund mode is activated with a default start value of 100.0 def start ( self ): # Activate the fund mode and set the default value at 100 self . broker . set_fundmode ( fundmode = True , fundstartval = 100.00 )

During stop

The fund ROI is calculated. Because the start value is 100.0 the operation is rather simple def stop ( self ): # calculate the actual returns ... self . froi = self . broker . get_fundvalue () - self . val_start

The execution

$ ./buy-and-hold.py --bh-more-fund --strat monthly_cash=5000 --plot ROI: 1460.99% Fund Value: 37.31%

In this case:

The same incredible plain ROI as before is achieved which is 1460.99%

The actual ROI when considering it as Fund is a more modest and realistic 37.31% , given the sample data.

Note The output chart is the same as in the previous execution with 5000 money units.

The sample script