90 lines
2.9 KiB
JavaScript
90 lines
2.9 KiB
JavaScript
|
|
/**
|
||
|
|
* 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 };
|
||
|
|
})();
|