This article introduces readers to the mean-variance optimization of asset portfolios. The underlying formulas are implemented in Python. Market data has been downloaded from Google Finance. The case study is available here.
Calculation of assets' weights, returns and covariances
Our example starts with the calculation of vector of asset weights \(\mathbf{W}_{n \times 1}\), expected annual returns \(\mathbf{R}_{n \times 1}\) and covariances \(\mathbf{C}_{n \times n}\). Top nine S&P companies sorted by market capitalization are used as asset classes for the purpose of this article: XOM, AAPL, MSFT, JNJ, GE, GOOG, CVX, PG and WFC, as of 1 Jul 2013.
The time frame of historical data should depend on planned investment horizon. For the purpose of our analysis, we will consider daily prices for recent two years. The function call from previously mentioned example implementing the loader is as follows:
# Load names, prices, capitalizations from yahoo finance
names, prices, caps = load_data()
# Estimate assets's expected return and covariances
names, W, R, C = assets_meanvar(names, prices, caps) rf = .015 # Define Risk-free rate
The data downloaded in this example will yield to the historical returns, deviations and capitalization-based weights as shown below. In the next section, we will use these figures to calculate portfolio risk/return characteristics and to optimize its asset weights.
Name Weight Return Dev
XOM 16.0% 7.3% 19.8%
AAPL 15.6% 13.0% 30.3%
MSFT 11.3% 17.2% 22.6%
JNJ 9.7% 14.4% 13.9%
GE 9.4% 14.0% 24.3%
GOOG 11.6% 33.2% 25.9%
CVX 9.2% 9.2% 22.6%
PG 8.5% 11.2% 15.9%
WFC 8.7% 26.9% 30.0%
Mean-variance trade-off and diversification benefit
In the investment management process, almost every risky asset class exhibits some degree of trade-off between the returns and uncertainty of these returns. Mean returns are quantitatively measured by geometrically averaging series of daily or weekly returns over a certain period of time, while the uncertainty is expressed by the variance or standard deviation of such series.
Naturally more volatile the asset is, higher is also the expected return. This relationship is determined by the supply and demand forces on a capital market and may be mathematically expressed by one of the capital pricing models, such as APT, CAPM or others.
Most of these pricing models are based on an assumption that only systematic risk is reflected in capital prices and therefore investors shouldn't expect additional risk premium by holding poorly diversified or standalone investments.
Assuming normal distribution of asset returns, we can quantitatively measure this diversification benefit by calculating correlation between two price series, which is equal or less than one.
Let's consider a simple example of portfolio containing two assets, 50% of asset A and 50% of asset B, where asset returns are \(r_{A}=4\%\), \(r_{B} = 5\%\) and variances are \(\sigma_{A}^{2}=7\%^{2}\) and \(\sigma_{B}^{2}=8\%^{2}\).
In case there is no diversification benefit, portfolio expected return and variance will be just linear combination of the asset weights, in this case \(r_{p}=4.5\%\), \(\sigma_{p}^{2}=7.5\%^{2}\).
However, in case the correlation between these two assets is less than one and therefore diversification potential is available, portfolio variance will be less than linear combination of weights: \(\sigma_{p}^{2}<7.5\%^{2}\). The mean-variance trade-offs for different levels of diversification are shown on the Figure 1.
The generalized formula for calculating portfolio return \(r_{p}\) and variance \(\sigma_{p}\) consisting of \(n\) different assets is then defined as:
$$ r_{p} = \mathbf{W} \cdot \mathbf{R} $$
, where \(\mathbf{W}_{n\times 1}\) is the vector of asset weights and \(\mathbf{R}_{n\times 1}\) vector of corresponding asset returns.
Portfolio variance is then defined as:
$$ \sigma_{p}^{2} = \mathbf{W} \cdot \mathbf{C} \cdot \mathbf{W} $$
, where \(\mathbf{C}_{n\times n}\) is the covariance matrix of asset returns.
The corresponding code in our python example:
# Calculate portfolio historical return and variance mean, var = port_mean_var(W, R, C)
Portfolio Optimization
Considering the starting vector of weights \(\mathbf(W_{n \times 1})\), the optimization process is tailored towards maximizing some kind of mean-variance utility function, such as Sharpe ratio:
$$ s=\frac{r_{p} - r_{f} }{\sigma_{p}} $$
, because we know that optimal risky portfolio with highest sharpe ratio \(s\) lies on a tangency of efficient frontier with the capital market line (CML).
Given the historical asset returns \(\mathbf{R}_{n \times 1}\) and covariances \(\mathbf{C}_{n \times n}\), the optimization code using SciPy's SLSQP method may look for example as follows:
# Given risk-free rate, assets returns and covariances, this function calculates # weights of tangency portfolio with respect to sharpe ratio maximization def solve_weights(R, C, rf): def fitness(W, R, C, rf):
# calculate mean/variance of the portfolio mean, var = port_mean_var(W, R, C) util = (mean - rf) / sqrt(var) # utility = Sharpe ratio return 1/util # maximize the utility n = len(R) W = ones([n])/n # start with equal weights b_ = [(0.,1.) for i in range(n)] # weights between 0%..100%.
# No leverage, no shorting c_ = ({'type':'eq', 'fun': lambda W: sum(W)-1. }) # Sum of weights = 100% optimized = scipy.optimize.minimize(fitness, W, (R, C, rf),
method='SLSQP', constraints=c_, bounds=b_)
if not optimized.success: raise BaseException(optimized.message) return optimized.x # Return optimized weights
The function returns vector of optimized weights, which together with original vector of returns and deviations looks as follows:
# optimize W = solve_weights(R, C, rf) mean, var = port_mean_var(W, R, C) # calculate tangency portfolio front_mean, front_var = solve_frontier(R, C, rf) # calculate min-var frontier
We can see that this optimization results in a concentrated portfolio consisting just of two constituents: Johnson&Johnson and Google:
Name Weight Return Dev
XOM 0.0% 7.3% 19.8%
AAPL -0.0% 13.0% 30.3%
MSFT -0.0% 17.2% 22.6%
JNJ 45.1% 14.4% 13.9%
GE 0.0% 14.0% 24.3%
GOOG 52.8% 33.2% 25.9%
CVX 0.0% 9.2% 22.6%
PG -0.0% 11.2% 15.9%
WFC 2.1% 26.9% 30.0%
It is not surprising that these assets have one of the lowest correlations among all pairs (0.431). Companies Apple and Exxon were not selected despite the fact that they have even lower mutual correlation. This is because their stand-alone risk/return trade-off is not that great indeed.
Correlation between assets |
In the previous section, we have used optimization technique to find the best combination of weights in order to maximize the risk/return profile (Sharpe ratio) of the portfolio. This resulted into a single optimal risky portfolio represented by a single point in the mean-variance graph. Although the utility function is clear, to maximize the Sharpe ratio, it has two degrees of freedom - the mean and the variance.
In order to calculate the minimum variance frontier, we need to iterate through all levels of investor's risk aversity, represented by required return (y-axis) and use the optimization algorithm in order to minimize the portfolio variance. In each iteration, the required return is hold constant for the purpose of optimization, reducing the problem to one degree of freedom. The output of such optimization will be a vector of minimum variances, one value for each level of risk aversity, also called the minimum variance frontier.
The corresponding Python function may look for example as follows:
# Given risk-free rate, assets returns and covariances, this function calculates # min-var frontier and returns its [x,y] points in two arrays def solve_frontier(R, C, rf): def fitness(W, R, C, r): # For given level of return r, find weights which minimizes # portfolio variance. mean, var = port_mean_var(W, R, C)
# Big penalty for not meeting stated portfolio return
# effectively serves as optimization constraint penalty = 100*abs(mean-r) return var + penalty frontier_mean, frontier_var = [], [] n = len(R) # Number of assets in the portfolio
# Iterate through the range of returns on Y axis for r in linspace(min(R), max(R), num=20): W = ones([n])/n # start optimization with equal weights b_ = [(0,1) for i in range(n)] c_ = ({'type':'eq', 'fun': lambda W: sum(W)-1. }) optimized = scipy.optimize.minimize(fitness, W, (R, C, r),
method='SLSQP', constraints=c_, bounds=b_) if not optimized.success: raise BaseException(optimized.message) # add point to the min-var frontier [x,y] = [optimized.x, r] frontier_mean.append(r) frontier_var.append(port_var(optimized.x, C)) return array(frontier_mean), array(frontier_var)
Again, we have chosen Sequential Least Square Programming method (SLSQP), which allows us to perform the constrained optimization based on the following constraints:
- Sum of weights must be 0
- Weights must be in range (0,1) - no short-selling or leveraged holdings.
- Portfolio required return is given
The first constraint is provided using the lambda function lambda W: sum(W)-1., which is equality constraint saying that the whole expression must be as close to zero as possible. The second constraint are effectively search bounds passed to the optimization function and third constraint is implemented in a fitness function itself. It imposes a big penalty for portfolio mean return not meeting the target return "r".
After solving minimum variance portfolios for all twenty levels of risk aversity, we will get the following minimum variance frontier with optimal risky portfolio represented by dot (o) and individual constituents represented by cross (x):
The major drawback of mean-variance optimization based on historical returns is that such optimization leads to undiversified portfolios, as seen in our example. The reason behind this observation is that market prices are often far from their equilibriums on a risk-adjusted basis, as modeled by the Capital Market Pricing Model.
Moreover, extrapolated historical returns are notoriously bad proxies to be used instead of expected mean returns, which are direct inputs into the mean-variance optimization algorithm.
In the next post, we will illustrate how to use Black-Litterman model to overcome this problem by deriving returns from the market capitalization weights in the process known as reverse optimization.
How can i set a constraint where i say max weight that a portfolio is not greater then say 0.15
ReplyDeleteYou may either alter the fitness function fitness(W, R, C, r) to impose penalty on weights above 15% or to alter bounds vector b_ = [(0.,0.15) for i in range(n)], which is then passes into the optimizer function scipy.optimize.
ReplyDeleteI used that but many a times this bounds and constraints are not properly honored by the optimizer.
DeleteHi,
ReplyDeleteGreat code!
When I played around with the number of dots to calculate in the efficient frontier I noticed that sometimes I get dots that are not aligned with the frontier. After checking over and over, I realized that adjusting the penalty calculation (for instance, to be 4 times larger than the difference and not 10 times larger) may help the dots to be aligned with the frontier. Things is that when I change the number of dots, it gets messed up again. Do you have any idea why is this happening? (btw, I tried eliminating some tickers, it help a bit but not completely)
Thanks!
Eran.
Just wanted to say thanks for this example! I was using cvxopt to do my optimizations before, but this way is faster and allows me to use bounds!
ReplyDeleteI believe that the x and y axis legend have been inverse - sould be returns on the X... isn it ?
ReplyDeleteYes, thank you for noting that.
Delete