Portfolio Analysis, Efficient Frontier & Monte Carlo
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')
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']
pf_data.head()
pf_data.info()
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
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()
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
portfolio_return = returns.dot(equal_weights)
portfolio_return
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')));
ER = portfolio_return.mean()
STD = portfolio_return.std()
ASTD = STD * 252 ** 0.5
ASTD
AER = ER * 252
AER
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
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
log_returns = np.log(pf_data/pf_data.shift(1))
log_returns.head()
cov = log_returns.cov()
cov
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
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
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
Max return portfolio: The portfolio with the highest return regardless of risk
max_er_port = portfolios.iloc[portfolios['Return'].idxmax()]
max_er_port
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')));
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
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
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'])
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()
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')