DISCLAIMER!

Before proceeding, please make sure that you note the following important information:

NOT FINANCIAL ADVICE!

My content is intended to be used and must be used for informational and educational purposes only. I am not an attorney, CPA, or financial advisor, nor am I holding myself out to be, and the information contained on this blog/notebook is not a substitute for financial advice None of the information contained here constitutes an offer (or solicitation of an offer) to buy or sell any security or financial instrument, to make any investment, or to participate in any particular trading strategy. Always seek advice from a professional who is aware of the facts and circumstances of your individual situation. Or, Independently research and verify any information that you find on my blog/notebook and wish to rely upon in making any investment decision or otherwise. I accept no liability whatsoever for any loss or damage you may incur

import numpy as np
import pandas as pd
import yfinance as yf
from pandas_datareader import data as wb
from chart_studio import plotly as py
import plotly.express as px
import plotly.graph_objs as go
from IPython.display import HTML
from plotly.offline import init_notebook_mode, iplot
init_notebook_mode(connected=True)  
import matplotlib.pyplot as plt
%matplotlib inline
plt.style.use('seaborn-darkgrid')

Portfolio construction and analysis:

assets = ['TXN','CSCO','INTC','AAPL','MSFT',
          'NVDA','INFY','INTU','SAP','ADI',
          'ANSS','CRM','ADBE','FB','AMD',
          'AMZN','MA','VMW','GOOG','SNPS']
pf_data = pd.DataFrame()
for a in assets:
    pf_data[a] = yf.download(a, start="2012-05-20", end="2021-12-31", index_col = 'Date', parse_dates=True)['Adj Close'] 
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
pf_data.head()
TXN CSCO INTC AAPL MSFT NVDA INFY INTU SAP ADI ANSS CRM ADBE FB AMD AMZN MA VMW GOOG SNPS
Date
2012-05-21 22.610977 12.278630 19.480406 17.139450 24.290533 2.821950 4.160825 50.348274 49.764915 28.088888 61.000000 37.262501 32.009998 34.029999 6.30 218.110001 38.773224 69.075218 305.908386 28.040001
2012-05-22 22.518942 12.322823 19.391010 17.007828 24.298697 2.787507 4.215657 51.022617 49.680264 28.034071 61.750000 37.362499 32.009998 31.000000 6.16 215.330002 39.043331 68.748131 299.278229 28.040001
2012-05-23 22.350197 12.293363 18.951487 17.422827 23.767975 2.856392 4.168488 51.460045 49.773380 27.940096 62.119999 37.652500 32.180000 32.000000 6.08 217.279999 39.388008 69.177422 303.592072 28.200001
2012-05-24 22.158449 12.072392 19.107927 17.262810 23.735321 2.780620 4.243171 51.387135 48.842247 28.190681 61.689999 36.552502 31.540001 33.029999 6.02 215.240005 39.646744 65.027603 300.702881 29.850000
2012-05-25 22.196800 12.028200 19.174969 17.170284 23.727154 2.847208 4.172420 51.441814 48.308960 28.339464 62.090000 36.750000 31.600000 31.910000 6.22 212.889999 39.092426 64.066803 294.660553 29.889999
pf_data.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 2420 entries, 2012-05-21 to 2021-12-30
Data columns (total 20 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   TXN     2420 non-null   float64
 1   CSCO    2420 non-null   float64
 2   INTC    2420 non-null   float64
 3   AAPL    2420 non-null   float64
 4   MSFT    2420 non-null   float64
 5   NVDA    2420 non-null   float64
 6   INFY    2420 non-null   float64
 7   INTU    2420 non-null   float64
 8   SAP     2420 non-null   float64
 9   ADI     2420 non-null   float64
 10  ANSS    2420 non-null   float64
 11  CRM     2420 non-null   float64
 12  ADBE    2420 non-null   float64
 13  FB      2420 non-null   float64
 14  AMD     2420 non-null   float64
 15  AMZN    2420 non-null   float64
 16  MA      2420 non-null   float64
 17  VMW     2420 non-null   float64
 18  GOOG    2420 non-null   float64
 19  SNPS    2420 non-null   float64
dtypes: float64(20)
memory usage: 397.0 KB

I’ll use a built-in method in DataFrame that computes the percent change from one row to another

returns = pf_data.pct_change(1).dropna()
returns
TXN CSCO INTC AAPL MSFT NVDA INFY INTU SAP ADI ANSS CRM ADBE FB AMD AMZN MA VMW GOOG SNPS
Date
2012-05-22 -0.004070 0.003599 -0.004589 -0.007679 0.000336 -0.012206 0.013178 0.013394 -0.001701 -0.001952 0.012295 0.002684 0.000000 -0.089039 -0.022222 -0.012746 0.006966 -0.004735 -0.021674 0.000000
2012-05-23 -0.007493 -0.002391 -0.022666 0.024400 -0.021842 0.024712 -0.011189 0.008573 0.001874 -0.003352 0.005992 0.007762 0.005311 0.032258 -0.012987 0.009056 0.008828 0.006244 0.014414 0.005706
2012-05-24 -0.008579 -0.017975 0.008255 -0.009184 -0.001374 -0.026527 0.017916 -0.001417 -0.018707 0.008969 -0.006922 -0.029214 -0.019888 0.032187 -0.009868 -0.009389 0.006569 -0.059988 -0.009517 0.058511
2012-05-25 0.001731 -0.003661 0.003509 -0.005360 -0.000344 0.023947 -0.016674 0.001064 -0.010919 0.005278 0.006484 0.005403 0.001902 -0.033909 0.033223 -0.010918 -0.013981 -0.014775 -0.020094 0.001340
2012-05-29 0.014513 0.015922 0.013598 0.017749 0.017206 0.025806 0.023316 0.003189 0.020151 0.019619 0.015623 -0.008912 0.014240 -0.096208 0.038585 0.008737 0.005048 0.009253 0.004750 0.009368
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2021-12-23 0.002514 0.012189 0.006671 0.003644 0.004472 0.008163 0.008153 0.006412 0.004878 0.002148 0.007363 0.001345 0.010000 0.014495 0.015707 0.000184 0.008672 0.012912 0.001317 0.003618
2021-12-27 0.023693 0.018304 0.012278 0.022975 0.023186 0.044028 0.024262 0.026836 0.008638 0.016275 0.018905 0.020384 0.014150 0.032633 0.056247 -0.008178 -0.000749 -0.005219 0.006263 0.025782
2021-12-28 -0.003857 0.001734 -0.003466 -0.005767 -0.003504 -0.020133 -0.001184 -0.004580 0.001557 -0.006212 -0.003161 -0.011034 -0.014402 0.000116 -0.007839 0.005844 0.001304 0.010320 -0.010914 -0.009159
2021-12-29 -0.001518 0.006768 0.001353 0.000502 0.002051 -0.010586 0.003162 -0.002693 -0.010388 0.006537 -0.006977 -0.003562 -0.000123 -0.009474 -0.031929 -0.008555 0.001414 0.003405 0.000386 0.003466
2021-12-30 -0.007337 -0.005316 -0.001736 -0.006578 -0.007691 -0.013833 0.001182 -0.007206 0.002571 -0.004216 -0.003390 0.003104 0.002178 0.004141 -0.020977 -0.003289 -0.000830 -0.005260 -0.003427 -0.007043

2419 rows × 20 columns

I expected the stocks in this portfolio to have a large number of positive correlations considering they are all part of the same sector in fact no pair is negatively correlated

The top 3 most correlated stocks are: Analog devices and Texas Instruments which have a strong positive correlation with 0.80 both companies are in the business of designing and fabrication of semiconductors and semiconductor devices which is a sub-industry of the overall technology sector,

The other two sets of stocks have a moderate positive correlation which is Ansys Inc, and Synopsys inc with 0.69 and Synopsys inc again but this time with financial software company Intuit inc with 0.66

corr = returns.corr()
fig = px.imshow(corr)
fig.update_layout(width=1000, height=800)
fig.update_layout(template = "plotly_dark", title = 'The Correlation coefficient of the Assets in the Portfolio') 
fig.show()
corr.unstack().sort_values().drop_duplicates()
FB    AMD     0.234715
AMD   INFY    0.235012
FB    INFY    0.236786
INFY  AMZN    0.248299
VMW   AMD     0.264527
                ...   
MSFT  ADBE    0.662720
SNPS  INTU    0.672901
ANSS  SNPS    0.698040
ADI   TXN     0.803991
TXN   TXN     1.000000
Length: 191, dtype: float64

Creating an equal weight (EW) portfolio:

Equal weight is a type of proportional measuring method that gives the same importance to each stock in a portfolio, index, or index fund. So stocks of the smallest companies are given equal statistical significance, or weight, to the largest companies when it comes to evaluating the overall group's performance.

N = len(returns.columns)
equal_weights = N * [1/N] # Shows 1/20, 20 times. Its not multiplication, but repetition! 20*["A"]
equal_weights
[0.05,
 0.05,
 0.05,
 0.05,
 0.05,
 0.05,
 0.05,
 0.05,
 0.05,
 0.05,
 0.05,
 0.05,
 0.05,
 0.05,
 0.05,
 0.05,
 0.05,
 0.05,
 0.05,
 0.05]
portfolio_return = returns.dot(equal_weights)
portfolio_return
Date
2012-05-22   -0.006508
2012-05-23    0.003661
2012-05-24   -0.004807
2012-05-25   -0.002338
2012-05-29    0.008578
                ...   
2021-12-23    0.006743
2021-12-27    0.019035
2021-12-28   -0.004217
2021-12-29   -0.002838
2021-12-30   -0.004248
Length: 2419, dtype: float64
pf_data.index
dates = pf_data.index.to_frame().reset_index(drop=True)

The returns were noticeably volatile in 2018 November as that year a lot was happening like the federal reserve interest hike but the most notable event was a lot of the big tech were under scrutiny at the time and considering this is a tech portfolio the volatility shouldn’t be surprising

Another noticeable moment here is the pandemic in 2020, volatility was extremely high, in fact, On March 16, 2020, the VIX closed at a record high of 82.69 The markets were tumbling and a lot of trades were being made, some were covering short positions while others buying “the dip” and last but not least you have countless algorithims and retail traders day trading and taking advantage of the high volatility

fig = go.Figure()
fig.add_trace(go.Scatter(x=dates['Date'], y=portfolio_return,
                    mode='lines',
                    line=dict(color='firebrick',width=2),
                    name='lines'))
fig.update_layout(template = "plotly_dark")
display(HTML(fig.to_html(include_plotlyjs='cdn')));                    
cum_equal_returns =   (1 + portfolio_return).cumprod() - 1
cum_equal_returns_perc = pd.Series(100 * cum_equal_returns)

The EW has done pretty well returning more than 1000%!

fig = go.Figure([go.Scatter(x=dates['Date'], y=cum_equal_returns_perc)])
fig.update_layout(template = "plotly_dark", title = 'Cummulative % Return') 
fig.show()
display(HTML(fig.to_html(include_plotlyjs='cdn')));

Sharpe Ratio

In the next step i am going to calcluate sharpe ratio, but first we need the annual volatility, annual return and risk free rate

ER = portfolio_return.mean()
STD = portfolio_return.std()
ASTD = STD * 252 ** 0.5
ASTD
0.21152541130551866
AER = ER * 252 
AER
0.2784416783450839
rf = 0.03 #risk free rate is the 10 year trasury bond as of april 2022
excess_return = AER - rf
SR = excess_return/ASTD
SR
1.1745240291070507

A Sharpe ratio of 1.17 is not the best but also considering that tech stocks are very volatile maybe there is a weight combination that would have a higher Sharpe ratio and/or lower volatility or even a higher expected return and that is what I’ll try to uncover in the next section

Modern Portfolio Theory & Monte Carlo simulation:

Monte Carlo simulations are used to model the probability of different outcomes in a process that cannot easily be predicted due to the intervention of random variables. It is a technique used to understand the impact of risk and uncertainty in prediction and forecasting models.

Modern portfolio theory refers to the quantitative practice of asset allocation that maximizes projected (ex ante) return for a portfolio while holding constant its overall exposure to risk. Or, inversely, minimizing overall risk for a given target portfolio return. The theory considers the covariance of constituent assets or asset classes within a portfolio, and the impact of an asset allocation change on the overall expected risk/return profile of the portfolio.
The theory was originally proposed by nobel-winning economist Harry Markowitz in the 1952 Journal of Finance, and is now a cornerstone of portfolio management practice. Modern portfolio theory generally supports a practice of diversifying toward a mix of assets and asset classes with a low degree of mutual correlation.

Hence, I’m going to find the optimal portfolio using Monte Carlo simulations by building thousands of portfolios, using randomly assigned weights, and visualizing the results.

num_assets = len(pf_data.columns)
num_assets
20
log_returns = np.log(pf_data/pf_data.shift(1))
log_returns.head()
TXN CSCO INTC AAPL MSFT NVDA INFY INTU SAP ADI ANSS CRM ADBE FB AMD AMZN MA VMW GOOG SNPS
Date
2012-05-21 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2012-05-22 -0.004079 0.003593 -0.004599 -0.007709 0.000336 -0.012280 0.013093 0.013305 -0.001702 -0.001953 0.012220 0.002680 0.000000 -0.093255 -0.022473 -0.012828 0.006942 -0.004746 -0.021912 0.000000
2012-05-23 -0.007521 -0.002394 -0.022927 0.024107 -0.022083 0.024411 -0.011252 0.008536 0.001873 -0.003358 0.005974 0.007732 0.005297 0.031749 -0.013072 0.009015 0.008790 0.006225 0.014311 0.005690
2012-05-24 -0.008616 -0.018139 0.008221 -0.009227 -0.001375 -0.026885 0.017758 -0.001418 -0.018885 0.008929 -0.006946 -0.029650 -0.020089 0.031680 -0.009917 -0.009433 0.006548 -0.061863 -0.009562 0.056863
2012-05-25 0.001729 -0.003668 0.003502 -0.005374 -0.000344 0.023665 -0.016815 0.001063 -0.010979 0.005264 0.006463 0.005389 0.001901 -0.034497 0.032683 -0.010978 -0.014080 -0.014886 -0.020299 0.001339
cov = log_returns.cov()
cov
TXN CSCO INTC AAPL MSFT NVDA INFY INTU SAP ADI ANSS CRM ADBE FB AMD AMZN MA VMW GOOG SNPS
TXN 0.000283 0.000149 0.000206 0.000162 0.000165 0.000267 0.000120 0.000163 0.000135 0.000244 0.000171 0.000170 0.000175 0.000147 0.000268 0.000141 0.000157 0.000150 0.000140 0.000161
CSCO 0.000149 0.000248 0.000156 0.000132 0.000144 0.000181 0.000104 0.000137 0.000117 0.000147 0.000131 0.000150 0.000143 0.000112 0.000170 0.000115 0.000141 0.000148 0.000117 0.000125
INTC 0.000206 0.000156 0.000349 0.000157 0.000176 0.000243 0.000121 0.000161 0.000134 0.000202 0.000161 0.000155 0.000166 0.000145 0.000229 0.000131 0.000152 0.000150 0.000138 0.000152
AAPL 0.000162 0.000132 0.000157 0.000317 0.000167 0.000223 0.000107 0.000154 0.000123 0.000165 0.000157 0.000161 0.000167 0.000169 0.000218 0.000154 0.000150 0.000128 0.000148 0.000145
MSFT 0.000165 0.000144 0.000176 0.000167 0.000262 0.000233 0.000109 0.000178 0.000135 0.000164 0.000170 0.000189 0.000199 0.000160 0.000212 0.000168 0.000162 0.000147 0.000168 0.000161
NVDA 0.000267 0.000181 0.000243 0.000223 0.000233 0.000664 0.000142 0.000232 0.000170 0.000276 0.000236 0.000260 0.000262 0.000227 0.000473 0.000216 0.000208 0.000203 0.000201 0.000231
INFY 0.000120 0.000104 0.000121 0.000107 0.000109 0.000142 0.000334 0.000124 0.000107 0.000121 0.000116 0.000124 0.000119 0.000100 0.000159 0.000085 0.000124 0.000108 0.000096 0.000107
INTU 0.000163 0.000137 0.000161 0.000154 0.000178 0.000232 0.000124 0.000282 0.000132 0.000166 0.000182 0.000196 0.000202 0.000159 0.000213 0.000146 0.000178 0.000143 0.000149 0.000176
SAP 0.000135 0.000117 0.000134 0.000123 0.000135 0.000170 0.000107 0.000132 0.000257 0.000138 0.000136 0.000152 0.000150 0.000122 0.000181 0.000119 0.000139 0.000134 0.000118 0.000124
ADI 0.000244 0.000147 0.000202 0.000165 0.000164 0.000276 0.000121 0.000166 0.000138 0.000325 0.000178 0.000179 0.000183 0.000151 0.000277 0.000135 0.000167 0.000151 0.000142 0.000168
ANSS 0.000171 0.000131 0.000161 0.000157 0.000170 0.000236 0.000116 0.000182 0.000136 0.000178 0.000286 0.000198 0.000202 0.000150 0.000226 0.000154 0.000166 0.000150 0.000147 0.000184
CRM 0.000170 0.000150 0.000155 0.000161 0.000189 0.000260 0.000124 0.000196 0.000152 0.000179 0.000198 0.000434 0.000248 0.000214 0.000246 0.000192 0.000187 0.000192 0.000169 0.000179
ADBE 0.000175 0.000143 0.000166 0.000167 0.000199 0.000262 0.000119 0.000202 0.000150 0.000183 0.000202 0.000248 0.000340 0.000192 0.000249 0.000190 0.000177 0.000170 0.000171 0.000189
FB 0.000147 0.000112 0.000145 0.000169 0.000160 0.000227 0.000100 0.000159 0.000122 0.000151 0.000150 0.000214 0.000192 0.000521 0.000201 0.000199 0.000161 0.000141 0.000186 0.000149
AMD 0.000268 0.000170 0.000229 0.000218 0.000212 0.000473 0.000159 0.000213 0.000181 0.000277 0.000226 0.000246 0.000249 0.000201 0.001315 0.000217 0.000204 0.000211 0.000167 0.000230
AMZN 0.000141 0.000115 0.000131 0.000154 0.000168 0.000216 0.000085 0.000146 0.000119 0.000135 0.000154 0.000192 0.000190 0.000199 0.000217 0.000350 0.000152 0.000132 0.000178 0.000141
MA 0.000157 0.000141 0.000152 0.000150 0.000162 0.000208 0.000124 0.000178 0.000139 0.000167 0.000166 0.000187 0.000177 0.000161 0.000204 0.000152 0.000276 0.000147 0.000154 0.000154
VMW 0.000150 0.000148 0.000150 0.000128 0.000147 0.000203 0.000108 0.000143 0.000134 0.000151 0.000150 0.000192 0.000170 0.000141 0.000211 0.000132 0.000147 0.000471 0.000127 0.000134
GOOG 0.000140 0.000117 0.000138 0.000148 0.000168 0.000201 0.000096 0.000149 0.000118 0.000142 0.000147 0.000169 0.000171 0.000186 0.000167 0.000178 0.000154 0.000127 0.000250 0.000135
SNPS 0.000161 0.000125 0.000152 0.000145 0.000161 0.000231 0.000107 0.000176 0.000124 0.000168 0.000184 0.000179 0.000189 0.000149 0.000230 0.000141 0.000154 0.000134 0.000135 0.000241
num_ports = 20000 #the number of trials i will run
all_weights = np.zeros((num_ports,num_assets))
ret_arr = np.zeros(num_ports)
vol_arr = np.zeros(num_ports)
sharpe_arr = np.zeros(num_ports)

for ind in range(num_ports):

    #weigths
    weights = np.array(np.random.random(num_assets))
    weights = weights/np.sum(weights)

    #save weigths
    all_weights[ind,:] = weights

    #expected return
    ret_arr[ind] = np.sum((log_returns.mean() * weights) * 250)

    #expected volatility
    vol_arr[ind] = np.sqrt(np.dot(weights.T,np.dot(log_returns.cov()*250,weights)))

    #sharpe ratio
    sharpe_arr[ind] = (ret_arr[ind] - rf)/vol_arr[ind]

After the monte carlo is done it's time to inspect and locate the results and look at the weightings of the portfolios we need a portfolio that might be better than my initial equal weighted portfolio, rememebr returns alone are not the objective but also volatility, we want the highest return for the lowest volatility possible hence the highest sharpe ratio

In the next step i will be creating a data frame that will contain not just the weigthings but also the expected return, volatility and even the sharpe ratio of all the portfolios generated which will help me locate where the optimal or tangency portfolio, the portfolio with minimum volatility and the portfolio with maximum expected return are at in the weightings that were generated

data = pd.DataFrame({'Return': ret_arr, 'Volatility': vol_arr, 'Sharpe Ratio': sharpe_arr})#(ret_arr - rf) /vol_arr})
for counter, symbol in enumerate(pf_data.columns.tolist()):
    data[symbol + 'weight'] = [w[counter]for w in all_weights]

portfolios = pd.DataFrame(data)
portfolios
Return Volatility Sharpe Ratio TXNweight CSCOweight INTCweight AAPLweight MSFTweight NVDAweight INFYweight ... ANSSweight CRMweight ADBEweight FBweight AMDweight AMZNweight MAweight VMWweight GOOGweight SNPSweight
0 0.235060 0.213741 0.959388 0.061536 0.091934 0.040965 0.067888 0.076020 0.054105 0.049063 ... 0.003004 0.092469 0.040359 0.081557 0.032928 0.003992 0.066299 0.014090 0.053930 0.019123
1 0.229757 0.219309 0.910850 0.061311 0.031462 0.050643 0.049502 0.001270 0.077914 0.029312 ... 0.033376 0.040043 0.078451 0.053226 0.073149 0.071961 0.062006 0.077717 0.030547 0.007948
2 0.239854 0.218571 0.960119 0.022931 0.034186 0.000097 0.095811 0.055255 0.098486 0.032107 ... 0.039784 0.038417 0.094715 0.038167 0.052099 0.029348 0.002343 0.069246 0.053960 0.017094
3 0.223821 0.206283 0.939588 0.021978 0.088269 0.028764 0.031951 0.023287 0.044314 0.061802 ... 0.084773 0.025944 0.023086 0.070774 0.019001 0.004489 0.067659 0.011433 0.067834 0.080970
4 0.252654 0.220076 1.011718 0.032829 0.038141 0.031621 0.038156 0.032453 0.071135 0.023667 ... 0.049051 0.001107 0.104740 0.069692 0.071773 0.051470 0.017169 0.034197 0.086193 0.106864
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
19995 0.236976 0.217382 0.952134 0.065015 0.062946 0.031630 0.024278 0.054483 0.060273 0.038499 ... 0.071690 0.029728 0.083362 0.078441 0.073044 0.005775 0.036472 0.059114 0.075507 0.040374
19996 0.211920 0.207550 0.876511 0.059174 0.066972 0.080858 0.028952 0.050625 0.050136 0.074528 ... 0.066560 0.097057 0.004428 0.037434 0.029613 0.009752 0.050356 0.032781 0.063095 0.061517
19997 0.218922 0.215106 0.878273 0.020343 0.000088 0.041725 0.001719 0.004495 0.014127 0.094459 ... 0.033807 0.010043 0.056615 0.023852 0.106904 0.110114 0.094245 0.093785 0.106817 0.022204
19998 0.231003 0.213849 0.939929 0.070349 0.038615 0.025927 0.055969 0.053822 0.084375 0.079402 ... 0.067857 0.047777 0.030630 0.029430 0.042781 0.011784 0.023835 0.074925 0.056096 0.032982
19999 0.214351 0.207301 0.889289 0.038387 0.083181 0.062824 0.052065 0.058076 0.032348 0.044598 ... 0.071942 0.011572 0.003619 0.072106 0.048186 0.023791 0.094538 0.051598 0.099134 0.007197

20000 rows × 23 columns

The tangency portfolio:

The tangency or maximum Sharpe ratio portfolio in the Markowitz procedure possesses the highest potential return-for-risk tradeoff.

optimal_risky_portfolio = portfolios.iloc[portfolios['Sharpe Ratio'].idxmax()]
optimal_risky_portfolio
Return          0.263745
Volatility      0.215477
Sharpe Ratio    1.084784
TXNweight       0.041522
CSCOweight      0.060248
INTCweight      0.001923
AAPLweight      0.089765
MSFTweight      0.099979
NVDAweight      0.083038
INFYweight      0.066468
INTUweight      0.103850
SAPweight       0.026235
ADIweight       0.014330
ANSSweight      0.024099
CRMweight       0.008061
ADBEweight      0.076826
FBweight        0.000352
AMDweight       0.056392
AMZNweight      0.091473
MAweight        0.010688
VMWweight       0.009695
GOOGweight      0.061586
SNPSweight      0.073470
Name: 13834, dtype: float64

Minim vol is also known as the minimum variance portfolio:

The minimum variance portfolio (mvp) is the portfolio that provides the lowest variance (standard deviation) among all possible portfolios of risky assets.

min_vol_port = portfolios.iloc[portfolios['Volatility'].idxmin()]
min_vol_port
Return          0.202356
Volatility      0.197514
Sharpe Ratio    0.872627
TXNweight       0.042340
CSCOweight      0.105379
INTCweight      0.077549
AAPLweight      0.026698
MSFTweight      0.002516
NVDAweight      0.014645
INFYweight      0.102012
INTUweight      0.034337
SAPweight       0.058483
ADIweight       0.076789
ANSSweight      0.010081
CRMweight       0.010085
ADBEweight      0.006261
FBweight        0.039513
AMDweight       0.017248
AMZNweight      0.033863
MAweight        0.028866
VMWweight       0.053479
GOOGweight      0.132173
SNPSweight      0.127684
Name: 5947, dtype: float64

Max return portfolio: The portfolio with the highest return regardless of risk

max_er_port = portfolios.iloc[portfolios['Return'].idxmax()]
max_er_port
Return          0.267209
Volatility      0.228768
Sharpe Ratio    1.036900
TXNweight       0.086701
CSCOweight      0.024911
INTCweight      0.005748
AAPLweight      0.085139
MSFTweight      0.021823
NVDAweight      0.135536
INFYweight      0.039539
INTUweight      0.023250
SAPweight       0.006508
ADIweight       0.000708
ANSSweight      0.064148
CRMweight       0.091271
ADBEweight      0.014365
FBweight        0.006770
AMDweight       0.076369
AMZNweight      0.109782
MAweight        0.038671
VMWweight       0.031825
GOOGweight      0.061691
SNPSweight      0.075245
Name: 6471, dtype: float64

With a scatter plot I’ll be able to visually see the portfolios and where they lay on the frontier, but remember the correlation of the stocks, there was virtually no negative correlation and modern portfolio theory is about diversifying with UNCORELATED assets, hence I do not expect the plot to form the usual bullet like shape, but i will still be able to see the optimal portfolios across the edges of the fronteir

plt.figure(figsize=(20,10))
plt.scatter(portfolios['Volatility'],portfolios['Return'],c=sharpe_arr,cmap='RdBu')#ret_arr,vol_arr
plt.colorbar(label='Sharpe Ratio')
plt.xlabel('Risk (Volatility)')
plt.ylabel('Expected Returns')

plt.scatter(optimal_risky_portfolio[1],optimal_risky_portfolio[0], c='green', s=80)
plt.scatter(min_vol_port[1],min_vol_port[0], c='purple', s=80)#
plt.scatter(max_er_port[1],max_er_port[0], c='yellow', s=80)
plt.style.use('dark_background')
display(HTML(fig.to_html(include_plotlyjs='cdn')));
C:\Users\one\AppData\Roaming\Python\Python37\site-packages\ipykernel_launcher.py:4: MatplotlibDeprecationWarning:

Auto-removal of grids by pcolor() and pcolormesh() is deprecated since 3.5 and will be removed two minor releases later; please call grid(False) first.

Now that we know the weights of each portfolio, I'll Assign the weights to the stocks and check the cumulative returns of each of the portfolios

But, NOTE: You’ve might have noticed from the observations produced by the simulation, the tangency portfolio has a lower sharp than my initial equal weight portfolio which now means based on ALL the observations I have, the EW portfolio is my tangency/Optimal portfolio and i will treat it as my optimal portfolio going forward

min_vol_weights = all_weights[5947,:]
min_vol_weights
array([0.04234009, 0.10537944, 0.07754858, 0.02669783, 0.00251591,
       0.0146454 , 0.10201163, 0.03433662, 0.0584827 , 0.07678887,
       0.0100807 , 0.0100849 , 0.00626119, 0.03951333, 0.01724793,
       0.03386276, 0.02886554, 0.05347914, 0.13217338, 0.12768405])
min_vol_port_return = returns.dot(min_vol_weights)
cum_minvol_returns =   (1 + min_vol_port_return).cumprod() - 1
cum_minvol_returns_perc = pd.Series(100 * cum_minvol_returns)

#Plot
fig = go.Figure([go.Scatter(x=dates['Date'], y=cum_minvol_returns_perc)])
fig.update_layout(template = "plotly_dark", title = 'Cummulative % Return of the minimum variance portfolio') 
fig.show()
display(HTML(fig.to_html(include_plotlyjs='cdn')));
max_er_weights = all_weights[6471,:]
max_er_weights
array([0.08670101, 0.02491136, 0.00574761, 0.08513945, 0.02182338,
       0.13553581, 0.03953875, 0.02325034, 0.00650826, 0.00070845,
       0.06414761, 0.09127111, 0.01436485, 0.00676964, 0.07636874,
       0.1097819 , 0.03867063, 0.03182504, 0.06169064, 0.07524542])
max_er_port_return = returns.dot(max_er_weights)
cum_maxer_returns =   (1 + max_er_port_return).cumprod() - 1
cum_maxer_returns_perc = pd.Series(100 * cum_maxer_returns)

#Plot
fig = go.Figure([go.Scatter(x=dates['Date'], y=cum_maxer_returns_perc)])
fig.update_layout(template = "plotly_dark", title = 'Cummulative % Return of the maximum expected return portfolio') 
fig.show()
display(HTML(fig.to_html(include_plotlyjs='cdn')));

Portfolio vs benchmarks:

It’s time to compare the all the portfolios against certain benchmarks which are going to be the Invesco QQQ fund which is a technology ETF, I chose this particular tech ETF as a benchmark because it has the highest NAV of 135 billion as of April 2022

I'll also include the NASDAQ composite as a benchmark as it is widely followed and considered as a benchmark by investors, the Nasdaq composite is even more relevant here because more than 50% of the stocks in the index are technology companies

QQQ = pd.DataFrame(yf.download('QQQ', start="2012-05-20", end="2021-12-31", index_col = 'Date', parse_dates=True)['Adj Close']) 
NASDAQ = pd.DataFrame(yf.download('^IXIC', start="2012-05-20", end="2021-12-31", index_col = 'Date', parse_dates=True)['Adj Close']) 
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
for funds in (QQQ,NASDAQ):
    funds['Daily Return'] = funds.pct_change(1).dropna()
    funds['Cumulative Return'] = (1 + funds['Daily Return']).cumprod() - 1
    funds['Cumulative % Return'] = funds['Cumulative Return'] * 100

#creating a data frame that has all the portfolios and benchmarks cummulative % return
data = {'QQQ':QQQ['Cumulative % Return'],
       'NASDAQ':NASDAQ['Cumulative % Return'],
       'Optimal Port':cum_equal_returns_perc,
       'Min Variance':cum_minvol_returns_perc,
       'Max ER Port':cum_maxer_returns_perc}
funds_cumm = pd.DataFrame(data)
funds_cumm.reset_index(drop=True, inplace=True)
funds_cumm.insert(loc=0, column="Dates", value=dates)
funds_cumm.tail()
Dates QQQ NASDAQ Optimal Port Min Variance Max ER Port
2415 2021-12-23 598.161713 449.779269 1057.179336 765.628694 1652.657742
2416 2021-12-27 609.700431 457.432012 1079.205886 780.031449 1690.668661
2417 2021-12-28 606.411117 454.287177 1074.233314 776.898982 1679.724874
2418 2021-12-29 606.305615 453.742785 1070.900768 777.018843 1670.698787
2419 2021-12-30 604.194880 452.876668 1065.927132 773.809387 1659.601642

Both benchmarks performed poorly compared to the portfolios but this is largely due to concentration, as the portfolios have only 20 stocks while the benchmarks usually have hundreds of stocks in them, but the portfolio's strongest point is also its weakest as a concentrated portfolio will probably have a much larger drawdown even during corrections let alone recessions

The max expected return portfolio outperformed all the other portfolios and the benchmarks as expected

While my initial EW portfolio now turned into my tangency optimal portfolio faired well by beating both the benchmarks by a mile!

fig = px.line(funds_cumm, x="Dates", y=funds_cumm.columns,
              hover_data={"Dates": "|%B %d, %Y"},
              title='Commulative % Return')
fig.update_xaxes(
    rangeslider_visible=True,
    rangeselector=dict(
        buttons=list([
            dict(count=1, label="1m", step="month", stepmode="backward"),
            dict(count=6, label="6m", step="month", stepmode="backward"),
            dict(count=1, label="YTD", step="year", stepmode="todate"),
            dict(count=1, label="1y", step="year", stepmode="backward"),
            dict(step="all")
        ])
    )
)
fig.update_layout(template = "plotly_dark", title = '10 years Cummulative % Return of all tech portfolios and benchmarks')
fig.show()
display(HTML(fig.to_html(include_plotlyjs='cdn')));

Conclusion:

Assuming I started with a $5,000 in 2012 and invested in the ideal two best portfolios Max return portfolio and Optimal portfolio, by comparison, how much would my investment be by the end of 2021? (without rebalancing)

The max expected return portfolio seems to have a better outcome and seems very attractive especially considering the Sharpe ratio difference isn't that big but this portfolio was picked because of its high expected return unfortunately, it's not practical due to the presence of large estimation errors in those expected return estimate. As I have estimated them using historical data and have assumed past performance will be the same in the future which is unlikely as businesses change ESPECIALLY in the ever-changing technology industry

Hence the more reliable portfolio based on this research would either be the min vol or EW/Tangency portfolio

Initial_Investment = 5000


#Minimum Variance
Min_ASRr = str(round(portfolios['Sharpe Ratio'][5947],2))
Min_AERr = str(round(portfolios['Return'][5947]* 100,2)) + '%' 
Min_ASTDr = str(round(portfolios['Volatility'].min()*100,2)) + '%'
cumm = str(round(cum_minvol_returns_perc[2418],2)) + '%'
EW_Value = Initial_Investment * (cum_minvol_returns_perc[2418]/100)
Absolute_Value = '$' + str(round(EW_Value,2))
print('THE MINIMUM VARIANCE PORTFOLIO:')
print(f'The annual sharpe ratio of the minimum variance portfolio is {Min_ASRr}')
print(f'The annual Volatility of the minimum variance portfolio is {Min_ASTDr}')
print(f'The annual Expected Return of the minimum variance portfolio is {Min_AERr}')
print(f'The 10 yr cummulative return of the minimum variance portfolio is {cumm}')
print(f'A ${Initial_Investment} investment in minimum variance portfolio in 2012 would be worth {Absolute_Value} by the end of 2021')

#Tangency/Optimal Portfolio
ASRr = str(round(SR,2))
AERr = str(round(AER* 100,2)) + '%' 
ASTDr = str(round(ASTD*100,2)) + '%'
cumm3 = str(round(cum_equal_returns_perc[2418],2)) + '%'
EW_Value3 = Initial_Investment * (cum_equal_returns_perc[2418]/100)
Absolute_Value3 = '$' + str(round(EW_Value3,2))
print('\nTHE OPTIMAL PORTFOLIO:')
print(f'The annual sharpe ratio of the optimal portfolio is {ASRr}')
print(f'The annual Volatility of the optimal portfolio is {ASTDr}')
print(f'The annual Expected Return of the optimal portfolio is {AERr}')
print(f'The 10 yr cummulative return of the optimal portfolio is {cumm3}')
print(f'A ${Initial_Investment} investment in the optimal portfolio in 2012 would be worth {Absolute_Value3} by the end of 2021')
THE MINIMUM VARIANCE PORTFOLIO:
The annual sharpe ratio of the minimum variance portfolio is 0.87
The annual Volatility of the minimum variance portfolio is 19.75%
The annual Expected Return of the minimum variance portfolio is 20.24%
The 10 yr cummulative return of the minimum variance portfolio is 773.81%
A $5000 investment in minimum variance portfolio in 2012 would be worth $38690.47 by the end of 2021

THE OPTIMAL PORTFOLIO:
The annual sharpe ratio of the optimal portfolio is 1.17
The annual Volatility of the optimal portfolio is 21.15%
The annual Expected Return of the optimal portfolio is 27.84%
The 10 yr cummulative return of the optimal portfolio is 1065.93%
A $5000 investment in the optimal portfolio in 2012 would be worth $53296.36 by the end of 2021