Ryzyko portfela: VaR i CVaR metodą Monte Carlo

Portfel 5 spółek z GPW, 100 000 PLN, dane 2015-2026

Opublikowano

13 października 2026

Ostrzeżenie

Projekt edukacyjny. Raport ilustruje metody pomiaru ryzyka na danych historycznych. Nie jest rekomendacją inwestycyjną ani oceną spółek.

1 Streszczenie

  • Portfel: po 20% w PKO BP, PZU, CD Projekt, KGHM i Orlen, wartość 100 000 PLN. Dane dzienne z Yahoo Finance, 2015-01-05 – 2026-10-02 (2938 sesji).
  • Jednodniowy VaR 99% wynosi od 3 395 PLN (rozkład normalny) do 3 741 PLN (Monte Carlo z t-Studentem). Metody różnią się niewiele przy VaR, ale bardzo przy CVaR 99%: normalny 3 887 PLN, historyczny 5 049 PLN. Rozkład normalny zaniża średnią stratę w najgorszym 1% dni o około 23%.
  • Zwroty mają grube ogony (kurtoza nadwyżkowa portfela 3,80) i skupiska zmienności (silna autokorelacja kwadratów zwrotów).
  • Horyzont 10 dni: metody zakładające niezależne dni (normalna, MC) dają VaR 99% ok. 10 068 PLN, a historyczne 10-dniowe okna 12 077 PLN. Spadki przychodzą seriami, więc metody zakładające niezależne dni zaniżają ryzyko wielodniowe o ok. 15–20%.
  • Backtest (2438 dni, okno 500): przy 99% oczekujemy 24,4 przekroczeń. Normalny ma 42 (test Kupca odrzuca), historyczny i MC-t po 32 (Kupiec przechodzi, ale przekroczenia tworzą skupiska: Christoffersen odrzuca). GARCH najlepiej reaguje na marzec 2020, ale jego podstawowa wersja przy 99% też ma za dużo przekroczeń.

2 Dane i kontrola jakości

Surowe dane z Yahoo Finance (przed czyszczeniem)
ticker pierwszy_dzien ostatni_dzien wiersze braki_adjusted wolumen_zero ujemne_lub_zero_ceny
PKO.WA 2015-01-01 2026-10-12 2995 1 55 0
PZU.WA 2015-01-01 2026-10-12 2995 1 55 0
CDR.WA 2015-01-01 2026-10-12 2995 1 55 0
KGH.WA 2015-01-01 2026-10-12 2995 1 55 0
PKN.WA 2015-01-01 2026-10-12 2995 1 55 0

Co znalazłem i jak to obsłużyłem:

  1. Sztuczne wiersze z zerowym wolumenem (55 dni, wspólne dla wszystkich spółek): święta GPW do 2020 r. (Yahoo wstawiało cenę z poprzedniego dnia) oraz kilka pojedynczych dziur. Cena się w nie nie zmienia, więc usunąłem je. Zostawienie ich wstawiłoby sztuczne zerowe zwroty i zaniżyło zmienność.
  2. Dziura na końcu próby: 5–9.10.2026 cała seria jest płaska z zerowym wolumenem, a 12.10.2026 to niepełna sesja (brak ceny zamknięcia). Próbę kończę na 2026-10-02, ostatnim wiarygodnym dniu.
  3. Splity: Yahoo zgłasza tylko split PZU 1:10 (30.11.2015). Ceny są już o niego skorygowane (brak skoku w szeregu).
  4. Dywidendy: cena Adjusted uwzględnia dywidendy. W 38 dniach zwrot z Adjusted różni się od zwrotu z Close o więcej niż 1 pp. To dni odcięcia dywidendy (PZU i Orlen co roku, PKO BP, KGHM). Bez korekty liczylibyśmy dywidendę jako stratę.
  5. Luki między sesjami: najdłuższa przerwa to 6 dni kalendarzowych. Wszystkie luki powyżej 4 dni to Wielkanoc i Boże Narodzenie.
  6. Obserwacje odstające (|zwrot| > 6 odchyleń standardowych) to prawdziwe zdarzenia, więc je zostawiłem:
Zwroty dzienne powyżej 6 sd (opisy zdarzeń: kontekst rynkowy, nie analiza przyczyn)
ticker data zwrot ile_sd kontekst
PKO BP 2020-03-12 -0.1387 -6.8 COVID, krach na rynkach
PKO BP 2022-02-24 -0.1842 -9.0 Inwazja Rosji na Ukrainę
PKO BP 2022-02-25 0.1246 6.1 Odbicie po 24.02.2022
PKO BP 2025-08-22 -0.1304 -6.4 zdarzenie specyficzne dla spółki
PZU 2020-03-12 -0.1180 -6.9 COVID, krach na rynkach
CD Projekt 2015-06-09 0.1863 7.0 zdarzenie specyficzne dla spółki
CD Projekt 2021-01-29 -0.1789 -6.7 zdarzenie specyficzne dla spółki
KGHM 2020-03-12 -0.1742 -6.7 COVID, krach na rynkach
Orlen 2020-11-09 0.1345 6.4 Ogłoszenie skuteczności szczepionki, rajd
Rysunek 1: Ceny skorygowane, indeks 100 na starcie

3 Eksploracja zwrotów

Statystyki dziennych zwrotów logarytmicznych
seria srednia_dzienna zmiennosc_roczna min max skosnosc kurtoza_nadwyzkowa JB_p
PKO BP 0.00053 0.324 -0.184 0.1250 -0.3170 5.64 0
PZU 0.00039 0.270 -0.118 0.0823 -0.1310 2.69 0
CD Projekt 0.00095 0.424 -0.179 0.1860 -0.0959 4.34 0
KGHM 0.00043 0.413 -0.174 0.1100 -0.0797 2.30 0
Orlen 0.00056 0.332 -0.101 0.1340 -0.0363 1.86 0
Portfel 0.00057 0.240 -0.130 0.0843 -0.3050 3.80 0

Każda spółka i portfel mają ujemną skośność i kurtozę nadwyżkową wyraźnie powyżej 0. Test Jarque-Bera odrzuca normalność przy każdym rozsądnym poziomie istotności. Dywersyfikacja działa: zmienność portfela (24,0% rocznie) jest niższa niż zmienność każdej spółki z osobna.

Liczba dni ze zwrotem poniżej −3 sd vs liczba oczekiwana w rozkładzie normalnym
seria dni_ponizej_3sd oczekiwane_normalny
PKO BP 18 4
PZU 15 4
CD Projekt 27 4
KGHM 13 4
Orlen 17 4
Portfel 16 4

Autokorelacja. Same zwroty portfela prawie nie mają pamięci (Ljung-Box, 10 opóźnień: p = 0,123), ale ich kwadraty mają silną i długą autokorelację. To typowy efekt skupisk zmienności (ARCH): po dniu dużej zmiany częściej przychodzi kolejny dzień dużej zmiany. Właśnie to założenie łamią wszystkie metody „iid” i dlatego w rozdziale 7 testuję GARCH.

Korelacje. Średnia korelacja par spółek to 0,34, ale w 20% dni o najwyższej zmienności (mierzonej na poprzednich 21 sesjach) rośnie do 0,41. Dywersyfikacja słabnie właśnie wtedy, gdy jest najbardziej potrzebna.

4 VaR i CVaR: metoda historyczna i parametryczna

Strata w PLN to \(L = V\,(1 - e^{r})\), gdzie \(r\) to logarytmiczny zwrot portfela w horyzoncie, a \(V\) = 100 000 PLN. VaR na poziomie \(\alpha\) to strata, której nie przekroczymy z prawdopodobieństwem \(\alpha\). CVaR (Expected Shortfall) to średnia strata w dniach, w których VaR został przekroczony.

  • Historyczna: kwantyl empiryczny zwrotów portfela. Dla 10 dni używam zachodzących na siebie 10-dniowych okien.
  • Parametryczna normalna: \(r_h \sim N(h\mu,\, h\sigma^2)\). CVaR liczony dokładnym wzorem dla straty \(V(1-e^r)\) przy normalnym \(r\).
Rysunek 2
Kod: VaR/CVaR z próby i z rozkładu normalnego (R/ryzyko.R)
var_cvar_probka
function (r, alfa, V = WARTOSC) 
{
    q <- quantile(r, 1 - alfa, names = FALSE, type = 7)
    c(VaR = strata_pln(q, V), CVaR = mean(strata_pln(r[r <= q], 
        V)))
}
Kod: VaR/CVaR z próby i z rozkładu normalnego (R/ryzyko.R)
var_cvar_normalny
function (mu, sigma, alfa, h = 1, V = WARTOSC) 
{
    m <- mu * h
    s <- sigma * sqrt(h)
    p <- 1 - alfa
    q <- m + qnorm(p) * s
    e_exp <- exp(m + s^2/2) * pnorm((q - m - s^2)/s)/p
    c(VaR = strata_pln(q, V), CVaR = V * (1 - e_exp))
}

5 Monte Carlo

Symuluję dzienne zwroty 5 spółek z zachowaniem korelacji (rozkład Choleskiego macierzy kowariancji \(\Sigma = U^\top U\), \(X = \mu + Z U\)), liczę zwrot portfela i sumuję dni w horyzoncie. Trzy warianty:

  1. MC normalny: \(Z \sim N(0, I)\). Służy jako kontrola poprawności, bo musi zbiec do wzoru parametrycznego.
  2. MC t-Student: wielowymiarowy rozkład t ze wspólnym mianownikiem \(\sqrt{(\nu-2)/W}\), \(W\sim\chi^2_\nu\). Krach dotyka wtedy wszystkich spółek naraz (zależność w ogonach), a kowariancja dalej równa się \(\Sigma\). Stopnie swobody \(\nu\) estymuję metodą największej wiarygodności: ν = 5,97.
  3. MC bootstrap: losowanie całych historycznych dni ze zwracaniem. Zachowuje rzeczywiste ogony i korelacje, ale nie zachowuje kolejności dni.
Stopnie swobody t-Studenta (MLE): wielowymiarowy i jednowymiarowe
seria nu
Wielowymiarowy t (5 spółek, MLE) 5.97
PKO.WA PKO BP 4.90
PZU.WA PZU 5.26
CDR.WA CD Projekt 3.45
KGH.WA KGHM 5.31
PKN.WA Orlen 5.88
Portfel Portfel 5.79

Wszystkie symulacje są wektorowe: jedna macierz \(n \cdot h \times 5\) i mnożenie macierzy, bez pętli po scenariuszach. Ziarno: set.seed(2.0261013\times 10^{7}), \(N\) = 100 000.

Kod: symulacja t-Studenta (R/mc.R)
sim_mc_t
function (n, mu, Sigma, nu, w = WAGI, h = 1) 
{
    U <- chol(Sigma)
    m <- n * h
    Z <- matrix(rnorm(m * length(mu)), ncol = length(mu))
    skala <- sqrt((nu - 2)/rchisq(m, df = nu))
    X <- sweep((Z %*% U) * skala, 2, mu, "+")
    sumuj_horyzont(as.numeric(X %*% w), n, h)
}

Kontrola poprawności. Przy \(10^6\) symulacji MC normalny trafia we wzór parametryczny, a MC-t w dokładny kwantyl rozkładu t. Portfel z wielowymiarowego t o wspólnym ν ma bowiem jednowymiarowy rozkład t. Błędy są poniżej 1%:

MC (10^6 symulacji) vs wartość dokładna; błąd w %
model alfa horyzont VaR_MC VaR_wzor CVaR_MC CVaR_wzor blad_VaR_proc blad_CVaR_proc
normalny 0.95 1 2393.07 2396.22 3008.76 3008.31 -0.13 0.01
normalny 0.99 1 3399.33 3394.99 3895.84 3886.75 0.13 0.23
normalny 0.95 10 7004.68 7020.33 8829.09 8842.08 -0.22 -0.15
normalny 0.99 10 9974.68 9995.95 11419.65 11430.21 -0.21 -0.09
t-Student 0.95 1 2310.76 2309.64 3217.58 3225.52 0.05 -0.25
t-Student 0.99 1 3733.02 3745.13 4762.37 4790.54 -0.32 -0.59
Rysunek 3
Rysunek 4

CVaR zbiega wolniej niż VaR, bo zależy od kilku najbardziej skrajnych scenariuszy. Przy 99% potrzeba ok. 104–105 symulacji, żeby błąd spadł do ~1%. Błąd maleje jak \(1/\sqrt{N}\): dziesięć razy dokładniej oznacza sto razy więcej symulacji.

6 Porównanie metod

VaR i CVaR [PLN] dla portfela 100 000 PLN (kolumny: miara_poziom_horyzont)
metoda VaR_95_1d CVaR_95_1d VaR_99_1d CVaR_99_1d VaR_95_10d CVaR_95_10d VaR_99_10d CVaR_99_10d
Historyczna 2 324 3 262 3 652 5 049 6 850 10 378 12 077 16 585
Normalna 2 396 3 008 3 395 3 887 7 020 8 842 9 996 11 430
MC normalny 2 400 3 019 3 416 3 895 7 050 8 900 10 068 11 550
MC t-Student 2 301 3 222 3 741 4 797 6 992 8 967 10 211 11 850
MC bootstrap 2 312 3 264 3 611 5 069 7 056 9 221 10 496 12 628

Wnioski z porównania

  • Przy 95% wszystkie metody dają prawie to samo (2 301 PLN–2 400 PLN). Wybór metody ma znaczenie dopiero w ogonie.
  • Przy 99% rozkład normalny zaniża VaR o kilka procent, a CVaR o ~23%. MC-t i bootstrap odtwarzają gruby ogon.
  • Przy 10 dniach metody MC (t, bootstrap) zbliżają się do normalnej, bo suma 10 niezależnych dni „normalnieje” (CLT). Historyczne 10-dniowe straty są wyższe, bo dni nie są niezależne: spadki przychodzą seriami. Lepszy model niezależnych dni tego nie naprawi, potrzebny jest model zmiennej zmienności.

7 Backtest (okno kroczące 500 dni)

Dla każdego dnia \(t\) od 2016-12-30 liczę VaR 1-dniowy wyłącznie z 500 poprzednich dni (\(t-500, \dots, t-1\)). Przekroczenie to dzień, w którym rzeczywisty zwrot był niższy niż prognozowany kwantyl. Brak look-ahead bias sprawdzam w kodzie dwoma sposobami: (1) po „zepsuciu” danych od dnia 1500 prognozy na wcześniejsze dni są bit w bit identyczne, (2) liczby przekroczeń i statystyki LR zgadzają się z rugarch::VaRTest.

  • Test Kupca (POF): czy odsetek przekroczeń jest równy \(1-\alpha\).
  • Test Christoffersena: czy przekroczenia są niezależne (nie tworzą skupisk) oraz test łączny (conditional coverage).
Backtest VaR 1-dniowego, wszystkie metody (p < 0,05 = model odrzucony)
metoda alfa dni przekr. oczek. p Kupiec przekr. dzień po dniu p niezależność p łączny śr. VaR [PLN]
Historyczna 0.95 2438 123 121.9 0.919 10 0.137 0.3301 2367
Historyczna 0.99 2438 32 24.4 0.139 3 0.008 0.0098 3626
Normalna 0.95 2438 119 121.9 0.787 12 0.017 0.0549 2426
Normalna 0.99 2438 42 24.4 0.001 4 0.006 0.0001 3434
MC t-Student 0.95 2438 128 121.9 0.574 12 0.050 0.1258 2348
MC t-Student 0.99 2438 32 24.4 0.139 4 0.001 0.0010 3751
GARCH-norm 0.95 2438 131 121.9 0.403 9 0.453 0.5318 2336
GARCH-norm 0.99 2438 43 24.4 0.001 3 0.045 0.0004 3316
GARCH-t 0.95 2438 133 121.9 0.309 10 0.306 0.3528 2308
GARCH-t 0.99 2438 37 24.4 0.017 3 0.019 0.0037 3581

Interpretacja

  • Normalna 99%: 42 przekroczeń zamiast ~24. Test Kupca odrzuca (p = 0,001). Rozkład normalny ma za cienkie ogony.
  • Historyczna i MC-t 99%: liczba przekroczeń jest do przyjęcia (Kupiec p = 0,14), ale przekroczenia przychodzą seriami (Christoffersen odrzuca niezależność). Model ma dobrą średnią, ale zły moment: w spokojnych okresach przeszacowuje ryzyko, a w kryzysie je zaniża.
  • Przy 95% wszystkie metody przechodzą test Kupca. Wtedy liczy się głównie środek rozkładu, a nie ogon.

7.1 Okres stresu: marzec 2020

Marzec 2020: przekroczenia VaR (oczekiwane przy poprawnym modelu: 0,22 dla 99%, 1,1 dla 95%)
metoda alfa dni przekroczenia oczekiwane p_dwumian VaR_srednio_PLN
Historyczna 0.95 22 7 1.10 6.8e-05 2389
Historyczna 0.99 22 4 0.22 6.3e-05 4124
Normalna 0.95 22 7 1.10 6.8e-05 2490
Normalna 0.99 22 5 0.22 2.3e-06 3491
MC t-Student 0.95 22 7 1.10 6.8e-05 2450
MC t-Student 0.99 22 5 0.22 2.3e-06 3709
GARCH-norm 0.95 22 3 1.10 9.5e-02 5139
GARCH-norm 0.99 22 3 0.22 1.3e-03 7195
GARCH-t 0.95 22 3 1.10 9.5e-02 5198
GARCH-t 0.99 22 3 0.22 1.3e-03 7785

W marcu 2020 r. wszystkie modele bez zmiennej zmienności miały 4–5 przekroczeń VaR 99% w 22 sesjach (oczekiwane 0,22). Okno 500 dni zawiera prawie wyłącznie spokojny okres, więc jeden krach prawie nie zmienia kwantyla. VaR historyczny skoczył dopiero wtedy, gdy krach z 12.03 sam znalazł się w oknie.

8 Rozszerzenie: GARCH(1,1)

Model \(\sigma_t^2 = \omega + \alpha\,\varepsilon_{t-1}^2 + \beta\,\sigma_{t-1}^2\) (pakiet rugarch) z innowacjami normalnymi lub t-Studenta. Estymacja w tym samym oknie kroczącym 500 dni, reestymacja co 20 dni (ugarchroll, refit.window = "moving"). Między reestymacjami zmienność aktualizuje się codziennie nowymi danymi, ale parametry pozostają stałe. W kodzie sprawdzam, że każde okno estymacji kończy się przed pierwszym dniem, na który model prognozuje.

Przekroczenia VaR 99% w podziale na lata
rok Historyczna Normalna MC t-Student GARCH-norm GARCH-t dni
2016 0 0 0 0 0 1
2017 1 1 1 1 1 250
2018 9 8 6 8 5 246
2019 0 1 0 3 3 248
2020 9 13 11 11 10 252
2021 0 2 0 2 2 251
2022 4 4 4 4 4 251
2023 0 0 0 0 0 250
2024 3 3 1 4 4 249
2025 5 5 5 5 5 249
2026 1 5 4 5 3 191

Co daje GARCH

  • W marcu 2020 GARCH miał 3 przekroczenia zamiast 4–5, a jego VaR 99% wzrósł w ciągu kilku dni do ponad 10 000 PLN. Na tym polega wartość zmiennej zmienności: model ostrzega już w trakcie kryzysu, a nie dopiero po nim.
  • Przy 95% GARCH ma najwyższe p-wartości testu niezależności (0,45 i 0,31), a model normalny bez GARCH jest w tym teście odrzucany. Przekroczenia przestają się grupować.
  • Przy 99% podstawowy GARCH(1,1)-t ma jednak 37 przekroczeń (Kupiec p = 0,017). Średnio jest niższy niż VaR historyczny, bo w spokojnych latach szybko „zapomina” o ryzyku. Dodatkowo estymowana średnia dzienna z hossy (np. 2025–2026) obniża prognozowany kwantyl.

Odporność specyfikacji GARCH-t (wariant główny ustalony z góry: sGARCH ze stałą w średniej):

Warianty GARCH-t w backteście
model srednia_w_modelu alfa przekroczenia oczekiwane p_kupiec p_christoffersen_cc
sGARCH TRUE 0.95 133 121.90 0.309 0.353
sGARCH TRUE 0.99 37 24.38 0.017 0.004
gjrGARCH TRUE 0.95 124 121.90 0.846 0.942
gjrGARCH TRUE 0.99 34 24.38 0.065 0.144
sGARCH FALSE 0.95 123 121.90 0.919 0.942
sGARCH FALSE 0.99 35 24.38 0.042 0.006
gjrGARCH FALSE 0.95 115 121.90 0.518 0.785
gjrGARCH FALSE 0.99 32 24.38 0.139 0.248

Oba warianty asymetrycznego GJR-GARCH (spadki zwiększają zmienność mocniej niż wzrosty) przechodzą testy Kupca i Christoffersena na obu poziomach przy α = 5%. Najlepiej wypada wariant bez stałej w średniej. Ten wariant wybrałem jednak po obejrzeniu wyników, więc traktuję go jako wskazówkę do dalszej pracy, a nie dowód.

Bieżąca prognoza GARCH-t (ostatnie 500 dni, stan na 2026-10-02):

GARCH-t: VaR/CVaR na kolejny dzień i 10 dni (10 dni: 50 000 symulowanych ścieżek GARCH)
metoda alfa horyzont VaR CVaR sigma_dzienna_teraz sigma_dzienna_dlugookresowa
GARCH-t 0.95 1 1783 2364 1,21% 1,37%
GARCH-t 0.99 1 2718 3257 1,21% 1,37%
GARCH-t 0.95 10 4608 6352 1,21% 1,37%
GARCH-t 0.99 10 7373 8910 1,21% 1,37%

Bieżąca zmienność (1,21% dziennie) jest niższa niż poziom długookresowy wynikający z parametrów modelu i niż średnia z 2015–2026 (1,51%), więc GARCH podaje dziś niższy VaR niż metody statyczne. To zgodne z modelem, ale niesie ryzyko: niska zmienność potrafi się skończyć z dnia na dzień.

9 Ograniczenia

  • Przeszłość ≠ przyszłość. Wszystkie metody estymują ryzyko z historii 2015–2026 (lub z ostatnich 500 dni). Szok spoza próby (np. ryzyko regulacyjne spółek Skarbu Państwa) nie jest w nich uwzględniony.
  • Zwrot portfela jako \(w^\top r\) (średnia ważona zwrotów log) to przybliżenie dokładnego zwrotu portfela o stałych wagach. Dla 1 dnia VaR 99% z przybliżenia jest wyższy o 39 PLN (ok. 1%), czyli przybliżenie jest lekko konserwatywne.
  • Założenie iid w metodach historycznej, normalnej i MC. Kwadraty zwrotów są silnie autokorelowane, więc wielodniowy VaR z tych metod jest zaniżony (patrz porównanie 10 dni).
  • Stała korelacja. W kryzysie korelacje rosną (0,30 → 0,41), a żaden z modeli tego nie modeluje (potrzebny byłby np. DCC-GARCH lub kopuły).
  • Wspólne ν w MC-t wymusza ten sam „ogon” dla wszystkich spółek, choć CD Projekt ma wyraźnie grubszy ogon (ν ≈ 3,5) niż pozostałe.
  • Historyczne okna 10-dniowe nakładają się, więc obserwacje nie są niezależne. Efektywna liczba niezależnych obserwacji jest ok. 10 razy mniejsza.
  • Dane Yahoo Finance: brak oficjalnego źródła GPW do porównania (stooq blokuje pobieranie automatyczne), więc ceny skorygowane zależą od metody korekty Yahoo. Nie uwzględniam kosztów transakcyjnych, płynności ani podatków.
  • Backtest 1-dniowy: testuję tylko VaR 1-dniowy. CVaR i VaR 10-dniowy nie mają tu formalnego backtestu.
  • Wariant GJR-GARCH, który wypadł najlepiej, wybrałem po obejrzeniu danych (ryzyko przeuczenia).
Ostrzeżenie

Projekt edukacyjny. Wyniki nie stanowią rekomendacji inwestycyjnej.