Value at Risk berekenen: drie methoden uitgelegd
Bereken de historische, parametrische en Monte Carlo VaR voor SPY rendementen. Ontdek ook de expected shortfall die de VaR methode volledig buiten beschouwing laat.
Value at Risk (VaR) berekenen
Value at Risk, of VaR, wordt berekend door een tijdshorizon en een betrouwbaarheidsniveau vast te stellen, waarna een percentiel wordt afgelezen van een reeks rendementen. Een 1-daagse 99% VaR van twee procent betekent dat het verlies op negenennegentig van de honderd handelsdagen onder de twee procent blijft en op de honderdste dag wordt overschreden. Drie standaardmethoden beantwoorden die vraag op basis van identieke input, en de uitkomsten verschillen meer dan de meeste mensen verwachten.
Wat Value at Risk daadwerkelijk meet
VaR is een kwantiel van een verliesverdeling. Sorteer elk dagrendement in een steekproef van slechtst naar best, loop één procent van de weg vanaf de negatieve kant naar binnen, en het rendement waar u op uitkomt, genoteerd als een positief verlies, is de 1-daagse 99% historische VaR. Niets in die constructie belooft een worst-case scenario. Het markeert de grens van het gebied dat de schatting nog beschrijft.
Dat is de eigenschap die lezers het vaakst verkeerd begrijpen. Een 99% VaR stelt dat de slechtste één procent van de dagen voorbij de drempelwaarde ligt, en zwijgt over hoe ver voorbij die grens. Maximum drawdown beantwoordt een andere vraag, namelijk het verlies van piek naar dal dat een portefeuille daadwerkelijk heeft ondergaan, en beide kunnen hetzelfde paar portefeuilles in een tegengestelde volgorde rangschikken.
Elk cijfer hieronder is gebaseerd op één reeks: de dagelijkse slotkoersen van SPY vanaf het begin van 2010 tot en met het einde van 2025, omgerekend naar procentuele veranderingen van slot tot slot. Het venster is vastgezet in plaats van voortschrijdend, en elk paneel herbouwt de reeks op basis van diezelfde data, zodat de getallen niet verschuiven tussen berekeningen.
De exacte SQL achter elk getal
WITH
px AS
(
SELECT date, max(toFloat64(close)) AS close_px
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2009-12-01'
AND date < '2026-01-01'
GROUP BY date
),
rets AS
(
SELECT date, 100 * (close_px / prev_close - 1) AS ret_pct
FROM
(
SELECT date, close_px,
lagInFrame(close_px, 1) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_close
FROM px
)
WHERE date >= '2010-01-01' AND prev_close > 0
)
SELECT
toString(toYear(date)) AS year,
count() AS sessions,
round(avg(ret_pct), 3) AS mean_return_pct,
round(stddevSamp(ret_pct), 2) AS daily_sigma_pct
FROM rets
GROUP BY year
ORDER BY yearDe reeks beslaat 16 kalenderjaren, met 252 handelssessies in alleen al 2010. Twee kenmerken hiervan zijn van belang voor alles wat volgt. Het gemiddelde per sessie is op deze horizon vrijwel irrelevant: 0.054% in 2010, tegenover een dagelijkse standaarddeviatie, sigma, van 1.13%. En sigma is geen constante. 2020 noteerde 2.11% per dag, 2017 op 0.43%. Eén sigma kan niet beide beschrijven.
Hoe value at risk wordt berekend, drie methoden
Historische VaR: het percentiel aflezen uit het verleden
Sorteer de werkelijke rendementen en bepaal het percentiel. Er wordt geen verdeling verondersteld, wat de aantrekkingskracht van deze methode is. De methode gaat er wel vanuit dat de steekproef al de soort dag bevat die de schatting moet afdekken. Als het betrouwbaarheidsniveau ver genoeg wordt opgeschoven, bepalen alleen de slechtste paar sessies in de gehele periode het resultaat.
Parametrische VaR: gemiddelde minus z maal sigma
Vat de reeks samen met het gemiddelde en de sigma, en veronderstel vervolgens dat de rendementen een normale verdeling volgen. VaR is z maal sigma minus het gemiddelde, waarbij z het standaardnormale kwantiel is: 1,645 bij 95%, 2,326 bij 99%, 3,090 bij 99,9%. De berekening is direct en de aanname faalt in een specifieke richting. Dagelijkse aandelenrendementen clusteren in het midden sterker dan een normale verdeling en reiken aan de uitersten veel verder. Sigma is hier de noemer die ook de Sharpe ratio bepaalt, en deze brengt dezelfde blinde vlek met zich mee voor data met 'fat tails'.
Monte Carlo VaR: simuleren met een vastgelegde seed
Trek een grote synthetische steekproef uit een verondersteld proces en lees het percentiel af van de getrokken waarden. Het onderstaande paneel gebruikt veertigduizend standaardnormale trekkingen, opgebouwd via de Box-Muller-transformatie uit een uniform gegenereerde reeks met een vaste seed, geschaald naar het gemiddelde en de sigma van de reeks. De seed staat in de SQL, waardoor de trekkingen bij elke herberekening identiek zijn. Simulatie biedt flexibiliteit, pad-afhankelijkheid en correlatie tussen meerdere activa, maar geen extra realisme: voer het een normale verdeling en het geeft het parametrische antwoord terug, inclusief steekproefruis. Het opnieuw samplen van de waargenomen rendementen, de techniek achter bootstrapped betrouwbaarheidsintervallen, houdt de werkelijke uitersten in het model.
De exacte SQL achter elk getal
WITH
px AS
(
SELECT date, max(toFloat64(close)) AS close_px
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2009-12-01'
AND date < '2026-01-01'
GROUP BY date
),
rets AS
(
SELECT date, 100 * (close_px / prev_close - 1) AS ret_pct
FROM
(
SELECT date, close_px,
lagInFrame(close_px, 1) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_close
FROM px
)
WHERE date >= '2010-01-01' AND prev_close > 0
),
emp AS
(
SELECT
avg(ret_pct) AS mu,
stddevSamp(ret_pct) AS sd,
quantileExact(0.100)(ret_pct) AS h90,
quantileExact(0.050)(ret_pct) AS h95,
quantileExact(0.025)(ret_pct) AS h975,
quantileExact(0.010)(ret_pct) AS h99,
quantileExact(0.005)(ret_pct) AS h995,
quantileExact(0.001)(ret_pct) AS h999
FROM rets
),
draws AS
(
SELECT
quantileExact(0.100)(z) AS z90,
quantileExact(0.050)(z) AS z95,
quantileExact(0.025)(z) AS z975,
quantileExact(0.010)(z) AS z99,
quantileExact(0.005)(z) AS z995,
quantileExact(0.001)(z) AS z999
FROM
(
SELECT sqrt(-2 * log(u1)) * cos(2 * pi() * u2) AS z
FROM
(
SELECT
(cityHash64('var-seed-u1', i) % 999999937 + 1) / 999999938.0 AS u1,
(cityHash64('var-seed-u2', i) % 999999937 + 1) / 999999938.0 AS u2
FROM (SELECT arrayJoin(range(40000)) AS i)
)
)
)
SELECT
tupleElement(lvl, 1) AS confidence,
round(-1 * tupleElement(lvl, 2), 2) AS historical_var_pct,
round(tupleElement(lvl, 3) * sd - mu, 2) AS parametric_var_pct,
round(-1 * (mu + sd * tupleElement(lvl, 4)), 2) AS monte_carlo_var_pct,
round(-1 * tupleElement(lvl, 2) - (tupleElement(lvl, 3) * sd - mu), 2) AS method_spread
FROM
(
SELECT
mu,
sd,
arrayJoin([
('90.0%', h90, 1.281552, z90, 1),
('95.0%', h95, 1.644854, z95, 2),
('97.5%', h975, 1.959964, z975, 3),
('99.0%', h99, 2.326348, z99, 4),
('99.5%', h995, 2.575829, z995, 5),
('99.9%', h999, 3.090232, z999, 6)
]) AS lvl
FROM emp
CROSS JOIN draws
)
ORDER BY tupleElement(lvl, 5)Op het 95.0%-niveau liggen de drie methoden binnen één punt van elkaar: 1.66% historisch, 1.74% parametrisch, 1.71% gesimuleerd. Op het 99.9%-niveau lopen de resultaten uiteen: 5.85% historisch versus 3.31% parametrisch, een verschil van 2.54 procentpunt op identieke data. De gesimuleerde kolom staat op elk niveau naast de parametrische kolom, wat eerder de les is dan een defect. Een simulatie reproduceert de verdeling die erin is gestopt.
Lees het paneel per kolom en elke stap in betrouwbaarheid verbreedt de drempelwaarde. Lees het per rij en de keuze voor de methode is in het midden van de verdeling nauwelijks merkbaar, terwijl deze aan de uitersten domineert. Een VaR-limiet die wordt geciteerd zonder de bijbehorende methode en periode is geen getal dat iemand anders kan reproduceren.
Wat VaR u niet vertelt: expected shortfall
Expected shortfall, ook wel conditional VaR genoemd, berekent het gemiddelde van de verliezen op de dagen waarop de VaR wordt overschreden. VaR markeert het begin van de staart van de verdeling. Expected shortfall meet wat zich in die staart bevindt.
De exacte SQL achter elk getal
WITH
px AS
(
SELECT date, max(toFloat64(close)) AS close_px
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2009-12-01'
AND date < '2026-01-01'
GROUP BY date
),
rets AS
(
SELECT date, 100 * (close_px / prev_close - 1) AS ret_pct
FROM
(
SELECT date, close_px,
lagInFrame(close_px, 1) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_close
FROM px
)
WHERE date >= '2010-01-01' AND prev_close > 0
),
qs AS
(
SELECT
quantileExact(0.100)(ret_pct) AS q90,
quantileExact(0.050)(ret_pct) AS q95,
quantileExact(0.025)(ret_pct) AS q975,
quantileExact(0.010)(ret_pct) AS q99,
quantileExact(0.005)(ret_pct) AS q995,
quantileExact(0.001)(ret_pct) AS q999
FROM rets
),
tails AS
(
SELECT
any(q90) AS var90,
any(q95) AS var95,
any(q975) AS var975,
any(q99) AS var99,
any(q995) AS var995,
any(q999) AS var999,
avgIf(ret_pct, ret_pct <= q90) AS es90,
avgIf(ret_pct, ret_pct <= q95) AS es95,
avgIf(ret_pct, ret_pct <= q975) AS es975,
avgIf(ret_pct, ret_pct <= q99) AS es99,
avgIf(ret_pct, ret_pct <= q995) AS es995,
avgIf(ret_pct, ret_pct <= q999) AS es999
FROM rets
CROSS JOIN qs
HAVING countIf(ret_pct <= q999) > 0
)
SELECT
tupleElement(lvl, 1) AS confidence,
round(-1 * tupleElement(lvl, 2), 2) AS historical_var_pct,
round(-1 * tupleElement(lvl, 3), 2) AS expected_shortfall_pct,
round(tupleElement(lvl, 3) / tupleElement(lvl, 2), 2) AS es_to_var_ratio
FROM
(
SELECT
arrayJoin([
('90.0%', var90, es90, 1),
('95.0%', var95, es95, 2),
('97.5%', var975, es975, 3),
('99.0%', var99, es99, 4),
('99.5%', var995, es995, 5),
('99.9%', var999, es999, 6)
]) AS lvl
FROM tails
)
ORDER BY tupleElement(lvl, 4)Bij 99.0% bedraagt de VaR voor deze reeks 3.09% en de expected shortfall 4.43%, oftewel 1.44 keer de drempelwaarde. Bij een normale verdeling zou die verhouding op hetzelfde niveau rond de 1,15 liggen. Zelfs bij 99.9%, waar de drempelwaarde al 5.85% heeft bereikt, komt de gemiddelde overschrijdingsdag uit op 8.14%. Een limiet die uitsluitend op VaR is gebaseerd, behandelt elke overschrijding als een identieke gebeurtenis, en de kolom met de verhoudingen geeft aan in hoeverre dat onjuist is.
Betekent 1-daagse 99% VaR hetzelfde voor een institutionele belegger en een trader?
Nee, en het verschil zit hem grotendeels in de tijdshorizon. Een trader die zijn positie 's nachts sluit (flat gaat), loopt risico gedurende enkele uren; een drempelwaarde van één sessie sluit dus aan bij de aanhoudperiode, hoewel het intraday koersverloop aanzienlijk verder kan afwijken dan een cijfer op basis van slotkoersen. Een institutionele partij die langlopende verplichtingen financiert, houdt posities jarenlang aan, de blootstelling beslaat kwartalen, en het 1-daagse 99%-cijfer fungeert eerder als een instrument voor kapitaalvereisten en monitoring dan als een beschrijving van het werkelijke risico. Bankkapitaalregels waren jarenlang gebaseerd op een 1-daagse 99% VaR, waarna het Basel-raamwerk voor marktrisico de maatstaf later heeft verlegd naar een 97,5% expected shortfall.
De standaardbrug tussen tijdshorizonten is het schalen met de wortel uit de tijd: vermenigvuldig het cijfer van één sessie met de wortel uit de horizon in sessies. Die stap veronderstelt dat rendementen onafhankelijk zijn met een constante sigma, en de kolom met jaar-op-jaar sigma hierboven laat al zien dat aan die tweede voorwaarde niet wordt voldaan.
De exacte SQL achter elk getal
WITH
px AS
(
SELECT date, max(toFloat64(close)) AS close_px
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2009-11-01'
AND date < '2026-01-01'
GROUP BY date
),
multi AS
(
SELECT
date,
100 * (close_px / p1 - 1) AS r1,
100 * (close_px / p5 - 1) AS r5,
100 * (close_px / p10 - 1) AS r10,
100 * (close_px / p20 - 1) AS r20
FROM
(
SELECT
date,
close_px,
lagInFrame(close_px, 1) OVER (ORDER BY date ROWS BETWEEN 20 PRECEDING AND CURRENT ROW) AS p1,
lagInFrame(close_px, 5) OVER (ORDER BY date ROWS BETWEEN 20 PRECEDING AND CURRENT ROW) AS p5,
lagInFrame(close_px, 10) OVER (ORDER BY date ROWS BETWEEN 20 PRECEDING AND CURRENT ROW) AS p10,
lagInFrame(close_px, 20) OVER (ORDER BY date ROWS BETWEEN 20 PRECEDING AND CURRENT ROW) AS p20
FROM px
)
WHERE date >= '2010-01-01' AND p20 > 0
),
q AS
(
SELECT
quantileExact(0.01)(r1) AS q1,
quantileExact(0.01)(r5) AS q5,
quantileExact(0.01)(r10) AS q10,
quantileExact(0.01)(r20) AS q20
FROM multi
)
SELECT
tupleElement(h, 1) AS horizon,
round(-1 * tupleElement(h, 3), 2) AS actual_var_pct,
round(-1 * q1 * sqrt(tupleElement(h, 2)), 2) AS sqrt_scaled_var_pct,
round(tupleElement(h, 3) / (q1 * sqrt(tupleElement(h, 2))), 2) AS actual_to_scaled_ratio
FROM
(
SELECT
q1,
arrayJoin([
('1 session', 1.0, q1, 1),
('5 sessions', 5.0, q5, 2),
('10 sessions', 10.0, q10, 3),
('20 sessions', 20.0, q20, 4)
]) AS h
FROM q
)
ORDER BY tupleElement(h, 4)De eerste rij is een identiteitscontrole: bij 1 session is het geschaalde cijfer gelijk aan het gemeten cijfer, een ratio van 1. Bij 20 sessions lopen de twee uiteen, en niet in de richting waarvoor bij de kortere weg gewoonlijk wordt gewaarschuwd: het schalen van het cijfer van één sessie geeft 13.8%, terwijl de rendementen gemeten over die horizon uitkomen op 10.7%, een ratio van 0.78. In dit venster ligt het geschaalde cijfer boven de gemeten tail over meerdere sessies. Twee patronen werken hier tegen elkaar in. Een single-session verdeling met dikke tails wordt dunner naarmate rendementen worden opgeteld, en het geaggregeerde 99%-kwantiel wordt vervolgens minder breed dan de wortel uit de horizon. Volatility clustering werkt de andere kant op en stapelt volatiele sessies binnen één venster. In deze reeks is het eerste patroon dominanter, en geen van beide richtingen is gegarandeerd in een ander venster of bij een ander activum; dat is precies het punt: de vermenigvuldiger is een aanname, geen meting. Die clustering is ook de eigenschap waar volatility targeting op leunt bij het aanpassen van de positiegrootte.
Eén verdere beperking, helder gesteld. Alles hierboven is VaR voor één enkel activum in een breed indexfonds. Een portefeuille van tien gecorreleerde namen draagt concentratierisico met zich mee dat een portefeuille-VaR afhandelt via een correlatieschatting, en correlaties bewegen het meest op het moment dat de schatting er echt toe doet.
Methodische opmerkingen en conventies
- Percentielen zijn afkomstig van een exact kwantiel in plaats van een steekproefschatting, en de Monte Carlo-trekkingen komen uit een uniform verdeelde reeks met een hash-seed die in de SQL is vastgelegd, waardoor elk cijfer hier opnieuw naar dezelfde waarde berekent.
- Rendementen zijn koersveranderingen van slot tot slot zonder herbelegging van dividenden, de gebruikelijke conventie voor een VaR van één sessie.
- Het horizon-paneel maakt gebruik van overlappende vensters: opeenvolgende rendementen over 20 sessies delen 19 dagen, waardoor de tail rust op veel minder onafhankelijke observaties dan het aantal rijen suggereert.
Veelgestelde vragen
Wat is een 1-daagse 99% VaR?
Dit is het verliesniveau dat op de slechtste één van de honderd handelsdagen wordt overschreden, gemeten over één sessie. In de bovenstaande reeks bedraagt de historische schatting 3.09%. Dit cijfer geeft een drempelwaarde aan en zegt niets over de omvang van de verliezen die daarboven liggen.
Welke VaR-berekeningsmethode is het meest accuraat?
Geen van de drie is in abstracte zin accuraat, aangezien elke methode de vraag beantwoordt op basis van verschillende aannames. Historische VaR is getrouw aan de gebruikte steekproef en zegt niets over gebeurtenissen die buiten de steekproef vallen. Parametrische VaR is goedkoop en onderschat de 'tails' van aandelenrendementen. Monte Carlo is slechts zo goed als de verdeling die als input wordt gebruikt.
Wat is het verschil tussen VaR en expected shortfall?
VaR is de drempelwaarde bij een gekozen betrouwbaarheidsniveau. Expected shortfall berekent het gemiddelde van de verliezen op de dagen dat deze drempel wordt overschreden. In deze reeks bedraagt de expected shortfall op het 99%-niveau 1.44 keer de VaR, tegenover ruwweg 1,15 keer bij een normale verdeling.
Kunt u een 1-daagse VaR omrekenen naar een 10-daagse VaR?
Vermenigvuldigen met de wortel van tien is de standaardmethode; hierbij wordt ervan uitgegaan dat rendementen onafhankelijk zijn en een constante volatiliteit hebben. Het horizon-paneel meet het verschil: over twintig sessies kwam het geschaalde cijfer uit op 13.8% tegenover een gemeten 10.7%.
Elk bovenstaand paneel bevat de SQL-code die het heeft gegenereerd. Om dezelfde drie berekeningen uit te voeren voor een andere ticker of periode, kunt u dit in begrijpelijke taal opvragen via de Strasmore-terminal.