7. Portfolios#

7.1. 🎯 Learning Objectives#

By the end of this notebook, you will be able to:

  1. See risk through a portfolio lens — Grasp Markowitz’s insight that an asset’s danger depends on what it adds to the whole mix, not on its stand-alone volatility

  2. Express returns, means, and variance with matrix algebra — Use \(R_p = W'R\), \(E[R_p]=W'E[R]\), and \(\text{Var}(R_p)=W'\Sigma W\) to scale from two assets to hundreds

  3. Quantify diversification benefits — Investigate how adding a higher-volatility asset can lower portfolio variance when correlations are below one

  4. Compute and interpret portfolio weights — Distinguish long-only, short, and leveraged positions; verify that weights sum to one

7.2. 📋 Table of Contents#

  1. Setup

  2. Why Think in Portfolios?

  3. Portfolio Weights

  4. Our Dataset

  5. Portfolio Returns

  6. Portfolio Expected Returns

  7. Portfolio Variance

  8. Diversification

  9. The Mean-Variance Frontier

  10. Exercises

  11. Key Takeaways

#@title 🛠️ Setup: Run this cell first <a id="setup"></a>

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

# Set consistent plot style
plt.style.use('seaborn-v0_8-whitegrid')
plt.rcParams['figure.figsize'] = [10, 6]

7.3. Why Think in Portfolios? #

Harry Markowitz’s great insight was to think of risk in terms of what an asset adds to your portfolio, not its stand-alone volatility.

💡 Key Insight:

Just like sugar can be good for you if you’re not eating any, but terrible if you’re eating a lot of it—what investors should care about is their final diet.

If a stock brings a lot of what you already have, it will be risky for you.

Why volatility alone is misleading:

  • If you have only 1% in a stock and it drops to ZERO, you lose only 1% of your portfolio

  • The stock’s volatility doesn’t matter much at small positions

  • As you buy more, it becomes risky for you because your portfolio moves more like it

  • If this stock moves together with your other holdings, even a small position can feel very risky

📌 Remember:

The covariance across stocks is a key determinant of how much we can hold volatile stocks without adding much overall risk.


7.4. Portfolio Weights #

The portfolio weight for stock \(j\), denoted \(w_j\), is the fraction of portfolio value held in stock \(j\):

\[w_j = \frac{\text{Dollars held in stock } j}{\text{Total portfolio value}}\]

📌 Remember:

Portfolio weights sum to one:

\[\sum_{j=1}^N w_j = 1 \quad \text{or in matrix notation:} \quad \mathbf{1}'W = 1\]

This doesn’t mean you can’t borrow—just that negative weights offset positive ones.

📌 Warning:

We often separate the weight on the risk-free asset

\[\sum_{j=1}^N w_j +w_{rf}= 1 \quad \text{risky weights can be larger than 1 if $w_{rf}$ negative} \quad \sum_{j=1}^N w_j =1-w_{rf}\]

Type

Description

Example

Long-only

All weights \(\geq 0\)

\(W=[0.6,\,0.3],\; w_{rf}=0.1\)

Short

Some \(w_i < 0\)

\(W=[1.2,\,-0.4],\; w_{rf}=0.2\)

Leveraged

Risky weights sum to \(>1\)

\(W=[1.5,\,0.5],\; w_{rf}=-1\)

We will often work with excess returns. These are portfolios that are long a risky asset and short the risk-free asset


7.5. Our Dataset #

We’ll work with monthly excess returns for:

  • 🇺🇸 US equities (S&P 500, MKT)

  • 🌍 International developed markets (WorldxUSA)

  • 🌏 Emerging markets

  • 📈 US government bonds

  • 🌐 International government bonds

#@title 📊 Load Data

url = "https://raw.githubusercontent.com/amoreira2/UG54/main/assets/data/GlobalFinMonthly.csv"
Data = pd.read_csv(url)
Data['Date'] = pd.to_datetime(Data['Date'])
Data = Data.set_index('Date')

# Replace sentinel values (e.g. -99) with NaN before any computation
Data = Data[Data > -1].dropna()

print(f"Data shape: {Data.shape}")
print(f"Date range: {Data.index[0].strftime('%Y-%m')} to {Data.index[-1].strftime('%Y-%m')}")
Data.head()
# Convert to excess returns
Rf = Data['RF']
Data = Data.drop(columns=['RF']).subtract(Rf, axis=0)
Data = Data.dropna()
Data.head()

7.6. Portfolio Returns #

Portfolio returns are the dollar-weighted average of individual position returns:

\[r_p = \sum_{j=1}^N w_j r_j = W'R\]

Where:

  • \(R\) is the \(N \times 1\) vector of asset returns

  • \(W\) is the \(N \times 1\) vector of weights

We can rewrite the portfolio of regular assets in terms of a portfolio of excess returns,

\[R_p=\sum_{i=2}^N w_i r_i+w_1 r_1=\sum_{i=2}^N w_i r^e_i+r_1 =W'R^e+r_f\]

where \(R^e\) is the vector of risky assets excess returns and \(W=[w_2,w_3,...w_N]\) is the vector of risky assets weights

  • Note that even though we impose that the weights \(w_i\) add up to 1, the weights W can add up to values below 1 or above 1

  • Why? because we represented everything in terms of self-funded portfolios

  • Here we are working directly with excess returns

# Construct an equal-weighted portfolio
N = len(Data.columns)
W = np.ones((N, 1)) / N

print(f"Number of assets: {N}")
print(f"Weights: {W.flatten()}")
print(f"Weights sum to: {W.sum():.4f}")
Number of assets: 5
Weights: [0.2 0.2 0.2 0.2 0.2]
Weights sum to: 1.0000

🐍 Python Insight: Matrix multiplication with @

The @ operator performs matrix multiplication in Python:

portfolio_return = Data @ W  # T×N matrix @ N×1 vector = T×1 vector

This replaces messy for loops with a single line!

# Compute portfolio returns for ALL dates at once
Rp = Data @ W
# Plot cumulative returns
fig, ax = plt.subplots(figsize=(10, 6))
(1 + Rp).cumprod().plot(ax=ax, linewidth=2)
ax.set_title('Equal-Weighted Global Portfolio: Cumulative Returns', fontsize=14)
ax.set_ylabel('Growth of $1')
plt.show()
../../_images/3beb7fb34273871cb78d87de57722f3fdd0cc228d25094858c7987048ea137c8.png

7.7. Portfolio Expected Returns #

Since expectations are linear, the expected portfolio return is:

\[E[r_p^e] = E\left[\sum_{j=1}^N w_j r_j^e\right] = \sum_{j=1}^N w_j E[r_j^e] = W'E[R^e]\]

If the porfolio expected excess return is \(E[r_p^e]\) say 5%, what is the expected return?

# Estimate expected returns from historical data
E_hat = Data.mean()

print("Monthly expected excess returns:")
print(E_hat.round(4))
print("\nAnnualized expected excess returns:")
print((E_hat * 12).round(4))
Monthly expected excess returns:
MKT                 0.0052
USA30yearGovBond    0.0025
EmergingMarkets     0.0068
WorldxUSA           0.0042
WorldxUSAGovBond    0.0021
dtype: float64

Annualized expected excess returns:
MKT                 0.0622
USA30yearGovBond    0.0304
EmergingMarkets     0.0814
WorldxUSA           0.0500
WorldxUSAGovBond    0.0246
dtype: float64
# Two equivalent ways to compute portfolio expected return
E_rp_method1 = W.T @ E_hat  # Matrix multiplication
E_rp_method2 = Rp.mean()     # Direct average of portfolio returns

print(f"Via W'E[R]: {E_rp_method1[0]:.6f}")
print(f"Via mean(Rp): {E_rp_method2[0]:.6f}")
print(f"\nAnnualized: {E_rp_method1[0] * 12:.2%}")
Via W'E[R]: 0.004144
Via mean(Rp): 0.004144

Annualized: 4.97%

7.8. Portfolio Variance #

Portfolio variance is NOT the weighted average of individual variances!

Two-asset case:

\[\text{Var}(r^e) = w_1^2 \text{Var}(r_1^e) + 2w_1 w_2 \text{Cov}(r_1^e, r_2^e) + w_2^2 \text{Var}(r_2^e)\]

N-asset case:

\[\text{Var}(r_p^e) = \sum_{j=1}^N \sum_{i=1}^N w_j w_i \text{Cov}(r_j^e, r_i^e) = W' \Sigma W\]

where \(\Sigma\) is the \(N \times N\) variance-covariance matrix.

💡 Key Insight:

For 50 assets, the formula has 50 variance terms and 2,450 covariance terms! Matrix algebra saves us from nested for loops.

# Estimate the covariance matrix
Cov_hat = Data.cov().to_numpy()

print("Covariance matrix (monthly):")
display(Cov_hat.round(6))
Covariance matrix (monthly):
array([[ 1.950e-03,  1.110e-04,  1.298e-03,  1.265e-03,  1.870e-04],
       [ 1.110e-04,  1.229e-03, -2.040e-04, -1.300e-05,  2.640e-04],
       [ 1.298e-03, -2.040e-04,  3.550e-03,  1.664e-03,  2.490e-04],
       [ 1.265e-03, -1.300e-05,  1.664e-03,  2.185e-03,  4.220e-04],
       [ 1.870e-04,  2.640e-04,  2.490e-04,  4.220e-04,  4.070e-04]])
# Compute portfolio variance using matrix algebra
Var_rp = (Data@W).var()[0]
Vol_rp = np.sqrt(Var_rp)
# Compute portfolio variance using matrix algebra
Var_rp_m = (W.T @ Cov_hat @ W)
Vol_rp_m = np.sqrt(Var_rp_m)

print(f"Monthly variance: {Var_rp}")
print(f"Annualized volatility: {Vol_rp * np.sqrt(12)}")


print(f"Monthly variance: {Var_rp_m}")
print(f"Annualized volatility: {Vol_rp_m * np.sqrt(12)}")
Monthly variance: 0.0007923455067886972
Annualized volatility: 0.09750972300988432
Monthly variance: [[0.00079235]]
Annualized volatility: [[0.09750972]]

7.9. Diversification #

The famous advice: “Don’t put all your eggs in one basket.”

Let’s examine this from a US investor’s perspective considering international stocks.

# Correlation and volatility of US vs International
print("Correlation matrix:")
display(Data[['MKT', 'WorldxUSA']].corr().round(3))

print("\nAnnualized volatilities:")
print((Data[['MKT', 'WorldxUSA']].std() * np.sqrt(12)).round(4))

🤔 Think and Code:

The international market is more volatile than the US market. Which portfolio do you expect to have the lowest volatility?

  • 100% US?

  • 100% International?

  • Something in between?

# Trace how volatility varies with international allocation
D = Data[['MKT', 'WorldxUSA']]
Cov_2 = D.cov()

results = []
for w_intl in np.arange(0, 1.01, 0.01):
    W_2 = np.array([[1 - w_intl], [w_intl]])
    var = (W_2.T @ Cov_2 @ W_2)[0, 0]
    vol_annual = np.sqrt(var) * np.sqrt(12)
    results.append([w_intl, vol_annual])

results = pd.DataFrame(results, columns=['Weight_Intl', 'Volatility'])
# Plot the diversification benefit
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(results['Weight_Intl'], results['Volatility'], linewidth=2, color='steelblue')
ax.axhline(y=results['Volatility'].min(), color='red', linestyle='--', alpha=0.7, 
           label=f"Minimum vol: {results['Volatility'].min():.2%}")

# Mark pure portfolios
ax.scatter([0, 1], [results.iloc[0]['Volatility'], results.iloc[-1]['Volatility']], 
           s=100, zorder=5, color='darkred')
ax.annotate('100% US', (0, results.iloc[0]['Volatility']), 
            xytext=(0.05, results.iloc[0]['Volatility'] + 0.005))
ax.annotate('100% Intl', (1, results.iloc[-1]['Volatility']), 
            xytext=(0.85, results.iloc[-1]['Volatility'] + 0.005))

ax.set_xlabel('Weight on International', fontsize=12)
ax.set_ylabel('Annualized Volatility', fontsize=12)
ax.set_title('Diversification Benefit: US + International', fontsize=14)
ax.legend()
plt.show()

💡 Key Insight:

Adding a more volatile asset can reduce portfolio volatility when correlations are below 1! This is the power of diversification.


7.10. The Mean-Variance Frontier #

Now let’s trace the investment frontier: how expected returns change with risk.

# Compute mean-variance frontier
E_2 = D.mean() * 12  # Annualized expected returns

frontier = []
for w_intl in np.arange(0, 1.01, 0.01):
    W_2 = np.array([[1 - w_intl], [w_intl]])
    
    # Expected return
    er = (W_2.T @ E_2)[0]
    
    # Volatility
    var = (W_2.T @ Cov_2 @ W_2)[0, 0]
    vol = np.sqrt(var) * np.sqrt(12)
    
    frontier.append([w_intl, vol, er])

frontier = pd.DataFrame(frontier, columns=['Weight_Intl', 'Volatility', 'Expected_Return'])
# Plot the mean-variance frontier
fig, ax = plt.subplots(figsize=(10, 6))

# Color by weight on international
scatter = ax.scatter(frontier['Volatility'], frontier['Expected_Return'], 
                     c=frontier['Weight_Intl'], cmap='coolwarm', s=50)
plt.colorbar(scatter, label='Weight on International')

# Mark pure portfolios
ax.scatter([frontier.iloc[0]['Volatility']], [frontier.iloc[0]['Expected_Return']], 
           s=150, color='blue', marker='s', zorder=5, label='100% US')
ax.scatter([frontier.iloc[-1]['Volatility']], [frontier.iloc[-1]['Expected_Return']], 
           s=150, color='red', marker='s', zorder=5, label='100% Intl')

ax.set_xlabel('Annualized Volatility', fontsize=12)
ax.set_ylabel('Annualized Expected Excess Return', fontsize=12)
ax.set_title('Mean-Variance Frontier: US + International', fontsize=14)
ax.legend()
plt.show()

7.11. 📝 Exercises #

7.11.1. Exercise 1: Warm-up — Custom Portfolio#

🔧 Exercise:

Create a portfolio with:

  • 50% in MKT (US equities)

  • 30% in WorldxUSA (International)

  • 20% in Bonds (if available, else EmergingMarkets)

Compute:

  1. The portfolio’s annualized expected excess return

  2. The portfolio’s annualized volatility

  3. The portfolio’s Sharpe ratio

# Your code here

7.11.2. Exercise 2: Extension — Minimum Variance Portfolio#

🤔 Think and Code:

Using only MKT and WorldxUSA:

  1. Find the weight on international that minimizes portfolio volatility

  2. What is the minimum achievable volatility?

  3. Compare this to holding 100% US—what’s the volatility reduction?

# Your code here

7.11.3. Exercise 3: Open-ended — Three-Asset Frontier#

🤔 Think and Code:

Extend the analysis to three assets: MKT, WorldxUSA, and EmergingMarkets.

  1. Create a grid of weights (w_us, w_intl, w_em) that sum to 1

  2. Compute expected return and volatility for each combination

  3. Plot the three-asset frontier

  4. Does adding emerging markets improve the frontier?

# Your code here

7.12. 🧠 Key Takeaways #

  1. Matrix algebra is your friend — A single line replaces nested loops when calculating returns, expectations, and variance

  2. Diversification is all about correlations — Pairing volatile but imperfectly correlated assets can cut overall risk more than simply holding the lower-volatility asset alone

  3. How risky is an asset? — It fundamentally depends on the portfolio of who is asking

  4. The key formulas:

Quantity

Formula

Portfolio return

\(R_p = W'R\)

Expected return

\(E[R_p] = W'E[R]\)

Variance

\(\text{Var}(R_p) = W'\Sigma W\)