Files
options-pricer/frontend/assets/blackscholes.js

90 lines
2.9 KiB
JavaScript
Raw Normal View History

/**
* 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 };
})();