Building and Analyzing a Portfolio with Python
Investing requires careful analysis of stocks and their performance metrics. In this article, we’ll explore how to use Python to analyze a portfolio of stocks from various sectors, calculate key metrics like Sharpe Ratio, and visualize the results.
Code Walkthrough
- Fetching Stock Data: We start by importing stock data using the
yfinance
library. The selected stocks (AAPL
,MSFT
,XOM
,JPM
,GOOGL
) represent technology, energy, and financial sectors.
# List of stock tickers from different sectors
tickers = ['AAPL', 'MSFT', 'XOM', 'JPM', 'GOOG'] # Tech, energy, finance
# Download data for all stocks
data = {}
for ticker in tickers:
try:
data[ticker] = yf.download(ticker, start="2020-01-01", end="2023-01-01")
except Exception as e:
print(f"Error downloading data for {ticker}: {e}")
data[ticker] = pd.DataFrame() # Assign an empty DataFrame if download fails
# Combine 'Close' prices into a single DataFrame
adj_close = pd.DataFrame()
for ticker in tickers:
if not data[ticker].empty and 'Close' in data[ticker].columns:
adj_close[ticker] = data[ticker]['Close']
else:
print(f"Warning: 'Close' data missing for {ticker}")
adj_close.head()
We ensure the data is consistent, handling errors gracefully by assigning empty data frames for failed downloads.
2. Preparing the Data
We extracted the closing prices for each stock in the previous step, and now we will calculate daily returns by dropping first row of daily returns due to missing data.
# Calculate daily returns for all stocks
returns = adj_close.pct_change().dropna()
# Preview Returns
print("\nDaily Returns:")
print(returns.head())
3. Portfolio Analysis
We assign weights to each stock and calculate portfolio returns, variance, and Sharpe Ratio. The Sharpe Ratio helps evaluate the risk-adjusted return of the portfolio.
weights = [0.2, 0.3, 0.1, 0.2, 0.2]
port_returns = (returns * weights).sum(axis=1)
port_variance= port_returns.var()
port_std_dev = port_returns.std()
risk_free_rate = 0.02
sharpe_ratio = (port_returns.mean() - risk_free_rate) / port_std_dev
print(f"Portfolio Variance: {port_variance:.2f}")
print(f"Portfolio Stadard Deviation: {port_std_dev:.2f}")
print(f"Sharpe Ratio: {sharpe_ratio:.2f}")
4. Correlation Analysis
A heatmap is used to display the correlations between stock returns, revealing diversification opportunities.
import seaborn as sns
import matplotlib.pyplot as plt
corr = returns.corr()
plt.figure(figsize = (15, 5))
sns.heatmap(corr, annot = True, linewidth = .5)
plt.title("Correlation Analysis")
plt.show()
5. Risk Analysis
We calculate and visualize rolling volatility and maximum drawdown.
- Rolling Volatility: Tracks changes in risk over time using a 30-day rolling window.
- Maximum Drawdown: Quantifies the portfolio’s worst decline from a previous peak.
# Rolling standard deviation (volatility)
rolling_volatility = returns.rolling(window=30).std()
# Plot rolling volatility
plt.figure(figsize=(10, 6))
plt.plot(rolling_volatility, color='orange', label='30-Day Rolling Volatility')
plt.title('Portfolio Rolling Volatility')
plt.xlabel('Date')
plt.ylabel('Volatility')
plt.legend()
plt.show()
6. Maximum Drawdown
Drawdown measures the peak-to-trough decline during a specific period. Result will be maximum drawdown quantifies the worst performance decline.
# Calculate cumulative returns
cumulative_returns = (1 + returns).cumprod()
# Calculate running maximum
running_max = cumulative_returns.cummax()
# Calculate drawdown
drawdown = (cumulative_returns - running_max) / running_max
# Extract the minimum drawdown value as a scalar
max_drawdown = drawdown.min()
# If max_drawdown is still not scalar, explicitly extract it
if isinstance(max_drawdown, pd.Series):
max_drawdown = max_drawdown.iloc[0] # Get the first (or desired) value
# Print the formatted result
print(f"Maximum Drawdown: {max_drawdown:.4f}")
7. Monte Carlo Simulation
We simulate 10,000 random portfolios to find the efficient frontier. Portfolios on this curve maximize returns for a given risk level.
import numpy as np
# Monte Carlo simulation for random portfolios
num_portfolios = 10000
results = np.zeros((3, num_portfolios))
for i in range(num_portfolios):
# Random weights
weights = np.random.random(len(tickers))
weights /= np.sum(weights)
# Portfolio returns and risk
port_return = np.sum(weights * returns.mean())
port_std = np.sqrt(np.dot(weights.T, np.dot(returns.cov(), weights)))
# Save results
results[0, i] = port_return
results[1, i] = port_std
results[2, i] = (port_return - risk_free_rate) / port_std # Sharpe Ratio
# Plot efficient frontier
plt.scatter(results[1, :], results[0, :], c=results[2, :], cmap='viridis')
plt.colorbar(label='Sharpe Ratio')
plt.xlabel('Risk (Standard Deviation)')
plt.ylabel('Return')
plt.title('Efficient Frontier')
plt.show()
Conclusion
By combining historical analysis, correlation insights, and Monte Carlo simulations, we gain a comprehensive view of portfolio performance. This process enables investors to optimize their risk-return trade-off.
Good Luck!