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.0A 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 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