Sharpe Ratio and Portfolio Values


> import numpy as np
+ import pandas as pd
+ import pandas_datareader
+ import pandas_datareader.data as web
+ import datetime
+ import matplotlib.pyplot as plt

Portfolio Statistics

> start = datetime.datetime(2012, 1, 1)
+ end = datetime.datetime(2017, 1, 1)
+ 
+ aapl = web.DataReader("AAPL", 'yahoo', start, end)
+ cisco = web.DataReader("CSCO", 'yahoo', start, end)
+ ibm = web.DataReader("IBM", 'yahoo', start, end)
+ amzn = web.DataReader("AMZN", 'yahoo', start, end)
> aapl.head()
                 High        Low       Open      Close       Volume  Adj Close
Date                                                                          
2012-01-03  14.732142  14.607142  14.621428  14.686786  302220800.0  12.669562
2012-01-04  14.810000  14.617143  14.642858  14.765715  260022000.0  12.737655
2012-01-05  14.948215  14.738214  14.819643  14.929643  271269600.0  12.879071
2012-01-06  15.098214  14.972143  14.991786  15.085714  318292800.0  13.013705
2012-01-09  15.276786  15.048214  15.196428  15.061786  394024400.0  12.993064
Adjusted Prices/Returns
> #Adjusted Close
+ aapl.iloc[0]['Adj Close']
12.669562339782715
> # Calc cumulative adjusted return
+ for stock_df in (aapl,cisco,ibm,amzn):
+     stock_df['Adj Return'] = stock_df['Adj Close'
+                      ]/stock_df.iloc[0]['Adj Close']
> aapl = aapl.iloc[:,[5,6]]
+ cisco = cisco.iloc[:,[5,6]]
+ ibm = ibm.iloc[:,[5,6]]
+ amzn = amzn.iloc[:,[5,6]]
> aapl.head()
            Adj Close  Adj Return
Date                             
2012-01-03  12.669562    1.000000
2012-01-04  12.737655    1.005374
2012-01-05  12.879071    1.016536
2012-01-06  13.013705    1.027163
2012-01-09  12.993064    1.025534
> aapl.tail()
            Adj Close  Adj Return
Date                             
2016-12-23  27.588572    2.177547
2016-12-27  27.763786    2.191377
2016-12-28  27.645403    2.182033
2016-12-29  27.638296    2.181472
2016-12-30  27.422834    2.164466
Allocations

We can use these sample weights to generate portfolio values:

  • 30% in Apple
  • 20% in Google/Alphabet
  • 40% in Amazon
  • 10% in IBM

\[\text{weight }\times \text{cumulative adjusted returns } = Multiplier \]

\[Multiplier \times \text{starting portfolio value}= \text{ending portfolio value}\]

> for stock_df,allo in zip([aapl,cisco,ibm,amzn],[.3,.2,.4,.1]):
+     stock_df['Multiplier'] = stock_df['Adj Return']*allo
> aapl.head()
            Adj Close  Adj Return  Multiplier
Date                                         
2012-01-03  12.669562    1.000000    0.300000
2012-01-04  12.737655    1.005374    0.301612
2012-01-05  12.879071    1.016536    0.304961
2012-01-06  13.013705    1.027163    0.308149
2012-01-09  12.993064    1.025534    0.307660
Investment

Example - $1,000,000 invested in the portfolio:

> for stock_df in [aapl,cisco,ibm,amzn]:
+     stock_df['Position Values'] = stock_df['Multiplier']*1000000
> aapl.head()
            Adj Close  Adj Return  Multiplier  Position Values
Date                                                          
2012-01-03  12.669562    1.000000    0.300000    300000.000000
2012-01-04  12.737655    1.005374    0.301612    301612.344871
2012-01-05  12.879071    1.016536    0.304961    304960.918702
2012-01-06  13.013705    1.027163    0.308149    308148.890339
2012-01-09  12.993064    1.025534    0.307660    307660.128540
Total Portfolio Value
> portfolio_val = pd.concat([aapl['Position Values'],
+                            cisco['Position Values'],
+                            ibm['Position Values'],
+                            amzn['Position Values']],axis=1)
> portfolio_val.columns = ['AAPL Pos','CISCO Pos',
+                         'IBM Pos','AMZN Pos']
> portfolio_val.head()
                 AAPL Pos      CISCO Pos        IBM Pos       AMZN Pos
Date                                                                  
2012-01-03  300000.000000  200000.000000  400000.000000  100000.000000
2012-01-04  301612.344871  203864.709407  398368.285025   99150.977890
2012-01-05  304960.918702  203113.264529  396478.844766   99206.837860
2012-01-06  308148.890339  202361.806268  391927.106403  101999.665897
2012-01-09  307660.128540  203650.060582  389887.383182   99737.473483
> portfolio_val['Total Pos'] = portfolio_val.sum(axis=1)
> portfolio_val['Total Pos'].plot(figsize=(10,8));
+ plt.title('Total Portfolio Value');
+ plt.show()

> portfolio_val.drop('Total Pos',axis=1).plot(kind='line');
+ plt.show()

> portfolio_val.tail()
                 AAPL Pos      CISCO Pos  ...       AMZN Pos     Total Pos
Date                                      ...                             
2016-12-23  653264.196708  377461.364751  ...  424839.430286  1.870044e+06
2016-12-27  657413.071691  379315.872784  ...  430877.523138  1.883154e+06
2016-12-28  654609.894965  376101.306537  ...  431285.265122  1.875182e+06
2016-12-29  654441.614937  376595.863426  ...  427386.488092  1.872629e+06
2016-12-30  649339.740259  373628.575623  ...  418851.589248  1.854508e+06

[5 rows x 5 columns]
Portfolio Statistics

DAILY RETURNS

> portfolio_val['Daily Return'] = portfolio_val[
+     'Total Pos'].pct_change(1)

CUMULATIVE RETURN

> cum_ret = 100 * (portfolio_val['Total Pos'][-1]/
+                  portfolio_val['Total Pos'][0] -1 )
+ print('Our return was {} percent'.format(cum_ret))
Our return was 85.45083664605563 percent

AVG DAILY RETURN

> portfolio_val['Daily Return'].mean()
0.0005472977564542413

STD DAILY RETURN

> portfolio_val['Daily Return'].std()
0.01057021022119259
> portfolio_val['Daily Return'].plot(kind='hist',bins=100);
+ plt.show()

> portfolio_val['Daily Return'].plot(kind='kde');
+ plt.show()

Sharpe Ratio

The Sharpe Ratio is a measure for calculating risk-adjusted return.

Sharpe ratio = (Portfolio return − Risk-free rate)/Standard deviation of portfolio return

\[Sharpe\space Ratio = \frac{R_p-R_f}{\sigma_p}\]

The original Sharpe Ratio used annual returns. The calculation needs to be adjusted for different sample types.

Annualized Sharpe Ratio = K-value * SR

K-values for various sampling rates:

  • Daily = \(\sqrt{252}\)
  • Weekly = \(\sqrt{52}\)
  • Monthly = \(\sqrt{12}\)

Since the current risk-free rate is extremely low (let’s just say its ~0%), it’s not very important to the calculation. However,if needed you can convert a yearly rate to a daily rate:

\[daily\space rate = ((1.0 + yearly\space rate)^{(1/252)})-1\]

It is typical to use the 3-month treasury bill or LIBOR.

Read more: Sharpe Ratio http://www.investopedia.com/terms/s/sharperatio

> portfolio_val['Daily Return'].mean()
0.0005472977564542413
> portfolio_val['Daily Return'].std()
0.01057021022119259
> # Sharpe Ratio
+ SR = portfolio_val['Daily Return'].mean()/portfolio_val['Daily Return'].std()
+ SR
0.051777376703156255
> # Adjusted Sharpe Ratio
+ ASR = (252**0.5)*SR
+ ASR
0.8219403737751652
> aapl['Adj Close'].pct_change(1).plot(kind='kde');
+ ibm['Adj Close'].pct_change(1).plot(kind='kde');
+ amzn['Adj Close'].pct_change(1).plot(kind='kde');
+ cisco['Adj Close'].pct_change(1).plot(kind='kde');
+ plt.show()

Portfolio Optimization


“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.

Monte Carlo Simulation

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
> # mean daily return
+ mean_daily_ret = stocks.pct_change(1).mean()
+ mean_daily_ret
aapl     0.000750
cisco    0.000599
ibm      0.000094
amzn     0.001328
dtype: float64
> # correlation
+ stocks.pct_change(1).corr()
           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
Simulating Thousands of Possible Allocations
> # adjusted returns
+ stock_normed = stocks/stocks.iloc[0]
+ stock_normed.plot();
+ plt.show()

> stock_daily_ret = stocks.pct_change(1)
+ stock_daily_ret.head()
                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
Switch to Log Returns

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/

> log_ret = np.log(stocks/stocks.shift(1))
+ log_ret.head()
                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
> log_ret.hist(bins=100,figsize=(12,6));
+ plt.tight_layout()
+ plt.show()

> log_ret.describe().transpose()
        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]
> log_ret.mean()
aapl     0.000614
cisco    0.000497
ibm      0.000025
amzn     0.001139
dtype: float64
> log_ret.mean() * 252
aapl     0.154803
cisco    0.125287
ibm      0.006261
amzn     0.287153
dtype: float64
> # Compute pairwise covariance of columns
+ log_ret.cov()
           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
> log_ret.cov()*252 # multiply by days
           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
Single Run for a Random Allocation
> 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]
> sharpe_arr.max() # max SR
1.0301143368565366
> sharpe_arr.argmax() # location
1419
> # optimal weights
+ all_weights[1419,:]
array([0.26188068, 0.20759516, 0.00110226, 0.5294219 ])
> max_sr_ret = ret_arr[1419]
+ max_sr_vol = vol_arr[1419]
Plotting the Data
> 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()

Mathematical Optimization

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)

> from scipy.optimize import minimize
> def neg_sharpe(weights):
+     return  get_ret_vol_sr(weights)[2] * -1
> # 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})
> # 0-1 bounds for each weight
+ bounds = ((0, 1), (0, 1), (0, 1), (0, 1))
> # Initial Guess (equal distribution)
+ init_guess = [0.25,0.25,0.25,0.25]
> # Sequential Least SQuares Programming (SLSQP).
+ opt_results = minimize(neg_sharpe,init_guess,
+         method='SLSQP',bounds=bounds,constraints=cons)
> opt_results
     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])
> # weights
+ opt_results.x
array([0.26627042, 0.20391775, 0.        , 0.52981183])
> # return, volatility, Sharpe Ratio
+ get_ret_vol_sr(opt_results.x)
array([0.21890489, 0.21242837, 1.030488  ])
All Optimal Portfolios (Efficient Frontier)

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)
> def minimize_volatility(weights):
+     return  get_ret_vol_sr(weights)[1] 
> 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()

Capital Asset Pricing Model


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)\]

  • The \(\beta\) term implies that the return of a stock is equivalent to the return of the market multiplied by this \(\beta\) factor plus some residual \(\alpha\) term.

CAPM of a portfolio:

\[ r_p(t) = \beta_pr_m(t) + \sum\limits_{i}^{n}w_i \alpha_i(t)\]

> # Model CAPM as a simple linear regression
+ from scipy import stats
> start = pd.to_datetime('2010-01-04')
+ end = pd.to_datetime('2017-07-18')
+ 
+ spy_etf = web.DataReader('SPY','yahoo',start,end)
> spy_etf.head()
                  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]
> aapl = web.DataReader('AAPL','yahoo',start,end)
+ aapl.head()
                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()

Compare Cumulative Return
> 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()

Get Daily Return
> aapl['Daily Return'] = aapl['Adj Close'].pct_change(1)
+ spy_etf['Daily Return'] = spy_etf['Adj Close'].pct_change(1)
> plt.scatter(aapl['Daily Return'],
+             spy_etf['Daily Return'],alpha=0.3);
+ plt.show()

The returns are not perfectly correlated so the \(\beta\) should not be \(1\).

> aapl['Daily Return'].hist(bins=100);
+ plt.show()

> spy_etf['Daily Return'].hist(bins=100);
+ plt.show()

Get Beta and Alpha
> beta,alpha,r_value,p_value,std_err = \
+ stats.linregress(aapl['Daily Return'].iloc[1:],
+                  spy_etf['Daily Return'].iloc[1:])
> beta
0.32457053271872316
> alpha
0.0002007541506383689
> r_value
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.