Delta, gamma (Cornish-Fisher and simulated), and full-valuation VaR for a single-option portfolio, illustrated with a common set of Monte Carlo draws
For a position of \(m\) contracts (negative for a short position) in a single option on one underlying, the dollar change in portfolio value over a \(K\)-trading-day horizon is (Christoffersen 2012, chap. 11)
\[
\Delta P = m\,\bigl(c(S_{t+\tau}, \tilde T - \tau) - c(S_t, \tilde T)\bigr),
\]
where \(c(S, T)\) is the option price as a function of spot \(S\) and time to maturity \(T\), \(S_t\) is today’s spot, \(\tilde T\) is today’s time to maturity in calendar days, and \(\tau\) is the risk horizon in calendar days (with \(\tau = K\times 365/252\) when \(K\) is measured in trading days). Four common VaR estimators at tail probability \(p\) are:
Note
Convention on \(\tau\). This page uses the exact conversion \(\tau = K\times 365/252 \approx 1.4484\,K\), which preserves the annualized time. Christoffersen (2012) rounds this to \(\tau = 14\) calendar days for \(K = 10\) trading days (a ratio of \(1.4\)). For short horizons the difference is numerically small but non-zero, so VaR values here will not match the textbook to the last decimal.
Delta-based (analytical). Linearize \(\Delta P\) around \(S_t\) and assume a normal return: \[
\text{VaR}^{\,p} = |m\,\delta|\,S_t\,\sigma_{\text{tr}}\sqrt{K}\,\lvert\Phi_p^{-1}\rvert,
\] where \(\delta = \partial c/\partial S\) is today’s option delta, \(\sigma_{\text{tr}}\) is the daily trading-day volatility of the underlying, and \(\Phi_p^{-1}\) is the \(p\)-quantile of the standard normal distribution.
Gamma-based, Cornish–Fisher. Add the gamma term \(\gamma = \partial^{2} c/\partial S^{2}\), compute the first three moments of the quadratic approximation analytically, and apply the Cornish–Fisher expansion: \[
\text{VaR}^{\,p} = -\mu_{\Delta P} - \Bigl(\Phi_p^{-1} + \tfrac{1}{6}\!\bigl((\Phi_p^{-1})^{2} - 1\bigr)\,\zeta_{1,\Delta P}\Bigr)\,\sigma_{\Delta P},
\] where \(\mu_{\Delta P}\), \(\sigma_{\Delta P}\), and \(\zeta_{1,\Delta P}\) are the mean, standard deviation, and skewness of \(\Delta P\) under the quadratic approximation.
Gamma-based, simulated. Draw \(R_h \sim \mathcal N(0, K\sigma_{\text{tr}}^{2})\) for \(h = 1, \dots, \text{MC}\) and compute \(\widehat{\Delta P}_h = m\,\delta\,S_t R_h + \tfrac{1}{2}\,m\,\gamma\,S_t^{2} R_h^{2}\); take the empirical \(p\)-quantile.
Full valuation. Use the same \(R_h\), set \(\hat S_h = S_t\,e^{R_h}\), and reprice the option at the reduced maturity: \(\widehat{\Delta P}_h = m\,\bigl(c(\hat S_h, \tilde T - \tau) - c(S_t, \tilde T)\bigr)\).
The first three use local greeks; only full valuation handles the true non-linearity, including time decay.
normalCDF = x => {const a1 =0.254829592, a2 =-0.284496736, a3 =1.421413741const a4 =-1.453152027, a5 =1.061405429, p =0.3275911const sign = x <0?-1:1const z =Math.abs(x) /Math.sqrt(2)const t =1.0/ (1.0+ p * z)const y =1- (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t *Math.exp(-z * z)return0.5* (1+ sign * y)}
normalPDF = x =>Math.exp(-x * x /2) /Math.sqrt(2*Math.PI)
bsm = (S, X, T, r, q, sig, type) => {if (T <=0) {const intrinsic = type ==="call"?Math.max(S - X,0) :Math.max(X - S,0)return { price: intrinsic,delta: type ==="call"? (S > X ?1:0) : (S < X ?-1:0),gamma:0 } }const sqrtT =Math.sqrt(T)const d1 = (Math.log(S / X) + (r - q +0.5* sig * sig) * T) / (sig * sqrtT)const d2 = d1 - sig * sqrtTconst eqT =Math.exp(-q * T), erT =Math.exp(-r * T)const Nd1 =normalCDF(d1), Nd2 =normalCDF(d2), pdf =normalPDF(d1)let price, deltaif (type ==="call") { price = S * eqT * Nd1 - X * erT * Nd2 delta = eqT * Nd1 } else { price = X * erT *normalCDF(-d2) - S * eqT *normalCDF(-d1) delta = eqT * (Nd1 -1) }const gamma = eqT * pdf / (S * sig * sqrtT)return { price, delta, gamma }}
mulberry32 = (seed) => {let a = seed >>>0return () => { a = (a +0x6D2B79F5) >>>0let t = a t =Math.imul(t ^ (t >>>15), t |1) t = t +Math.imul(t ^ (t >>>7), t |61) ^ treturn ((t ^ (t >>>14)) >>>0) /4294967296 }}
fmtMoney = x => (!isFinite(x) ||isNaN(x)) ?"N/A": (x <0?"-$":"$") +Math.abs(x).toFixed(2)
Setting up the portfolio
Tip
How to experiment
The defaults are a short one-contract at-the-money call on an 80-unit underlying. The short gamma pulls the true P&L distribution sharply negative, so the delta-based VaR understates risk. Move the option deep in-the-money and all four VaRs collapse onto each other: linearity returns. Flip to a long call and the linear approximation instead overstates downside risk, because gamma is now on your side. Change the horizon or volatility to see how the gap between methods scales.
viewof vmSt = Inputs.range([20,200], { label:"Sₜ current spot",step:0.5,value:80 })
viewof vmTail = Inputs.range([0.1,10], { label:"Tail probability p (%)",step:0.1,value:1.0 })
viewof vmMC = Inputs.range([500,20000], { label:"Monte Carlo draws MC",step:500,value:5000 })
mutable vmSeed =12345
viewof vmReseed = {const btn =html`<button style="background:#2f71d5;color:white;border:none;border-radius:6px;padding:0.35rem 1rem;font-weight:500;font-size:0.9rem;cursor:pointer;">↻ New random draws</button>` btn.onclick= () => { mutable vmSeed++ }return btn}
vmGreeks = {const S = vmSt, X = vmXconst sig = vmSig /100const T = vmDays /365const r = vmR /100, q = vmQ /100const b =bsm(S, X, T, r, q, sig, vmType)return { S, X, T, sig, r, q,type: vmType,price: b.price,delta: b.delta,gamma: b.gamma }}
vmSim = {const g = vmGreeksconst m = vmPositionconst K = vmKconst MC = vmMCconst p = vmTail /100// Trading-day vol: σ_ann uses 252 trading daysconst sigTr = g.sig/Math.sqrt(252)const sigK = sigTr *Math.sqrt(K)// Generate N(0,1) draws via Box-Muller on mulberry32const rng =mulberry32(vmSeed)const z = []for (let i =0; i < MC; i +=2) {const [a, b] =boxMullerPair(rng(),rng()) z.push(a)if (i +1< MC) z.push(b) }// K-day return draws (arithmetic, used both as R for the quadratic approx and as the log return for full valuation)const R = z.map(v => v * sigK)// Reduced maturity for full valuation: τ = K × (365/252) calendar daysconst tauCal = K *365/252const Tnew =Math.max((g.T*365- tauCal) /365,0.001/365)// Per-draw ΔP under the three simulation-based methodsconst dPDelta =newArray(MC)const dPGamma =newArray(MC)const dPFull =newArray(MC)const cMkt = g.priceconst delta_e = m * g.deltaconst gamma_e = m * g.gammafor (let i =0; i < MC; i++) {const Ri = R[i] dPDelta[i] = delta_e * g.S* Ri dPGamma[i] = delta_e * g.S* Ri +0.5* gamma_e * g.S* g.S* Ri * Riconst Snew = g.S*Math.exp(Ri)const cNew =bsm(Snew, g.X, Tnew, g.r, g.q, g.sig, g.type).price dPFull[i] = m * (cNew - cMkt) }// Analytical delta VaRconst zp =Math.abs(qnorm(p))const varDeltaAnalytic =Math.abs(m * g.delta) * g.S* sigTr *Math.sqrt(K) * zp// Cornish-Fisher moments for gamma VaRconst sigmaK2 = K * sigTr * sigTrconst mu =0.5* gamma_e * g.S* g.S* sigmaK2const var_ = delta_e * delta_e * g.S* g.S* sigmaK2+0.5* gamma_e * gamma_e *Math.pow(g.S,4) * sigmaK2 * sigmaK2const sigma =Math.sqrt(Math.max(var_,1e-30))const term1 = (9/2) * delta_e * delta_e * gamma_e *Math.pow(g.S,4) * sigmaK2 * sigmaK2const term2 = (15/8) *Math.pow(gamma_e,3) *Math.pow(g.S,6) *Math.pow(sigmaK2,3)const term3 =-3* (delta_e * delta_e * g.S* g.S* sigmaK2+0.75* gamma_e * gamma_e *Math.pow(g.S,4) * sigmaK2 * sigmaK2) * muconst term4 =2*Math.pow(mu,3)const skew = (term1 + term2 + term3 + term4) /Math.pow(sigma,3)const zpSigned =qnorm(p)const varGammaCF =-mu - (zpSigned + (1/6) * (zpSigned * zpSigned -1) * skew) * sigma// Empirical percentile for the three simulated distributionsconst percentile = (arr, p) => {const sorted = arr.slice().sort((x, y) => x - y)const idx = p * (sorted.length-1)const lo =Math.floor(idx), hi =Math.ceil(idx)if (lo === hi) return sorted[lo]return sorted[lo] * (hi - idx) + sorted[hi] * (idx - lo) }const varDeltaSim =-percentile(dPDelta, p)const varGammaSim =-percentile(dPGamma, p)const varFullSim =-percentile(dPFull, p)// ES for full valuation (empirical)const cutoff =percentile(dPFull, p)const tailLosses = dPFull.filter(x => x <= cutoff)const esFullSim = tailLosses.length>0?-tailLosses.reduce((a, b) => a + b,0) / tailLosses.length:NaNreturn { m, K, p, MC, sigTr, sigK, Tnew, R, dPDelta, dPGamma, dPFull, varDeltaAnalytic, varGammaCF, varDeltaSim, varGammaSim, varFullSim, esFullSim, mu, sigma, skew }}
// Shared bins across methods for fair histogram comparisonvmBins = {const all = [...vmSim.dPDelta,...vmSim.dPGamma,...vmSim.dPFull]const lo =Math.min(...all)const hi =Math.max(...all)const n =60const w = (hi - lo) / nconst bin = (arr) => {const b =newArray(n).fill(0).map((_, i) => ({xMid: lo + (i +0.5) * w,x0: lo + i * w,x1: lo + (i +1) * w,count:0 }))for (const v of arr) {let i =Math.floor((v - lo) / w)if (i === n) i = n -1if (i >=0&& i < n) b[i].count++ }return b }return { lo, hi,delta:bin(vmSim.dPDelta).map(d => ({ ...d,method:"Delta (sim)" })),gamma:bin(vmSim.dPGamma).map(d => ({ ...d,method:"Gamma (sim)" })),full:bin(vmSim.dPFull ).map(d => ({ ...d,method:"Full valuation" })) }}
html`<p style="color:#666;font-size:0.85rem;">Three P&L distributions built from the <em>same</em> ${fmt(vmSim.MC,0)} return draws: the linear (delta) projection, the quadratic (gamma) projection, and the exact (full-valuation) reprice. Dashed red lines mark the empirical VaR at the ${fmt(vmSim.p*100,1)}% tail. The delta distribution is symmetric by construction; gamma introduces skewness; full valuation is the benchmark.</p>`
{const v = vmSimconst full = v.varFullSimconst ratio = x => full >0?100* x / full :NaNreturnhtml`<p style="color:#666;font-size:0.85rem;">Full valuation (blue) is the benchmark. Delta-analytic / delta-sim converge as MC grows. Ratios to full valuation: delta-analytic ${fmt(ratio(v.varDeltaAnalytic),0)}%, delta-sim ${fmt(ratio(v.varDeltaSim),0)}%, gamma-CF ${fmt(ratio(v.varGammaCF),0)}%, gamma-sim ${fmt(ratio(v.varGammaSim),0)}%.</p>`}
Why can gamma-simulated and full-valuation disagree? Even though both use the same \(R_h\) draws, gamma-sim truncates the Taylor series at second order and ignores time decay. Full valuation reprices the option with reduced maturity \(\tilde T - \tau\), so it captures theta as well as higher-order curvature terms. The two methods converge as \(K \to 0\) and as the option moves away from the money.
References
Christoffersen, Peter F. 2012. Elements of Financial Risk Management. 2nd ed. Academic Press.