- The expected return, volatility and correlations of various asset classes are known OR you believe that past returns are the best estimator of future returns.
- Returns follow a normal or log-normal distribution.
- You have a CRRA (constant relative risk aversion) utility function, or similar.
- You are a mean-variance investor. That is, you only care about the probability distribution of outcomes.

A quick primer on the CRRA utility function. An utility function specifies your personal trade-off between risk and reward. Suppose that you are presented with two choices:

A) you immediately receive a bonus of 10% of your current net worth.

B) you have X% chance of receiving a bonus of 20% of your net worth, otherwise nothing happens.

When X<50% it is obvious that A is the best option. When X = 100%, it is obvious that B is the best option. There is a specific value of X in which you think the two options are equivalent, that specific value of X completely defines your personal risk preferences. Basically, the utility function defines how much risk you are willing to take for a given reward.

Your coefficient or relative risk aversion is denoted by γ (gamma). Here are a few sample values of γ and the resulting (approximate) asset allocation:

γ=5 corresponds to an asset allocation of 40% stocks, 60% bonds

γ=4 corresponds to an asset allocation of 50% stocks, 50% bonds

γ=3 corresponds to an asset allocation of 65% stocks, 35% bonds

γ=2 corresponds to an asset allocation of 100% stocks

γ=1 corresponds to an asset allocation of 200% stocks

γ=0 corresponds to an asset allocation of infinitely many stocks.

Time horizon does not impact your risk aversion*. Age is probably unrelated to risk aversion. Your remaining human capital will have a big impact on your risk aversion. According to own calculations based on the theory discussed in the lifecycle investing book, your risk aversion becomes arbitrary low when your liquid net worth is negligible compared to your remaining human capital.

*according to some, some asset classes exhibit mean-reversing behavior which reduces the variance as the time horizon increases. This does impact your asset allocation, but is outside the scope of this topic.

Methodology:

I'll keep this short. If all assets are (log)normally distributed, then any linear combination of assets is also (log)normally distributed. If your asset allocation is normally distributed as X, the CRRA utility is

*approximately*E[X] - .5 * γ * VAR[X]. To find the optimal portfolio for a given γ, we only have to figure out which asset allocation maximizes this equation. We repeat this for all γ to find the optimal portfolio for different investors.

It is possible to modify the optimizer to drop the assumption for (log)normally distributed returns, but this is somewhat computationally expensive and drastically limits the time period.

Trivial case:

For this example, we have estimated: (expected excess return / annualized standard deviation)

market returns at 6% / 15.5%, data from ken french 1963-2020

total fond market returns at 1.68% / 5%, data from simba's backtesting spreadsheet, 1934-2018

correlation between stocks and bonds = 0

In the top pane, we see the results of the utility function with various γ. γ = 0 represents a risk-neutral investor, and can be interpreted as the traditional efficient frontier. The other values of γ represent the expected utility (technically, the certainty equivalent return) for investors with various coefficients of relative risk aversion, the colored x represents the location of the optimal portfolio.

In the bottom pane, you will find the composition of the optimal portfolio. To find your optimal portfolio, first look up the X coordinate of your "X" in the top pane, then look at the same X coordinate in the bottom pane. The bottom pane shows the composition of your optimal asset allocation.

The X axis is a bit wide to ensure we have sufficient axis space available when we investigate leveraged portfolio choice.

adding long term bonds:

For this example I have substituted total bond market for intermediate term bonds to ensure we only have exposure to the term premium (government bonds), not the credit premium (corporate bonds). We estimated:

market returns at 6% / 15.5%, data from ken french 1963-2020

ITT bond returns at 1.66% / 5.65%, data from simba's backtesting spreadsheet 1934-2018

LTT bond returns at 2.01% / 9.79%, data from simba's backtesting spreadsheet 1934-2018

correlation between stocks and bonds = 0

correlation between ITT and LTT = 0.8

As you can see, the resulting asset allocation has changed slightly, but the top pane is nearly identical. Adding long term bonds looks like a micro-optimization. Just use total bond market.

leverage:

We substitute LTT for TMF (3x 20x treasury bonds) and add UPRO (3x leveraged S&P500).

TMF is simulated as having an expense ratio of 1.1% and an exposure of 3.0 on 20y treasury bonds.

UPRO is simulated with an expense ratio of 2% and an exposure of 3.0 on market and -.48 on smb.

The expected return, volatility and correlation of the size factor are taken from ken french data library based on FF5 model. The expected return of the factors was discounted by a factor of 2 to reduce the impact of survivorship bias inside the factor set.

For investors with arbitrary low risk aversion, the optimal choice is 100% UPRO.

This specific scenario doesn't include LTT. But if we had included LTT, it's allocation would be 0%.

One particular thing to note is that the optimal ratio between TMF and UPRO varies according to your risk tolerance. There is no single ratio that is "optimal". The optimal ratio depends on your risk aversion. Strategies dat recommend a single ratio are suspect for this reason.

The asset allocation for γ = 2, which previously held a 100% stock allocation, now holds 62% total stock market, 16% UPRO and 22% TMF. This equates to a factor exposure of approximately 1.1 market and .6 term. This asset allocation has a certainty equivalent return of 4.4%, compared to 4.0% for the 100% stock allocation.

Options and futures are substantially cheaper than UPRO/TMF. You should probably use them instead of leveraged ETF's.

If we assume frequent rebalancing, the impact of volatility decay is probably negligible. In fact, if stock returns were independently distributed (not necessary normal), daily rebalancing would be optimal. If you actually want to use high leverage, you should probably optimize for the highest expected utility by using historical returns data. This way you can avoid making any distributional assumptions and simultaneously optimize for transaction costs and tax impact. Last time I checked I found that the choice between options/futures and leveraged ETF's depends on a lot of things.

Factor funds

We substitute leveraged funds for factor funds. We use Fama & French 5 factor model + momentum as our factor model. The correlation data was taken from A Five-Factor Asset Pricing Model. The expected return and volatility was calculated based on ken french 5-factor US 1963-2020 data. The expected return of FF5 factors was discounted by 2 and the expected return of momentum was discounted by a factor of 4 to reduce the impact of survivorship bias (and to be extra conservative, in general).

Value was simuated by IUSV (iShares Core S&P U.S. Value ETF). The expense ratio of this ETF is .04%. I estimated the factor exposure at .97 mkt, .03 smb, .3 hml, .15 rmw, .21 cma, -.02 mom.

Small cap Value was simuated by IJS ( iShares S&P Small-Cap 600 Value ETF). The expense ratio of this ETF is .25%. I estimated the factor exposure at 1.04 mkt, .86 smb, .21 hml, .19 rmw, .09 cma, -.05 mom.

Even though we discounted the returns of factors by 2, factor funds still dominate ordinary index funds.

I use IJS and IUSV because they are relatively long running and simple. I don't necessarily recommend these funds.

With DFSVX thrown into the mix, the optimizer seems to have difficulty deciding whether IJS or DFSVX is better after expenses.

With these assumptions, everyone should factor tilt, not just those with low risk aversion.

This scenario suggests that a value tilt may be a better idea than a small cap value tilt for most individuals. I don't think that conclusion is statistically significant. I usually suggest 1/3 TSM 1/3 value 1/3 small cap value as a compromise, it's definitely possible to argue for more aggressive tilts.

Multifactor Factor funds

We add everyone's favorite multifactor fund, VFMF.

The factor exposure of VFMF was estimated as: .18 ter, .98 mkt, .47 smb, .36 hml, .16 rmw, .12 cma, .19 mom

We see that VFMF mostly dominates small cap value. This makes sense, since VFMF is basically a slightly more efficient combination of value and small cap value. Surprisingly (to me), the optimizer still things that there is a value-add from adding a plain value fund.

factor funds + leverage

We use UPRO/TMF for leverage but add the possibility of factor funds.

The earlier result in which we investigated UPRO/TMF is mostly unchanged, except that total stock market is replaced with factor funds.

Using historical factor returns

So far we have discounted all factor returns by a factor of 2 (momentum by 4). What happens if we don't?

When assuming full factor returns, small cap value dominates TMF. We can infer that users that have high allocations to TMF have very optimistic views about future bond returns and/or very pessimistic views on future factor returns.

Comments:

All return figures above are excess returns vs T-bills.

The correct asset allocation depends on your personal risk preferences. One-size-fits-all Portfolio's (permanent portfolio, all-weather) can never work.

Unless you are willing to use leveraged bonds, don't try to cherry pick the bond market. Just use total bond market .

Even with very pessimistic forecasts (half the historical factor premia), factor funds dominate ordinary index funds. It would appear that in order to rationalize not investing in factor funds, you must be extremely skeptical of the Fama & French 3/5 factor model.

When you run a backtest, the ending balance (which is related to the CAGR) is calculated as:

ending balance = X

_{1}* X

_{2}* X

_{3}* ... * X

_{n}.

Or equivalently:

log(ending balance) = log(X

_{1}) + log(X

_{2}) + log(X

_{x}) + ... + log(X

_{n})

Whereas the expected utility is calculated as:

expected utility = average(U(X

_{1}) + U(X

_{2}) + U(X

_{3}) + ... + U(X

_{n}))

With U being the utility function. When your coefficient of relative risk aversion γ is 1, U(x) = log(x). Therefore, finding the max CAGR by repeatedly running backtests is equivalent to optimizing for max utility with γ = 1. That's why I said that this framework is intended to replace backtests: observing the CAGR value of a backtest is only meaningful if γ = 1, but it is very unlikely your personal coefficient of relative risk aversion is equal to 1!

With these assumptions it is optimal to rebalance continuously (it should be obvious why). The real question is which rebalance frequency or methodology is the best trade-off between the optimal asset allocation, tax impact, transaction costs and possible mean-reversing or momentum behavior on short time horizons.

Disadvantages:

I am aware that historical bond yields are far higher than current yields. I don't believe that necessarily implies that the term premium is a thing of the past. Anyway, the correct asset allocation for bonds is primarily determined by the difference between ITT and LTT, not their absolute return. If we assume E[ITT] = .66% and E[LTT] = 1.01% (down from 1.66% and 2.01%), then the allocation to TMF decreases (It's high expense ratio makes it very unattractive when expected bond returns are low), but the other scenario's are mostly unchanged.

Mean-variance optimization is optimal under the condition that you know the expected return, volatility and correlations of various asset classes or risk factors exactly. You can only estimate these characteristics with low accuracy. Attempting to perform mean-variance optimization with too many assets or too complicated models can (will) result in a massive amplification of measurement errors. You should check if the estimated variables are statistically significant before attempting mean-variance optimization. This is particularly important when it comes to alternative asset classes such as gold or REIT's. My philosophy is that you should select the asset classes or risk factors that you are most confident in (highest statistical confidence), and then run a mean-variance analysis or other expected utility analysis to select the correct asset allocation. If you skip the first step your results will be meaningless.

Our mean-variance optimizer assumes i.i.d. normally distributed returns. Real market returns are not independent and not normally distributed. aacalc.com has investigated the efficient frontier for small cap value without any distributional assumptions, and finds that without distributional assumptions, small cap value looks slightly better. Highly concentrated small cap value funds have extremely skewed return distributions and probably likely to perform much poorer than their standard deviation suggests. See How Diversification Impacts Investment Outcomes: A Case Study on Global Large Caps from DFA research for more background information. As mentioned earlier, it's possible to modify the optimizer to drop the normally distributed assumption.

We assume that FF5 fully explains return differences between diversified portfolio's by fitting a long-short factor model. But the funds we use are long-only. research shows that long-only factor funds are capable of capturing the factor premia, but the mismatch between a long-short factor model and long-only funds obviously introduces inaccuracies.

To minimize overfitting, I only used US data in this analysis. But you should really use international diversification. This type of analysis is not suited to determine which US/international split is optimal, because the difference between US and international is not statistically significant.

Source code:

Requires python 3 with packages numpy, scipy, matplotlib. Your assets can be defined around line 56 and 77. It should take around 10 seconds to run.

Code: Select all

```
# coding=utf-8
import numpy as np
import matplotlib.pyplot as plt
import scipy.optimize
import scipy.stats
from matplotlib.ticker import StrMethodFormatter
def asset_def(name, ter=0.0,
mkt=0.0, smb=0.0, hml=0.0, rmw=0.0, cma=0.0, mom=0.0,
itt=0.0, ltt=0.0):
return name, {
'TER': ter,
'MKT': mkt,
'SmB': smb,
'HmL': hml,
'RmW': rmw,
'CmA': cma,
'MOM': mom,
'ITT': itt,
'LTT': ltt,
}
# return assumptions
# monthly arithmetic return, monthly std
factors = np.array([
['TER', -1 / 12.0, 0],
['MKT', .53, 4.44],
# US factor data, 1963 - july 2020
['SmB', .21 / 2, 3.02], # estimate only half the historical factor premia
['HmL', .25 / 2, 2.87], # estimate only half the historical factor premia
['RmW', .26 / 2, 2.15], # estimate only half the historical factor premia
['CmA', .26 / 2, 1.99], # estimate only half the historical factor premia
['MOM', .66 / 4, 4.7], # estimate only a quarter the historical factor premia
['ITT', 1.66 / 12, 5.67 / 12**.5],
['LTT', 2.01 / 12, 9.79 / 12**.5],
])
factor_labels = list(factors[:, 0])
factor_mean = factors[:, 1].astype(np.float)
factor_std = factors[:, 2].astype(np.float)
factor_mean = np.array(factor_mean) / 100
factor_std = np.array(factor_std) / 100
print('factor mean & std (monthly)')
print(np.vstack((factor_labels, factor_mean*100, factor_std*100)).transpose())
cash_asset = asset_def('cash')
mkt_asset = asset_def('total stock market', mkt=1)
sp500_asset = asset_def('S&P 500', mkt=1, smb=-.16)
iusv_ff5_asset = asset_def('IUSV (value)', ter=0.04, mkt=.97, smb=.03, hml=.30, rmw=.15, cma=.21, mom=-0.02)
ijs_ff5_asset = asset_def('IJS (scv)', ter=.25, mkt=1.04, smb=.86, hml=.21, rmw=.19, cma=.09, mom=-0.05)
dfsvx_ff5_asset = asset_def('DFSVX (scv)', ter=.4, mkt=1.03, smb=.87, hml=.40, rmw=.14, cma=.08, mom=-0.08)
vfmf_ff5_asset = asset_def('VFMF', ter=.18, mkt=.98, smb=.47, hml=.36, rmw=.16, cma=.12, mom=.19)
itt_asset = asset_def('ITT', ter=0, itt=1)
ltt_asset = asset_def('LTT', ter=0, ltt=1)
tmf_asset = asset_def('TMF', ter=1.1, ltt=3)
upro_asset = asset_def('UPRO', ter=2.0, mkt=3, smb=-.16 * 3)
BOUNDS = (0, 1)
CASH_BOUNDS = (0, 1)
SHORTING_COST = .5 / 100
X_AXIS = (0, .50) # annualized standard deviation
Y_AXIS = (0, .12) # annualized return
chosen_assets = [
cash_asset,
mkt_asset,
itt_asset,
iusv_ff5_asset,
ijs_ff5_asset,
]
def asset_to_weight_array(asset):
a = np.zeros(len(factor_labels))
for k, v in asset[1].items():
if k in factor_labels:
a[list(factor_labels).index(k)] = v
return a
asset_labels = [a[0] for a in chosen_assets]
assets = np.array([
asset_to_weight_array(asset) for asset in chosen_assets
])
correlations = {
# collected from FF, A Five-Factor Asset Pricing Model (2014)
('MKT', 'SmB'): .28,
('MKT', 'HmL'): -.30,
('MKT', 'RmW'): -.21,
('MKT', 'CmA'): -.39,
('SmB', 'HmL'): -.11,
('SmB', 'RmW'): -.36,
('SmB', 'CmA'): -.11,
('HmL', 'RmW'): .08,
('HmL', 'CmA'): .70,
('RmW', 'CmA'): -.11,
('ITT', 'LTT'): .8,
}
correlation_coef = np.identity(len(factor_labels))
for k, v in correlations.items():
a, b = k
try:
a = factor_labels.index(a)
b = factor_labels.index(b)
correlation_coef[a, b] = v
correlation_coef[b, a] = v
except ValueError:
pass
class Model:
def __init__(self):
self.init_weights = np.zeros(len(asset_labels), dtype=np.float)
self.init_weights[0] = 1
self.init_weights[:len(asset_labels)] = 1.0/len(asset_labels)
def extract_weights(self, weights):
return weights[:len(weights) / 2]
def bounds(self):
b = [BOUNDS for i in range(len(self.init_weights))]
b[0] = CASH_BOUNDS
return b
def extract_weights(self, weights):
return weights
def get_mean(self, weights):
factor_loadings = np.dot(weights, assets)
mean = np.dot(factor_loadings, factor_mean)
return mean
def mean_and_std(self, weights):
factor_loadings = np.dot(weights, assets)
mean = np.dot(factor_loadings, factor_mean)
std = np.array(factor_loadings * factor_std, ndmin=2)
covariance_matrix = np.matmul(std.transpose(), std) * correlation_coef
variance = np.sum(covariance_matrix)
std = np.sqrt(variance)
return mean, std
model = Model()
def find_weights(expected_std):
w_bound = model.bounds()
constraints = [
({'type': 'eq', 'fun': lambda w: sum(model.extract_weights(w)) - 1.}), # weights must sum to 1
({'type': 'eq', 'fun': lambda w: model.mean_and_std(w)[1] - expected_std}) # std must be equal to expected_std
]
w = scipy.optimize.minimize(
lambda w: -model.get_mean(w) - np.sum(np.clip(w, -np.inf, 0)) * SHORTING_COST,
model.init_weights,
method='SLSQP',
bounds=w_bound,
constraints=constraints,
tol=0.00000000005
)
return w.x
X = []
Y = []
asset_ratios = []
for expected_std in np.linspace(X_AXIS[0], X_AXIS[1], 200):
vars = find_weights(expected_std / 12 **.5)
weights = model.extract_weights(vars)
if not (0.999 <= sum(weights) <= 1.001):
print('fail')
continue
asset_ratios.append(weights)
factor_loadings = np.matmul(weights, assets)
mean, std = model.mean_and_std(vars)
# print(expected_std, mean, std, expected_std - std, weights, factor_loadings)
print("mean: {:.2%}, std: {:.2%}".format(mean * 12, std * 12 ** .5))
Y.append(mean * 12)
X.append(std * 12 ** .5)
asset_ratios = np.array(asset_ratios)
def isoeastic_utility(X, Y, gamma):
return np.array(Y) - .5 * np.power(X, 2) * gamma
def plot_utility_function(X, Y, gamma, label, color):
U = isoeastic_utility(X, Y, gamma)
plt.plot(X, U, label=label, color=color)
max_u = np.max(U)
mask = max_u == U
# draw a colored "X" at the maximum
plt.scatter(np.array(X)[mask], U[mask], color=color, marker='x')
plt.subplots(2, 1, figsize=(8, 7))
plt.subplot('211')
plt.title('efficient frontier, for various coefficients of relative risk aversion')
plt.ylabel('expected utility')
plt.xlabel('risk (annualized stddev)')
plt.grid()
plt.xlim(X_AXIS)
plt.ylim(Y_AXIS)
plt.gca().yaxis.set_major_formatter(StrMethodFormatter('{x:,.0%}'))
plt.gca().xaxis.set_major_formatter(StrMethodFormatter('{x:,.0%}'))
plot_utility_function(X, Y, 0, u'γ = 0', 'C1')
plot_utility_function(X, Y, 0.5, u'γ = 0.5', 'C2')
plot_utility_function(X, Y, 1, u'γ = 1', 'C3')
plot_utility_function(X, Y, 2, u'γ = 2', 'C4')
plot_utility_function(X, Y, 3, u'γ = 3', 'C5')
plot_utility_function(X, Y, 5, u'γ = 5', 'C6')
plt.text(.30, .005, 'γ = 0 is the efficient frontier\nγ = 1 maximizes geometric growth,\nalso known as the kelly criterion',
bbox=dict(boxstyle="square",
ec=(.6, .6, .6),
fc=(1, 1, 1),
)
)
plt.legend(loc='upper left')
plt.subplot('212')
plt.title('asset weights')
plt.ylabel('asset weight')
plt.xlabel('risk (annualized stddev)')
plt.grid()
plt.xlim(X_AXIS)
plt.ylim((0, 1))
plt.gca().xaxis.set_major_formatter(StrMethodFormatter('{x:,.0%}'))
for i in range(len(assets)):
plt.plot(X, asset_ratios[:, i], label=asset_labels[i])
plt.legend()
plt.subplots_adjust(hspace=.3)
plt.subplots_adjust(left=.1)
plt.subplots_adjust(right=.9)
plt.subplots_adjust(top=.95)
plt.subplots_adjust(bottom=.08)
plt.show()
```