Portfolio CVaR and its derivatives#

The payoff of modelling returns with a normal variance-mean mixture is that tail risk becomes tractable. Conditional on the latent \(Y\), a portfolio is Gaussian, so the Conditional Value-at-Risk \(\operatorname{CVaR}_\alpha\) (RockafellarUryasev2000) — and its gradient and Hessian in the portfolio weights (Tasche1999, RauHasanov2004) — can be computed by a positive weighted sum over \(Y\). Quadrature supplies the default scenarios. This tutorial fits a mixture, computes CVaR, verifies the analytic derivatives, and takes a few gradient steps to reduce risk. Formal derivations: CVaR Derivatives for Normal Mixture Distributions.

import jax
jax.config.update("jax_enable_x64", True)
import jax.numpy as jnp
import numpy as np
import pandas as pd
from pathlib import Path

from normix import NormalInverseGaussian, UnivariateNormalInverseGaussian
from normix.finance import CVaR
from normix.utils.plotting import set_theme

set_theme()
np.set_printoptions(precision=5, suppress=True)

A fitted mixture and a portfolio#

basket = ["AAPL", "MSFT", "JPM", "XOM"]
data_path = Path("../../../data/sp500_returns.csv").resolve()
R = pd.read_csv(data_path, index_col="Date", parse_dates=True)[basket].dropna()
X = jnp.asarray(R.values, dtype=jnp.float64)

model = NormalInverseGaussian.default_init(X).fit(
    X, max_iter=120, tol=1e-4, e_step_backend="cpu").model

w = jnp.array([0.4, 0.3, 0.2, 0.1])
proj = model.project(w)                    # 1-D return distribution of wᵀX
alpha = 0.05
cvar = CVaR(alpha)
print(f"portfolio mean {float(proj.mean()):+.5f}, std {float(proj.std()):.5f}")
portfolio mean +0.00082, std 0.01315

VaR, CVaR, and quadrature#

VaR is a deterministic quantile via the projected ppf. CVaR integrates over a deterministic quadrature measure of \(Y\):

Q = proj.subordinator().quadrature()
var_a = float(cvar.var(proj))
cvar_a = float(cvar.value(proj, Q))
Y_mc = proj.subordinator().rvs(100_000, seed=0)
print(f"conditional-MC CVaR = {float(cvar.value(proj, Y_mc)):.8f}")
print(f"VaR_{1-alpha:.0%}  = {var_a:.5f}")
print(f"CVaR_{1-alpha:.0%} = {cvar_a:.5f}")
conditional-MC CVaR = 0.03083693
VaR_95%  = 0.02045
CVaR_95% = 0.03074

We validate against a brute-force quantile estimate from one million return draws — agreement checks the tail-risk scale:

draws = jnp.sort(proj.rvs(1_000_000, seed=1))
k = int(alpha * draws.shape[0])
cvar_brute = float(-draws[:k].mean())
print(f"quadrature CVaR     : {cvar_a:.5f}")
print(f"brute-force MC CVaR : {cvar_brute:.5f}")
quadrature CVaR     : 0.03074
brute-force MC CVaR : 0.03083

Derivatives in the scalar parametrization#

CVaR differentiates the risk analytically in the projected parameters \((\tilde\mu, \tilde\gamma, \tilde\sigma)\). We check the gradient against a central finite difference using the same fixed measure \(Q\):

sub = proj.subordinator()
mt, gt, st = float(proj._mu_scalar), float(proj._gamma_scalar), float(proj._sigma_scalar)

def rebuild(m, g, s):
    return UnivariateNormalInverseGaussian.from_classical(
        mu=m, gamma=g, sigma=s ** 2, mu_ig=float(sub.mu), lam=float(sub.lam))

g_analytic = np.asarray(cvar.gradient_scalar(proj, Q))
eps = 1e-5
g_fd = []
for i, base in enumerate((mt, gt, st)):
    hi, lo = [mt, gt, st], [mt, gt, st]
    hi[i] += eps; lo[i] -= eps
    g_fd.append((float(cvar.value(rebuild(*hi), Q)) - float(cvar.value(rebuild(*lo), Q))) / (2 * eps))

print("∂CVaR/∂(μ̃, γ̃, σ̃) analytic:", g_analytic)
print("∂CVaR/∂(μ̃, γ̃, σ̃) FD      :", np.array(g_fd))
∂CVaR/∂(μ̃, γ̃, σ̃) analytic: [-1.      -2.01185  2.08651]
∂CVaR/∂(μ̃, γ̃, σ̃) FD      : [-1.      -2.01185  2.08651]

The leading \(-1\) is exact: shifting the mean shifts CVaR one-for-one.

Derivatives in weight space#

For optimization we need the gradient in the weights \(w\) directly. gradient_w and hessian_w apply the chain rule through the projection; we verify the gradient against a finite difference of value_w:

gw_analytic = np.asarray(cvar.gradient_w(model, w, Q))
gw_fd = []
for i in range(len(w)):
    hi, lo = w.at[i].add(eps), w.at[i].add(-eps)
    gw_fd.append((float(cvar.value_w(model, hi, Q)) - float(cvar.value_w(model, lo, Q))) / (2 * eps))

print("∇_w CVaR analytic:", gw_analytic)
print("∇_w CVaR FD      :", np.array(gw_fd))
print("Hessian_w shape  :", np.asarray(cvar.hessian_w(model, w, Q)).shape)
∇_w CVaR analytic: [0.0369  0.03155 0.02388 0.01739]
∇_w CVaR FD      : [0.0369  0.03155 0.02388 0.01739]
Hessian_w shape  : (4, 4)

A few risk-reducing steps#

Projected gradient descent on the weights (keeping \(\sum_i w_i = 1\)) reduces the portfolio CVaR. We hold \(Q\) fixed across steps for a clean comparison:

w_opt = w
path = [float(cvar.value_w(model, w_opt, Q))]
lr = 0.5
for _ in range(15):
    g = cvar.gradient_w(model, w_opt, Q)
    g = g - g.mean()                       # project onto the simplex tangent (Σ Δw = 0)
    w_opt = w_opt - lr * g
    path.append(float(cvar.value_w(model, w_opt, Q)))

print(f"start weights : {np.asarray(w)}")
print(f"final weights : {np.asarray(w_opt)}  (sum {float(w_opt.sum()):.4f})")
print(f"CVaR {path[0]:.5f} → {path[-1]:.5f}")
start weights : [0.4 0.3 0.2 0.1]
final weights : [0.33747 0.2756  0.22248 0.16445]  (sum 1.0000)
CVaR 0.03074 → 0.02952
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot(path, "o-")
ax.set_xlabel("gradient step"); ax.set_ylabel(r"$\mathrm{CVaR}_{95\%}$")
ax.set_title("Projected-gradient CVaR reduction")
plt.show()
../../_images/9523138cc9f7458ec0396557a4bc09de77204f5600ba0308d905f2e7f9839191.png

The weight gradient lets you plug normix’s tail risk straight into any constrained optimizer; the analytic Hessian enables second-order methods.

Takeaways#

  • A fitted mixture yields portfolio VaR through PINV and CVaR through quadrature over the subordinator \(Y\), checked against brute-force sampling.

  • CVaR provides analytic gradients/Hessians in both \((\tilde\mu, \tilde\gamma, \tilde\sigma)\) and the weights \(w\), checked against finite differences.

  • Those weight-space derivatives drive gradient-based CVaR optimization.

Next: Mean-risk optimization and the efficient surface turns this into a full mean-risk optimizer, reducing the problem to a two-dimensional efficient surface.

Quadrature defaults and a comparison with explicit Monte Carlo scenarios: Quadrature scenarios for portfolio CVaR.