σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademySkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
Skills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python skill›Module 5 · Portfolio optimisation›Topic 5.1·~45 minIntermediate

Covariance and correlation matrices

Skill positionModule 5 of 11 · Topic 1 of 5
5.1 Covariance and correlation matrices
—Lab score— / 100Total XP—Streak—

From one asset to many

Everything so far looked at one return series. A portfolio holds many assets, and its risk depends not only on how risky each one is but on how they move *together*. The covariance of two assets is the average product of their deviations from the mean: positive if they tend to rise and fall together, negative if one rises as the other falls. For n assets there are n * n covariances, laid out as a covariance matrix, which is symmetric and has the variances on its diagonal.

import numpy as np

# Five days of returns for two assets: shape (days, assets)
returns = np.array([[0.01, 0.02], [-0.02, -0.01], [0.03, 0.01], [0.00, -0.02], [0.01, 0.03]])

centered = returns - returns.mean(axis=0)            # subtract each column's mean
cov = centered.T @ centered / (len(returns) - 1)     # (assets x days) @ (days x assets) -> (assets x assets)
print(np.round(cov, 6))
print(np.allclose(cov, np.cov(returns, rowvar=False)))     # True

The sample covariance matrix

With a table of returns of shape (days, assets), the whole matrix is one line of linear algebra. Subtract each column's mean (axis=0, the mean over days), and multiply the centred table's transpose by itself: entry (i, j) of centered.T @ centered is the sum over days of x_i * x_j. Divide by days - 1, as for a sample variance. This is what np.cov(returns, rowvar=False) and DataFrame.cov() compute; note the rowvar=False, since np.cov expects the assets in the rows.

Correlation: covariance without the units

A covariance of 0.0004 says little: it depends on how volatile the two assets are. Dividing by the product of the two standard deviations removes the scale and gives the correlation, always between -1 and 1: corr[i, j] = cov[i, j] / (std[i] * std[j]), with std the square root of the diagonal. In code, np.outer(std, std) builds the table of products and one division rescales the whole matrix, with ones on the diagonal. Correlation tells how tightly two assets move; covariance, which keeps the scale, is what the risk formulas need.

# From covariance to correlation and annualised volatilities
import numpy as np

returns = np.array([[0.01, 0.02], [-0.02, -0.01], [0.03, 0.01], [0.00, -0.02], [0.01, 0.03]])
cov = np.cov(returns, rowvar=False)

std = np.sqrt(np.diag(cov))
corr = cov / np.outer(std, std)
print(np.round(corr, 4))
print(np.round(np.sqrt(np.diag(cov) * 252), 4))      # daily variances scaled to a year: volatility grows with sqrt(252)

Can this matrix be real?

A random table of numbers is not a covariance matrix. It must be symmetric, and positive semi-definite: for any weights w, the number w' C w is the variance of a portfolio, so it cannot be negative. Equivalently, no eigenvalue is below zero. Estimated matrices pass, but hand-edited ones, or a patchwork of correlations from different sources (for example A~B = 0.9, B~C = 0.9, A~C = -0.9), fail. Check with np.linalg.eigvalsh, which is for symmetric matrices and returns real eigenvalues, allowing a tiny tolerance for rounding.

# A validity check: symmetry and no negative eigenvalue
import numpy as np

def is_valid_cov(matrix, tol=1e-10):
    symmetric = bool(np.all(np.abs(matrix - matrix.T) <= tol))
    return symmetric and bool(np.linalg.eigvalsh(matrix).min() >= -tol)

good = np.array([[1.0, 0.5, 0.2], [0.5, 1.0, 0.3], [0.2, 0.3, 1.0]])
bad = np.array([[1.0, 0.9, -0.9], [0.9, 1.0, 0.9], [-0.9, 0.9, 1.0]])
print(is_valid_cov(good), np.round(np.linalg.eigvalsh(good), 3))
print(is_valid_cov(bad), np.round(np.linalg.eigvalsh(bad), 3))
# Few days, many assets: the sample covariance matrix is singular
import numpy as np

rng = np.random.default_rng(0)
returns = rng.normal(0, 0.01, size=(5, 8))              # 5 days, 8 assets
cov = np.cov(returns, rowvar=False)
print(np.linalg.matrix_rank(cov), "of", cov.shape[0])  # 4 of 8: some portfolios seem to have zero variance
Few observations, many assetsA matrix for 500 stocks has 125,250 distinct entries, estimated from perhaps 250 days. With fewer days than assets the sample matrix is singular: some portfolio appears to have zero variance, which no optimiser should believe. Practitioners shrink the matrix towards a simple target, or use a factor model. The estimate is also an average over the past: calm and crisis periods have very different covariances.
  • A covariance matrix of a (days, assets) table is centered.T @ centered / (days - 1): symmetric, variances on the diagonal.
  • Correlation is cov / np.outer(std, std): unit-free, between -1 and 1, ones on the diagonal; annualise variances by periods, volatilities by its square root.
  • A valid covariance matrix is symmetric with no negative eigenvalue (np.linalg.eigvalsh); estimates from too few days, or patched by hand, can break this.

Next up: portfolio return and variance, turning weights and this matrix into a portfolio's risk.

Lab · what's graded

  • Build the covariance table with cov_matrix25 pts
  • Rescale it to correlations with corr_from_cov25 pts
  • Read each asset's volatility with annualised_vols25 pts
  • Test whether it could be real with is_valid_cov25 pts
Explorer · lab-82-1
▾ lab-82-1
…
bash · in-browser
learner@quantcraft:~/lab-82-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-82-1 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes

Log in to run your code.

Every lecture is free to read. Create an account or log in to run code, run tests and submit this lab.

  • In-browser IDE with a bash terminal — nothing to install
  • Instant grading, XP and your place on the skill
Create an accountLog in
OR
Continue with GitHub