σμ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.2·~1 hrAdvanced

Multi-factor models: Fama-French

Skill positionModule 7 of 11 · Topic 2 of 9
7.2 Multi-factor models: Fama-French
—Lab score— / 100Total XP—Streak—

More than one reason to move

CAPM has one factor, the market. In 1992 Fama and French showed that two more explain a lot of the differences in stock returns: size (small companies earned more than large ones, factor SMB, small minus big) and value (cheap stocks by book-to-market earned more than expensive ones, factor HML, high minus low). Later models added momentum, profitability and investment. Each factor is itself a return series, and an asset's loading on it is how strongly it moves with that series.

import numpy as np

rng = np.random.default_rng(1)
n = 250
market = rng.normal(0.0004, 0.010, size=n)               # excess returns of three factors
smb = rng.normal(0.0001, 0.005, size=n)
hml = rng.normal(0.0001, 0.005, size=n)
factors = np.column_stack([market, smb, hml])             # shape (n, k) = (250, 3)

asset = 0.0002 + factors @ np.array([1.1, 0.5, -0.3]) + rng.normal(0, 0.004, size=n)
X = np.column_stack([np.ones(n), factors])                # design matrix: a column of ones for alpha
print(X.shape)                                            # (250, 4)
# The design matrix of three days and two factors
import numpy as np

factors = np.array([[0.010, -0.002], [-0.004, 0.001], [0.006, 0.003]])
X = np.column_stack([np.ones(len(factors)), factors])
print(X)                                           # a column of ones, then market and SMB

Least squares in matrix form

With several factors the regression is asset = alpha + b1 * f1 + ... + bk * fk + error. Stack the factors next to a column of ones in a matrix X, and the least-squares coefficients solve the normal equations (X'X) c = X'y. In NumPy that is np.linalg.solve(X.T @ X, X.T @ y). It is the same machinery as the earlier single-asset regression, now with k + 1 unknowns. The first coefficient is alpha, the rest are the loadings.

import numpy as np

rng = np.random.default_rng(1)
n = 250
factors = np.column_stack([rng.normal(0.0004, 0.010, size=n), rng.normal(0.0001, 0.005, size=n), rng.normal(0.0001, 0.005, size=n)])
asset = 0.0002 + factors @ np.array([1.1, 0.5, -0.3]) + rng.normal(0, 0.004, size=n)

X = np.column_stack([np.ones(n), factors])
coef = np.linalg.solve(X.T @ X, X.T @ asset)
print(np.round(coef, 4))                                   # [alpha, market, SMB, HML]: the loadings come out near [1.1, 0.5, -0.3]

residual = asset - X @ coef
r2 = 1 - np.sum(residual ** 2) / np.sum((asset - asset.mean()) ** 2)
print(round(float(r2), 4))

How much do the factors explain?

R-squared answers it: the share of the asset's variation that the factors explain, 1 - SSres / SStot, between 0 (nothing) and 1 (everything). A diversified equity portfolio often has an R-squared above 0.9 on the Fama–French factors: its risk is almost all factor risk. A single stock has a lower one, and the rest, the residual variation, is specific to the company. Adding a factor can never lower R-squared in sample, so a higher number alone does not prove a factor matters.

# What the model implies: the expected return from the loadings and the factor premia
import numpy as np

betas = np.array([1.1, 0.5, -0.3])             # market, SMB, HML loadings
premia = np.array([0.0006, 0.0002, 0.0002])    # expected daily factor returns
rf = 0.0001

expected = rf + np.sum(betas * premia)
print(round(float(expected), 6))               # 0.0008 per day, about 20% a year on 250 days
Factors are bets, not free lunchesA factor premium is the reward for a risk that hurts in bad times, and it can vanish or reverse for years. Loadings estimated from a short sample are noisy, and with correlated factors the individual coefficients can be unstable even when the fit is good. The use of a factor model is mainly to decompose risk and performance: how much came from the market, size, value, and how much, the alpha, is left.
  • A factor model regresses excess returns on several factors: X = [ones | factors], coefficients [alpha, beta_1..beta_k].
  • Solve the normal equations np.linalg.solve(X.T @ X, X.T @ y); R-squared is 1 - SSres / SStot and never falls when you add a factor.
  • The model implies rf + sum(betas * premia); loadings and premia are estimates, and a factor premium can disappear.

Next up: the information coefficient, measuring whether a signal predicts returns at all.

Lab · what's graded

  • Set up the regression with design_matrix25 pts
  • Estimate the loadings with fit_factor_model25 pts
  • Score the fit with r_squared25 pts
  • Price the exposures with factor_expected_return25 pts
Explorer · lab-94-1
▾ lab-94-1
…
bash · in-browser
learner@quantcraft:~/lab-94-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

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