σμ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 8 · Stochastic models & machine learning›Topic 8.7·~50 minAdvanced

Control variates

Skill positionModule 8 of 11 · Topic 7 of 10
8.7 Control variates
—Lab score— / 100Total XP—Streak—

Using what you already know

Simulating a call, you also simulate the stock price at expiry S_T, whose true mean S0 * exp(r * T) you know exactly. In your sample the average of S_T came out a little off, say too high. Since the call payoff and S_T move together, the call's sample mean is probably too high as well. A control variate uses this: it subtracts a multiple of the observed error of the quantity you know from the estimate of the one you want.

import numpy as np

rng = np.random.default_rng(1)
z = rng.standard_normal(2000)
s_t = 100.0 * np.exp((0.05 - 0.5 * 0.2 ** 2) + 0.2 * z)             # terminal prices
call = np.maximum(s_t - 100.0, 0.0)                                  # the payoff we want

known = 100.0 * np.exp(0.05)                                         # E[S_T], exactly
print(round(float(s_t.mean()), 3), round(known, 3))                  # the sample mean of S_T vs its true mean
print(round(float(s_t.mean() - known), 3))                           # the observed error of the control

The best multiple

How much of the control's error should you subtract? The multiple b that minimises the variance of payoff - b * control is b = cov(payoff, control) / var(control), exactly the slope of a regression of the payoff on the control (the same sums as beta in the CAPM topic). The controlled estimate is mean(payoff) - b * (mean(control) - known_mean). It is unbiased, because the term being subtracted has expectation zero.

import numpy as np

def control_coefficient(payoff, control):
    dev = control - control.mean()
    return float(np.sum(dev * (payoff - payoff.mean())) / np.sum(dev ** 2))

rng = np.random.default_rng(1)
z = rng.standard_normal(2000)
s_t = 100.0 * np.exp((0.05 - 0.5 * 0.2 ** 2) + 0.2 * z)
call = np.maximum(s_t - 100.0, 0.0)
known = 100.0 * np.exp(0.05)

b = control_coefficient(call, s_t)
controlled = call.mean() - b * (s_t.mean() - known)
print(round(b, 4), round(float(call.mean()), 4), round(float(controlled), 4))       # the plain mean and the corrected one
# A payoff that is exactly proportional to the control is estimated without any error
import numpy as np

x = np.random.default_rng(2).standard_normal(50) + 5.0          # the control, known mean 5.0
y = 2.0 * x                                                      # the payoff
dev = x - x.mean()
b = np.sum(dev * (y - y.mean())) / np.sum(dev ** 2)
print(round(float(b), 6), round(float(y.mean() - b * (x.mean() - 5.0)), 6))      # 2.0 and exactly 10.0

How much error is saved

The adjusted samples payoff - b * control are what carry the noise, so the standard error of the controlled estimate is their standard deviation (ddof=1) divided by sqrt(n). With correlation rho between payoff and control, the variance falls by a factor 1 - rho^2: a correlation of 0.9 removes 81% of it, and 0.99 removes 98%. The control costs no extra simulation, only a few extra lines.

import numpy as np

def control_variate_error(payoff, control):
    dev = control - control.mean()
    b = np.sum(dev * (payoff - payoff.mean())) / np.sum(dev ** 2)
    return float((payoff - b * control).std(ddof=1) / np.sqrt(len(payoff)))

rng = np.random.default_rng(1)
z = rng.standard_normal(20000)
s_t = 100.0 * np.exp((0.05 - 0.5 * 0.2 ** 2) + 0.2 * z)
for strike in (80.0, 100.0, 120.0):
    call = np.maximum(s_t - strike, 0.0)
    plain = float(call.std(ddof=1) / np.sqrt(len(call)))
    print(strike, round(plain, 4), round(control_variate_error(call, s_t), 4), round(plain / control_variate_error(call, s_t), 2))
A good control is correlated and knownBoth conditions matter. The control must move with the payoff (otherwise b is near 0 and nothing is gained) and its exact mean must be known (otherwise the correction is a guess). Good controls for an Asian arithmetic average are the geometric average, whose price has a formula, or the terminal price itself. You can use several controls at once, and the coefficients then come from a multiple regression.
  • A control variate is a simulated quantity with a known mean; subtract a multiple of its observed error from the estimate: mean(Y) - b * (mean(X) - E[X]).
  • The best multiple is the regression slope b = cov(Y, X) / var(X); the error is the standard deviation of Y - b * X over sqrt(n).
  • The variance drops by 1 - rho^2; a good control is highly correlated with the payoff and has an exactly known mean, and it costs no extra simulation.

Next up: scikit-learn pipelines for returns, fitting and evaluating predictive models on return data.

Lab · what's graded

  • Choose the right multiple with control_coefficient25 pts
  • Correct the estimate with control_variate_estimate25 pts
  • Shrink the error bar with control_variate_error25 pts
  • Count the gain with error_reduction25 pts
Explorer · lab-108-1
▾ lab-108-1
…
bash · in-browser
learner@quantcraft:~/lab-108-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

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