1. Home
2. Article Catalog
3. Portfolio optimization in Modern Portfolio Theory

Article

Portfolio optimization in Modern Portfolio Theory

Alex Putkov

## Modern Portfolio Theory

Economist Harry Markowitz introduced Modern Portfolio Theory in a 1952 publication in the Journal of Finance titled “Portfolio Selection”, which later earned him a Nobel Prize in Economics. Modern portfolio theory, or MPT (also known as mean-variance analysis), is a mathematical framework for assembling a portfolio of assets to maximize expected return for a given level of market risk. MPT argues that an investment's risk and return characteristics should not be viewed alone but should be evaluated by how the investment affects the overall portfolio's risk and return. MPT shows that an investor can construct a portfolio of multiple assets that will maximize returns for a given level of risk. Likewise, given a desired level of expected return, an investor can construct a portfolio with the lowest possible risk. Based on statistical measures such as variance and correlation, an individual investment's return is less important than how the investment behaves in the context of the entire portfolio. MPT assumes that investors are risk averse, meaning that given two portfolios that offer the same expected return, investors will prefer the one with less market risk. Conversely, an investor who wants higher expected returns must accept higher risk. The exact trade-off will not be the same for all investors. Different investors will evaluate the trade-off differently based on individual risk aversion characteristics. The implication is that a rational investor will not invest in a portfolio if a second portfolio exists with a more favorable risk-expected return profile – i.e., if for that level of risk an alternative portfolio exists that has better expected returns.

The expected return of the portfolio is calculated as a weighted sum of the individual assets' returns. The portfolio's risk is a function of the variances of each asset and the correlations of each pair of assets. To calculate the risk of a four-asset portfolio, an investor needs each of the four assets' variances and six correlation values, since there are six possible two-asset combinations with four assets. Because of the asset correlations, the total portfolio risk, or standard deviation, is lower than what would be calculated by  a weighted sum.

## Portfolio Optimization

In our example we consider a portfolio of 6 large cap US stocks and we will optimize the portfolio, i.e. calculate the amount of each stock we need to hold in our portfolio to maximize the expected return for a given level of market risk (standard deviation of portfolio returns).

First we retrieve the daily price history for each stock for the last 3 years.

In [2]:

    	
stocks_list = ['KO.N','AAPL.O','AMZN.O','FB.O','BAC.N','MMM.N']
def get_prices(stocks, start_date):
stock_prices, err = ek.get_data(stocks,
['TR.PriceClose.date','TR.PriceClose'],
{'SDate':start_date, 'EDate':'0D'})
stock_prices['Date'] = pd.to_datetime(stock_prices['Date'])
stock_prices = stock_prices.set_index(['Date','Instrument'])
stock_prices = stock_prices.unstack()
stock_prices.columns = stock_prices.columns.get_level_values(1)
return stock_prices
prices = get_prices(stocks_list, start_date='-3Y')
ax = prices.rebase().plot()





Out [2]:

Then using f.fn library we calculate a bunch of stats for each stock including Sharpe and Sortino ratios.

In [3]:

    	
np.seterr(all='ignore')
stats = prices.calc_stats()
stats.display()





Out [3]:

    	
Stat                 AAPL.O      AMZN.O      BAC.N       FB.O        KO.N        MMM.N
-------------------  ----------  ----------  ----------  ----------  ----------  ----------
Start                2016-11-29  2016-11-29  2016-11-29  2016-11-29  2016-11-29  2016-11-29
End                  2019-11-27  2019-11-27  2019-11-27  2019-11-27  2019-11-27  2019-11-27
Risk-free rate       0.00%       0.00%       0.00%       0.00%       0.00%       0.00%

Total Return         140.30%     138.49%     64.71%      67.12%      31.11%      -1.25%
Daily Sharpe         1.31        1.20        0.83        0.74        0.70        0.09
Daily Sortino        2.14        2.00        1.38        1.11        1.09        0.12
CAGR                 34.04%      33.70%      18.15%      18.72%      9.47%       -0.42%
Max Drawdown         -38.73%     -34.10%     -30.79%     -42.96%     -14.38%     -41.72%
Calmar Ratio         0.88        0.99        0.59        0.44        0.66        -0.01

MTD                  7.67%       2.36%       6.88%       5.40%       -0.88%      3.41%
3m                   31.19%      3.22%       26.26%      11.42%      -1.41%      9.54%
6m                   49.66%      -0.26%      18.59%      11.57%      8.75%       2.72%
YTD                  69.80%      21.07%      35.63%      54.09%      13.94%      -10.46%
1Y                   53.72%      14.99%      20.48%      49.63%      9.28%       -14.99%
3Y (ann.)            34.04%      33.70%      18.15%      18.72%      9.47%       -0.42%
5Y (ann.)            34.04%      33.70%      18.15%      18.72%      9.47%       -0.42%
10Y (ann.)           34.04%      33.70%      18.15%      18.72%      9.47%       -0.42%
Since Incep. (ann.)  34.04%      33.70%      18.15%      18.72%      9.47%       -0.42%

Daily Sharpe         1.31        1.20        0.83        0.74        0.70        0.09
Daily Sortino        2.14        2.00        1.38        1.11        1.09        0.12
Daily Mean (ann.)    32.35%      32.81%      19.43%      21.45%      10.09%      1.84%
Daily Vol (ann.)     24.61%      27.45%      23.44%      29.00%      14.36%      21.12%
Daily Skew           -0.33       0.39        -0.14       -1.38       -0.92       -1.67
Daily Kurt           4.91        7.47        2.43        18.92       13.19       14.19
Best Day             7.04%       13.22%      7.16%       10.82%      6.07%       5.91%
Worst Day            -9.96%      -7.82%      -5.92%      -18.96%     -8.44%      -12.95%

Monthly Sharpe       1.22        1.22        0.76        0.77        0.89        0.10
Monthly Sortino      2.30        2.41        1.42        1.84        1.44        0.15
Monthly Mean (ann.)  33.58%      33.51%      18.17%      21.56%      10.39%      2.06%
Monthly Vol (ann.)   27.52%      27.48%      23.84%      28.02%      11.69%      21.36%
Monthly Skew         -0.53       -0.16       -0.34       0.85        -0.99       -0.68
Monthly Kurt         0.57        1.91        -0.35       1.39        1.37        0.00
Best Month           19.62%      24.06%      15.54%      27.16%      6.32%       9.67%
Worst Month          -18.40%     -20.22%     -13.24%     -11.19%     -9.18%      -15.70%

Yearly Sharpe        0.93        1.91        0.59        0.59        1.68        0.03
Yearly Sortino       9.28        inf         1.84        1.84        inf         0.08
Yearly Mean          36.37%      35.15%      17.56%      27.25%      9.27%       0.77%
Yearly Vol           39.21%      18.39%      29.54%      45.87%      5.50%       27.22%
Yearly Skew          -1.05       1.43        -1.72       -1.73       -1.07       1.54
Yearly Kurt          -           -           -           -           -           -
Best Year            69.80%      55.96%      35.63%      54.09%      13.94%      31.81%
Worst Year           -6.79%      21.07%      -16.53%     -25.71%     3.20%       -19.05%

Avg. Drawdown        -3.05%      -3.32%      -3.13%      -3.36%      -2.39%      -3.15%
Avg. Drawdown Days   21.20       23.79       36.18       23.95       30.33       43.65
Avg. Up Month        7.82%       7.44%       6.25%       7.24%       2.75%       4.10%
Avg. Down Month      -5.09%      -4.52%      -5.12%      -5.01%      -2.47%      -5.99%
Win Year %           66.67%      100.00%     66.67%      66.67%      100.00%     33.33%
Win 12m %            80.77%      92.31%      61.54%      73.08%      80.77%      34.62%




Now let’s consider an initial portfolio with randomly assigned weights for each stock and calculate expected return and volatility of this portfolio.

In [4]:

    	
# portfolio weights
weights = np.asarray([0.4,0.2,0.1,0.1,0.1,0.1])

returns = prices.pct_change()

# mean daily return and covariance of daily returns
mean_daily_returns = returns.mean()
cov_matrix = returns.cov()

# portfolio return and volatility
pf_return = round(np.sum(mean_daily_returns * weights) * 252, 3)
pf_std_dev = round(np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights))) * np.sqrt(252), 3)

print("Expected annualized return: " + "{:.1%}".format(pf_return))
print("Volatility: " + "{:.1%}".format(pf_std_dev))





Out [4]:

    	
Expected annualized return: 24.8%
Volatility: 18.3%




Let's calculate portfolio weights that maximize Sharpe ratio and compare the return and volatility of this portfolio to our initial portfolio using pypfopt library.

In [5]:

    	
# Expected returns and sample covariance
exp_returns = expected_returns.mean_historical_return(prices)
covar = risk_models.sample_cov(prices)

# Optimise portfolio for maximum Sharpe Ratio
ef = EfficientFrontier(exp_returns, covar)
raw_weights = ef.max_sharpe()
pf = ef.clean_weights()
print(pf)
perf = ef.portfolio_performance(verbose=True)





Out [5]:

    	
{'AAPL.O': 0.38866, 'AMZN.O': 0.2362, 'BAC.N': 0.09652, 'FB.O': 0.0, 'KO.N': 0.27862, 'MMM.N': 0.0}
Expected annual return: 25.0%
Annual volatility: 16.8%
Sharpe Ratio: 1.37




You see that in the previous step two stocks were removed from our portfolio (they were assigned zero weights). What if we allow short positions in our portfolio and optimize it by minimizing risk for a given target return?

In [6]:

    	
ef = EfficientFrontier(exp_returns, covar, weight_bounds=(-1, 1))
pf = ef.efficient_return(target_return=perf[0])
print(pf)
perf = ef.portfolio_performance(verbose=True)





Out [6]:

    	
{'AAPL.O': 0.303965600580418, 'AMZN.O': 0.1776183858965163, 'BAC.N': 0.20777261447797482, 'FB.O': -0.011781001328141041, 'KO.N': 0.6027146347675877, 'MMM.N': -0.2802902343943557}
Expected annual return: 25.0%
Annual volatility: 15.1%
Sharpe Ratio: 1.53




Comparing the result with our long only portfolio for the same return we see slightly lower risk and higher Sharpe ratio.

The weights calculated for our optimized portfolio don't tell us how much of each stock we should hold. They may also result in fractional numbers of stocks, which is impractical. In the next step we use integer programming method to create discrete allocations for our portfolio. In other words given the cash amount we have to invest we calculate integer number for each stock holding and the amount of leftover cash.

In [7]:

    	
latest_prices = discrete_allocation.get_latest_prices(prices)
allocation, leftover = discrete_allocation.DiscreteAllocation(pf, latest_prices,
total_portfolio_value=100000).lp_portfolio()
print(allocation)
print("Funds remaining: ${:.2f}".format(leftover))   Out [7]:   0 out of 6 tickers were removed Allocating long sub-portfolio: 0 out of 4 tickers were removed Allocating short sub-portfolio: 0 out of 2 tickers were removed {'AAPL.O': 87, 'AMZN.O': 8, 'BAC.N': 481, 'KO.N': 854, 'FB.O': -6, 'MMM.N': -168} Funds remaining:$127.04




## Efficient frontier

In modern portfolio theory, the efficient frontier is an investment portfolio which occupies the 'efficient' parts of the risk-return spectrum. Formally, it is the set of portfolios which satisfy the condition that no other portfolio exists with a higher expected return but with the same standard deviation of return. Using pypfopt library we calculate volatility and expected return on our portfolio given a range of target returns. When we plot calculated return against volatility we get classic textbook chart of efficient frontier for a portfolio without a risk free asset.

In [8]:

    	
def risk_return(row):
pf = ef.efficient_return(target_return=row.Target_Return)
perf = ef.portfolio_performance()
row.Return = perf[0]
row.Volatility = perf[1]
return row
df = pd.DataFrame(columns=['Target_Return','Volatility','Return'])
df['Target_Return'] = np.linspace(0.01, 0.5)
df = df.apply(risk_return, axis=1)
ax = df.plot(x='Volatility', y='Return', xlim = (0.05, 0.35), title='Efficient Frontier')
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: '{:.0%}'.format(x)))
ax.yaxis.set_major_formatter(FuncFormatter(lambda y, _: '{:.0%}'.format(y)))





Out [8]: