Portfolio optimization: from the highest Sharpe Ratio to minimum volatility
Jakub Polec
20+ yrs in Tech & Finance & Quant | ex-Microsoft/Oracle/CERN | IT / Cloud Architecture Leader | AI/ML Data Scientist | SaaS & Fintech
In this article, you will learn about the following:
Read the full article at https://quantjourney.substack.com/
Let's assume that we want to create an optimal portfolio for the top growth companies that outperformed in 2023, also known as the "Magnificent 7". First, we will obtain the historical adjusted closing prices of these companies from Yahoo Finance.
start = '2021-01-01'
end = '2024-01-24'
tickers = ['META', 'TSLA','AAPL', 'MSFT', 'AMZN', 'GOOGL', 'NVDA']
data = yf.download(tickers, start=start, end=end)['Adj Close'][tickers]
Stock performance
To analyse the stock performance, we calculate daily log returns and plot them over time to examine our data.
daily_log_returns = np.log(data) - np.log(data.shift(1))
daily_log_returns.plot(figsize=(13, 8))
Correlation heatmap
Now, let's perform a correlation analysis of log returns and plot a heatmap using the seaborn library.
corr = daily_log_returns.corr()
plt.figure(figsize=(10, 10))
sns.heatmap(corr, annot=True, cmap="viridis")
plt.title('Log Returns Correlation Heatmap')
plt.show()
In the correlation heatmap, the colors represent the strength of the correlation between the returns of each pair of stocks. A value of 1 indicates a perfect positive correlation, while numbers approaching 0 indicate little to no correlation. The heatmap allows us to visualize how similarly the stocks move in relation to each other.
Visualise returns distribution of the Portfolio
Moving forward, we will assess the performance of our portfolio by analyzing key metrics over time. These metrics include expected return, volatility, skewness, and the Sharpe ratio. To achieve diversification, we have created an equally weighted portfolio, where each stock is allocated the same proportion of the total investment.
stocks = len(tickers)
weights = np.full((stocks,), 1 / stocks)
daily_portfolio_returns = daily_log_returns.dot(weights)
sns.displot(daily_portfolio_returns, bins=50)
Let's plot daily returns using the sns library.
Returns Distribution Statistics
expected_return = daily_portfolio_returns.mean() * 252
vol = daily_portfolio_returns.std() * np.sqrt(252)
skew = daily_portfolio_returns.skew()
sr = expected_return / vol
print(f"{expected_return=}")
print(f"{vol=}")
print(f"{skew=}")
print(f"{sr=}")
which gives the output:
expected_return=0.154953778394305
vol=0.3183505654118577
skew=-0.14207782986445805
领英推荐
sr=0.4867394477337824
Simulate 10.000 portfolios with Monte Carlo
In 1952, Harry Markowitz introduced this concept, which fundamentally changed how investors approach portfolio construction. It is a method for determining the optimal asset allocation in a portfolio to maximise returns for a given level of risk.
By combining assets with different expected returns and volatilities, investors can create a portfolio that maximises returns while minimizing risk. This involves solving a constrained optimisation problem, where the goal is to minimize the portfolio's variance. Mathematically, this is expressed as minimising wT*Σ, where w is the vector of portfolio weights, and Σ is the covariance matrix of the asset returns.
Efficient Frontier
When the target return (μ) is adjusted, the optimal weights (w) also undergo changes, leading to the formation of different portfolios. As a result, the Efficient Frontier is created, which is a curve on a risk-return graph. This curve represents the collection of portfolios that offer the lowest risk for a given level of expected return, and vice versa.
To further explore portfolio optimisation, we conduct a Monte Carlo simulation with 10,000 iterations and plot the results to visualise the Efficient Frontier approach. This method entails randomly generating portfolio weights and assessing their performance in terms of returns, volatility, and the Sharpe ratio. The objective of this exercise is to identify the most efficient allocation that offers the highest return per unit of risk.
returns = daily_log_returns
mean_returns = returns.mean() * 252
cov_matrix = returns.cov() * np.sqrt(252)
# Assuming 'tickers', 'mean_returns', and 'cov_matrix' are already defined
num_portfolios = 10000
# DataFrame to store the results
results = pd.DataFrame(columns=['returns', 'volatility', 'sharpe', 'weights'], index=range(num_portfolios))
# store all weights
weights_record = []
for i in range(num_portfolios):
# Normalised randomly generated weights
weights = np.random.random(len(tickers))
weights /= np.sum(weights)
# Calculate returns and volatility
returns = np.dot(weights, mean_returns)
volatility = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
# Store results
results.loc[i, 'returns'] = returns
results.loc[i, 'volatility'] = volatility
results.loc[i, 'sharpe'] = results.loc[i, 'returns'] / results.loc[i, 'volatility']
results.loc[i, 'weights'] = ','.join(str(np.round(weight, 4)) for weight in weights)
# record weights
weights_record.append(weights)
print(results.sort_values('sharpe', ascending=False).head(10))
The results are as follows:
The portfolio with the highest Sharpe ratio of 3.42 is built with the following weights:
META (1.1%),
TSLA (1.0%),
AAPL (14%),
MSFT (36%),
AMZN (5.2%),
GOOGL (7.2%),
NVDA (34%).
Finding maximum Sharpe ratio and minimum volatility portfolio
From the efficient frontier, we can select multiple portfolios. As you saw, we can choose a portfolio that maximises the Sharpe ratio. Additionally, we can select the portfolio with the maximum return for a given target risk.
With the code below, we can obtain the maximum Sharpe portfolio and the minimum volatility portfolio. The maximum Sharpe portfolio is expected to offer the highest return per unit of risk, while the minimum volatility portfolio is, in fact, the most optimal portfolio with the lowest amount of risk. It can be considered a special case, as it serves as the "lowest risk benchmark" for all other portfolios on the efficient frontier. Over time, it has shown surprisingly good performance, leading some managers to choose it as their preferred portfolio.
# Efficient Frontier with max sharpe portfolio and min volatility portfolio
max_sharpe_portfolio = results.iloc[pd.to_numeric(results['sharpe']).idxmax()]
min_volatility_portfolio = results.iloc[pd.to_numeric(results['volatility']).idxmin()]
Conclusion:
With just a few lines of Python code, you can create a diverse portfolio and evaluate its performance. By adjusting and analyzing strategies that have worked well in the past, you can make informed decisions for future investments.
In addition, we will discuss factor-based portfolio optimisation in future posts. This approach considers factors like company size, value, and market risk to potentially improve returns and manage risk effectively.
Read the full article at https://quantjourney.substack.com/
Co-Founder & CEO of Hoick
9 个月Jakub, your explanations are super simple and practical. Love it! Sanjay Nadkarni, Christof Elsener