σμ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 7 · Alpha research & risk›Topic 7.1·~55 minAdvanced

CAPM and beta

Skill positionModule 7 of 11 · Topic 1 of 9
7.1 CAPM and beta
—Lab score— / 100Total XP—Streak—

One number for market risk

Most stocks rise when the market rises and fall when it falls. The beta of an asset measures by how much: a beta of 1.5 means that, on average, a 1% market move comes with a 1.5% move in the asset. It is the slope of the asset's returns plotted against the market's, covariance(asset, market) / variance(market). Beta is the part of the risk you cannot diversify away, because every asset in the market carries it.

import numpy as np

market = np.array([0.01, -0.02, 0.015, 0.005, -0.01, 0.02])
asset = 1.5 * market + np.array([0.002, -0.001, 0.003, 0.0, -0.002, 0.001])      # moves 1.5x the market, plus noise

market_dev = market - market.mean()
covariance = np.sum((asset - asset.mean()) * market_dev) / (len(market) - 1)
variance = np.sum(market_dev ** 2) / (len(market) - 1)
print(round(float(covariance / variance), 4))                 # 1.5972: about 1.5, the noise moves it a little
print(round(float(np.polyfit(market, asset, 1)[0]), 4))       # the regression slope gives the same number
# Beta does not care about a constant, and scales with the asset
import numpy as np

def beta(asset, market):
    market_dev = market - market.mean()
    return float(np.sum((asset - asset.mean()) * market_dev) / np.sum(market_dev ** 2))

rng = np.random.default_rng(4)
market = rng.normal(0, 0.01, size=100)
print(round(beta(2.5 * market, market), 4))               # 2.5   an asset that moves 2.5x the market
print(round(beta(market + 0.01, market), 4))              # 1.0   adding a constant changes nothing
print(round(beta(-market, market), 4))                    # -1.0  a perfect hedge

The Capital Asset Pricing Model

CAPM turns that into a price of risk. Investors are paid for bearing market risk, and for nothing else, so an asset's expected return is the risk-free rate plus its beta times the market risk premium (market return minus the risk-free rate): E[R] = rf + beta * (E[R_m] - rf). A beta of 0 earns the risk-free rate, a beta of 1 earns the market return, and a beta of 2 earns the risk-free rate plus twice the premium.

def capm_expected_return(rf, beta_value, market_return):
    return rf + beta_value * (market_return - rf)

rf, market_return = 0.01, 0.06
for b in (0.0, 0.5, 1.0, 1.5, 2.0):
    print(b, round(capm_expected_return(rf, b, market_return), 4))     # 0.01, 0.035, 0.06, 0.085, 0.11

Alpha: what is left over

If CAPM is right, an asset earns exactly the return its beta justifies. The difference between what it earned and what CAPM expected is Jensen's alpha: mean(asset) - (rf + beta * (mean(market) - rf)). A positive alpha means it did better than its market risk explains; that is what active managers sell, and what is hardest to find. The part of the asset's movement the market does not explain is the residual risk, measured by the standard deviation of the regression residuals, which can be diversified away.

# Alpha and residual risk of an asset with a little skill in it
import numpy as np

rng = np.random.default_rng(2)
market = rng.normal(0.0004, 0.01, size=500)
asset = 0.0003 + 1.2 * market + rng.normal(0, 0.005, size=500)       # true alpha 3 bps a day, beta 1.2, residual 0.5%
rf = 0.0001

market_dev = market - market.mean()
b = np.sum((asset - asset.mean()) * market_dev) / np.sum(market_dev ** 2)
alpha = asset.mean() - (rf + b * (market.mean() - rf))
a = asset.mean() - b * market.mean()
residual = asset - (a + b * market)
print(round(float(b), 3), round(float(alpha), 5), round(float(np.sqrt(np.sum(residual ** 2) / (len(asset) - 2))), 4))
Beta is a statistic, not a constantA beta estimated on 60 days has a wide margin of error, and it changes with the market regime and with the choice of index. Using daily versus monthly returns gives different numbers. CAPM itself is a simplification: later work found more factors that explain returns, the subject of the next topic. Treat a beta as a measured, uncertain input.
  • Beta = covariance(asset, market) / variance(market), the slope against the market; it is the undiversifiable part of risk.
  • CAPM: rf + beta * (market - rf); Jensen's alpha is the average return minus that, the skill (or luck) beyond market risk.
  • Residual risk is the standard deviation of asset - (a + b * market) with n - 2; it can be diversified; beta itself is noisy.

Next up: multi-factor models (Fama–French), explaining returns with more than the market.

Lab · what's graded

  • Measure the market sensitivity with beta25 pts
  • Price the risk with capm_expected_return25 pts
  • Find the skill, if any, with jensen_alpha25 pts
  • Size the leftover risk with residual_vol25 pts
Explorer · lab-93-1
▾ lab-93-1
…
bash · in-browser
learner@quantcraft:~/lab-93-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-93-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