This Python Jupyter notebook explores investment portfolios that have decent return (approximately 8 percent on average over the last ten years) and lower risk than the overall stock market.
If you are fortunate to have some cash available for investment you have to decide how to invest this cash (not making an explicit investment decision is a decision).
The US stock market, over the long term, has yielded good returns. Unfortunately it is an inescapable fact that risk and return are related. The returns provided by the stock market come with the risk of losses in your investment portfolio, at least in the short to medium term. At the time this notebook was written (November 2021) the stock market had dramatic returns after a COVID-19 inspired market crash. In such a time it is important to remember that there have been many periods where "the market" has had substantial downturns and low returns.
As any good financial adviser will tell you, your investment strategy should depend on your stage in life. Is retirement decades away, close or are you retired?
If your retirement is decades away, one of the simplest ways to achieve good investment returns is "dollar cost averaging" where you invest a certain amount every month in one or more low fee market index funds or ETFs.
When there is a market downturn your share purchase cost will be lower resulting in gains when the market recovers. Many people take advantage of dollar cost averaging by allocating a fraction of their salary for investment in their employer's 401K retirement plan.
If you are at or near retirement age then the inevitable market downturns are much less acceptable since you will not be able to take advantage of market cycles that could last for years.
This notebook explores conservative investment portfolios that have lower risk and lower correlation with the stock market (a.k.a., lower market beta). These are portfolios that may be appropriate for people who are retired or near retirement.
The portfolio that originally inspired this notebook is based on the Bridgewater Associates "All Weather" portfolio proposed by Bridgewater founder Ray Dalio. This portfolio is discussed in a Bridgewater promotional white paper The All Weather Story From the white paper:
What the average person needs is a good, reliable asset allocation they can hold for the long-run. Bridgewater’s answer is All Weather, the result of three decades of learning how to invest in the face of uncertainty.
The results in this notebook show that, in the last ten years (from 2021) the performance a portfolio that is similar to the Bridgewater "all weather" portfolio is no better than a simple 40 percent stock, 60 percent bond portfolio that is often recommended for those at or near retirement. This notebook shows a portfolio consisting of just two ETFs VTI (40%) and SCHP (60%) has less volatility than the "all weather" portfolio with returns that are only slightly lower.
The 40% market/60% bond mix is difficult to beat when it comes to risk and return. A number of dividend assets are examined in this notebook in an attempt to find a portfolio with higher return at a similar risk level. This search was not successful.
Most of the ETFs that were looked at in this notebook have only been in existence for about ten years. The past portfolio performance generally starts in January 2011 (or in some cases 2010).
The last decade has been a time of generally exceptional market returns. This has been driven at least in part by very low interest rates (sometimes rates close to zero). With fewer options for asset returns, money has flooded into the stock market, resulting in high market returns.
The results in this notebook suggest that 40% of the assets in "the market" and 60% in bonds yield the decent returns with lower risk. The market asset examined in this notebook is VTI, which is an ETF that mirrors the S&P 500.
I looked at a variety of other market ETFs (small cap, mid-cap, etc...) None of these had better return in the last ten years than VTI. One of the certainties in life is change and the market will not stay the same. A more diversified "market" asset, consisting of ETFs that hold value stocks for small and medium capitalization companies, along with an ETF holding foreign stocks would provide a way to deliver higher diversification for "the market" asset. I may look at the performance of a portfolio with a more diversified "market asset" in a future notebook.
An analysis and practical examples of the All Weather Portfolio is published on the Optimized Portfolio web site The portfolio proposed on the Optimized Portfolio website (and used in this notebook) replaces Dalio's commodity assets with utilities.
Ray Dalio's All Weather Portfolio consists of the following assets:
The Optimized Portfolio article on Dalio's All Weather portfolio lists a set of ETFs (Exchange Traded Funds) that approximate Dalio's portfolio recommendation, replacing commodities with utility assets.
The ETFs that make up the Optimized Portfolio version of the all weather portfolio are outlined below.
This ETF version of the All Weather portfolio is compared with a portfolio that only consists of the market (VTI) and bond assets (VGLT and VGIT). This make up of this 40% market/60% bond portfolio is listed below.
The ETFs in the portfolios are briefly described in the table below:
This ETF offers broad exposure to the U.S. equity market, investing in thousands of different securities across all sectors.
Expense ratio: 0.03%
Inception date: May 24, 2001
MSCI rating: BBB
This ETF offers exposure to long term government bonds, focusing on Treasuries that mature in ten years or more.
Expense ratio: 0.05%
Inception date: Nov 19, 2009
MSCI rating: A
This ETF offers exposure to intermediate term government bonds, focusing on Treasuries that mature in three to ten years.
Expense ratio: 0.05%
Inception date: Nov 19, 2009
MSCI rating: AThis Vanguard ETF offers exposure to the domestic utilities sector, a corner of the U.S. market that has historically exhibited low volatility and often features an attractive distribution yield.
Expense ratio: 0.10%
Inception date: Jan 26, 2004
MSCI rating: AA
This fund offers exposure to one of the world’s most famous metals, gold. IAU is designed to track the spot price of gold bullion by holding gold bars in a secure vault, allowing investors to free themselves from finding a place to store the metal.
Expense ratio: 0.25%
Inception date: Jan 21, 2005
MSCI rating: none provided, but rated B on etf.com
For this set of ETFs, earliest common start date is November 19, 2009
Leverage refers to using borrowed money to purchase an asset. Leverage increases both the potential profit if the asset increases in value and loss if the asset declines in value.
There are a number of ETFs that provide leverage for their asset class. The ETF selection here is from Optimized Portfolio. Due to the cost of leverage (e.g., borrowed money) the expense ratios for these ETFs is higher (expense ratios of 0.91 to 0.95 percent).
At the end of 2021 the stock market appears over priced and it seems inevitable that interest rates will rise. This suggests possible declines in both market funds and bond funds. In such an environment, leveraged ETFs may not be the best choice. A 40/60 stock/bond fund is included here to explore the impact of leverage.
This ETF offers 2x daily long leverage to the S&P 500 Index, making it a powerful tool for investors with a bullish short-term outlook for large cap equities.
Expense ratio: 0.91%
Inception date: Jun 19, 2006
MSCI rating: A
This ETF offers 2x long leveraged exposure to the broad-based Barclays Capital U.S. 20+ Year Treasury Index, making it a powerful tool for investors with a bullish short-term outlook for U.S. long-term treasuries.
Expense ratio: 0.95%
Inception date: Jan 19, 2010
MSCI rating: A
This ETF offers 2x long leveraged exposure to the broad-based Barclays Capital U.S. 7-10 Year Treasury Index, making it a powerful tool for investors with a bullish short-term outlook for U.S. long-term treasuries.
Expense ratio: 0.95%
Inception date: Jan 19, 2010
MSCI rating: A
Earliest common date for the leveraged ETFs: January 19, 2010
Two of the assets, VGLT and VGIT, for the unleveraged portfolio and UBT and UST for the leveraged portfolio are treasury bond ETFs. This gives the All Weather portfolios a weighting of 55% in treasury bonds and 30% stocks, with 15% in other assets (utilities and gold).
The All Weather portfolio allocation is close to the retirement portfolio allocation recommend by Charles Schwab (see Structuring Your Retirement Portfolio) for ages 60 to 69 (35% stocks, 60% bonds and 5% money market). A discussion of broad portfolio allocation strategies is published on the Optimized Portfolio site (see Portfolio Asset Allocation by Age)
This notebook shows that the contribution of the Gold ETFs (IAU) and the utility ETFs (VPU) are not significant compared to a 40/60 stock/bond portfolio, in terms of volatility and return. The "secret sause" of the All Weather portfolio doesn't deliver much, if anything in terms of return and lowered risk.
The AOM ETF is described as a conservative mix of assets. This notebook briefly looks at whether the AOM ETF could replace the 40/60 stock/bond portfolio investigated here.
AOM is one of four iShares Core target-risk ETFs. The fund is a fund-of-funds that invests exclusively in a global portfolio of iShares ETFs with 40% allocation to equity and 60% allocation to fixed income.
from datetime import datetime, timedelta
from tabulate import tabulate
from typing import List, Tuple
import pypfopt as pyopt
from pypfopt import expected_returns
from pypfopt import risk_models
from pandas_datareader import data
import matplotlib.pyplot as plt
import scipy.stats as stats
import pandas as pd
import numpy as np
from pathlib import Path
import tempfile
# QuantStrats was written by Ran Aroussi. See https://aroussi.com/
# https://pypi.org/project/QuantStats/
# https://github.com/ranaroussi/quantstats
# qs.drawdown()
import quantstats as qs
# See also https://seekingalpha.com/instablog/42079636-kayode-omotosho/5377452-computing-maximum-drawdown-of-stocks-in-python
plt.style.use('seaborn-whitegrid')
"""
Ideally this function would go in a local package. However, I want this Jupyter notebook
to display on github and I don't know of a way to get the local package import install
and import to work.
"""
def get_market_data(file_name: str,
data_col: str,
symbols: List,
data_source: str,
start_date: datetime,
end_date: datetime) -> pd.DataFrame:
"""
file_name: the file name in the temp directory that will be used to store the data
data_col: the type of data - 'Adj Close', 'Close', 'High', 'Low', 'Open', Volume'
symbols: a list of symbols to fetch data for
data_source: yahoo, etc...
start_date: the start date for the time series
end_date: the end data for the time series
Returns: a Pandas DataFrame containing the data.
If a file of market data does not already exist in the temporary directory, fetch it from the
data_source.
"""
temp_root: str = tempfile.gettempdir() + '/'
file_path: str = temp_root + file_name
temp_file_path = Path(file_path)
file_size = 0
if temp_file_path.exists():
file_size = temp_file_path.stat().st_size
if file_size > 0:
close_data = pd.read_csv(file_path, index_col='Date')
else:
panel_data: pd.DataFrame = data.DataReader(symbols, data_source, start_date, end_date)
close_data: pd.DataFrame = panel_data[data_col]
close_data.to_csv(file_path)
return close_data
Many of the assets in the portfolios investigated in this notebook pay a dividend. Factoring in this dividend is critical to arriving at a correct portfolio evaluation. This requires properly applying the asset close price and adjusted close price.
The close price for a stock is the market price of the stock at the end of a trading day. This is the (approximate) price that the asset can be purchased for in the market.
The adjusted close price is the end of market stock price adjusted for a variety of "corporate actions" which include dividend payments and stock splits. The return on the asset adjusted close price is used to calcuate portfolio value.
Dividend multipliers are calculated based on dividend as a percentage of the price, primarily to avoid negative historical pricing.
For example (from yahoo.com):
data_source = 'yahoo'
# yyyy-mm-dd
start_date_str = '2010-01-01'
start_date: datetime = datetime.fromisoformat(start_date_str)
end_date: datetime = datetime.today() - timedelta(days=1)
yyy_start_date = datetime.fromisoformat('2013-07-01')
yyy_close = get_market_data(file_name='yyy_close', data_col='Close', symbols=['yyy'],
data_source=data_source, start_date=yyy_start_date, end_date=end_date)
yyy_adjClose = get_market_data(file_name='yyy_adjClose', data_col='Adj Close', symbols=['yyy'],
data_source=data_source, start_date=yyy_start_date, end_date=end_date)
yyy_time_series = yyy_close
yyy_time_series.columns = ['Close']
yyy_time_series['Adj Close'] = yyy_adjClose
An example of the ETF YYY showing both the close price and the adjusted close price.
# Plot the close price and the adjusted close price
yyy_time_series.plot(grid=True, title='ETF YYY', figsize=(10,6))
<AxesSubplot:title={'center':'ETF YYY'}, xlabel='Date'>
As the table above show, an ETF that is similar to YYY is PCEF. Unlike YYY, PCEF has not lost as much share face value, although the yield is less (adjusted for costs, PCEF's yearly return is 4.34% vs. 6.65 for YYY).
The close price and adjusted close price for PCEF are plotted below. For comparison purposes the time period is the same as the previous plot.
pcef_close = get_market_data(file_name='pcef_close', data_col='Close', symbols=['pcef'],
data_source=data_source, start_date=yyy_start_date, end_date=end_date)
pcef_adjClose = get_market_data(file_name='pcef_adjClose', data_col='Adj Close', symbols=['pcef'],
data_source=data_source, start_date=yyy_start_date, end_date=end_date)
pcef_time_series = pcef_close
pcef_time_series.columns = ['Close']
pcef_time_series['Adj Close'] = pcef_adjClose
pcef_time_series.plot(grid=True, title='ETF PCEF', figsize=(10,6))
etf_symbols = ["VTI", "VGLT", "VGIT", "VPU", "IAU"]
# Fetch the close prices for the entire time period
#
etf_close_file = 'etf_close'
# Fetch all of the close prices: this is faster than fetching only the dates needed.
etf_close: pd.DataFrame = get_market_data(file_name=etf_close_file,
data_col="Close",
symbols=etf_symbols,
data_source=data_source,
start_date=start_date,
end_date=end_date)
etf_weights = {"VTI": 0.30, "VGLT": 0.40, "VGIT": 0.15, "VPU": 0.08, "IAU": 0.07}
prices = etf_close[0:1]
def calc_portfolio_holdings(initial_investment: int, weights: pd.DataFrame, prices: pd.DataFrame) -> Tuple[pd.DataFrame, pd.DataFrame]:
"""
Calculate the initial portfolio holdings given am amount of cash to invest.
:param initial_investment: The initial investment used to purchase the portfolio (no partial shares)
:param weights: a data frame containing the the weights for each asset as symbol: fraction
:param prices: the share prices
:return: the dollar value of the share holdings and the number of shares
"""
weights_np: np.array = np.zeros(weights.shape[1])
prices_np: np.array = np.zeros(weights.shape[1])
for ix, col in enumerate(weights.columns):
weights_np[ix] = weights[col]
prices_np[ix] = prices[col]
budget_np = weights_np * float(initial_investment)
shares = budget_np // prices_np
holdings = shares * prices_np
holdings_df: pd.DataFrame = pd.DataFrame(holdings).transpose()
holdings_df.columns = weights.columns
shares_df: pd.DataFrame = pd.DataFrame(shares).transpose()
shares_df.columns = weights.columns
return holdings_df, shares_df
The simple return for a time period t is:
$\ R_t = \large \frac{R_t - R_{t-1}}{R_{t-1}} = \frac{R_t}{R_{t-1}} - 1$
The portfolio value calculated via continuously compounded returns is:
$\ portfolio\ value\ = V_t = V_{t-1} + V_{t-1} \times R_{t} $
where $\ V_{0} = initial\ investment $
def simple_return(time_series: List, period: int) -> List :
return list(((time_series[i]/time_series[i-period]) - 1.0 for i in range(period, len(time_series), period)))
def return_df(time_series_df: pd.DataFrame) -> pd.DataFrame:
"""
Given a data frame consisting of price time series, return a data frame
that consists of the simple returns for the time series. The returned data
frame will have the same columns, but the time index will be one time period
less.
"""
r_df: pd.DataFrame = pd.DataFrame()
col_names = time_series_df.columns
for col in col_names:
col_vals = time_series_df[col]
col_ret = simple_return(col_vals, 1)
r_df[col] = col_ret
index = time_series_df.index
return r_df.set_index(index[1:len(index)])
The portflio return is calculated by iteratively applying the asset return from the initial portfolio value.
For a stock with no dividends and no splits the asset value calculated via continuously compounded returns should be the same as the close price. An example of a stock without dividends is Google (Alphabet), symbol GOOGL.
google_close_file = "google_close"
google_start_date_str = '2019-01-01'
google_end_date_str = '2019-12-31'
google_start_date: datetime = datetime.fromisoformat(google_start_date_str)
google_end_date: datetime = datetime.fromisoformat(google_end_date_str)
google_sym = 'GOOGL'
google_close: pd.DataFrame = get_market_data(file_name=google_close_file,
data_col='Close',
symbols=[google_sym],
data_source=data_source,
start_date=google_start_date,
end_date=google_end_date)
google_close = round(google_close, 2)
google_returns = return_df(google_close)
google_returns = google_returns[google_sym]
google_returns.plot(title=f'Google Returns {google_start_date.strftime("%Y-%m-%d")} to {google_end_date.strftime("%Y-%m-%d")}')
<AxesSubplot:title={'center':'Google Returns 2019-01-01 to 2019-12-31'}, xlabel='Date'>
The Google daily returns are plotted above.
google_val_np = np.zeros(google_close.shape, dtype=np.float64)
google_val_np[0] = google_close[0:1]
for t in range(1, google_close.shape[0]):
google_val_np[t] = google_val_np[t-1] + (google_val_np[t-1] * google_returns[t-1])
google_val_np = google_val_np.round(2)
google_val_df: pd.DataFrame = pd.DataFrame(google_val_np, index=google_close.index, columns=google_close.columns)
compare_rslt = 'the same' if (int(google_val_df.eq(google_close).sum()) == google_close.shape[0]) else 'different'
print(f'The close price and the value computed by continuously compounded returns are {compare_rslt}')
The close price and the value computed by continuously compounded returns are the same
A portfolio allocates a percentage of the investment cash to each asset categry which is used buy shares in each of the assets. There are no fractional shares, so the amount invested may be less than the total amount of cash.
# initial investment, in dollars
initial_investment = 10000
weights_df: pd.DataFrame = pd.DataFrame(etf_weights.values()).transpose()
weights_df.columns = etf_weights.keys()
holdings, shares = calc_portfolio_holdings(initial_investment=initial_investment,
weights=weights_df,
prices=prices)
print("Portfolio weights:")
print(tabulate(weights_df, headers=['', *weights_df.columns], tablefmt='fancy_grid'))
print("Number of Shares:")
print(tabulate(shares, headers=['As of Date', *shares.columns], tablefmt='fancy_grid'))
print(f'Total invested from {initial_investment} is {int(holdings.sum(axis=1))}')
print("Value of share holdings:")
print(tabulate(holdings, headers=['As of Date', *holdings.columns], tablefmt='fancy_grid'))
Portfolio weights: ╒════╤═══════╤════════╤════════╤═══════╤═══════╕ │ │ VTI │ VGLT │ VGIT │ VPU │ IAU │ ╞════╪═══════╪════════╪════════╪═══════╪═══════╡ │ 0 │ 0.3 │ 0.4 │ 0.15 │ 0.08 │ 0.07 │ ╘════╧═══════╧════════╧════════╧═══════╧═══════╛ Number of Shares: ╒══════════════╤═══════╤════════╤════════╤═══════╤═══════╕ │ As of Date │ VTI │ VGLT │ VGIT │ VPU │ IAU │ ╞══════════════╪═══════╪════════╪════════╪═══════╪═══════╡ │ 0 │ 52 │ 69 │ 25 │ 12 │ 31 │ ╘══════════════╧═══════╧════════╧════════╧═══════╧═══════╛ Total invested from 10000 is 9877 Value of share holdings: ╒══════════════╤═════════╤═════════╤════════╤════════╤═════════╕ │ As of Date │ VTI │ VGLT │ VGIT │ VPU │ IAU │ ╞══════════════╪═════════╪═════════╪════════╪════════╪═════════╡ │ 0 │ 2980.12 │ 3957.84 │ 1472.5 │ 785.64 │ 681.318 │ ╘══════════════╧═════════╧═════════╧════════╧════════╧═════════╛
The All Weather portfolio has 30% of it's assets in a stock market ETF (VTI: Vanguard Total Stock Market for the unleveraged portfolio and SSO: ProShares Ultra S&P 500 for the leveraged portfolio. If the overall stock market is changing in value, the value of these ETF shares will increase or decrease and the percentage of the portfolio held in "the market" will increase or decrease as well. To maintain a 30% share in the stock market asset the portfolio may need to be rebalanced.
Portfolio rebalancing is usually done either quarterly, every six months or yearly:
There are potential short term capital gains taxes from rebalancing a portfolio in a period under a year and a day, but they should not be significant.
trading_days = 253
days_in_quarter = trading_days // 4
half_year = trading_days // 2
def do_rebalance(cash_holdings: pd.DataFrame, weights_df: pd.DataFrame, portfolio_range: float) -> bool:
"""
cash_holdings: a data frame with the amount of cash for each holding.
weights: a data frame containing the portfolio target weights
range: the portfolio should be rebalanced if an asset is not +/- percent of the target weight
"""
total_cash: int = int(cash_holdings.sum(axis=1))
current_percent = round(cash_holdings.div(total_cash), 3)
# Calculate the allowed weight range for the portfolio
weight_range = weights_df.apply(lambda x: (x - (x * portfolio_range), x * (1 + portfolio_range)), axis=0)
# are any of the current portfolio weights outside of the allowed portfolio range
rebalance = list(float(current_percent[col]) < float(weight_range[col][0]) or float(current_percent[col]) > float(weight_range[col][1]) for col in current_percent.columns)
return any(rebalance)
def portfolio_rebalance(cash_holdings: pd.DataFrame,
weights: pd.DataFrame,
portfolio_range: float,
prices: pd.DataFrame) -> pd.DataFrame:
"""
cash_holdings: a data frame containing the current portfolio cash holdings by stock
weights: a data frame containing the portfolio weights
range: the +/- percentage range for rebalancing (e.g., +/- 0.05)
prices: the market prices on the day the portfolio will be rebalanced.
Return: rebalanced cash holdings
"""
new_holdings = cash_holdings
if do_rebalance(cash_holdings=cash_holdings, weights_df=weights, portfolio_range=portfolio_range):
total_cash = cash_holdings.sum(axis=1)
new_holdings, shares = calc_portfolio_holdings(initial_investment=total_cash,
weights=weights,
prices=prices)
return new_holdings
etf_adj_close_file = 'etf_adj_close'
# Fetch the adjusted close price for the unleveraged "all weather" set of ETFs'
# VTI, VGLT, VGIT, VPU and IAU
etf_adj_close: pd.DataFrame = get_market_data(file_name=etf_adj_close_file,
data_col="Adj Close",
symbols=etf_symbols,
data_source=data_source,
start_date=start_date,
end_date=end_date)
# +/- for each asset for portfolio rebalancing
portfolio_range = 0.05
returns = return_df(etf_adj_close)
portfolio_np = np.zeros(etf_close.shape, dtype=np.float64)
portfolio_total_np = np.zeros(etf_close.shape[0])
portfolio_total_np[0] = holdings.sum(axis=1)
# initialize the first row with the dollar value of the portfolio holdings.
portfolio_np[0,] = holdings
for t in range(1, portfolio_np.shape[0]):
for col, stock in enumerate(holdings.columns):
portfolio_np[t, col] = portfolio_np[t-1, col] + (portfolio_np[t-1, col] * returns[stock][t-1])
portfolio_total_np[t] = portfolio_total_np[t] + portfolio_np[t,col]
The plot below shows the portfolio value without rebalancing. In this case the initial portfolio is purchased using the "all weather" percentages and the portfolio is allowed to grow without rebalancing.
portfolio_total_df: pd.DataFrame = pd.DataFrame(portfolio_total_np, index=etf_close.index, columns=['Portfolio'])
portfolio_total_df.plot(title="Portfolio Value (without rebalancing)", grid=True, figsize=(10,8))
<AxesSubplot:title={'center':'Portfolio Value (without rebalancing)'}, xlabel='Date'>
Over time the fraction of the portfolio invested in the market index fund is likely to move outside of the 30% allocated to the market index if the portfolio is not rebalanced.
The plot below shows the percentage of the portfolio invested in the market, without portfolio rebalancing.
portfolio_total_np = portfolio_np.sum(axis=1)
portfolio_percent_np = portfolio_np / portfolio_total_np[:,None]
market_percent = portfolio_percent_np[:,0]
market_percent_df: pd.DataFrame = pd.DataFrame(market_percent, index=etf_close.index, columns=['Market'])
market_percent_df.plot(title="Percentage of Portfolio invested in the Market", grid=True, figsize=(10,8))
def calc_rebalanced_portfolio(holdings: pd.DataFrame,
etf_close: pd.DataFrame,
returns: pd.DataFrame,
weights: pd.DataFrame,
rebalance_days: int) -> Tuple[pd.DataFrame, pd.DataFrame]:
portfolio_np = np.zeros(etf_close.shape, dtype=np.float64)
portfolio_total_np = np.zeros(etf_close.shape[0])
portfolio_total_np[0] = holdings.sum(axis=1)
portfolio_np[0,] = holdings
for t in range(1, portfolio_np.shape[0]):
for col, stock in enumerate(holdings.columns):
portfolio_np[t, col] = portfolio_np[t-1, col] + (portfolio_np[t-1, col] * returns[stock][t-1])
portfolio_total_np[t] = portfolio_total_np[t] + portfolio_np[t,col]
if (t % rebalance_days) == 0:
current_holdings: pd.DataFrame = pd.DataFrame( portfolio_np[t,]).transpose()
current_holdings.columns = holdings.columns
date = etf_close.index[t]
current_holdings.index = [date]
close_prices_t = etf_close[t:t+1]
portfolio_np[t, ] = portfolio_rebalance(cash_holdings=current_holdings,
weights=weights,
portfolio_range=portfolio_range,
prices=close_prices_t)
portfolio_df: pd.DataFrame = pd.DataFrame(portfolio_np, index=etf_close.index, columns=etf_close.columns)
portfolio_total_df: pd.DataFrame = pd.DataFrame(portfolio_total_np, index=etf_close.index)
portfolio_total_df.columns = ['Portfolio Value']
return portfolio_df, portfolio_total_df
def plot_portfolio_weights(asset_values_df: pd.DataFrame, portfolio_total_df: pd.DataFrame) -> None:
portfolio_total_np = np.array(portfolio_total_df)
asset_values_np = np.array(asset_values_df)
portfolio_percent_np = asset_values_np / portfolio_total_np
col_names = asset_values_df.columns
fig, ax = plt.subplots(asset_values_np.shape[1], figsize=(10,8))
for col in range(0, asset_values_np.shape[1]):
asset_percent = portfolio_percent_np[:,col]
label = f'Portfolio Percent for {col_names[col]}'
ax[col].set_xlabel(label)
ax[col].grid(True)
ax[col].plot(asset_percent)
fig.tight_layout()
plt.show()
portfolio_quarterly_df, portfolio_total_quarterly_df = calc_rebalanced_portfolio(holdings=holdings,
etf_close=etf_close,
returns=returns,
weights=weights_df,
rebalance_days=days_in_quarter)
The "all weather" portfolio is designed to limit exposure to market downturns. The plot above shows that the market exposure grows as the percentage of the market asset increases.
The plot below shows the portfolio rebalanced every quarter.
portfolio_total_quarterly_df.plot(title="Portfolio Value (quarterly rebalanced)", grid=True, figsize=(10,8))
<AxesSubplot:title={'center':'Portfolio Value (quarterly rebalanced)'}, xlabel='Date'>
The portfolio is very volatile. The asset percentages in the all weather portfolio are:
print(tabulate(weights_df, headers=['', *weights_df.columns], tablefmt='fancy_grid'))
╒════╤═══════╤════════╤════════╤═══════╤═══════╕ │ │ VTI │ VGLT │ VGIT │ VPU │ IAU │ ╞════╪═══════╪════════╪════════╪═══════╪═══════╡ │ 0 │ 0.3 │ 0.4 │ 0.15 │ 0.08 │ 0.07 │ ╘════╧═══════╧════════╧════════╧═══════╧═══════╛
To verify that the portfolio rebalancing is producing the right asset percentages, the asset percentages are plotted below.
plot_portfolio_weights(portfolio_quarterly_df, portfolio_total_quarterly_df)