Portfolio Analysis in Python

· 6 min read · Updated March 7, 2026 · intermediate
portfolio finance investment analysis sharpe-ratio

In this tutorial, you’ll learn how to analyze a portfolio of multiple stocks. You’ll calculate weighted returns, measure portfolio volatility, compute the Sharpe ratio as a risk-adjusted performance metric, and visualize your results. These skills are essential for building and evaluating investment strategies.

This tutorial builds on the previous ones in this series. We assume you know how to fetch stock data with yfinance and calculate returns. If you need a refresher, start with the earlier tutorials on fetching stock data and calculating returns.

Building a Portfolio

A portfolio is a collection of assets held together. Each asset has a weight representing what fraction of total capital it represents. If you have $10,000 and put $5,000 in Apple and $5,000 in Google, each stock has a 50% weight.

import yfinance as yf
import pandas as pd
import numpy as np

# Define our portfolio: stocks and their weights
portfolio = {
    "AAPL": 0.30,    # 30% Apple
    "MSFT": 0.25,    # 25% Microsoft  
    "GOOGL": 0.20,   # 20% Google
    "AMZN": 0.15,    # 15% Amazon
    "JPM": 0.10      # 10% JPMorgan
}

# Download historical data for all stocks
tickers = list(portfolio.keys())
data = yf.download(tickers, start="2024-01-01", end="2024-12-31")["Close"]

print(f"Downloaded {len(data)} trading days for {len(tickers)} stocks")
print(data.head())

The weights should sum to 1.0 (or 100%). Python won’t enforce this, but your portfolio won’t behave as expected if they don’t.

Calculating Portfolio Returns

Portfolio return is the weighted average of individual asset returns. You multiply each stock’s return by its weight, then sum the results.

# Calculate daily returns for each stock
returns = data.pct_change().dropna()

# Method 1: Calculate portfolio returns using dot product
weights = np.array(list(portfolio.values()))
portfolio_returns = returns.dot(weights)

print("Portfolio Daily Returns:")
print(portfolio_returns.head(10))

The dot() method multiplies each column by its corresponding weight and sums the results. This gives you the daily return of the entire portfolio.

You can verify this manually:

# Method 2: Manual calculation (same result)
weighted_returns = (
    returns["AAPL"] * portfolio["AAPL"] +
    returns["MSFT"] * portfolio["MSFT"] +
    returns["GOOGL"] * portfolio["GOOGL"] +
    returns["AMZN"] * portfolio["AMZN"] +
    returns["JPM"] * portfolio["JPM"]
)

print("Methods match:", np.allclose(portfolio_returns, weighted_returns))

Both methods produce identical results. The dot product is cleaner and scales better if you add more stocks.

Portfolio Performance Metrics

Now let’s calculate the key metrics for evaluating portfolio performance.

Total Return

The total return tells you how much your portfolio gained or lost over the period.

# Calculate total return
total_return = (1 + portfolio_returns).prod() - 1
print(f"Total Return: {total_return:.2%}")

# Calculate annualized return
trading_days = len(portfolio_returns)
years = trading_days / 252
annualized_return = (1 + total_return) ** (1 / years) - 1
print(f"Annualized Return: {annualized_return:.2%}")

For a portfolio that gained 20% over the year, the annualized return is also about 20%. Over longer periods, the compounding effect makes annualized returns more informative.

Portfolio Volatility

Volatility measures risk. A higher volatility means wider price swings and more uncertainty about returns.

# Calculate daily and annualized volatility
daily_vol = portfolio_returns.std()
annualized_vol = daily_vol * np.sqrt(252)

print(f"Daily Volatility: {daily_vol:.4f}")
print(f"Annualized Volatility: {annualized_vol:.2%}")

This portfolio’s volatility of around 18-22% is typical for a diversified US stock portfolio. Pure equity portfolios often have volatility between 15% and 30%.

Sharpe Ratio

The Sharpe ratio measures risk-adjusted returns. It tells you how much return you get per unit of risk. Higher is better.

# Calculate Sharpe ratio
risk_free_rate = 0.05  # Assume 5% risk-free rate
excess_returns = portfolio_returns - risk_free_rate / 252
sharpe_ratio = np.sqrt(252) * excess_returns.mean() / portfolio_returns.std()

print(f"Sharpe Ratio: {sharpe_ratio:.2f}")

A Sharpe ratio above 1.0 is generally considered good. Above 2.0 is excellent. Our example portfolio’s Sharpe ratio depends on the actual returns during the period, but a well-diversified portfolio typically ranges from 0.5 to 1.5.

The Sharpe ratio answers: “Am I being compensated well for the risk I’m taking?” If you have high volatility but low returns, your Sharpe ratio will be poor.

Correlation and Diversification

Correlation measures how two assets move together. Assets with low or negative correlation provide diversification benefits — when one drops, the other might rise or stay stable.

# Calculate correlation matrix
correlation_matrix = returns.corr()

print("Correlation Matrix:")
print(correlation_matrix.round(2))

Lower correlations between assets reduce portfolio volatility. For example, if tech stocks and bonds have low correlation, adding bonds to a tech-heavy portfolio reduces overall risk.

# Visualize correlation as a heatmap
import matplotlib.pyplot as plt

plt.figure(figsize=(8, 6))
plt.imshow(correlation_matrix, cmap='RdYlGn', vmin=-1, vmax=1)
plt.colorbar(label='Correlation')
plt.xticks(range(len(tickers)), tickers)
plt.yticks(range(len(tickers)), tickers)
plt.title('Stock Correlation Matrix')

# Add correlation values to cells
for i in range(len(tickers)):
    for j in range(len(tickers)):
        plt.text(j, i, f'{correlation_matrix.iloc[i, j]:.2f}', 
                ha='center', va='center', fontsize=10)

plt.tight_layout()
plt.savefig('correlation_matrix.png', dpi=100)
plt.show()

This visualization makes it easy to spot highly correlated (green) versus negatively correlated (red) pairs.

Cumulative Returns Visualization

Visualizing cumulative returns helps you understand how your portfolio performed over time relative to individual assets.

# Calculate cumulative returns
cumulative_returns = (1 + returns).cumprod() - 1
portfolio_cumulative = (1 + portfolio_returns).cumprod() - 1

# Plot
plt.figure(figsize=(12, 6))

# Plot individual stocks
for ticker in tickers:
    plt.plot(cumulative_returns[ticker] * 100, label=ticker, alpha=0.7)

# Plot portfolio
plt.plot(portfolio_cumulative * 100, label='Portfolio', 
         linewidth=3, color='black')

plt.xlabel('Date')
plt.ylabel('Cumulative Return (%)')
plt.title('Portfolio vs Individual Stock Returns')
plt.legend(loc='upper left')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('portfolio_performance.png', dpi=100)
plt.show()

The portfolio line (black) typically falls between the best and worst individual stocks. This is the essence of diversification: you give up some maximum upside in exchange for reduced downside risk.

Risk-Return Analysis

Let’s compare our portfolio against a simple benchmark and visualize the risk-return tradeoff.

# Download S&P 500 as benchmark
sp500 = yf.download("^GSPC", start="2024-01-01", end="2024-12-31")["Close"]
sp500_returns = sp500.pct_change().dropna()

# Calculate metrics for both
metrics = pd.DataFrame({
    'Portfolio': {
        'Annualized Return': annualized_return * 100,
        'Annualized Volatility': annualized_vol * 100,
        'Sharpe Ratio': sharpe_ratio
    },
    'S&P 500': {
        'Annualized Return': ((1 + sp500_returns).prod() - 1) ** (252/len(sp500_returns)) - 1,
        'Annualized Volatility': sp500_returns.std() * np.sqrt(252) * 100,
        'Sharpe Ratio': np.sqrt(252) * (sp500_returns.mean() - risk_free_rate/252) / sp500_returns.std()
    }
})

print(metrics.round(2))

This comparison shows whether your portfolio outperformed the market on a risk-adjusted basis. A well-constructed portfolio should have a higher Sharpe ratio than the S&P 500, even if absolute returns are similar.

Rolling Performance

Performance changes over time. Rolling metrics show how your portfolio’s characteristics evolve.

# Calculate 30-day rolling Sharpe ratio
rolling_vol = portfolio_returns.rolling(window=30).std() * np.sqrt(252)
rolling_return = portfolio_returns.rolling(window=30).mean() * 252
rolling_sharpe = (rolling_return - risk_free_rate) / rolling_vol

plt.figure(figsize=(12, 4))
plt.plot(rolling_sharpe.dropna() * 100, label='Rolling 30-day Sharpe Ratio')
plt.axhline(y=0, color='r', linestyle='--', alpha=0.5)
plt.xlabel('Date')
plt.ylabel('Sharpe Ratio')
plt.title('Rolling Portfolio Sharpe Ratio')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('rolling_sharpe.png', dpi=100)
plt.show()

During volatile periods, the rolling Sharpe ratio fluctuates significantly. This helps you understand when your strategy worked well and when it struggled.

Summary

You now know how to:

  • Define a portfolio with stocks and weights
  • Calculate portfolio returns using weighted averages
  • Compute annualized returns and volatility
  • Calculate the Sharpe ratio for risk-adjusted performance
  • Analyze correlation between assets
  • Visualize cumulative returns and correlations
  • Compare portfolio performance against benchmarks
  • Calculate rolling performance metrics

These techniques form the foundation of portfolio analysis. From here, you can explore more advanced topics like portfolio optimization, factor models, and risk management.

Next Steps

Continue building your finance skills with more tutorials in this series. Each new concept builds on what you’ve learned here, taking you from basic data fetching through increasingly sophisticated quantitative analysis.