σμ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.7·~55 minAdvanced

Stress testing

Skill positionModule 7 of 11 · Topic 7 of 9
7.7 Stress testing
—Lab score— / 100Total XP—Streak—

Asking "what if?" instead of "how likely?"

VaR and expected shortfall are estimated from past returns, so they cannot see what has never happened. A stress test drops the probabilities and applies a scenario you choose: a 20% equity crash, a 2-point jump in rates, the 2008 crisis replayed, a currency devaluation. The portfolio's loss in that scenario is a number a risk committee can discuss, and it reveals concentrations that a statistical average hides.

import numpy as np

values = np.array([500000.0, 300000.0, -200000.0])       # two longs and a short
shocks = np.array([-0.20, -0.10, 0.15])                  # one scenario: stocks -20%, bonds -10%, the shorted stock +15%

pnl = values * shocks                                    # per position
print(pnl)                                               # [-100000.  -30000.  -30000.]  the short loses when its stock rises
print(float(pnl.sum()))                                  # -160000.0

A table of scenarios

Risk teams keep a standing list of scenarios, historical (1987, 2008, March 2020) and hypothetical. Put them in a matrix with one row per scenario and one column per position: the total loss under every scenario is then one matrix-vector product, scenarios @ values. Sorting or taking the minimum of that vector names the worst case, and which scenario hurts most is itself information, since it tells where the portfolio is exposed.

import numpy as np

values = np.array([500000.0, 300000.0, -200000.0])
scenarios = np.array([[-0.20, -0.10,  0.15],             # equity crash
                      [-0.05,  0.02,  0.00],             # mild dip
                      [ 0.10,  0.05, -0.20]])            # the shorted stock falls 20%: the short gains
names = ["equity crash", "mild dip", "rally and short gain"]

pnl = scenarios @ values
for name, p in zip(names, pnl):
    print(name, round(float(p)))
worst = int(np.argmin(pnl))
print("worst:", names[worst], round(float(pnl[worst])))
# A shock does not have to be a price move: a rate jump hits a bond through its duration
duration = 7.5                                    # years, from the duration topic
bond_value = 1_000_000.0
rate_jump = 0.02                                  # +2 percentage points
print(round(bond_value * (-duration * rate_jump)))   # -150000   the shock expressed as a price return of -15%

Reverse stress testing

Reverse the question: how bad would a shock have to be to cost us a given amount, such as the capital buffer? Take a direction of shocks (say, everything falls together) and scale it by a factor k. The loss is -k * (values @ direction), linear in k, so the factor that loses exactly loss_limit is loss_limit / -(values @ direction). If the direction does not lose money, no factor ever reaches the limit.

import numpy as np

def reverse_stress(values, direction, loss_limit):
    change = float(values @ direction)
    if change >= 0:
        return float("inf")
    return loss_limit / -change

values = np.array([500000.0, 300000.0, -200000.0])
direction = np.array([-1.0, -1.0, 0.0])                    # the two longs fall by the same percentage
k = reverse_stress(values, direction, 100000.0)
print(round(k, 4))                                          # 0.125  a 12.5% fall in both longs loses 100,000
print(round(float(-(values @ (k * direction))), 2))         # 100000.0
A scenario is only as good as your imaginationThe hard part is choosing scenarios that are severe yet plausible, and that move the correlated assets together, as they do in a crisis. The result is also a point estimate with no probability attached: a bad number is a reason to look at the position, and a good one proves nothing about scenarios you did not think of. Stress tests complement VaR; they do not replace it.
  • A stress test applies chosen shocks: position pnl is values * shocks, negative for losses and positive for the short that profits.
  • Many scenarios at once are one product, scenarios @ values; the minimum names the worst case and shows where the portfolio is exposed.
  • Reverse stress: the multiple of a shock that loses exactly the limit is loss_limit / -(values @ direction), or infinity if the direction never loses.

Next up: ARIMA models, forecasting a series from its own past.

Lab · what's graded

  • Hit each position with shocked_pnl25 pts
  • Run many scenarios at once with scenario_pnl25 pts
  • Name the worst case with worst_scenario25 pts
  • Work backwards from a loss with reverse_stress25 pts
Explorer · lab-99-1
▾ lab-99-1
…
bash · in-browser
learner@quantcraft:~/lab-99-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

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