Portfolio Analysis in pandas: Correlation and Covariance Matrices, Weighted Portfolio Volatility and a Rebalancing Simulation
Compute annualised covariance and correlation matrices from returns, derive portfolio volatility and risk contributions with NumPy, and simulate drift versus calendar rebalancing.
What you will learn
- How to align several price series into one returns DataFrame and compute
.cov()and.corr()correctly annualised. - The matrix formula for portfolio volatility,
w.T @ Σ @ w, and how to read each asset's contribution to risk. - How to simulate a buy-and-hold portfolio that drifts versus one rebalanced on a calendar.
- Which numerical traps (misaligned dates, mixed currencies, look-ahead in the covariance window) produce wrong answers.
Explanation
Correlation and portfolio heat (Risk & Trade Management, lesson 6), combining two systems (Systematic Trading, lesson 10) and rebalancing rules (ETFs, lesson 10) were taught elsewhere; this lesson implements them for any number of assets in code.
From prices to a returns matrix
Load each instrument's adjusted close (lesson 4) into one DataFrame, one column per ticker: prices = pd.concat(series_dict, axis=1). Holidays that differ between markets create NaN rows; decide explicitly between prices.dropna() (common days only) and prices.ffill() (carry the last price forward). Then rets = prices.pct_change().dropna(). Convert everything to one currency first; a US ETF held by a euro investor must be translated with the EUR/USD series, or its volatility and correlations describe the wrong thing.
Covariance, correlation and annualisation
rets.cov() is the sample covariance matrix of daily returns; multiply by 252 to annualise (variance scales with time). rets.corr() is unit-free and needs no scaling. The diagonal holds each asset's variance, so np.sqrt(np.diag(cov_a)) gives annualised volatilities. The relation Σ_ij = ρ_ij × σ_i × σ_j lets you build a matrix from volatilities and correlations to test a hypothetical scenario such as "correlations rise to 0.8 in a crisis".
Portfolio volatility and risk contributions
w = np.array([0.5, 0.3, 0.2]) # weights, sum to 1cov_a = rets.cov().values * 252port_var = w @ cov_a @ wport_vol = np.sqrt(port_var)marginal = cov_a @ w # d(var)/d(w)risk_contrib = w * marginal / port_var # shares that sum to 1
@ is matrix multiplication. risk_contrib tells you what fraction of the portfolio's variance each asset is responsible for; it is usually far from the capital weight, and that gap is the starting point of risk budgeting (Portfolio Construction, lesson 1).
A rebalancing simulation
Buy-and-hold lets weights drift: winners grow into a larger share. Calendar rebalancing resets to targets at fixed dates. In pandas, resample returns to the rebalancing frequency and loop over the periods:
period_rets = (1 + rets).resample('ME').prod() - 1 # monthly returnstarget = np.array([0.6, 0.4]); hold = 10_000 * target; path = []for _, r in period_rets.iterrows():hold = hold * (1 + r.values) # grow each sleevepath.append(hold.sum())hold = hold.sum() * target # rebalance to targets
For the drifting version, delete the last line. Use 'QE' for quarterly and 'YE' for yearly (pandas 2.2 and later; older versions use 'M', 'Q', 'A'). Turnover of a rebalance is abs(new_hold - old_hold).sum(); multiply by a cost rate such as 0.1 % and subtract. In a German taxable account rebalancing also realises gains; check the tax effect with a Steuerberater.
Worked example
Hypothetical example with invented inputs; fees, spreads and taxes excluded.
Part 1: three-asset volatility. Annualised volatilities 20 %, 10 %, 15 %; correlations ρ₁₂ = 0.2, ρ₁₃ = 0.6, ρ₂₃ = 0.1; weights 50 / 30 / 20 %.
- Covariances: Σ₁₂ = 0.2 × 0.20 × 0.10 = 0.0040; Σ₁₃ = 0.6 × 0.20 × 0.15 = 0.0180; Σ₂₃ = 0.1 × 0.10 × 0.15 = 0.0015. Diagonal: 0.0400, 0.0100, 0.0225.
- Portfolio variance = Σ wᵢ wⱼ Σᵢⱼ = 0.25 × 0.04 + 0.09 × 0.01 + 0.04 × 0.0225 + 2 × (0.5 × 0.3 × 0.004 + 0.5 × 0.2 × 0.018 + 0.3 × 0.2 × 0.0015) = 0.0100 + 0.0009 + 0.0009 + 2 × (0.0006 + 0.0018 + 0.00009) = 0.01678.
- Portfolio volatility = √0.01678 = 12.95 %, against a weighted average of volatilities of 16.0 %; the difference is the diversification benefit.
- Risk contributions: asset 1 carries 73.9 % of the variance with 50 % of the capital; asset 2 carries 9.5 % with 30 %; asset 3 carries 16.6 % with 20 %.
Part 2: drift versus rebalancing. €10,000 split 60 / 40 between asset A and asset B. Annual returns: A +20 %, −10 %, +15 %; B +2 %, +4 %, +1 %.
- Drift: A: 6,000 × 1.20 × 0.90 × 1.15 = €7,452; B: 4,000 × 1.02 × 1.04 × 1.01 = €4,285.63. Total €11,737.63; A's weight has drifted to 63.5 %.
- Yearly rebalancing: year 1 total = 7,200 + 4,080 = €11,280 → reset to 6,768 / 4,512; year 2 = 6,091.20 + 4,692.48 = €10,783.68 → reset; year 3 = 7,440.74 + 4,356.61 = €11,797.35.
- Rebalancing ended €59.72 higher because it sold A after +20 % and bought back after −10 %. With a steadily trending A, drift would have won; rebalancing controls risk, it does not guarantee return.
Common mistake
rets.cov() over 2015–2025 and uses the result to set weights in 2015 is using information from the future. Estimate the covariance on a rolling or expanding window (rets.rolling(252).cov() or rets.loc[:date].cov()) and apply it only to the following period. Correlations also rise in crises, so a calm-period matrix understates the risk of the moment when it matters.Practice activity
On paper: two assets with annualised volatilities 18 % and 6 %, correlation 0.3, weights 70 / 30 %. (a) Build the 2 × 2 covariance matrix. (b) Compute portfolio variance and volatility. (c) Compute each asset's risk contribution as a share of variance. (d) Recompute the volatility with the correlation at 0.9 and state in one sentence why this matters.
Show solution
(a) Σ₁₁ = 0.0324; Σ₂₂ = 0.0036; Σ₁₂ = 0.3 × 0.18 × 0.06 = 0.00324.
(b) Variance = 0.49 × 0.0324 + 0.09 × 0.0036 + 2 × 0.7 × 0.3 × 0.00324 = 0.015876 + 0.000324 + 0.0013608 = 0.0175608; volatility = 13.25 %.
(c) Marginal: asset 1: 0.7 × 0.0324 + 0.3 × 0.00324 = 0.023652; asset 2: 0.7 × 0.00324 + 0.3 × 0.0036 = 0.003348. Contributions: 0.7 × 0.023652 / 0.0175608 = 94.3 %; 0.3 × 0.003348 / 0.0175608 = 5.7 %.
(d) With ρ = 0.9: Σ₁₂ = 0.00972; variance = 0.015876 + 0.000324 + 0.0040824 = 0.0202824; volatility = 14.24 %. The "diversifier" adds little when correlation rises, which is exactly what tends to happen in a sell-off; a portfolio should be judged on stressed correlations, not only on the historical average.
Limitations: sample covariance from a few years of daily data is noisy; small changes in the window can move correlations by 0.1 or more.
Key takeaways
- Align dates and currencies before computing returns; decide explicitly how to treat missing days.
- Annualise covariance by multiplying by the periods per year; correlation needs no scaling.
- Portfolio volatility is √(wᵀ Σ w); risk contributions w × (Σw) / variance show where the risk really sits.
- Simulate rebalancing by resampling returns and resetting holdings; estimate covariance only from data available at the time.
Next step
Lesson 12 automates the daily routine: a scheduled script that fetches data, computes signals and writes a report, with API keys and rate limits handled properly.
Self-check
Answer the questions, then check your answers. You can retry as often as you like.
Automatic self-check for your own revision. Your answers are checked in your browser and are not seen or assessed by us. It is not an exam and not a qualification.
Your progress
“Complete” only means you marked the lesson as done. It is not a test of your understanding. Progress and notes are saved only in this browser on this device — not in an account.