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')
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')
# 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.