Building a Portfolio Analyser in Python
A single stock is a bet. A portfolio is a strategy. This guide shows you how to analyse a portfolio of stocks β calculate weighted returns, measure risk, and find the optimal mix using Python.
Download Multiple Stocks
import yfinance as yf import pandas as pd import numpy as np import matplotlib.pyplot as plt # Our portfolio: 4 stocks tickers = ["AAPL", "MSFT", "JNJ", "JPM"] data = yf.download(tickers, start="2022-01-01", end="2024-01-01")["Close"] # Daily returns returns = data.pct_change().dropna() print(returns.describe())Weighted Portfolio Returns
In a real portfolio, you don't own equal amounts of everything. Weights represent what percentage of your money is in each stock.
# Portfolio weights (must sum to 1.0) weights = np.array([0.30, 0.30, 0.20, 0.20]) # 30% AAPL, 30% MSFT, 20% JNJ, 20% JPM # Daily portfolio return = sum of (weight * stock return) portfolio_daily = returns.dot(weights) # Cumulative portfolio value starting at $10,000 initial = 10000 portfolio_value = initial * (1 + portfolio_daily).cumprod() plt.figure(figsize=(12, 5)) plt.plot(portfolio_value.index, portfolio_value, color="mediumseagreen", linewidth=1.5) plt.title("Portfolio Value Over Time") plt.xlabel("Date") plt.ylabel("Value ($)") plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() print(f"Final value: ${{portfolio_value.iloc[-1]:,.2f}}") print(f"Total return: {{(portfolio_value.iloc[-1] / initial - 1):.2%}}")Correlation Matrix
Correlation tells you which stocks move together. If all your stocks are highly correlated, you're not actually diversified β they'll all drop at the same time.
# Correlation matrix corr = returns.corr() print(corr.round(2)) # A correlation of 1.0 = move in perfect lockstep # A correlation of 0.0 = no relationship # A correlation of -1.0 = move in opposite directions # AAPL and MSFT will be highly correlated (both big tech) # JNJ (healthcare) should be less correlated with techDiversification = Low Correlation
The whole point of diversification is owning assets that do not all move together. If stock A drops 5% but stock B goes up 3%, the portfolio cushions the blow. Correlation tells you how much cushion you actually have.
Portfolio Risk (Variance & Standard Deviation)
# Annualised portfolio return annual_return = returns.mean().dot(weights) * 252 # 252 trading days # Portfolio variance (accounts for correlations between stocks) cov_matrix = returns.cov() * 252 # annualised covariance matrix portfolio_variance = weights.T.dot(cov_matrix).dot(weights) portfolio_std = np.sqrt(portfolio_variance) print(f"Expected annual return: {{annual_return:.2%}}") print(f"Annual volatility (risk): {{portfolio_std:.2%}}") print(f"Sharpe ratio: {{(annual_return - 0.05) / portfolio_std:.2f}}") # Risk-free rate assumed at 5% (typical for current environment)Sharpe Ratio
Sharpe Ratio = Return Per Unit of Risk
Sharpe = (Portfolio Return - Risk Free Rate) / Portfolio Volatility. A Sharpe above 1.0 is good. Above 2.0 is excellent. It answers: am I being compensated enough for the risk I am taking?
Simple Efficient Frontier
The efficient frontier shows every possible portfolio you could build from these stocks, plotted by risk vs return. The βfrontierβ is the top edge β the best return you can get for each level of risk.
# Generate 10,000 random portfolios num_portfolios = 10000 results = np.zeros((num_portfolios, 3)) # return, risk, sharpe for i in range(num_portfolios): # Random weights that sum to 1 w = np.random.random(len(tickers)) w /= w.sum() # Portfolio return and risk ret = returns.mean().dot(w) * 252 risk = np.sqrt(w.T.dot(cov_matrix).dot(w)) sharpe = (ret - 0.05) / risk results[i] = [ret, risk, sharpe] # Plot the efficient frontier plt.figure(figsize=(10, 6)) scatter = plt.scatter( results[:, 1], results[:, 0], c=results[:, 2], cmap="RdYlGn", s=3, alpha=0.5 ) plt.colorbar(scatter, label="Sharpe Ratio") plt.title("Efficient Frontier (10,000 Random Portfolios)") plt.xlabel("Risk (Volatility)") plt.ylabel("Expected Return") plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() # Find the portfolio with the best Sharpe ratio best_idx = results[:, 2].argmax() print(f"Best Sharpe: {{results[best_idx, 2]:.2f}}") print(f"Return: {{results[best_idx, 0]:.2%}}, Risk: {{results[best_idx, 1]:.2%}}")