/**
* Black-Scholes pricing and Greeks
*/
import {
BlackScholesParams,
BlackScholesResult,
Greeks,
GreeksExtended,
} from "./types";
import { normalCDF, normalPDF, TRADING_DAYS_PER_YEAR, CALENDAR_DAYS_PER_YEAR } from "./utils";
/**
* Compute d1 and d2 parameters for Black-Scholes
*/
function computeD1D2(
spot: number,
strike: number,
rate: number,
vol: number,
timeToMaturity: number,
dividendYield: number
): { d1: number; d2: number } {
const sqrtT = Math.sqrt(timeToMaturity);
const d1 =
(Math.log(spot / strike) +
(rate - dividendYield + 0.5 * vol * vol) * timeToMaturity) /
(vol * sqrtT);
const d2 = d1 - vol * sqrtT;
return { d1, d2 };
}
/**
* Price a European option using Black-Scholes formula
*
* @param params - Black-Scholes parameters
* @returns Price and d1/d2 values
*/
export function priceBlackScholes(params: BlackScholesParams): BlackScholesResult {
const {
spot,
strike,
rate,
vol,
timeToMaturity,
dividendYield = 0,
optionType,
} = params;
// Edge cases
if (timeToMaturity <= 0) {
// At expiry
const intrinsic =
optionType === "call"
? Math.max(spot - strike, 0)
: Math.max(strike - spot, 0);
return { price: intrinsic, d1: 0, d2: 0 };
}
if (vol <= 0) {
// Zero vol: deterministic outcome
const forward = spot * Math.exp((rate - dividendYield) * timeToMaturity);
const df = Math.exp(-rate * timeToMaturity);
const price =
optionType === "call"
? Math.max(forward - strike, 0) * df
: Math.max(strike - forward, 0) * df;
return { price, d1: Infinity, d2: Infinity };
}
const { d1, d2 } = computeD1D2(
spot,
strike,
rate,
vol,
timeToMaturity,
dividendYield
);
const df = Math.exp(-rate * timeToMaturity);
const dfDiv = Math.exp(-dividendYield * timeToMaturity);
let price: number;
if (optionType === "call") {
price = spot * dfDiv * normalCDF(d1) - strike * df * normalCDF(d2);
} else {
price = strike * df * normalCDF(-d2) - spot * dfDiv * normalCDF(-d1);
}
return { price: Math.max(price, 0), d1, d2 };
}
/**
* Compute analytical Greeks for a European option
*
* Units:
* - Delta: absolute (e.g., 0.5 means $0.50 change per $1 spot move)
* - Gamma: absolute (change in delta per $1 spot move)
* - Theta: per calendar day (365 days/year) in $
* - Vega: per 1% (0.01) change in volatility in $
* - Rho: per 1% (0.01) change in interest rate in $
*
* @param params - Black-Scholes parameters
* @returns Greeks object with delta, gamma, theta, vega, rho
*/
export function greeksBlackScholes(params: BlackScholesParams): Greeks {
const {
spot,
strike,
rate,
vol,
timeToMaturity,
dividendYield = 0,
optionType,
} = params;
// Edge cases
if (timeToMaturity <= 0 || vol <= 0) {
return {
delta: optionType === "call" ? (spot > strike ? 1 : 0) : (spot < strike ? -1 : 0),
gamma: 0,
theta: 0,
vega: 0,
rho: 0,
};
}
const { d1, d2 } = computeD1D2(
spot,
strike,
rate,
vol,
timeToMaturity,
dividendYield
);
const sqrtT = Math.sqrt(timeToMaturity);
const df = Math.exp(-rate * timeToMaturity);
const dfDiv = Math.exp(-dividendYield * timeToMaturity);
const pdf_d1 = normalPDF(d1);
// Delta
let delta: number;
if (optionType === "call") {
delta = dfDiv * normalCDF(d1);
} else {
delta = -dfDiv * normalCDF(-d1);
}
// Gamma (same for call and put)
const gamma = (dfDiv * pdf_d1) / (spot * vol * sqrtT);
// Theta (annual) - then convert to per calendar day
let thetaAnnual: number;
const term1 = (-spot * dfDiv * pdf_d1 * vol) / (2 * sqrtT);
if (optionType === "call") {
thetaAnnual =
term1 -
rate * strike * df * normalCDF(d2) +
dividendYield * spot * dfDiv * normalCDF(d1);
} else {
thetaAnnual =
term1 +
rate * strike * df * normalCDF(-d2) -
dividendYield * spot * dfDiv * normalCDF(-d1);
}
// Convert to per calendar day (standard convention)
const theta = thetaAnnual / CALENDAR_DAYS_PER_YEAR;
// Vega (per 1% move in vol, so divide by 100)
const vega = (spot * dfDiv * sqrtT * pdf_d1) / 100;
// Rho (per 1% move in rate, so divide by 100)
let rho: number;
if (optionType === "call") {
rho = (strike * timeToMaturity * df * normalCDF(d2)) / 100;
} else {
rho = (-strike * timeToMaturity * df * normalCDF(-d2)) / 100;
}
return { delta, gamma, theta, vega, rho };
}
/**
* Compute extended Greeks with multiple theta representations
*
* @param params - Black-Scholes parameters
* @returns Extended Greeks with thetaPerTradingDay, thetaPerCalendarDay, and thetaAnnual
*/
export function greeksBlackScholesExtended(params: BlackScholesParams): GreeksExtended {
const baseGreeks = greeksBlackScholes(params);
// Theta per calendar day is already computed in baseGreeks
const thetaPerCalendarDay = baseGreeks.theta;
const thetaAnnual = thetaPerCalendarDay * CALENDAR_DAYS_PER_YEAR;
const thetaPerTradingDay = thetaAnnual / TRADING_DAYS_PER_YEAR;
return {
...baseGreeks,
thetaPerTradingDay,
thetaPerCalendarDay,
thetaAnnual,
};
}