Риск-менеджмент

Как рассчитать VaR портфеля: три метода с кодом

VaR (Value at Risk) отвечает на простой вопрос: какой убыток портфель не превысит с заданной вероятностью на выбранном горизонте. Ниже — три стандартных способа рассчитать его, с исполнимым кодом, корректными допущениями и тем, чего VaR принципиально не показывает.

Сначала важная развилка. Не путайте VaR (Value at Risk) — метрику рыночного риска — с VAR (векторной авторегрессией), эконометрической моделью временных рядов. Это разные вещи с похожим написанием; о второй у нас отдельная статья. Здесь речь только о Value at Risk.

Что такое VaR и чего он не показывает

VaR — это квантиль распределения убытков портфеля на заданном горизонте при выбранном уровне доверия. Формулировка «однодневный VaR при уровне доверия 99% равен 1,5 млн ₽» означает: с вероятностью 99% убыток за один день не превысит 1,5 млн ₽, либо, что то же самое, лишь в 1% дней убыток окажется больше этой величины. Три параметра задают VaR полностью: горизонт (1 день, 10 дней), уровень доверия (95%, 99%) и валюта/единица измерения позиции. Менять любой из них без оговорки нельзя — VaR 95% и VaR 99% это разные числа, и сравнивать их напрямую бессмысленно.

Выбор уровня доверия и горизонта — не технический, а управленческий вопрос. Уровень 99% и горизонт 10 дней исторически закреплены в банковском регулировании (надзорные требования к рыночному риску), тогда как для внутреннего управления портфелем чаще удобнее 95% и один день: на коротком горизонте у вас просто больше превышений, а значит больше данных для проверки модели. Чем выше уровень доверия, тем дальше в хвост распределения уходит оценка и тем меньше наблюдений её подкрепляют — оценка 99,9%-квантиля по году дневных данных опирается буквально на единицы точек, поэтому крайние уровни доверия требуют либо очень длинной истории, либо явной модели хвоста.

Ключевое ограничение: VaR — это порог, а не ожидание убытка за этим порогом. Он ничего не говорит о том, насколько тяжёлым окажется убыток в тех самых «плохих» процентах случаев. Два портфеля могут иметь одинаковый VaR, но совершенно разный размер катастрофического хвоста. Поэтому VaR дополняют метрикой Expected Shortfall (ES, она же CVaR) — средним убытком при условии, что порог VaR превышен. У ES есть важное теоретическое преимущество: это когерентная мера риска (в частности, субаддитивная), то есть риск диверсифицированного портфеля у неё не превышает суммы рисков компонентов. VaR в общем случае субаддитивностью не обладает, поэтому регуляторная практика (например, FRTB Базельского комитета) сместилась в сторону ES. Считать VaR по-прежнему полезно, но трактовать его в отрыве от ES не стоит.

Главное: VaR — это квантиль убытка («порог потерь»), а не их среднее за порогом. Всегда смотрите его вместе с Expected Shortfall.

Ещё одна общая для всех методов величина — это знак. По соглашению VaR приводят как положительное число, обозначающее размер убытка, хотя считается он по левому хвосту распределения доходностей (то есть по отрицательным значениям). В коде ниже мы последовательно берём квантиль доходностей и переводим его в положительный убыток умножением на −1; путаница в знаке — одна из самых частых причин расхождений между двумя реализациями одного и того же метода.

Ниже все три метода используют одни и те же синтетические данные — это иллюстративный пример, числа условны и не относятся к какому-либо реальному портфелю. На практике вместо генератора подставляются исторические доходности ваших инструментов. Мы сознательно генерируем данные из нормального распределения: на таких данных три метода должны давать близкие оценки, и это удобный самопроверочный признак корректности реализации. На реальных рядах с тяжёлыми хвостами оценки начнут расходиться — и именно характер расхождения подскажет, какое допущение нарушено.

import numpy as np
import pandas as pd
from scipy.stats import norm

rng = np.random.default_rng(42)

# Синтетический портфель: 3 актива, 1000 дневных доходностей (иллюстративно)
n_days, n_assets = 1000, 3
mu = np.array([0.0004, 0.0002, 0.0006])
vols = np.array([0.012, 0.018, 0.025])
corr = np.array([[1.0, 0.3, 0.2],
                 [0.3, 1.0, 0.5],
                 [0.2, 0.5, 1.0]])
cov = np.outer(vols, vols) * corr
returns = pd.DataFrame(rng.multivariate_normal(mu, cov, size=n_days),
                       columns=["A", "B", "C"])

# Веса и стоимость портфеля
weights = np.array([0.4, 0.35, 0.25])
portfolio_value = 10_000_000          # условная стоимость портфеля, ₽
port_returns = returns.values @ weights   # дневные доходности портфеля

alpha = 0.99                          # уровень доверия

Метод 1. Исторический VaR

Самый прямой подход: берём фактические доходности портфеля за окно наблюдений и находим эмпирический квантиль распределения убытков. Если доверие 99%, то VaR — это 1-й процентиль доходностей (умноженный на стоимость портфеля и взятый по модулю как величина убытка). Никаких предположений о форме распределения здесь не делается — хвост берётся таким, каким он был в данных.

def historical_var(port_returns, alpha, value):
    # Квантиль доходностей на уровне (1 - alpha): левый хвост
    q = np.quantile(port_returns, 1 - alpha)
    var = -q * value          # убыток как положительное число
    # Expected Shortfall: средний убыток за порогом
    tail = port_returns[port_returns <= q]
    es = -tail.mean() * value
    return var, es

var_h, es_h = historical_var(port_returns, alpha, portfolio_value)
print(f"Исторический VaR 99%: {var_h:,.0f} ₽")
print(f"Исторический ES 99%:  {es_h:,.0f} ₽")

Плюсы: не требует допущения о распределении, естественно захватывает асимметрию и тяжёлые хвосты, если они есть в выборке. Прозрачен и легко объясним. Минусы: полностью зависит от выбранного окна — короткое окно даёт шумную оценку, длинное смешивает разные режимы рынка. Метод не экстраполирует хвост: VaR не может оказаться больше худшего наблюдённого убытка, поэтому события, которых ещё не было в окне, он по построению недооценивает. Ещё одна тонкость — реакция на смену режима: в простой версии все наблюдения в окне равноправны, и после периода спокойствия модель медленно повышает оценку риска. Чтобы это исправить, применяют взвешенный исторический метод (age-weighting по Boudoukh — Richardson — Whitelaw), где недавним наблюдениям присваивается больший вес, или фильтрованный (filtered historical simulation), где доходности предварительно нормируют на текущую оценку волатильности. Это уже не одна строка кода, а методологическое решение.

Метод 2. Параметрический (дельта-нормальный) VaR

Параметрический, или variance-covariance, метод предполагает, что доходности портфеля подчиняются нормальному распределению, и тогда VaR выражается аналитически через волатильность. Для портфеля стоимостью V со стандартным отклонением дневной доходности σ и средней μ формула такова: VaR = (z_α · σ − μ) · V, где z_α — квантиль стандартного нормального распределения. Для 99% это z ≈ 2.326, для 95% — z ≈ 1.645. На коротком (дневном) горизонте слагаемым μ часто пренебрегают, считая VaR ≈ z_α · σ · V.

Волатильность портфеля считается через ковариационную матрицу активов: σ_p = √(wᵀ Σ w), где w — веса, а Σ — ковариационная матрица доходностей.

def parametric_var(returns, weights, alpha, value):
    cov = np.cov(returns.values, rowvar=False)   # ковариационная матрица
    sigma_p = np.sqrt(weights @ cov @ weights)    # волатильность портфеля
    mu_p = returns.values @ weights
    mu_p = mu_p.mean()
    z = norm.ppf(alpha)                           # z ≈ 2.326 при alpha = 0.99
    var = (z * sigma_p - mu_p) * value
    # ES для нормального распределения: pdf(z)/(1-alpha) * sigma
    es = (norm.pdf(z) / (1 - alpha) * sigma_p - mu_p) * value
    return var, es

var_p, es_p = parametric_var(returns, weights, alpha, portfolio_value)
print(f"Параметрический VaR 99%: {var_p:,.0f} ₽")
print(f"Параметрический ES 99%:  {es_p:,.0f} ₽")

Плюсы: быстрый, аналитический, не требует симуляций; удобен, когда позиций много, а связи между ними линейны. Минусы: предположение нормальности почти всегда недооценивает хвосты — реальные финансовые ряды имеют эксцесс выше нормального (тяжёлые хвосты) и часто асимметрию, поэтому настоящая вероятность крупных убытков больше, чем даёт нормальная модель. Метод также плохо работает с нелинейными инструментами (опционы): дельта-нормальное приближение игнорирует выпуклость (гамму).

Метод 3. Монте-Карло VaR

Здесь мы не опираемся ни на историческую выборку напрямую, ни на замкнутую формулу, а симулируем большое число сценариев доходностей из заданной модели, считаем для каждого PnL портфеля и берём эмпирический квантиль симулированного распределения убытков. В простом варианте сценарии генерируются из многомерного нормального распределения с оценённой ковариационной матрицей; модель легко заменить на более реалистичную (t-распределение с тяжёлыми хвостами, копулы, стохастическую волатильность).

def monte_carlo_var(returns, weights, alpha, value, n_sims=100_000, seed=7):
    rng = np.random.default_rng(seed)
    mu = returns.mean().values
    cov = np.cov(returns.values, rowvar=False)
    # Симуляция сценариев доходностей активов
    sims = rng.multivariate_normal(mu, cov, size=n_sims)
    pnl = (sims @ weights) * value                # PnL по каждому сценарию
    q = np.quantile(pnl, 1 - alpha)
    var = -q
    es = -pnl[pnl <= q].mean()
    return var, es

var_mc, es_mc = monte_carlo_var(returns, weights, alpha, portfolio_value)
print(f"Монте-Карло VaR 99%: {var_mc:,.0f} ₽")
print(f"Монте-Карло ES 99%:  {es_mc:,.0f} ₽")

Плюсы: максимально гибкий — позволяет заложить любое распределение, нелинейные инструменты, переоценку позиций по сценариям. Минусы: вычислительно дорогой (особенно с полной переоценкой деривативов), а результат ровно настолько хорош, насколько корректна заложенная модель: гарантия «как в реальности» отсутствует — это симуляция ваших предположений, а не рынка.

Отдельно стоит держать в голове ошибку симуляции. Квантиль, оценённый по конечному числу сценариев, сам по себе случаен: при малом числе симуляций оценка 99%-VaR заметно «дрожит» от запуска к запуску. Зафиксированный seed делает результат воспроизводимым, но не устраняет смещение — его уменьшает только увеличение числа сценариев или специальные приёмы снижения дисперсии. Поэтому Монте-Карло VaR разумно сопровождать оценкой его собственной погрешности, а не приводить как точное число.

Сравнение трёх методов

Метод Допущения о распределении Хвосты Стоимость вычислений Когда применять
Исторический Нет; берётся эмпирическое распределение Только то, что было в окне; не экстраполирует Низкая Достаточно данных и стабильный режим; нужен прозрачный метод
Параметрический Нормальность (или иное замкнутое распределение) Систематически недооценивает тяжёлые хвосты Минимальная (формула) Много линейных позиций; нужна скорость и грубая оценка
Монте-Карло Любая заданная модель доходностей Зависит от модели; можно заложить тяжёлые хвосты Высокая Нелинейные инструменты, сложные зависимости, сценарный анализ

Какое окно и какие данные брать

До выбора метода стоит решить вопрос о входных данных, потому что он влияет на результат сильнее, чем различие между тремя методами. Длина окна — это компромисс: слишком короткое даёт неустойчивую, шумную оценку и не видит редких событий; слишком длинное растворяет текущий режим в усреднении по разным эпохам рынка. Типичные значения — от одного до нескольких лет дневных наблюдений, но универсального ответа нет, и окно стоит фиксировать осознанно, а не по умолчанию.

Не менее важна качественная сторона данных. Доходности должны быть согласованы по частоте и часовому поясу, очищены от технических артефактов (нулевые доходности в нерабочие дни, ошибочные котировки), а ряды по разным инструментам — выровнены по датам, иначе ковариационная матрица окажется смещённой. Для портфеля важно считать VaR по доходности именно портфеля при текущих весах, а не агрегировать VaR отдельных позиций — последнее игнорирует диверсификацию и систематически завышает риск.

Бэктестинг VaR: проверяете ли вы то, что считаете

Любая оценка VaR требует проверки на данных: совпадает ли заявленный уровень доверия с фактической частотой превышений (exceptions). Профессиональный стандарт — тест Купика на долю превышений (Kupiec POF, proportion-of-failures). Идея проста: если VaR при 99% корректен, превышения должны происходить примерно в 1% дней; тест сравнивает наблюдённую долю с ожидаемой через статистику отношения правдоподобия, которая асимптотически распределена как χ² с одной степенью свободы. Слишком много превышений — модель недооценивает риск; слишком мало — переоценивает и связывает лишний капитал.

from scipy.stats import chi2

def kupiec_pof(returns, var_threshold, alpha):
    # var_threshold — доходность-порог (отрицательная) для уровня alpha
    exceptions = returns < var_threshold
    n = len(returns)
    x = exceptions.sum()                    # число превышений
    p = 1 - alpha                          # ожидаемая доля, напр. 0.01
    pi_hat = x / n
    # Статистика отношения правдоподобия Купика
    lr = -2 * (np.log((1 - p)**(n - x) * p**x)
              - np.log((1 - pi_hat)**(n - x) * pi_hat**x))
    p_value = 1 - chi2.cdf(lr, df=1)
    return x, n * p, lr, p_value

thr = np.quantile(port_returns, 1 - alpha)
x, expected, lr, pval = kupiec_pof(port_returns, thr, alpha)
print(f"Превышений: {x}, ожидалось ≈ {expected:.1f}, p-value Купика: {pval:.3f}")

На практике одного POF-теста мало: его дополняют тестом на независимость превышений (Christoffersen) — важно, чтобы превышения не группировались в кластеры, что указывало бы на неучтённую динамику волатильности. Стоит помнить и об обратной стороне: Expected Shortfall, в отличие от VaR, тестировать на превышениях напрямую нельзя — он не является элиситируемым в одиночку, поэтому его бэктест устроен сложнее (например, совместная проверка пары VaR-ES). Это ещё одна причина докладывать обе метрики, а не выбирать одну.

Типовые ошибки

  • Масштабирование √t без оснований. Перевод дневного VaR в 10-дневный по правилу VaR_10 = VaR_1 · √10 корректен только при независимых одинаково распределённых доходностях. При автокорреляции или кластеризации волатильности это правило искажает оценку.
  • Слепая нормальность. Параметрический VaR удобен, но предположение нормальности почти всегда занижает хвост. Проверяйте эксцесс и асимметрию; при тяжёлых хвостах используйте t-распределение или историю.
  • Слишком короткое окно. Окно в несколько десятков наблюдений даёт неустойчивый квантиль, который скачет от пересчёта к пересчёту и не захватывает редкие события.
  • Игнорирование ES. VaR молчит о размере убытка за порогом. Без Expected Shortfall вы не видите, насколько тяжёлым может быть «плохой» сценарий.

Как с этим работает StatGazer

VaR — это не одна строчка кода, а методологический выбор: горизонт, окно, модель хвоста, способ бэктеста. Мы строим риск-модели под конкретный портфель и регламент на странице финансовой аналитики, а если у вас уже есть своя модель VaR или ES — проводим её независимую проверку и валидацию: воспроизводим расчёт, проверяем допущения и бэктест, фиксируем границы применимости. Описать задачу можно в контактах.

Нужна модель, а не статья?

Опишите задачу.
Ответим в течение 1–2 рабочих дней.

Мы не только пишем о методах — мы строим риск-модели для фондов, бизнеса и исследователей и проверяем чужие.

NDA до передачи данных · границы работ, KPI и сроки фиксируются до старта · hello@statgazer.ru