“Modern Portfolio Theory (MPT), a hypothesis put forth by Harry Markowitz in his paper “Portfolio Selection,” (published in 1952 by the Journal of Finance) is an investment theory based on the idea that risk-averse investors can construct portfolios to optimize or maximize expected return based on a given level of market risk, emphasizing that risk is an inherent part of higher reward. It is one of the most important and influential economic theories dealing with finance and investment.
We could randomly try to find the optimal portfolio balance using Monte Carlo simulation
> stocks = pd.concat([aapl['Adj Close'],
+ cisco['Adj Close'],
+ ibm['Adj Close'],
+ amzn['Adj Close']],axis=1)
+
+ stocks.columns = ['aapl','cisco','ibm','amzn']
+ stocks.head() aapl cisco ibm amzn
Date
2012-01-03 12.669562 14.251211 134.349426 179.029999
2012-01-04 12.737655 14.526595 133.801376 177.509995
2012-01-05 12.879071 14.473050 133.166763 177.610001
2012-01-06 13.013705 14.419504 131.637955 182.610001
2012-01-09 12.993064 14.511300 130.952866 178.559998
aapl 0.000750
cisco 0.000599
ibm 0.000094
amzn 0.001328
dtype: float64
aapl cisco ibm amzn
aapl 1.000000 0.302071 0.297770 0.235723
cisco 0.302071 1.000000 0.423887 0.284914
ibm 0.297770 0.423887 1.000000 0.259079
amzn 0.235723 0.284914 0.259079 1.000000
aapl cisco ibm amzn
Date
2012-01-03 NaN NaN NaN NaN
2012-01-04 0.005374 0.019324 -0.004079 -0.008490
2012-01-05 0.011102 -0.003686 -0.004743 0.000563
2012-01-06 0.010454 -0.003700 -0.011480 0.028152
2012-01-09 -0.001586 0.006366 -0.005204 -0.022178
Most technical analyses require de-trending/normalizing the time series and using log returns is a nice way to do that.
See here for a full analysis of why log returns are useful:
https://quantivity.wordpress.com/2011/02/21/why-log-returns/
aapl cisco ibm amzn
Date
2012-01-03 NaN NaN NaN NaN
2012-01-04 0.005360 0.019139 -0.004088 -0.008526
2012-01-05 0.011041 -0.003693 -0.004754 0.000563
2012-01-06 0.010399 -0.003707 -0.011547 0.027763
2012-01-09 -0.001587 0.006346 -0.005218 -0.022428
count mean std ... 50% 75% max
aapl 1257.0 0.000614 0.016469 ... 0.000455 0.009743 0.085022
cisco 1257.0 0.000497 0.014281 ... 0.000000 0.007626 0.118987
ibm 1257.0 0.000025 0.011828 ... 0.000049 0.006510 0.049130
amzn 1257.0 0.001139 0.019362 ... 0.000563 0.011407 0.146225
[4 rows x 8 columns]
aapl 0.000614
cisco 0.000497
ibm 0.000025
amzn 0.001139
dtype: float64
aapl 0.154803
cisco 0.125287
ibm 0.006261
amzn 0.287153
dtype: float64
aapl cisco ibm amzn
aapl 0.000271 0.000071 0.000058 0.000075
cisco 0.000071 0.000204 0.000071 0.000079
ibm 0.000058 0.000071 0.000140 0.000059
amzn 0.000075 0.000079 0.000059 0.000375
aapl cisco ibm amzn
aapl 0.068351 0.017865 0.014491 0.019009
cisco 0.017865 0.051395 0.018012 0.019991
ibm 0.014491 0.018012 0.035254 0.014984
amzn 0.019009 0.019991 0.014984 0.094476
> np.random.seed(101)
+
+ # Random Weights
+ weights = np.array(np.random.random(4))
+
+ # Rebalance Weights
+ weights2 = weights / np.sum(weights)
+
+ # Expected Return
+ exp_ret = np.sum(log_ret.mean() * weights) *252
+
+ # Expected Volatility
+ exp_vol = np.sqrt(np.dot(weights.T, np.dot(log_ret.cov() * 252,
+ weights)))
+
+ # Sharpe Ratio
+ SR = exp_ret/exp_vol
+
+ print(' Stocks','\n',stocks.columns,'\n','\n',
+ 'Creating Random Weights','\n',weights,'\n','\n',
+ 'Rebalance to sum to 1.0','\n',weights2,'\n','\n',
+ 'Expected Portfolio Return','\n',exp_ret,'\n','\n',
+ 'Expected Volatility','\n',exp_vol,'\n','\n',
+ 'Sharpe Ratio','\n',SR) Stocks
Index(['aapl', 'cisco', 'ibm', 'amzn'], dtype='object')
Creating Random Weights
[0.51639863 0.57066759 0.02847423 0.17152166]
Rebalance to sum to 1.0
[0.40122278 0.44338777 0.02212343 0.13326603]
Expected Portfolio Return
0.20086881504190618
Expected Volatility
0.2382026672504394
Sharpe Ratio
0.843268538343941
Now we can just run this many times with a Loop
> num_ports = 15000
+
+ all_weights = np.zeros((num_ports,len(stocks.columns)))
+ ret_arr = np.zeros(num_ports)
+ vol_arr = np.zeros(num_ports)
+ sharpe_arr = np.zeros(num_ports)
+
+ for ind in range(num_ports):
+
+ # Create Random Weights
+ weights = np.array(np.random.random(4))
+
+ # Rebalance Weights
+ weights = weights / np.sum(weights)
+
+ # Save Weights
+ all_weights[ind,:] = weights
+
+ # Expected Return
+ ret_arr[ind] = np.sum((log_ret.mean() * weights) *252)
+
+ # Expected Volatility
+ vol_arr[ind] = np.sqrt(np.dot(weights.T,
+ np.dot(log_ret.cov() * 252, weights)))
+
+ # Sharpe Ratio
+ sharpe_arr[ind] = ret_arr[ind]/vol_arr[ind]1.0301143368565366
1419
array([0.26188068, 0.20759516, 0.00110226, 0.5294219 ])
> plt.figure(figsize=(12,8));
+ plt.scatter(vol_arr,ret_arr,c=sharpe_arr,cmap='plasma');
+ plt.colorbar(label='Sharpe Ratio');
+ plt.xlabel('Volatility');
+ plt.ylabel('Return');
+
+ # Add red dot for max SR
+ plt.scatter(max_sr_vol,max_sr_ret,
+ c='red',s=50,edgecolors='black');
+
+ plt.tight_layout()
+ plt.show()We can use optimization functions to find the ideal weights mathematically.
FUNCTIONALIZE RETURN AND SR OPERATIONS
> def get_ret_vol_sr(weights):
+ """
+ Takes in weights, returns array or return,
+ volatility, sharpe ratio
+ """
+ weights = np.array(weights)
+ ret = np.sum(log_ret.mean() * weights) * 252
+ vol = np.sqrt(np.dot(weights.T,
+ np.dot(log_ret.cov() * 252, weights)))
+ sr = ret/vol
+ return np.array([ret,vol,sr])Optimization works as a minimization function. Since we actually want to maximize the Sharpe Ratio, we will need to turn it negative so we can minimize the negative Sharpe Ratio (same as maximizing the postive Sharpe Ratio)
> # Contraints
+ def check_sum(weights):
+ '''
+ Returns 0 if sum of weights is 1.0
+ '''
+ return np.sum(weights) - 1> # By convention of minimize function it should
+ # be a function that returns zero for conditions
+ cons = ({'type':'eq','fun': check_sum})> # Sequential Least SQuares Programming (SLSQP).
+ opt_results = minimize(neg_sharpe,init_guess,
+ method='SLSQP',bounds=bounds,constraints=cons) fun: -1.030487997417029
jac: array([ 5.49107790e-05, 3.76105309e-05, 3.23809251e-01, -4.20957804e-05])
message: 'Optimization terminated successfully.'
nfev: 42
nit: 7
njev: 7
status: 0
success: True
x: array([0.26627042, 0.20391775, 0. , 0.52981183])
array([0.26627042, 0.20391775, 0. , 0.52981183])
array([0.21890489, 0.21242837, 1.030488 ])
The efficient frontier is the set of optimal portfolios that offers the highest expected return for a defined level of risk or the lowest risk for a given level of expected return. Portfolios that lie below the efficient frontier are sub-optimal, because they do not provide enough return for the level of risk. Portfolios that cluster to the right of the efficient frontier are also sub-optimal, because they have a higher level of risk for the defined rate of return.
Efficient Frontier: http://www.investopedia.com/terms/e/efficientfrontier
> # Our returns go from 0 to somewhere along 0.3
+ # Create a linspace number of points to calculate x on
+ frontier_y = np.linspace(0,0.3,100)> frontier_volatility = []
+
+ for possible_return in frontier_y:
+ # function for return
+ cons = ({'type':'eq','fun': check_sum},
+ {'type':'eq','fun': lambda w: get_ret_vol_sr(w)[0] - possible_return})
+
+ result = minimize(minimize_volatility,init_guess,
+ method='SLSQP',bounds=bounds,constraints=cons)
+
+ frontier_volatility.append(result['fun'])> plt.figure(figsize=(12,8));
+ plt.scatter(vol_arr,ret_arr,c=sharpe_arr,cmap='plasma');
+ plt.colorbar(label='Sharpe Ratio');
+ plt.xlabel('Volatility');
+ plt.ylabel('Return');
+
+ # Add frontier line
+ plt.plot(frontier_volatility,frontier_y,'g--',linewidth=3);
+
+ plt.tight_layout()
+ plt.show()Portfolio Returns:
\[r_p(t) = \sum\limits_{i}^{n}w_i r_i(t)\]
Market Weights:
\[w_i = \frac{MarketCap_i}{\sum_{j}^{n}{MarketCap_j}}\]
CAPM equation:
\[r_i(t) = \beta_ir_m(t) + \alpha_i(t)\]
CAPM of a portfolio:
\[ r_p(t) = \beta_pr_m(t) + \sum\limits_{i}^{n}w_i \alpha_i(t)\]
> start = pd.to_datetime('2010-01-04')
+ end = pd.to_datetime('2017-07-18')
+
+ spy_etf = web.DataReader('SPY','yahoo',start,end) High Low ... Volume Adj Close
Date ...
2010-01-04 113.389999 111.510002 ... 118944600.0 91.475693
2010-01-05 113.680000 112.849998 ... 111579900.0 91.717857
2010-01-06 113.989998 113.430000 ... 116074400.0 91.782425
2010-01-07 114.330002 113.180000 ... 131091100.0 92.169884
2010-01-08 114.620003 113.660004 ... 126402800.0 92.476562
[5 rows x 6 columns]
High Low Open Close Volume Adj Close
Date
2010-01-04 7.660714 7.585000 7.622500 7.643214 493729600.0 6.593426
2010-01-05 7.699643 7.616071 7.664286 7.656428 601904800.0 6.604825
2010-01-06 7.686786 7.526786 7.656428 7.534643 552160000.0 6.499768
2010-01-07 7.571429 7.466072 7.562500 7.520714 477131200.0 6.487752
2010-01-08 7.571429 7.466429 7.510714 7.570714 447610800.0 6.530883
> aapl['Adj Close'].plot(label='AAPL',figsize=(10,8));
+ spy_etf['Adj Close'].plot(label='SPY Index');
+ plt.legend();
+ plt.show()> aapl['Cumulative'] = aapl['Adj Close'
+ ]/aapl['Adj Close'].iloc[0]
+ spy_etf['Cumulative'] = spy_etf['Adj Close'
+ ]/spy_etf['Adj Close'].iloc[0]> aapl['Cumulative'].plot(label='AAPL',figsize=(10,8));
+ spy_etf['Cumulative'].plot(label='SPY Index');
+ plt.legend();
+ plt.title('Cumulative Return');
+ plt.show()> aapl['Daily Return'] = aapl['Adj Close'].pct_change(1)
+ spy_etf['Daily Return'] = spy_etf['Adj Close'].pct_change(1)The returns are not perfectly correlated so the \(\beta\) should not be \(1\).
> beta,alpha,r_value,p_value,std_err = \
+ stats.linregress(aapl['Daily Return'].iloc[1:],
+ spy_etf['Daily Return'].iloc[1:])0.32457053271872316
0.0002007541506383689
0.5553324739645302
The \(\beta\) is much lower than \(1\) and the \(\alpha\) is positive, which implies that the stock return will be based on factors other than just the market performance.