/** * Black-Scholes pricing & Greeks — vanilla JS port of backend/src/lib/blackscholes.ts. * Exposed as window.BS. No modules, no deps. * * Conventions (match backend): * - theta returned PER CALENDAR DAY (annual / 365) * - vega returned PER 1% VOL MOVE (raw / 100) */ (function () { "use strict"; function normalPDF(x) { return Math.exp(-0.5 * x * x) / Math.sqrt(2.0 * Math.PI); } // Abramowitz & Stegun 26.2.17, max error ~7.5e-8 function normalCDF(x) { const sign = x >= 0 ? 1 : -1; const absX = Math.abs(x); const a1 = 0.319381530, a2 = -0.356563782, a3 = 1.781477937, a4 = -1.821255978, a5 = 1.330274429, p = 0.2316419; const t = 1.0 / (1.0 + p * absX); const poly = t * (a1 + t * (a2 + t * (a3 + t * (a4 + t * a5)))); const approx = 1.0 - normalPDF(absX) * poly; return sign === 1 ? approx : 1.0 - approx; } function d1d2(S, K, T, r, sigma) { const sqrtT = Math.sqrt(T); const d1 = (Math.log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * sqrtT); return { d1, d2: d1 - sigma * sqrtT, sqrtT }; } /** Intrinsic payoff per share at expiry. */ function intrinsic(S, K, type) { return type === "call" ? Math.max(S - K, 0) : Math.max(K - S, 0); } /** * Black-Scholes theoretical price (per share). * If T <= 0 (or sigma <= 0), returns intrinsic value. */ function bsPrice(S, K, T, r, sigma, type) { if (T <= 0 || sigma <= 0) return intrinsic(S, K, type); const { d1, d2 } = d1d2(S, K, T, r, sigma); const disc = Math.exp(-r * T); return type === "call" ? S * normalCDF(d1) - K * disc * normalCDF(d2) : K * disc * normalCDF(-d2) - S * normalCDF(-d1); } /** * Black-Scholes Greeks (per share). theta per day, vega per 1% vol. * If T <= 0, returns degenerate Greeks (delta ±1/0, rest 0). */ function bsGreeks(S, K, T, r, sigma, type) { if (T <= 0 || sigma <= 0) { const itm = intrinsic(S, K, type) > 0; return { delta: type === "call" ? (itm ? 1 : 0) : (itm ? -1 : 0), gamma: 0, theta: 0, vega: 0, rho: 0, }; } const { d1, d2, sqrtT } = d1d2(S, K, T, r, sigma); const nd1 = normalPDF(d1); const disc = Math.exp(-r * T); const delta = type === "call" ? normalCDF(d1) : normalCDF(d1) - 1; const gamma = nd1 / (S * sigma * sqrtT); let thetaAnnual; if (type === "call") { thetaAnnual = -(S * nd1 * sigma) / (2 * sqrtT) - r * K * disc * normalCDF(d2); } else { thetaAnnual = -(S * nd1 * sigma) / (2 * sqrtT) + r * K * disc * normalCDF(-d2); } const theta = thetaAnnual / 365; const vega = (S * nd1 * sqrtT) / 100; let rhoRaw; if (type === "call") rhoRaw = K * T * disc * normalCDF(d2); else rhoRaw = -K * T * disc * normalCDF(-d2); const rho = rhoRaw / 100; return { delta, gamma, theta, vega, rho }; } window.BS = { normalCDF, normalPDF, bsPrice, bsGreeks, intrinsic }; })();