Overview

This is the third blog post of my Finaical Analysis in Python series.

This blog series covers topics like the following:

  • how to visualize the long-term investment plan
  • backtest dollar-cost averaging strategy for long-term investment
  • backtest value averaging strategy for long-term investment
  • compare different investment strategies for short-term investment

The jupyter notebooks can be downloaded here.

Disclaimer

Investing money into anything is often involved with risk. Please do your own research before investing and be responsible for your own investment decisions.

I am just learning investment on my own and want to share some codes that I have written that might be useful for others.

The content here is only for informational purpose (instead of taking them as professional investment advice).

Introduction

According to Michael E. Edleson’s book, value averaging method is an enhanced version of the dollar cost averaging method.

It performs better in a mixed market with high volatlity because it tends to secure the gains and buy more dips.

Here we are going to backtest the value averaging strategy using the SP500 index historical data.

Prepare data and define functions

# Import libraries
import datetime
import warnings

from matplotlib import pyplot as plt
import numpy as np
import pandas as pd
from pandas_datareader import data as pdr

plt.style.use('bmh')
warnings.filterwarnings("ignore")
# Get historical data
startdate = datetime.datetime(1988, 1, 1)
enddate = datetime.datetime(2021, 1, 1)

# Pick SP 500 total return index
SP = pdr.get_data_yahoo('^SP500TR', start=startdate, end=enddate)
SP['date'] = SP.index
# Group by year
SP['year'] = SP['date'].apply(lambda d: f'{d.year}')
df = SP[['year', 'Adj Close']].groupby('year').first()
df['return'] = df['Adj Close'].pct_change() # return rate for last year's investment
df['return_next_y'] = df['return'].shift(-1) # return rate by the begining of next year
df = df.reset_index()
df = df.drop(columns='return')

# You can use the following group by dataframe if you want to do value averging monthly or quarterly
# # Group by month
# SP['month'] = SP['date'].apply(lambda d: f'{d.month:02d}')
# df_m = SP[['year', 'month', 'Adj Close']].groupby(['year','month']).first()
# df_m['return'] = df_m['Adj Close'].pct_change()
# df_m['return_next'] = df_m['return'].shift(-1)
# df_m = df_m.reset_index()
# df_m = df_m.drop(columns='return')

# # Group by quarter
# SP['quarter'] = SP['month'].apply(lambda m: int(m)//4 + 1)
# df_q = SP[['year', 'quarter', 'Adj Close']].groupby(['year','quarter']).first()
# df_q['return'] = df_q['Adj Close'].pct_change()
# df_q['return_next'] = df_q['return'].shift(-1)
# df_q = df_q.reset_index()
# df_q = df_q.drop(columns='return')
df.round(2).head(5)
year Adj Close return_next_y
0 1988 256.02 0.12
1 1989 285.65 0.35
2 1990 386.16 -0.06
3 1991 363.44 0.32
4 1992 479.84 0.08
def calc_value_averaging_return(df: pd.DataFrame, value_per_period: float = 1.0, 
                                increase_investment_per_period: float = 0.0,
                                display_irr: bool = True) -> pd.DataFrame:
    """
    This function calculates the internal rate of return of the value averaging strategy.
    It also returns a dataframe that contains the amount of money should invest or withdraw per period.
    :param df: Original dataframe that contains the price at the beginning of each period
    :param value_per_period: Investment value per period
    :param increase_investment_per_period: Increase the investment by x each period
    :param display_irr: Whether to display IRR results or not
    :return: A dataframe that contains all the relevant info
    """
    df_va = pd.DataFrame.copy(df)
    value = value_per_period
    df_va.loc[0,'target_value'] = value
    df_va.loc[0,'shares_should_have_in_total'] = value_per_period/df_va.loc[0,'Adj Close']
    df_va.loc[0,'shares_to_buy_or_sell'] = value_per_period/df_va.loc[0,'Adj Close']
    df_va.loc[0,'should_invest_or_withdraw'] = value
    df_va.loc[0,'total_invest_or_withdraw'] = value

    for i in range(1,len(df_va.index)):
        value += value_per_period*((1 + increase_investment_per_period)**i)
        df_va.loc[i,'target_value'] = value
        df_va.loc[i,'shares_should_have_in_total'] = df_va.loc[i,'target_value']/df_va.loc[i,'Adj Close']
        df_va.loc[i,'shares_to_buy_or_sell'] = df_va.loc[i,'shares_should_have_in_total'] - df_va.loc[i-1,'shares_should_have_in_total']
        df_va.loc[i,'should_invest_or_withdraw'] = df_va.loc[i,'shares_to_buy_or_sell']*df_va.loc[i,'Adj Close']
        df_va.loc[i,'total_invest_or_withdraw'] = df_va.loc[i-1,'total_invest_or_withdraw'] + df_va.loc[i,'should_invest_or_withdraw']

    if display_irr:
        cf = (-df_va['should_invest_or_withdraw']).tolist()
        cf[-1] = (df_va.loc[len(df_va.index)-2,'shares_should_have_in_total'] *
                    df_va.loc[len(df_va.index)-1,'Adj Close'])
        print("The cash flow (the negative sign stands for investment):")
        display(cf)
        irr = round(100*np.irr(cf), 2)
        print(f"The internal return rate is {irr}% per period")
        
    return df_va

def calc_buy_only_value_averaging_return(df: pd.DataFrame, value_per_period: float = 1.0, 
                                         increase_investment_per_period: float = 0.0,
                                         min_investment_per_period: float = 0.0,
                                         max_investment_per_period: float = 5.0,
                                         display_returns: bool = True) -> pd.DataFrame:
    """
    This function calculates the overall return rate and the internal return rate of the 
    buy only value averaging strategy, with a minimum and maximum investment amount per period.
    It also returns a dataframe that contains the amount of money should invest per peiod. 
    :param df: Original dataframe that contains the price at the beginning of each period
    :param value_per_period: Investment value per period
    :param increase_investment_per_period: Increase the investment by x each period
    :param min_investment_per_period: minimum investment amount at the beginning of each period
    :param max_investment_per_period: maximum investment amount at the beginning of each period
    :param display_returns: Whether to display return results or not
    :return: A dataframe that contains all the relevant info
    """
    df_va = pd.DataFrame.copy(df)
    value = value_per_period
    shares = value_per_period/df_va.loc[0, 'Adj Close']
    df_va.loc[0, 'target_value'] = value   # target value to reach at the beginning of each period
    df_va.loc[0, 'target_shares'] = shares # target share to have at the beginning of each period
    df_va.loc[0, 'current_shares'] = 0     # current shares before the period
    df_va.loc[0, 'shares_to_buy'] = shares # shares to add based on the target
    df_va.loc[0, 'total_shares'] = shares  # total shares after the investment at the beginning of each period
    df_va.loc[0, 'should_invest'] = value  # investment at the beginning of each period
    df_va.loc[0, 'total_invest'] = value   # total amount of investment including this period
    df_va.loc[0, 'total_value'] = value    # total share values after the investment at the beginning of each period
    df_va.loc[0, 'total_value_next'] = df_va.loc[0, 'total_value'] * (1 + df_va.loc[0, 'return_next_y'])
    df_va.loc[0, 'overall_return_next'] = df_va.loc[0, 'total_value_next']/df_va.loc[0, 'total_invest'] - 1
    
    for i in range(1,len(df_va.index)):
        df_va.loc[i, 'current_shares'] = df_va.loc[i-1, 'total_shares']
        
        value += value_per_period*((1 + increase_investment_per_period)**i)
        df_va.loc[i,'target_value'] = value
        df_va.loc[i,'target_shares'] = df_va.loc[i,'target_value']/df_va.loc[i,'Adj Close']
        df_va.loc[i,'shares_to_buy'] = df_va.loc[i,'target_shares'] - df_va.loc[i,'current_shares']
        df_va.loc[i,'should_invest'] = df_va.loc[i,'shares_to_buy'] * df_va.loc[i, 'Adj Close']
        
        if df_va.loc[i, 'should_invest'] < min_investment_per_period:
            df_va.loc[i, 'should_invest'] = min_investment_per_period
            df_va.loc[i, 'shares_to_buy'] = min_investment_per_period/df_va.loc[i,'Adj Close']
            
        elif df_va.loc[i, 'should_invest'] > max_investment_per_period:
            df_va.loc[i, 'should_invest'] = max_investment_per_period
            df_va.loc[i, 'shares_to_buy'] = max_investment_per_period/df_va.loc[i,'Adj Close']
        
        df_va.loc[i, 'total_shares'] = df_va.loc[i-1, 'total_shares'] + df_va.loc[i, 'shares_to_buy']
        df_va.loc[i, 'total_invest'] = df_va.loc[i-1, 'total_invest'] + df_va.loc[i, 'should_invest']
        
        df_va.loc[i, 'total_value'] = df_va.loc[i, 'total_shares']*df_va.loc[i, 'Adj Close']
        
        df_va.loc[i, 'total_value_next'] = df_va.loc[i, 'total_value'] * (1 + df_va.loc[i, 'return_next_y'])
    
        df_va.loc[i, 'overall_return_next'] = df_va.loc[i, 'total_value_next']/df_va.loc[i, 'total_invest'] - 1
        
    if display_returns:     

        # Calculate IRR
        cf = (-df_va['should_invest']).tolist()
        cf[-1] = df_va.loc[len(df_va.index) - 2, 'total_value_next']
        print("The cash flow (the negative sign stands for investment):")
        display(cf)
        irr = round(100*np.irr(cf), 2)
        print(f"The internal return rate is {irr}% per period")
              
        # Calculate overall return
        overall_return = round(df_va.loc[len(df_va.index) - 2, 'overall_return_next']*100, 2)
        print(f"The overall return rate (IRR) is {overall_return}%")
        
        # Calculate CAGR
        cagr = round(100*((df_va.loc[len(df_va.index) - 2, 'total_value_next']/
                           df_va.loc[len(df_va.index) - 2, 'total_invest'])**(1/(len(df_va.index) - 1)) - 1), 2)
        print(f"The compound annual growth rate (CAGR) is {cagr}% per period")
        
    return df_va

def calc_dollar_cost_averaging_return(df: pd.DataFrame, value_per_period: float = 1.0, 
                                      increase_investment_per_period: float = 0.0,
                                      display_returns: bool = True) -> pd.DataFrame:
    """
    This function calculates the overall rate of return and the internal rate of return of dollar cost averaging method. 
    :param df: Original dataframe that contains the price at the beginning of each period
    :param value_per_period: Investment value per period
    :param increase_investment_per_period: Increase the investment by x each period
    :param display_returns: Whether to display return results or not
    :return: A dataframe that contains all the relevant info
    """
    df_dc = pd.DataFrame.copy(df)
    # amount to invest each period
    df_dc.loc[0, 'should_invest'] = value_per_period   
    # total amount of investment after the investment this period
    df_dc.loc[0, 'total_invest'] = value_per_period   
    # the total value by the beginning of next period
    df_dc.loc[0, 'total_value_next'] = value_per_period*(1 + df_dc.loc[0, 'return_next_y'])
    df_dc.loc[0, 'overall_return_next'] = df_dc.loc[0, 'total_value_next'] / df_dc.loc[0, 'total_invest'] - 1

    for i in range(1,len(df_dc.index)):
        df_dc.loc[i, 'should_invest'] = value_per_period * (1 + increase_investment_per_period)**i
        df_dc.loc[i, 'total_invest'] = df_dc.loc[i-1, 'total_invest'] + df_dc.loc[i, 'should_invest']
        df_dc.loc[i, 'total_value_next'] = ((df_dc.loc[i-1, 'total_value_next'] + 
                                            df_dc.loc[i, 'should_invest']) *
                                            (1 + df_dc.loc[i, 'return_next_y']) 
                                           )
        df_dc.loc[i, 'overall_return_next'] = df_dc.loc[i, 'total_value_next'] / df_dc.loc[i, 'total_invest'] - 1
        
    if display_returns:
        
        # Calculate IRR
        cf = (-df_dc['should_invest']).tolist()
        cf[-1] = df_dc.loc[len(df_dc.index) - 2, 'total_value_next']
        print("The cash flow (the negative sign stands for investment):")
        display(cf)
        irr = round(100*np.irr(cf), 2)
        print(f"The internal return rate (IRR) is {irr}% per period")
                
        # Calculate overall return
        overall_return = round(df_dc.loc[len(df_dc.index) - 2, 'overall_return_next']*100, 2)
        print(f"The overall return rate is {overall_return}%")
        
        # Calculate CAGR
        cagr = round(100*((df_dc.loc[len(df_dc.index) - 2, 'total_value_next']/
                           df_dc.loc[len(df_dc.index) - 2, 'total_invest'])**(1/(len(df_dc.index) - 1)) - 1), 2)
        print(f"The compound annual growth rate (CAGR) is {cagr}% per period")
        
    return df_dc


def plot_changes(df: pd.DataFrame,
                 y1: str = 'total_invest', y2: str = 'total_value_next',
                 xlabel: str = 'period', ylabel: str = 'value',
                 title: str = 'Visualize total investment and value each year',
                 target: int = None) -> None:
    """
    This function visualizes the total investment and the total value of the investment plan across time
    :param df: A dataframe contains total investment and total value
    :param y1: Column name
    :param y2: Column name
    :param xlabel: X label
    :param ylabel: Y label
    :param title: Title of the graph
    :return: A plot
    """
    plt.figure(figsize=(15, 5))
    plt.plot(df[y1], label=y1)
    plt.plot(df[y2], label=y2)
    if target:
        plt.axhline(y=target, c='black', label='target value')
    plt.xlim([0, len(df[y1])-1])
    plt.xlabel(xlabel)
    plt.ylabel(ylabel)
    plt.title(title)
    plt.legend()
    plt.show()

Plan 1: Value averaging method

df_va1 = calc_value_averaging_return(df, value_per_period=1200)

Output:

The cash flow (the negative sign stands for investment):



[-1200.0,
 -1061.1201961364186,
 -355.52589325481887,
 -1411.8085861271627,
 337.31005654283166,
 -747.849233851947,
 -486.97708484738405,
 -1075.2805992143667,
 2514.761361370703,
 1111.3480648011666,
 2960.6528724251534,
 2476.466314117218,
 1679.0158053734597,
 -2884.972366494456,
 -2680.227795866851,
 -4795.998397092098,
 3439.2227337015943,
 904.3614871476314,
 421.54897775958983,
 1948.1048236273016,
 -216.55745519229592,
 -9782.193899045382,
 5278.448809688893,
 2809.6319858651427,
 -471.3483862548249,
 3947.1013032192404,
 7523.195663896054,
 3551.4295421831143,
 -1241.8135883408345,
 3895.1078797634023,
 6652.273617097743,
 -3080.564625962192,
 50855.361827168424]


The internal return rate is 12.12% per period
df_va1.round(2)
year Adj Close return_next_y target_value shares_should_have_in_total shares_to_buy_or_sell should_invest_or_withdraw total_invest_or_withdraw
0 1988 256.02 0.12 1200.0 4.69 4.69 1200.00 1200.00
1 1989 285.65 0.35 2400.0 8.40 3.71 1061.12 2261.12
2 1990 386.16 -0.06 3600.0 9.32 0.92 355.53 2616.65
3 1991 363.44 0.32 4800.0 13.21 3.88 1411.81 4028.45
4 1992 479.84 0.08 6000.0 12.50 -0.70 -337.31 3691.14
5 1993 516.00 0.10 7200.0 13.95 1.45 747.85 4438.99
6 1994 567.10 0.01 8400.0 14.81 0.86 486.98 4925.97
7 1995 575.52 0.39 9600.0 16.68 1.87 1075.28 6001.25
8 1996 798.22 0.21 10800.0 13.53 -3.15 -2514.76 3486.49
9 1997 969.05 0.35 12000.0 12.38 -1.15 -1111.35 2375.14
10 1998 1305.04 0.28 13200.0 10.11 -2.27 -2960.65 -585.51
11 1999 1668.52 0.20 14400.0 8.63 -1.48 -2476.47 -3061.98
12 2000 2002.11 -0.11 15600.0 7.79 -0.84 -1679.02 -4740.99
13 2001 1785.86 -0.09 16800.0 9.41 1.62 2884.97 -1856.02
14 2002 1628.51 -0.20 18000.0 11.05 1.65 2680.23 824.21
15 2003 1303.17 0.24 19200.0 14.73 3.68 4796.00 5620.21
16 2004 1618.05 0.10 20400.0 12.61 -2.13 -3439.22 2180.98
17 2005 1784.96 0.08 21600.0 12.10 -0.51 -904.36 1276.62
18 2006 1918.96 0.14 22800.0 11.88 -0.22 -421.55 855.07
19 2007 2183.92 0.04 24000.0 10.99 -0.89 -1948.10 -1093.03
20 2008 2273.41 -0.34 25200.0 11.08 0.10 216.56 -876.47
21 2009 1499.17 0.25 26400.0 17.61 6.53 9782.19 8905.72
22 2010 1867.06 0.15 27600.0 14.78 -2.83 -5278.45 3627.27
23 2011 2138.30 0.03 28800.0 13.47 -1.31 -2809.63 817.64
24 2012 2192.40 0.17 30000.0 13.68 0.21 471.35 1288.99
25 2013 2568.55 0.28 31200.0 12.15 -1.54 -3947.10 -2658.11
26 2014 3286.69 0.15 32400.0 9.86 -2.29 -7523.20 -10181.31
27 2015 3768.68 -0.00 33600.0 8.92 -0.94 -3551.43 -13732.74
28 2016 3763.99 0.15 34800.0 9.25 0.33 1241.81 -12490.93
29 2017 4315.08 0.22 36000.0 8.34 -0.90 -3895.11 -16386.03
30 2018 5256.28 -0.05 37200.0 7.08 -1.27 -6652.27 -23038.31
31 2019 4990.56 0.32 38400.0 7.69 0.62 3080.56 -19957.74
32 2020 6609.29 NaN 39600.0 5.99 -1.70 -11255.36 -31213.11

In the last notebook, we already know that the IRR for dollar cost averaging is below 10% per year. Using the same test data, the IRR of the value averaging method is about 12% per year, which supports our hypothesis.

However, we can also observe that, because we are taking money out from the investment plan, the final value of the value averaging is way below that of the dollar cost averaing.

so the value averaging method is not really a good one for long term investment.

In the last notebook we know that, if we invest 1200 dollar per year starting from 1988, we will have about 261192 dollar by 2020. If we want to accumulate the same amount of money by 2020 using the value averaging, we need to set a higher amount of investment each year.

So let’s check the IRR if we increase the investment each year by 10% using the value averaging strategy.

Plan 2: Increase investment each year by 10%

df_va2 = calc_value_averaging_return(df, value_per_period=1200, 
                                     increase_investment_per_period=0.1)

Output:

The cash flow (the negative sign stands for investment):



[-1200.0,
 -1181.1201961364188,
 -565.3021879175599,
 -1830.8954733603034,
 26.743993103820433,
 -1380.5268715179054,
 -1208.9720776032202,
 -2169.4270511041677,
 2737.892833854878,
 657.8958460156231,
 3518.5181924009958,
 2769.824630978246,
 1364.3601939671873,
 -7321.194096811861,
 -7514.80857127775,
 -12629.617352887119,
 4909.707669716029,
 -1046.4989781541021,
 -2564.0511882120322,
 1137.4449047712715,
 -5256.666376415341,
 -35036.57899214694,
 11257.990836019511,
 3121.7177152010245,
 -9132.857546202227,
 7246.443979130866,
 22329.4920899403,
 5579.038139518119,
 -17505.61372633736,
 7077.75812974924,
 22115.78642833783,
 -34070.51321757376,
 319654.1856549351]


The internal return rate is 11.21% per period
df_va2.round(2)
year Adj Close return_next_y target_value shares_should_have_in_total shares_to_buy_or_sell should_invest_or_withdraw total_invest_or_withdraw
0 1988 256.02 0.12 1200.00 4.69 4.69 1200.00 1200.00
1 1989 285.65 0.35 2520.00 8.82 4.13 1181.12 2381.12
2 1990 386.16 -0.06 3972.00 10.29 1.46 565.30 2946.42
3 1991 363.44 0.32 5569.20 15.32 5.04 1830.90 4777.32
4 1992 479.84 0.08 7326.12 15.27 -0.06 -26.74 4750.57
5 1993 516.00 0.10 9258.73 17.94 2.68 1380.53 6131.10
6 1994 567.10 0.01 11384.61 20.08 2.13 1208.97 7340.07
7 1995 575.52 0.39 13723.07 23.84 3.77 2169.43 9509.50
8 1996 798.22 0.21 16295.37 20.41 -3.43 -2737.89 6771.61
9 1997 969.05 0.35 19124.91 19.74 -0.68 -657.90 6113.71
10 1998 1305.04 0.28 22237.40 17.04 -2.70 -3518.52 2595.19
11 1999 1668.52 0.20 25661.14 15.38 -1.66 -2769.82 -174.63
12 2000 2002.11 -0.11 29427.25 14.70 -0.68 -1364.36 -1538.99
13 2001 1785.86 -0.09 33569.98 18.80 4.10 7321.19 5782.20
14 2002 1628.51 -0.20 38126.98 23.41 4.61 7514.81 13297.01
15 2003 1303.17 0.24 43139.68 33.10 9.69 12629.62 25926.63
16 2004 1618.05 0.10 48653.64 30.07 -3.03 -4909.71 21016.92
17 2005 1784.96 0.08 54719.01 30.66 0.59 1046.50 22063.42
18 2006 1918.96 0.14 61390.91 31.99 1.34 2564.05 24627.47
19 2007 2183.92 0.04 68730.00 31.47 -0.52 -1137.44 23490.03
20 2008 2273.41 -0.34 76803.00 33.78 2.31 5256.67 28746.69
21 2009 1499.17 0.25 85683.30 57.15 23.37 35036.58 63783.27
22 2010 1867.06 0.15 95451.63 51.12 -6.03 -11257.99 52525.28
23 2011 2138.30 0.03 106196.79 49.66 -1.46 -3121.72 49403.56
24 2012 2192.40 0.17 118016.47 53.83 4.17 9132.86 58536.42
25 2013 2568.55 0.28 131018.12 51.01 -2.82 -7246.44 51289.98
26 2014 3286.69 0.15 145319.93 44.21 -6.79 -22329.49 28960.48
27 2015 3768.68 -0.00 161051.92 42.73 -1.48 -5579.04 23381.45
28 2016 3763.99 0.15 178357.12 47.39 4.65 17505.61 40887.06
29 2017 4315.08 0.22 197392.83 45.74 -1.64 -7077.76 33809.30
30 2018 5256.28 -0.05 218332.11 41.54 -4.21 -22115.79 11693.52
31 2019 4990.56 0.32 241365.32 48.36 6.83 34070.51 45764.03
32 2020 6609.29 NaN 266701.85 40.35 -8.01 -52952.33 -7188.30

With the increased amount of investment, the IRR of value averaging Plan 2 is still higher than that of the dollar cost averaging method.

However, there is a drawback of the value averaging method:

If we check the cashflow, we can see that, whenever there is a market crash (especially at the lates stage of the investment), we need to invest a huge amount of money (e.g., 2019).

What if we adjust the value averaging method into a buy and hold strategy? We can also set a minimum and maximum investment value per period.

Plan 3: No selling, with minimum and maximum investment pre period

df_va3 = calc_buy_only_value_averaging_return(df, value_per_period=1200, 
                                             increase_investment_per_period=0.10,
                                             min_investment_per_period=600,
                                             max_investment_per_period=6000)

Output:

The cash flow (the negative sign stands for investment):



[-1200.0,
 -1181.1201961364188,
 -600.0,
 -1798.239131977554,
 -600.0,
 -706.552415637322,
 -1208.9720776032202,
 -2169.4270511041677,
 -600.0,
 -600.0,
 -600.0,
 -600.0,
 -600.0,
 -600.0,
 -600.0,
 -6000.0,
 -600.0,
 -600.0,
 -600.0,
 -600.0,
 -1922.6955641279228,
 -6000.0,
 -6000.0,
 -6000.0,
 -6000.0,
 -6000.0,
 -600.0,
 -600.0,
 -600.0,
 -600.0,
 -600.0,
 -600.0,
 328826.905866578]


The internal return rate is 10.54% per period
The overall return rate (IRR) is 471.01%
The compound annual growth rate (CAGR) is 5.6% per period

We can see here that the IRR is still higher than the dollar averaging method, but the overall return by the next period still performs poorer compared to the dollar cost averaging method.

df_va3.round(2)
year Adj Close return_next_y target_value target_shares current_shares shares_to_buy total_shares should_invest total_invest total_value total_value_next overall_return_next
0 1988 256.02 0.12 1200.00 4.69 0.00 4.69 4.69 1200.00 1200.00 1200.00 1338.88 0.12
1 1989 285.65 0.35 2520.00 8.82 4.69 4.13 8.82 1181.12 2381.12 2520.00 3406.70 0.43
2 1990 386.16 -0.06 3972.00 10.29 8.82 1.55 10.38 600.00 2981.12 4006.70 3770.96 0.26
3 1991 363.44 0.32 5569.20 15.32 10.38 4.95 15.32 1798.24 4779.36 5569.20 7352.86 0.54
4 1992 479.84 0.08 7326.12 15.27 15.32 1.25 16.57 600.00 5379.36 7952.86 8552.18 0.59
5 1993 516.00 0.10 9258.73 17.94 16.57 1.37 17.94 706.55 6085.91 9258.73 10175.63 0.67
6 1994 567.10 0.01 11384.61 20.08 17.94 2.13 20.08 1208.97 7294.88 11384.61 11553.64 0.58
7 1995 575.52 0.39 13723.07 23.84 20.08 3.77 23.84 2169.43 9464.31 13723.07 19033.27 1.01
8 1996 798.22 0.21 16295.37 20.41 23.84 0.75 24.60 600.00 10064.31 19633.27 23835.05 1.37
9 1997 969.05 0.35 19124.91 19.74 24.60 0.62 25.22 600.00 10664.31 24435.05 32907.20 2.09
10 1998 1305.04 0.28 22237.40 17.04 25.22 0.46 25.68 600.00 11264.31 33507.20 42839.63 2.80
11 1999 1668.52 0.20 25661.14 15.38 25.68 0.36 26.03 600.00 11864.31 43439.63 52124.59 3.39
12 2000 2002.11 -0.11 29427.25 14.70 26.03 0.30 26.33 600.00 12464.31 52724.59 47029.75 2.77
13 2001 1785.86 -0.09 33569.98 18.80 26.33 0.34 26.67 600.00 13064.31 47629.75 43433.15 2.32
14 2002 1628.51 -0.20 38126.98 23.41 26.67 0.37 27.04 600.00 13664.31 44033.15 35236.31 1.58
15 2003 1303.17 0.24 43139.68 33.10 27.04 4.60 31.64 6000.00 19664.31 41236.31 51200.08 1.60
16 2004 1618.05 0.10 48653.64 30.07 31.64 0.37 32.01 600.00 20264.31 51800.08 57143.52 1.82
17 2005 1784.96 0.08 54719.01 30.66 32.01 0.34 32.35 600.00 20864.31 57743.52 62078.42 1.98
18 2006 1918.96 0.14 61390.91 31.99 32.35 0.31 32.66 600.00 21464.31 62678.42 71332.73 2.32
19 2007 2183.92 0.04 68730.00 31.47 32.66 0.27 32.94 600.00 22064.31 71932.73 74880.30 2.39
20 2008 2273.41 -0.34 76803.00 33.78 32.94 0.85 33.78 1922.70 23987.01 76803.00 50646.72 1.11
21 2009 1499.17 0.25 85683.30 57.15 33.78 4.00 37.79 6000.00 29987.01 56646.72 70547.59 1.35
22 2010 1867.06 0.15 95451.63 51.12 37.79 3.21 41.00 6000.00 35987.01 76547.59 87668.15 1.44
23 2011 2138.30 0.03 106196.79 49.66 41.00 2.81 43.80 6000.00 41987.01 93668.15 96038.00 1.29
24 2012 2192.40 0.17 118016.47 53.83 43.80 2.74 46.54 6000.00 47987.01 102038.00 119544.66 1.49
25 2013 2568.55 0.28 131018.12 51.01 46.54 2.34 48.88 6000.00 53987.01 125544.66 160645.64 1.98
26 2014 3286.69 0.15 145319.93 44.21 48.88 0.18 49.06 600.00 54587.01 161245.64 184892.16 2.39
27 2015 3768.68 -0.00 161051.92 42.73 49.06 0.16 49.22 600.00 55187.01 185492.16 185261.33 2.36
28 2016 3763.99 0.15 178357.12 47.39 49.22 0.16 49.38 600.00 55787.01 185861.33 213073.50 2.82
29 2017 4315.08 0.22 197392.83 45.74 49.38 0.14 49.52 600.00 56387.01 213673.50 260279.68 3.62
30 2018 5256.28 -0.05 218332.11 41.54 49.52 0.11 49.63 600.00 56987.01 260879.68 247691.48 3.35
31 2019 4990.56 0.32 241365.32 48.36 49.63 0.12 49.75 600.00 57587.01 248291.48 328826.91 4.71
32 2020 6609.29 NaN 266701.85 40.35 49.75 0.09 49.84 600.00 58187.01 329426.91 NaN NaN
plot_changes(df_va3, title='Value averaging with adjustment: No selling, with min/max investment amount', xlabel='year')

png

Compare with dollar cost averaging

# Dollar cost averaging: increase the investment per year by 2% 
df_dc = calc_dollar_cost_averaging_return(df, value_per_period=1200, 
                                          increase_investment_per_period=0.02)

Output:

The cash flow (the negative sign stands for investment):



[-1200.0,
 -1224.0,
 -1248.48,
 -1273.4496000000001,
 -1298.918592,
 -1324.8969638400001,
 -1351.3949031168002,
 -1378.422801179136,
 -1405.991257202719,
 -1434.1110823467732,
 -1462.7933039937088,
 -1492.049170073583,
 -1521.8901534750546,
 -1552.327956544556,
 -1583.374515675447,
 -1615.042005988956,
 -1647.3428461087349,
 -1680.2897030309098,
 -1713.8954970915281,
 -1748.1734070333584,
 -1783.1368751740258,
 -1818.7996126775063,
 -1855.1756049310566,
 -1892.2791170296778,
 -1930.1246993702714,
 -1968.7271933576767,
 -2008.1017372248302,
 -2048.263771969327,
 -2089.2290474087135,
 -2131.0136283568877,
 -2173.633900924026,
 -2217.106578942506,
 311273.63335198513]


The internal return rate (IRR) is 9.91% per period
The overall return rate is 486.51%
The compound annual growth rate (CAGR) is 5.68% per period
df_dc.round(2)
year Adj Close return_next_y should_invest total_invest total_value_next overall_return_next
0 1988 256.02 0.12 1200.00 1200.00 1338.88 0.12
1 1989 285.65 0.35 1224.00 2424.00 3464.67 0.43
2 1990 386.16 -0.06 1248.48 3672.48 4435.84 0.21
3 1991 363.44 0.32 1273.45 4945.93 7537.83 0.52
4 1992 479.84 0.08 1298.92 6244.85 9502.67 0.52
5 1993 516.00 0.10 1324.90 7569.75 11899.83 0.57
6 1994 567.10 0.01 1351.39 8921.14 13447.97 0.51
7 1995 575.52 0.39 1378.42 10299.56 20563.53 1.00
8 1996 798.22 0.21 1405.99 11705.55 26671.30 1.28
9 1997 969.05 0.35 1434.11 13139.67 37850.15 1.88
10 1998 1305.04 0.28 1462.79 14602.46 50262.40 2.44
11 1999 1668.52 0.20 1492.05 16094.51 62101.80 2.86
12 2000 2002.11 -0.11 1521.89 17616.40 56751.63 2.22
13 2001 1785.86 -0.09 1552.33 19168.73 53166.86 1.77
14 2002 1628.51 -0.20 1583.37 20752.10 43812.36 1.11
15 2003 1303.17 0.24 1615.04 22367.14 56403.85 1.52
16 2004 1618.05 0.10 1647.34 24014.49 64039.47 1.67
17 2005 1784.96 0.08 1680.29 25694.77 70653.45 1.75
18 2006 1918.96 0.14 1713.90 27408.67 82359.45 2.00
19 2007 2183.92 0.04 1748.17 29156.84 87554.08 2.00
20 2008 2273.41 -0.34 1783.14 30939.98 58912.25 0.90
21 2009 1499.17 0.25 1818.80 32758.78 75634.19 1.31
22 2010 1867.06 0.15 1855.18 34613.96 88746.75 1.56
23 2011 2138.30 0.03 1892.28 36506.23 92932.23 1.55
24 2012 2192.40 0.17 1930.12 38436.36 111137.90 1.89
25 2013 2568.55 0.28 1968.73 40405.09 144730.06 2.58
26 2014 3286.69 0.15 2008.10 42413.19 168257.17 2.97
27 2015 3768.68 -0.00 2048.26 44461.45 170093.50 2.83
28 2016 3763.99 0.15 2089.23 46550.68 197392.20 3.24
29 2017 4315.08 0.22 2131.01 48681.70 243042.96 3.99
30 2018 5256.28 -0.05 2173.63 50855.33 232820.20 3.58
31 2019 4990.56 0.32 2217.11 53072.44 311273.63 4.87
32 2020 6609.29 NaN 2261.45 55333.88 NaN NaN
# Dollar cost averaging: increase the investment per year by 2% 
plot_changes(df_dc, title='Yearly dollar cost averaging', xlabel='year')

png

# Compare dollar cost averaging(2% increase by year) with 
# value averaging method (no selling, with min and max value per period)
select_columns = ['should_invest', 'total_value_next', 'overall_return_next']
df_compare = pd.merge(df_dc[select_columns], df_va3[select_columns], 
         left_index=True, right_index=True, 
         suffixes=('_dc', '_va3')).round(2)
df_compare
should_invest_dc total_value_next_dc overall_return_next_dc should_invest_va3 total_value_next_va3 overall_return_next_va3
0 1200.00 1338.88 0.12 1200.00 1338.88 0.12
1 1224.00 3464.67 0.43 1181.12 3406.70 0.43
2 1248.48 4435.84 0.21 600.00 3770.96 0.26
3 1273.45 7537.83 0.52 1798.24 7352.86 0.54
4 1298.92 9502.67 0.52 600.00 8552.18 0.59
5 1324.90 11899.83 0.57 706.55 10175.63 0.67
6 1351.39 13447.97 0.51 1208.97 11553.64 0.58
7 1378.42 20563.53 1.00 2169.43 19033.27 1.01
8 1405.99 26671.30 1.28 600.00 23835.05 1.37
9 1434.11 37850.15 1.88 600.00 32907.20 2.09
10 1462.79 50262.40 2.44 600.00 42839.63 2.80
11 1492.05 62101.80 2.86 600.00 52124.59 3.39
12 1521.89 56751.63 2.22 600.00 47029.75 2.77
13 1552.33 53166.86 1.77 600.00 43433.15 2.32
14 1583.37 43812.36 1.11 600.00 35236.31 1.58
15 1615.04 56403.85 1.52 6000.00 51200.08 1.60
16 1647.34 64039.47 1.67 600.00 57143.52 1.82
17 1680.29 70653.45 1.75 600.00 62078.42 1.98
18 1713.90 82359.45 2.00 600.00 71332.73 2.32
19 1748.17 87554.08 2.00 600.00 74880.30 2.39
20 1783.14 58912.25 0.90 1922.70 50646.72 1.11
21 1818.80 75634.19 1.31 6000.00 70547.59 1.35
22 1855.18 88746.75 1.56 6000.00 87668.15 1.44
23 1892.28 92932.23 1.55 6000.00 96038.00 1.29
24 1930.12 111137.90 1.89 6000.00 119544.66 1.49
25 1968.73 144730.06 2.58 6000.00 160645.64 1.98
26 2008.10 168257.17 2.97 600.00 184892.16 2.39
27 2048.26 170093.50 2.83 600.00 185261.33 2.36
28 2089.23 197392.20 3.24 600.00 213073.50 2.82
29 2131.01 243042.96 3.99 600.00 260279.68 3.62
30 2173.63 232820.20 3.58 600.00 247691.48 3.35
31 2217.11 311273.63 4.87 600.00 328826.91 4.71
32 2261.45 NaN NaN 600.00 NaN NaN
def compare_strategy(df, str1, str2, str1_name, str2_name):
    print(f'number of times {str1_name} is better than {str2_name}: {df.loc[df[str1] > df[str2],:].shape[0]}')
    print(f'number of times {str1_name} is equal to {str2_name}: {df.loc[df[str1] == df[str2],:].shape[0]}')
    print(f'number of times {str1_name} is worse than {str2_name}: {df.loc[df[str1] < df[str2],:].shape[0]}')
compare_strategy(df_compare, 'overall_return_next_dc', 'overall_return_next_va3', 
                 'yearly dollar-cost-averaging', 'adjusted version of value averaging')

Output:

number of times yearly dollar-cost-averaging is better than adjusted version of value averaging: 10
number of times yearly dollar-cost-averaging is equal to adjusted version of value averaging: 2
number of times yearly dollar-cost-averaging is worse than adjusted version of value averaging: 20

Conclusion

Based on the results above, we can see that value averaging is a good method to increase IRR, but it does not fit the purpose of long term investment as it takes out the profit too early.

The adjusted version of the value averaging seems to outperform the dollar-cost averaging in most years. But the difference is not huge.

So for long term investment, it seems that dollar cost averaging with an annual increasement of 2% works better (with the least effort).

In the next blogpost, we are going to examine how to invest a fixed amount of money in short term. To be more specfic, we are going to compare the dollar-cost avearging method, value averaging with all-in moethod for short term invesment.