/**
* Math utilities for the quant engine
*/
/**
* Standard normal CDF using Abramowitz & Stegun approximation
* Accurate to about 7.5e-8
*/
export function normalCDF(x: number): number {
const a1 = 0.254829592;
const a2 = -0.284496736;
const a3 = 1.421413741;
const a4 = -1.453152027;
const a5 = 1.061405429;
const p = 0.3275911;
const sign = x < 0 ? -1 : 1;
x = Math.abs(x) / Math.SQRT2;
const t = 1.0 / (1.0 + p * x);
const y =
1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
return 0.5 * (1.0 + sign * y);
}
/**
* Standard normal PDF
*/
export function normalPDF(x: number): number {
return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
}
/**
* Generate a standard normal random variable using Box-Muller transform
*/
export function randomNormal(): number {
let u1: number, u2: number;
do {
u1 = Math.random();
u2 = Math.random();
} while (u1 === 0);
return Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
}
/**
* Compute mean of an array
*/
export function mean(arr: number[]): number {
if (arr.length === 0) return 0;
return arr.reduce((sum, val) => sum + val, 0) / arr.length;
}
/**
* Compute sample standard deviation
*/
export function stdDev(arr: number[], useSample = true): number {
if (arr.length < 2) return 0;
const avg = mean(arr);
const squaredDiffs = arr.map((val) => (val - avg) ** 2);
const divisor = useSample ? arr.length - 1 : arr.length;
return Math.sqrt(squaredDiffs.reduce((sum, val) => sum + val, 0) / divisor);
}
/**
* Compute log returns from price series
*/
export function logReturns(prices: number[]): number[] {
const returns: number[] = [];
for (let i = 1; i < prices.length; i++) {
returns.push(Math.log(prices[i] / prices[i - 1]));
}
return returns;
}
/**
* Compute simple returns from price series
*/
export function simpleReturns(prices: number[]): number[] {
const returns: number[] = [];
for (let i = 1; i < prices.length; i++) {
returns.push(prices[i] / prices[i - 1] - 1);
}
return returns;
}
/**
* Trading days per year (standard assumption for US equities)
*/
export const TRADING_DAYS_PER_YEAR = 252;
/**
* Calendar days per year (for theta conversion)
*/
export const CALENDAR_DAYS_PER_YEAR = 365;
/**
* Compute percentile of a sorted or unsorted array
* @param arr - Array of numbers
* @param p - Percentile in [0, 1] (e.g., 0.95 for 95th percentile)
* @returns The value at the given percentile
*/
export function percentile(arr: number[], p: number): number {
if (arr.length === 0) {
throw new Error("Cannot compute percentile of empty array");
}
if (p < 0 || p > 1) {
throw new Error("Percentile must be between 0 and 1");
}
const sorted = [...arr].sort((a, b) => a - b);
const index = p * (sorted.length - 1);
const lower = Math.floor(index);
const upper = Math.ceil(index);
if (lower === upper) {
return sorted[lower];
}
// Linear interpolation
const weight = index - lower;
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
}
/**
* Compute multiple percentiles at once (more efficient for sorted data)
*/
export function percentiles(
arr: number[],
ps: number[]
): Record<number, number> {
if (arr.length === 0) {
throw new Error("Cannot compute percentiles of empty array");
}
const sorted = [...arr].sort((a, b) => a - b);
const result: Record<number, number> = {};
for (const p of ps) {
if (p < 0 || p > 1) {
throw new Error(`Percentile ${p} must be between 0 and 1`);
}
const index = p * (sorted.length - 1);
const lower = Math.floor(index);
const upper = Math.ceil(index);
if (lower === upper) {
result[p] = sorted[lower];
} else {
const weight = index - lower;
result[p] = sorted[lower] * (1 - weight) + sorted[upper] * weight;
}
}
return result;
}