added more functions

This commit is contained in:
Bojan Kucera 2025-06-04 20:01:39 -04:00
parent a1c82ae0b8
commit cca9ac03dd
8 changed files with 1563 additions and 2 deletions

View file

@ -233,3 +233,170 @@ export function modifiedDuration(
const macDuration = macaulayDuration(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
return macDuration / (1 + yieldToMaturity / paymentsPerYear);
}
/**
* Calculate bond convexity
*/
export function bondConvexity(
faceValue: number,
couponRate: number,
yieldToMaturity: number,
periodsToMaturity: number,
paymentsPerYear: number = 2
): number {
const couponPayment = (faceValue * couponRate) / paymentsPerYear;
const discountRate = yieldToMaturity / paymentsPerYear;
let convexity = 0;
const bondPriceValue = bondPrice(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
for (let i = 1; i <= periodsToMaturity; i++) {
const presentValue = couponPayment / Math.pow(1 + discountRate, i);
convexity += (i * (i + 1) * presentValue) / Math.pow(1 + discountRate, 2);
}
const faceValuePV = faceValue / Math.pow(1 + discountRate, periodsToMaturity);
convexity += (periodsToMaturity * (periodsToMaturity + 1) * faceValuePV) / Math.pow(1 + discountRate, 2);
return convexity / (bondPriceValue * paymentsPerYear * paymentsPerYear);
}
/**
* Calculate dollar duration
*/
export function dollarDuration(
faceValue: number,
couponRate: number,
yieldToMaturity: number,
periodsToMaturity: number,
paymentsPerYear: number = 2,
basisPointChange: number = 0.01 // 1 basis point = 0.01%
): number {
const modifiedDur = modifiedDuration(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
const bondPriceValue = bondPrice(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
return modifiedDur * bondPriceValue * basisPointChange;
}
/**
* Calculate accrued interest
*/
export function accruedInterest(
faceValue: number,
couponRate: number,
daysSinceLastCoupon: number,
daysInCouponPeriod: number
): number {
return (faceValue * couponRate) * (daysSinceLastCoupon / daysInCouponPeriod);
}
/**
* Calculate clean price
*/
export function cleanPrice(dirtyPrice: number, accruedInterestValue: number): number {
return dirtyPrice - accruedInterestValue;
}
/**
* Calculate dirty price
*/
export function dirtyPrice(cleanPriceValue: number, accruedInterestValue: number): number {
return cleanPriceValue + accruedInterestValue;
}
/**
* Calculate dividend discount model (DDM)
*/
export function dividendDiscountModel(
currentDividend: number,
growthRate: number,
discountRate: number
): number {
if (discountRate <= growthRate) return NaN; // Indeterminate
return currentDividend * (1 + growthRate) / (discountRate - growthRate);
}
/**
* Calculate weighted average cost of capital (WACC)
*/
export function weightedAverageCostOfCapital(
costOfEquity: number,
costOfDebt: number,
equityWeight: number,
debtWeight: number,
taxRate: number
): number {
return (equityWeight * costOfEquity) + (debtWeight * costOfDebt * (1 - taxRate));
}
/**
* Calculate capital asset pricing model (CAPM)
*/
export function capitalAssetPricingModel(
riskFreeRate: number,
beta: number,
marketRiskPremium: number
): number {
return riskFreeRate + beta * marketRiskPremium;
}
/**
* Calculate Treynor ratio
*/
export function treynorRatio(
portfolioReturn: number,
riskFreeRate: number,
beta: number
): number {
return (portfolioReturn - riskFreeRate) / beta;
}
/**
* Calculate hurdle rate
*/
export function hurdleRate(
costOfCapital: number,
riskPremium: number
): number {
return costOfCapital + riskPremium;
}
/**
* Calculate degree of operating leverage (DOL)
*/
export function degreeOfOperatingLeverage(
contributionMargin: number,
operatingIncome: number
): number {
return contributionMargin / operatingIncome;
}
/**
* Calculate degree of financial leverage (DFL)
*/
export function degreeOfFinancialLeverage(
ebit: number,
earningsBeforeTax: number
): number {
return ebit / earningsBeforeTax;
}
/**
* Calculate degree of total leverage (DTL)
*/
export function degreeOfTotalLeverage(
dol: number,
dfl: number
): number {
return dol * dfl;
}
/**
* Calculate economic value added (EVA)
*/
export function economicValueAdded(
netOperatingProfitAfterTax: number,
capitalInvested: number,
wacc: number
): number {
return netOperatingProfitAfterTax - (capitalInvested * wacc);
}

View file

@ -101,6 +101,8 @@ export function pearsonCorrelation(
};
}
/**
* Calculate Spearman rank correlation coefficient
*/
@ -602,8 +604,276 @@ export function grangerCausalityTest(
optimalLag: bestLag
};
}
/**
* Calculate Distance Correlation
*/
export function distanceCorrelation(x: number[], y: number[]): CorrelationResult {
if (x.length !== y.length || x.length < 2) {
throw new Error('Arrays must have same length and at least 2 observations');
}
// Helper functions
const n = x.length;
// Calculate distance matrices
const a = Array(n).fill(null).map(() => Array(n).fill(0));
const b = Array(n).fill(null).map(() => Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
a[i][j] = Math.abs(x[i] - x[j]);
b[i][j] = Math.abs(y[i] - y[j]);
}
}
// Calculate double centered distance matrices
const aMeanRow = a.map(row => row.reduce((sum, val) => sum + val, 0) / n);
const bMeanRow = b.map(row => row.reduce((sum, val) => sum + val, 0) / n);
const aMeanTotal = aMeanRow.reduce((sum, val) => sum + val, 0) / n;
const bMeanTotal = bMeanRow.reduce((sum, val) => sum + val, 0) / n;
const A = Array(n).fill(null).map(() => Array(n).fill(0));
const B = Array(n).fill(null).map(() => Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
A[i][j] = a[i][j] - aMeanRow[i] - aMeanRow[j] + aMeanTotal;
B[i][j] = b[i][j] - bMeanRow[i] - bMeanRow[j] + bMeanTotal;
}
}
// Calculate distance covariance and variances
let dcov = 0;
let dvarX = 0;
let dvarY = 0;
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
dcov += A[i][j] * B[i][j];
dvarX += A[i][j] * A[i][j];
dvarY += B[i][j] * B[i][j];
}
}
dcov = Math.sqrt(dcov / (n * n));
dvarX = Math.sqrt(dvarX / (n * n));
dvarY = Math.sqrt(dvarY / (n * n));
const correlation = dvarX * dvarY === 0 ? 0 : dcov / Math.sqrt(dvarX * dvarY);
// Approximate p-value (permutation test)
let pValue = 1;
const numPermutations = 100;
for (let p = 0; p < numPermutations; p++) {
const yPermuted = shuffleArray([...y]);
const bPermuted = Array(n).fill(null).map(() => Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
bPermuted[i][j] = Math.abs(yPermuted[i] - yPermuted[j]);
}
}
const bMeanRowPermuted = bPermuted.map(row => row.reduce((sum, val) => sum + val, 0) / n);
const bMeanTotalPermuted = bMeanRowPermuted.reduce((sum, val) => sum + val, 0) / n;
const BPermuted = Array(n).fill(null).map(() => Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
BPermuted[i][j] = bPermuted[i][j] - bMeanRowPermuted[i] - bMeanRowPermuted[j] + bMeanTotalPermuted;
}
}
let dcovPermuted = 0;
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
dcovPermuted += A[i][j] * BPermuted[i][j];
}
}
dcovPermuted = Math.sqrt(dcovPermuted / (n * n));
if (dcovPermuted >= dcov) {
pValue++;
}
}
pValue /= (numPermutations + 1);
const significance = pValue < 0.05;
return {
correlation,
pValue,
significance
};
}
/**
* Calculate Mutual Information
*/
export function mutualInformation(x: number[], y: number[], numBins: number = 10): CorrelationResult {
if (x.length !== y.length || x.length < 2) {
throw new Error('Arrays must have same length and at least 2 observations');
}
const n = x.length;
// Calculate histograms
const xMin = Math.min(...x);
const xMax = Math.max(...x);
const yMin = Math.min(...y);
const yMax = Math.max(...y);
const xBinWidth = (xMax - xMin) / numBins;
const yBinWidth = (yMax - yMin) / numBins;
const jointHistogram = Array(numBins).fill(null).map(() => Array(numBins).fill(0));
const xHistogram = Array(numBins).fill(0);
const yHistogram = Array(numBins).fill(0);
for (let i = 0; i < n; i++) {
const xBin = Math.floor((x[i] - xMin) / xBinWidth);
const yBin = Math.floor((y[i] - yMin) / yBinWidth);
if (xBin >= 0 && xBin < numBins && yBin >= 0 && yBin < numBins) {
jointHistogram[xBin][yBin]++;
xHistogram[xBin]++;
yHistogram[yBin]++;
}
}
// Calculate probabilities
const jointProbabilities = jointHistogram.map(row => row.map(count => count / n));
const xProbabilities = xHistogram.map(count => count / n);
const yProbabilities = yHistogram.map(count => count / n);
// Calculate mutual information
let mi = 0;
for (let i = 0; i < numBins; i++) {
for (let j = 0; j < numBins; j++) {
if (jointProbabilities[i][j] > 0 && xProbabilities[i] > 0 && yProbabilities[j] > 0) {
mi += jointProbabilities[i][j] * Math.log(jointProbabilities[i][j] / (xProbabilities[i] * yProbabilities[j]));
}
}
}
const correlation = mi; // Use MI as correlation measure
// Approximate p-value (permutation test)
let pValue = 1;
const numPermutations = 100;
for (let p = 0; p < numPermutations; p++) {
const yPermuted = shuffleArray([...y]);
let miPermuted = 0;
const jointHistogramPermuted = Array(numBins).fill(null).map(() => Array(numBins).fill(0));
for (let i = 0; i < n; i++) {
const xBin = Math.floor((x[i] - xMin) / xBinWidth);
const yBin = Math.floor((yPermuted[i] - yMin) / yBinWidth);
if (xBin >= 0 && xBin < numBins && yBin >= 0 && yBin < numBins) {
jointHistogramPermuted[xBin][yBin]++;
}
}
const jointProbabilitiesPermuted = jointHistogramPermuted.map(row => row.map(count => count / n));
for (let i = 0; i < numBins; i++) {
for (let j = 0; j < numBins; j++) {
if (jointProbabilitiesPermuted[i][j] > 0 && xProbabilities[i] > 0 && yProbabilities[j] > 0) {
miPermuted += jointProbabilitiesPermuted[i][j] * Math.log(jointProbabilitiesPermuted[i][j] / (xProbabilities[i] * yProbabilities[j]));
}
}
}
if (miPermuted >= mi) {
pValue++;
}
}
pValue /= (numPermutations + 1);
const significance = pValue < 0.05;
return {
correlation,
pValue,
significance
};
}
/**
* Calculate Cross-Correlation
*/
export function crossCorrelation(x: number[], y: number[], maxLag: number): number[] {
const n = x.length;
if (n !== y.length) {
throw new Error('Arrays must have the same length');
}
const correlations: number[] = [];
for (let lag = -maxLag; lag <= maxLag; lag++) {
let sum = 0;
let count = 0;
for (let i = 0; i < n; i++) {
const yIndex = i + lag;
if (yIndex >= 0 && yIndex < n) {
sum += (x[i] - average(x)) * (y[yIndex] - average(y));
count++;
}
}
const stdX = Math.sqrt(x.reduce((sum, xi) => sum + Math.pow(xi - average(x), 2), 0) / (n - 1));
const stdY = Math.sqrt(y.reduce((sum, yi) => sum + Math.pow(yi - average(y), 2), 0) / (n - 1));
const correlation = count > 0 ? sum / ((count - 1) * stdX * stdY) : 0;
correlations.push(correlation);
}
return correlations;
}
/**
* Calculate Autocorrelation
*/
export function autocorrelation(x: number[], lag: number): number {
const n = x.length;
if (lag >= n) {
throw new Error('Lag must be less than the length of the array');
}
let sum = 0;
for (let i = lag; i < n; i++) {
sum += (x[i] - average(x)) * (x[i - lag] - average(x));
}
const std = Math.sqrt(x.reduce((sum, xi) => sum + Math.pow(xi - average(x), 2), 0) / (n - 1));
return sum / ((n - lag - 1) * std * std);
}
/**
* Helper function to shuffle an array (Fisher-Yates shuffle)
*/
function shuffleArray<T>(array: T[]): T[] {
const newArray = [...array];
for (let i = newArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
}
return newArray;
}
/**
* Helper function to calculate the average of an array of numbers
*/
function average(arr: number[]): number {
if (arr.length === 0) return 0;
return arr.reduce((a, b) => a + b, 0) / arr.length;
}
function getRanks(arr: number[]): number[] {
const sorted = arr.map((val, idx) => ({ val, idx })).sort((a, b) => a.val - b.val);

View file

@ -664,8 +664,293 @@ export function volumeConcentrationHHI(
return hhi * 10000; // Scale to 0-10000 range
}
/**
* Volume Profile
*/
export function volumeProfile(
ohlcv: OHLCVData[],
priceLevels: number
): { [price: number]: number } {
const profile: { [price: number]: number } = {};
// Helper functions
if (ohlcv.length === 0) return profile;
const minPrice = Math.min(...ohlcv.map(candle => candle.low));
const maxPrice = Math.max(...ohlcv.map(candle => candle.high));
const priceRange = maxPrice - minPrice;
const priceIncrement = priceRange / priceLevels;
for (let i = 0; i < priceLevels; i++) {
const priceLevel = minPrice + i * priceIncrement;
profile[priceLevel] = 0;
}
for (const candle of ohlcv) {
const typicalPrice = (candle.high + candle.low + candle.close) / 3;
const priceLevel = minPrice + Math.floor((typicalPrice - minPrice) / priceIncrement) * priceIncrement;
if (profile[priceLevel] !== undefined) {
profile[priceLevel] += candle.volume;
}
}
return profile;
}
/**
* Delta Neutral Hedging Ratio
*/
export function deltaNeutralHedgingRatio(
optionDelta: number
): number {
return -optionDelta;
}
/**
* Gamma Scalping Range
*/
export function gammaScalpingRange(
gamma: number,
theta: number,
timeIncrement: number
): number {
return Math.sqrt(2 * Math.abs(theta) * timeIncrement / gamma);
}
/**
* Optimal Order Size (based on market impact)
*/
export function optimalOrderSize(
alpha: number,
lambda: number
): number {
return alpha / (2 * lambda);
}
/**
* Adverse Selection Component of the Spread
*/
export function adverseSelectionComponent(
probabilityOfInformedTrader: number,
spread: number
): number {
return probabilityOfInformedTrader * spread;
}
/**
* Inventory Risk Component of the Spread
*/
export function inventoryRiskComponent(
inventoryHoldingCost: number,
orderArrivalRate: number
): number {
return inventoryHoldingCost * Math.sqrt(orderArrivalRate);
}
/**
* Quote Age
*/
export function quoteAge(
lastUpdate: Date
): number {
return Date.now() - lastUpdate.getTime();
}
/**
* Trade Classification (Lee-Ready algorithm)
*/
export function tradeClassification(
tradePrice: number,
bidPrice: number,
askPrice: number,
previousTradePrice: number
): 'buy' | 'sell' | 'unknown' {
if (tradePrice > askPrice) {
return 'buy';
} else if (tradePrice < bidPrice) {
return 'sell';
} else if (tradePrice >= previousTradePrice) {
return 'buy';
} else {
return 'sell';
}
}
/**
* Tick Rule
*/
export function tickRule(
tradePrice: number,
previousTradePrice: number
): 'buy' | 'sell' | 'unknown' {
if (tradePrice > previousTradePrice) {
return 'buy';
} else if (tradePrice < previousTradePrice) {
return 'sell';
} else {
return 'unknown';
}
}
/**
* Amihud's Lambda Variation with High-Frequency Data
*/
export function amihudIlliquidityHFT(
priceChanges: number[],
dollarVolumes: number[],
timeDeltas: number[]
): number {
let illiquiditySum = 0;
let validTrades = 0;
for (let i = 0; i < priceChanges.length; i++) {
if (dollarVolumes[i] > 0 && timeDeltas[i] > 0) {
illiquiditySum += Math.abs(priceChanges[i]) / (dollarVolumes[i] * timeDeltas[i]);
validTrades++;
}
}
return validTrades > 0 ? illiquiditySum / validTrades : 0;
}
/**
* Parkinson Volatility
*/
export function parkinsonVolatility(
highPrices: number[],
lowPrices: number[]
): number {
if (highPrices.length !== lowPrices.length || highPrices.length < 2) return 0;
let sumSquaredLogHL = 0;
for (let i = 0; i < highPrices.length; i++) {
const logHL = Math.log(highPrices[i] / lowPrices[i]);
sumSquaredLogHL += logHL * logHL;
}
const parkinsonVariance = (1 / (4 * highPrices.length * Math.log(2))) * sumSquaredLogHL;
return Math.sqrt(parkinsonVariance);
}
/**
* Garman-Klass Volatility
*/
export function garmanKlassVolatility(
openPrices: number[],
highPrices: number[],
lowPrices: number[],
closePrices: number[]
): number {
if (openPrices.length !== highPrices.length || openPrices.length !== lowPrices.length || openPrices.length !== closePrices.length || openPrices.length < 2) return 0;
let sumSquaredTerm1 = 0;
let sumSquaredTerm2 = 0;
let sumSquaredTerm3 = 0;
for (let i = 0; i < openPrices.length; i++) {
const logHO = Math.log(highPrices[i] / openPrices[i]);
const logLO = Math.log(lowPrices[i] / openPrices[i]);
const logCO = Math.log(closePrices[i] / openPrices[i]);
sumSquaredTerm1 += 0.5 * (logHO * logHO + logLO * logLO);
sumSquaredTerm2 += - (2 * Math.log(2) - 1) * (logCO * logCO);
}
const garmanKlassVariance = (1 / openPrices.length) * (sumSquaredTerm1 + sumSquaredTerm2);
return Math.sqrt(garmanKlassVariance);
}
/**
* Yang-Zhang Volatility
*/
export function yangZhangVolatility(
openPrices: number[],
highPrices: number[],
lowPrices: number[],
closePrices: number[],
previousClosePrices: number[]
): number {
if (openPrices.length !== highPrices.length || openPrices.length !== lowPrices.length || openPrices.length !== closePrices.length || openPrices.length !== previousClosePrices.length || openPrices.length < 2) return 0;
const k = 0.34 / (1.34 + (openPrices.length + 1) / (previousClosePrices.length - 1));
let sumSquaredTerm1 = 0;
let sumSquaredTerm2 = 0;
let sumSquaredTerm3 = 0;
for (let i = 0; i < openPrices.length; i++) {
const overnightReturn = Math.log(openPrices[i] / previousClosePrices[i]);
const openToHigh = Math.log(highPrices[i] / openPrices[i]);
const openToLow = Math.log(lowPrices[i] / openPrices[i]);
const closeToOpen = Math.log(closePrices[i] / openPrices[i]);
sumSquaredTerm1 += overnightReturn * overnightReturn;
sumSquaredTerm2 += openToHigh * openToHigh;
sumSquaredTerm3 += openToLow * openToLow;
}
const variance = sumSquaredTerm1 + k * sumSquaredTerm2 + (1 - k) * sumSquaredTerm3;
return Math.sqrt(variance);
}
/**
* Volume Order Imbalance (VOI)
*/
export function volumeOrderImbalance(
buyVolumes: number[],
sellVolumes: number[]
): number[] {
if (buyVolumes.length !== sellVolumes.length) return [];
const voi: number[] = [];
for (let i = 0; i < buyVolumes.length; i++) {
voi.push(buyVolumes[i] - sellVolumes[i]);
}
return voi;
}
/**
* Cumulative Volume Delta (CVD)
*/
export function cumulativeVolumeDelta(
buyVolumes: number[],
sellVolumes: number[]
): number[] {
if (buyVolumes.length !== sellVolumes.length) return [];
const cvd: number[] = [];
let cumulativeDelta = 0;
for (let i = 0; i < buyVolumes.length; i++) {
cumulativeDelta += buyVolumes[i] - sellVolumes[i];
cvd.push(cumulativeDelta);
}
return cvd;
}
/**
* Market Order Ratio
*/
export function marketOrderRatio(
marketOrders: number[],
limitOrders: number[]
): number[] {
if (marketOrders.length !== limitOrders.length) return [];
const ratios: number[] = [];
for (let i = 0; i < marketOrders.length; i++) {
const totalOrders = marketOrders[i] + limitOrders[i];
ratios.push(totalOrders > 0 ? marketOrders[i] / totalOrders : 0);
}
return ratios;
}
/**
* Helper function to calculate the average of an array of numbers
*/
function average(arr: number[]): number {
if (arr.length === 0) return 0;
return arr.reduce((a, b) => a + b, 0) / arr.length;
}
function calculateVolatility(returns: number[]): number {
if (returns.length < 2) return 0;

View file

@ -86,6 +86,58 @@ export function blackScholes(params: OptionParameters): OptionPricing {
};
}
export function impliedVolatility(
price: number, S: number, K: number, T: number, r: number, isCall = true
): number {
// …NewtonRaphson on σ to match blackScholesPrice
let sigma = 0.2; // Initial guess for volatility
const tolerance = 1e-6;
const maxIterations = 100;
let iteration = 0;
let priceDiff = 1; // Initialize to a non-zero value
while (Math.abs(priceDiff) > tolerance && iteration < maxIterations) {
const params: OptionParameters = {
spotPrice: S,
strikePrice: K,
timeToExpiry: T,
riskFreeRate: r,
volatility: sigma
};
const calculatedPrice = isCall ? blackScholes(params).callPrice : blackScholes(params).putPrice;
priceDiff = calculatedPrice - price;
// Calculate Vega
const greeks = calculateGreeks(params, isCall ? 'call' : 'put');
const vega = greeks.vega * 100; // Convert from percentage to absolute
if (vega === 0) {
break; // Avoid division by zero
}
sigma -= priceDiff / vega; // Update volatility estimate
iteration++;
}
if (iteration === maxIterations) {
console.warn('Implied volatility calculation did not converge');
}
if (sigma < 0) {
console.warn('Calculated implied volatility is negative, returning 0');
return 0;
}
if (sigma > 10) {
console.warn('Calculated implied volatility is too high, returning 10');
return 10; // Cap at a reasonable maximum
}
if (isNaN(sigma)) {
console.warn('Calculated implied volatility is NaN, returning 0');
return 0;
}
return sigma
}
/**
* Calculate option Greeks using Black-Scholes model
*/
@ -502,3 +554,165 @@ function boxMullerTransform(): number {
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}
/**
* Prices a straddle option strategy
*/
export function straddle(params: OptionParameters): { callPrice: number; putPrice: number; strategyCost: number } {
const callOption = blackScholes(params);
const putOption = blackScholes(params);
const strategyCost = callOption.callPrice + putOption.putPrice;
return {
callPrice: callOption.callPrice,
putPrice: putOption.putPrice,
strategyCost: strategyCost
};
}
/**
* Prices a strangle option strategy
*/
export function strangle(callParams: OptionParameters, putParams: OptionParameters): { callPrice: number; putPrice: number; strategyCost: number } {
const callOption = blackScholes(callParams);
const putOption = blackScholes(putParams);
const strategyCost = callOption.callPrice + putOption.putPrice;
return {
callPrice: callOption.callPrice,
putPrice: putOption.putPrice,
strategyCost: strategyCost
};
}
/**
* Prices a butterfly option strategy
*/
export function butterfly(
lowerStrikeParams: OptionParameters,
middleStrikeParams: OptionParameters,
upperStrikeParams: OptionParameters
): {
lowerCallPrice: number;
middleCallPrice: number;
upperCallPrice: number;
strategyCost: number;
} {
const lowerCall = blackScholes(lowerStrikeParams);
const middleCall = blackScholes(middleStrikeParams);
const upperCall = blackScholes(upperStrikeParams);
const strategyCost = lowerCall.callPrice - 2 * middleCall.callPrice + upperCall.callPrice;
return {
lowerCallPrice: lowerCall.callPrice,
middleCallPrice: middleCall.callPrice,
upperCallPrice: upperCall.callPrice,
strategyCost: strategyCost
};
}
/**
* Prices a condor option strategy
*/
export function condor(
lowerStrikeParams: OptionParameters,
middleLowerStrikeParams: OptionParameters,
middleUpperStrikeParams: OptionParameters,
upperStrikeParams: OptionParameters
): {
lowerCallPrice: number;
middleLowerCallPrice: number;
middleUpperCallPrice: number;
upperCallPrice: number;
strategyCost: number;
} {
const lowerCall = blackScholes(lowerStrikeParams);
const middleLowerCall = blackScholes(middleLowerStrikeParams);
const middleUpperCall = blackScholes(middleUpperStrikeParams);
const upperCall = blackScholes(upperStrikeParams);
const strategyCost = lowerCall.callPrice - middleLowerCall.callPrice - middleUpperCall.callPrice + upperCall.callPrice;
return {
lowerCallPrice: lowerCall.callPrice,
middleLowerCallPrice: middleLowerCall.callPrice,
middleUpperCallPrice: middleUpperCall.callPrice,
upperCallPrice: upperCall.callPrice,
strategyCost: strategyCost
};
}
/**
* Calculates combined Greeks for an option strategy
*/
export function calculateStrategyGreeks(
positions: Array<{
optionType: 'call' | 'put';
quantity: number;
params: OptionParameters;
}>
): GreeksCalculation {
let totalDelta = 0;
let totalGamma = 0;
let totalTheta = 0;
let totalVega = 0;
let totalRho = 0;
for (const position of positions) {
const greeks = calculateGreeks(position.params, position.optionType);
totalDelta += greeks.delta * position.quantity;
totalGamma += greeks.gamma * position.quantity;
totalTheta += greeks.theta * position.quantity;
totalVega += greeks.vega * position.quantity;
totalRho += greeks.rho * position.quantity;
}
return {
delta: totalDelta,
gamma: totalGamma,
theta: totalTheta,
vega: totalVega,
rho: totalRho
};
}
/**
* Black-Scholes option pricing model with greeks
*/
export function blackScholesWithGreeks(params: OptionParameters, optionType: 'call' | 'put' = 'call'): { pricing: OptionPricing; greeks: GreeksCalculation } {
const pricing = blackScholes(params);
const greeks = calculateGreeks(params, optionType);
return { pricing, greeks };
}
/**
* Calculates the breakeven point for a call option at expiration
*/
export function callBreakeven(strikePrice: number, callPrice: number): number {
return strikePrice + callPrice;
}
/**
* Calculates the breakeven point for a put option at expiration
*/
export function putBreakeven(strikePrice: number, putPrice: number): number {
return strikePrice - putPrice;
}
/**
* Estimates the probability of profit for a call option at expiration
*/
export function callProbabilityOfProfit(spotPrice: number, strikePrice: number, timeToExpiry: number, riskFreeRate: number, volatility: number): number {
const d1 = (Math.log(spotPrice / strikePrice) + (riskFreeRate + 0.5 * volatility * volatility) * timeToExpiry) / (volatility * Math.sqrt(timeToExpiry));
return normalCDF(d1);
}
/**
* Estimates the probability of profit for a put option at expiration
*/
export function putProbabilityOfProfit(spotPrice: number, strikePrice: number, timeToExpiry: number, riskFreeRate: number, volatility: number): number {
const d1 = (Math.log(spotPrice / strikePrice) + (riskFreeRate + 0.5 * volatility * volatility) * timeToExpiry) / (volatility * Math.sqrt(timeToExpiry));
return 1 - normalCDF(d1);
}

View file

@ -463,6 +463,233 @@ export function calculateStrategyMetrics(
};
}
/**
* Calculate Calmar Ratio
*/
export function calmarRatio(returns: number[], equityCurve: Array<{ value: number; date: Date }>, riskFreeRate: number = 0): number {
const maxDrawdown = analyzeDrawdowns(equityCurve).maxDrawdown;
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
return maxDrawdown === 0 ? 0 : (avgReturn - riskFreeRate) / maxDrawdown;
}
/**
* Calculate Sterling Ratio
*/
export function sterlingRatio(returns: number[], equityCurve: Array<{ value: number; date: Date }>, riskFreeRate: number = 0): number {
const averageDrawdown = analyzeDrawdowns(equityCurve).averageDrawdown;
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
return averageDrawdown === 0 ? 0 : (avgReturn - riskFreeRate) / averageDrawdown;
}
/**
* Calculate Ulcer Index
*/
export function ulcerIndex(equityCurve: Array<{ value: number; date: Date }>): number {
let sumSquaredDrawdown = 0;
let peak = equityCurve[0].value;
for (const point of equityCurve) {
peak = Math.max(peak, point.value);
const drawdownPercent = (peak - point.value) / peak * 100;
sumSquaredDrawdown += drawdownPercent * drawdownPercent;
}
return Math.sqrt(sumSquaredDrawdown / equityCurve.length);
}
/**
* Calculate Ulcer Performance Index (UPI)
*/
export function ulcerPerformanceIndex(returns: number[], equityCurve: Array<{ value: number; date: Date }>, riskFreeRate: number = 0): number {
const ui = ulcerIndex(equityCurve);
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
return ui === 0 ? 0 : (avgReturn - riskFreeRate) / ui;
}
/**
* Calculate Information Ratio
*/
export function informationRatio(portfolioReturns: number[], benchmarkReturns: number[]): number {
if (portfolioReturns.length !== benchmarkReturns.length) {
throw new Error("Portfolio and benchmark returns must have the same length.");
}
const excessReturns = portfolioReturns.map((portfolioReturn, index) => portfolioReturn - benchmarkReturns[index]);
const trackingError = calculateVolatility(excessReturns);
const avgExcessReturn = excessReturns.reduce((sum, ret) => sum + ret, 0) / excessReturns.length;
return trackingError === 0 ? 0 : avgExcessReturn / trackingError;
}
/**
* Calculate Treynor Ratio
*/
export function treynorRatio(portfolioReturns: number[], marketReturns: number[], riskFreeRate: number): number {
const beta = calculateBeta(portfolioReturns, marketReturns);
const avgPortfolioReturn = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length;
return beta === 0 ? 0 : (avgPortfolioReturn - riskFreeRate) / beta;
}
/**
* Calculate Jensen's Alpha (same as Alpha, but included for clarity)
*/
export function jensensAlpha(portfolioReturns: number[], marketReturns: number[], riskFreeRate: number): number {
return calculateAlpha(portfolioReturns, marketReturns, riskFreeRate);
}
/**
* Calculate Capture Ratio (Up Capture and Down Capture)
*/
export function captureRatio(portfolioReturns: number[], benchmarkReturns: number[]): { upCaptureRatio: number; downCaptureRatio: number } {
let upCapture = 0;
let downCapture = 0;
let upMarketPeriods = 0;
let downMarketPeriods = 0;
for (let i = 0; i < portfolioReturns.length; i++) {
if (benchmarkReturns[i] > 0) {
upCapture += portfolioReturns[i];
upMarketPeriods++;
} else if (benchmarkReturns[i] < 0) {
downCapture += portfolioReturns[i];
downMarketPeriods++;
}
}
const upCaptureRatio = upMarketPeriods > 0 ? (upCapture / upMarketPeriods) / (benchmarkReturns.filter(r => r > 0).reduce((sum, r) => sum + r, 0) / upMarketPeriods) : 0;
const downCaptureRatio = downMarketPeriods > 0 ? (downCapture / downMarketPeriods) / (benchmarkReturns.filter(r => r < 0).reduce((sum, r) => sum + r, 0) / downMarketPeriods) : 0;
return { upCaptureRatio, downCaptureRatio };
}
/**
* Calculate Sortino Ratio
*/
export function sortinoRatio(returns: number[], riskFreeRate: number = 0): number {
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
const downsideReturns = returns.filter(ret => ret < riskFreeRate);
const downsideDeviation = Math.sqrt(
downsideReturns.reduce((sum, ret) => sum + Math.pow(ret - riskFreeRate, 2), 0) / returns.length
);
return downsideDeviation === 0 ? 0 : (avgReturn - riskFreeRate) / downsideDeviation;
}
/**
* Calculate Tail Ratio
*/
export function tailRatio(returns: number[], tailPercent: number = 0.1): number {
const numReturns = returns.length;
const tailSize = Math.floor(numReturns * tailPercent);
if (tailSize === 0) return 0;
const sortedReturns = [...returns].sort((a, b) => a - b);
const worstTail = sortedReturns.slice(0, tailSize);
const bestTail = sortedReturns.slice(numReturns - tailSize);
const avgWorst = worstTail.reduce((sum, ret) => sum + ret, 0) / tailSize;
const avgBest = bestTail.reduce((sum, ret) => sum + ret, 0) / tailSize;
return avgWorst === 0 ? 0 : avgBest / Math.abs(avgWorst);
}
/**
* Calculate Value at Risk (VaR)
*/
export function valueAtRisk(returns: number[], confidenceLevel: number = 0.05): number {
const sortedReturns = [...returns].sort((a, b) => a - b);
const varIndex = Math.floor(confidenceLevel * returns.length);
return sortedReturns[varIndex];
}
/**
* Calculate Conditional Value at Risk (CVaR) / Expected Shortfall
*/
export function conditionalValueAtRisk(returns: number[], confidenceLevel: number = 0.05): number {
const sortedReturns = [...returns].sort((a, b) => a - b);
const varIndex = Math.floor(confidenceLevel * returns.length);
const tailReturns = sortedReturns.slice(0, varIndex + 1);
return tailReturns.reduce((sum, ret) => sum + ret, 0) / tailReturns.length;
}
/**
* Calculate Rolling Beta
*/
export function calculateRollingBeta(portfolioReturns: number[], marketReturns: number[], windowSize: number): number[] {
if (portfolioReturns.length !== marketReturns.length || portfolioReturns.length < windowSize) return [];
const rollingBetas: number[] = [];
for (let i = windowSize; i <= portfolioReturns.length; i++) {
const portfolioWindow = portfolioReturns.slice(i - windowSize, i);
const marketWindow = marketReturns.slice(i - windowSize, i);
rollingBetas.push(calculateBeta(portfolioWindow, marketWindow));
}
return rollingBetas;
}
/**
* Calculate Rolling Alpha
*/
export function calculateRollingAlpha(portfolioReturns: number[], marketReturns: number[], riskFreeRate: number, windowSize: number): number[] {
if (portfolioReturns.length !== marketReturns.length || portfolioReturns.length < windowSize) return [];
const rollingAlphas: number[] = [];
for (let i = windowSize; i <= portfolioReturns.length; i++) {
const portfolioWindow = portfolioReturns.slice(i - windowSize, i);
const marketWindow = marketReturns.slice(i - windowSize, i);
rollingAlphas.push(calculateAlpha(portfolioWindow, marketWindow, riskFreeRate));
}
return rollingAlphas;
}
/**
* Calculate Time Weighted Rate of Return (TWRR)
*/
export function timeWeightedRateOfReturn(cashFlows: Array<{ amount: number; date: Date; value: number }>): number {
let totalReturn = 1;
let previousValue = cashFlows[0].value;
for (let i = 1; i < cashFlows.length; i++) {
const current = cashFlows[i];
const periodReturn = (current.value - previousValue - current.amount) / (previousValue + current.amount);
totalReturn *= (1 + periodReturn);
previousValue = current.value;
}
return totalReturn - 1;
}
/**
* Calculate Money Weighted Rate of Return (MWRR) - Approximation using IRR
*/
export function moneyWeightedRateOfReturn(cashFlows: Array<{ amount: number; date: Date; value: number }>): number {
// Approximate MWRR using Internal Rate of Return (IRR)
// This requires a numerical method or library for accurate IRR calculation
// This is a simplified example and may not be accurate for all cases
let totalCashFlow = 0;
let totalWeightedCashFlow = 0;
const startDate = cashFlows[0].date.getTime();
for (const cf of cashFlows) {
const timeDiff = (cf.date.getTime() - startDate) / (1000 * 60 * 60 * 24 * 365); // Years
totalCashFlow += cf.amount;
totalWeightedCashFlow += cf.amount * timeDiff;
}
// Simplified approximation: MWRR ≈ totalCashFlow / totalWeightedCashFlow - 1
return totalCashFlow / totalWeightedCashFlow - 1;
}
// Helper functions
function calculateSharpeRatio(returns: number[], riskFreeRate: number = 0): number {

View file

@ -547,3 +547,95 @@ export function jensenAlpha(
const expectedReturn = capmExpectedReturn(riskFreeRate, marketReturn, portfolioBeta);
return portfolioReturn - expectedReturn;
}
/**
* Calculate Ulcer Index
*/
export function ulcerIndex(equityCurve: number[]): number {
if (equityCurve.length < 2) return 0;
let sumOfSquaredDrawdowns = 0;
let peak = equityCurve[0];
for (let i = 1; i < equityCurve.length; i++) {
if (equityCurve[i] > peak) {
peak = equityCurve[i];
}
const drawdown = Math.max(0, (peak - equityCurve[i]) / peak);
sumOfSquaredDrawdowns += drawdown * drawdown;
}
return Math.sqrt(sumOfSquaredDrawdowns / equityCurve.length);
}
/**
* Calculate Ulcer Performance Index (UPI)
*/
export function ulcerPerformanceIndex(returns: number[], equityCurve: number[], riskFreeRate: number = 0): number {
const annualizedReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length * 252; // Assuming daily returns
const ui = ulcerIndex(equityCurve);
return ui !== 0 ? (annualizedReturn - riskFreeRate) / ui : 0;
}
/**
* Calculate Rachev Ratio
*/
export function rachevRatio(
returns: number[],
confidenceLevel: number = 0.05
): number {
if (returns.length === 0) return 0;
const sortedReturns = [...returns].sort((a, b) => a - b);
const lossTailIndex = Math.floor(confidenceLevel * sortedReturns.length);
const gainTailIndex = Math.floor((1 - confidenceLevel) * sortedReturns.length);
const expectedLoss = sortedReturns.slice(0, lossTailIndex)
.reduce((sum, ret) => sum + ret, 0) / lossTailIndex;
const expectedGain = sortedReturns.slice(gainTailIndex)
.reduce((sum, ret) => sum + ret, 0) / (sortedReturns.length - gainTailIndex);
return expectedGain > 0 && Math.abs(expectedLoss) > 0 ? expectedGain / Math.abs(expectedLoss) : 0;
}
/**
* Calculate Conditional Sharpe Ratio
*/
export function conditionalSharpeRatio(returns: number[], threshold: number, riskFreeRate: number = 0): number {
const belowThresholdReturns = returns.filter(ret => ret <= threshold);
if (belowThresholdReturns.length < 2) return 0;
const mean = belowThresholdReturns.reduce((sum, ret) => sum + ret, 0) / belowThresholdReturns.length;
const variance = belowThresholdReturns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / (belowThresholdReturns.length - 1);
const stdDev = Math.sqrt(variance);
return stdDev !== 0 ? (mean - riskFreeRate) / stdDev : 0;
}
/**
* Calculate Adjusted Sharpe Ratio
*/
export function adjustedSharpeRatio(returns: number[], riskFreeRate: number = 0): number {
const sr = sharpeRatio(returns, riskFreeRate);
const sk = skewness(returns);
const kurt = kurtosis(returns);
return sr * (1 + (sk / 6) * sr - ((kurt - 3) / 24) * sr * sr);
}
/**
* Calculate Bernardo-Ledoit Ratio
*/
export function bernardoLedoitRatio(returns: number[], riskFreeRate: number = 0): number {
const excessReturns = returns.map(ret => ret - riskFreeRate);
const positiveReturns = excessReturns.filter(ret => ret > 0);
const negativeReturns = excessReturns.filter(ret => ret < 0);
const upsideMean = positiveReturns.length > 0 ? positiveReturns.reduce((sum, ret) => sum + ret, 0) / positiveReturns.length : 0;
const downsideMean = negativeReturns.length > 0 ? negativeReturns.reduce((sum, ret) => sum + Math.abs(ret), 0) / negativeReturns.length : 0;
return downsideMean !== 0 ? upsideMean / downsideMean : 0;
}

View file

@ -645,3 +645,196 @@ export function pivotPoints(ohlcv: OHLCVData[]): Array<{
return result;
}
/**
* Ichimoku Cloud
*/
export function ichimokuCloud(
ohlcv: OHLCVData[],
tenkanSenPeriod: number = 9,
kijunSenPeriod: number = 26,
senkouSpanBPeriod: number = 52
): {
tenkanSen: number[];
kijunSen: number[];
senkouSpanA: number[];
senkouSpanB: number[];
chikouSpan: number[];
} {
const { high, low, close } = {
high: ohlcv.map(item => item.high),
low: ohlcv.map(item => item.low),
close: ohlcv.map(item => item.close)
};
const tenkanSen = calculateTenkanSen(high, low, tenkanSenPeriod);
const kijunSen = calculateKijunSen(high, low, kijunSenPeriod);
const senkouSpanA = calculateSenkouSpanA(tenkanSen, kijunSen);
const senkouSpanB = calculateSenkouSpanB(high, low, senkouSpanBPeriod);
const chikouSpan = calculateChikouSpan(close, kijunSenPeriod);
return {
tenkanSen,
kijunSen,
senkouSpanA,
senkouSpanB,
chikouSpan
};
function calculateTenkanSen(high: number[], low: number[], period: number): number[] {
const tenkanSen: number[] = [];
for (let i = period - 1; i < high.length; i++) {
const sliceHigh = high.slice(i - period + 1, i + 1);
const sliceLow = low.slice(i - period + 1, i + 1);
const highestHigh = Math.max(...sliceHigh);
const lowestLow = Math.min(...sliceLow);
tenkanSen.push((highestHigh + lowestLow) / 2);
}
return tenkanSen;
}
function calculateKijunSen(high: number[], low: number[], period: number): number[] {
const kijunSen: number[] = [];
for (let i = period - 1; i < high.length; i++) {
const sliceHigh = high.slice(i - period + 1, i + 1);
const sliceLow = low.slice(i - period + 1, i + 1);
const highestHigh = Math.max(...sliceHigh);
const lowestLow = Math.min(...sliceLow);
kijunSen.push((highestHigh + lowestLow) / 2);
}
return kijunSen;
}
function calculateSenkouSpanA(tenkanSen: number[], kijunSen: number[]): number[] {
const senkouSpanA: number[] = [];
for (let i = 0; i < tenkanSen.length; i++) {
senkouSpanA.push((tenkanSen[i] + kijunSen[i]) / 2);
}
return senkouSpanA;
}
function calculateSenkouSpanB(high: number[], low: number[], period: number): number[] {
const senkouSpanB: number[] = [];
for (let i = period - 1; i < high.length; i++) {
const sliceHigh = high.slice(i - period + 1, i + 1);
const sliceLow = low.slice(i - period + 1, i + 1);
const highestHigh = Math.max(...sliceHigh);
const lowestLow = Math.min(...sliceLow);
senkouSpanB.push((highestHigh + lowestLow) / 2);
}
return senkouSpanB;
}
function calculateChikouSpan(close: number[], period: number): number[] {
const chikouSpan: number[] = [];
for (let i = 0; i < close.length - period; i++) {
chikouSpan.push(close[i]);
}
return chikouSpan;
}
}
/**
* Keltner Channels
*/
export function keltnerChannels(
ohlcv: OHLCVData[],
period: number = 20,
multiplier: number = 2
): {
upper: number[];
middle: number[];
lower: number[];
} {
const atrValues = atr(ohlcv, period);
const middle = sma(ohlcv.map(item => (item.high + item.low + item.close) / 3), period);
const upper: number[] = [];
const lower: number[] = [];
for (let i = 0; i < middle.length; i++) {
upper.push(middle[i] + multiplier * atrValues[i]);
lower.push(middle[i] - multiplier * atrValues[i]);
}
return {
upper,
middle,
lower
};
}
/**
* Donchian Channels
*/
export function donchianChannels(
ohlcv: OHLCVData[],
period: number = 20
): {
upper: number[];
middle: number[];
lower: number[];
} {
const upper: number[] = [];
const lower: number[] = [];
const middle: number[] = [];
for (let i = period - 1; i < ohlcv.length; i++) {
const slice = ohlcv.slice(i - period + 1, i + 1);
const highestHigh = Math.max(...slice.map(item => item.high));
const lowestLow = Math.min(...slice.map(item => item.low));
upper.push(highestHigh);
lower.push(lowestLow);
middle.push((highestHigh + lowestLow) / 2);
}
return {
upper,
middle,
lower
};
}
/**
* Elder-Ray Index
*/
export function elderRay(
ohlcv: OHLCVData[],
period: number = 13
): {
bullPower: number[];
bearPower: number[];
} {
const emaValues = ema(ohlcv.map(item => item.close), period);
const bullPower: number[] = [];
const bearPower: number[] = [];
for (let i = period - 1; i < ohlcv.length; i++) {
bullPower.push(ohlcv[i].high - emaValues[i - period + 1]);
bearPower.push(ohlcv[i].low - emaValues[i - period + 1]);
}
return {
bullPower,
bearPower
};
}
/**
* Force Index
*/
export function forceIndex(
ohlcv: OHLCVData[],
period: number = 13
): number[] {
const forceIndexValues: number[] = [];
for (let i = 1; i < ohlcv.length; i++) {
const change = ohlcv[i].close - ohlcv[i - 1].close;
const volume = ohlcv[i].volume;
forceIndexValues.push(change * volume);
}
const smaValues = sma(forceIndexValues, period);
return smaValues;
}

View file

@ -517,3 +517,116 @@ export function calculateYangZhangVolatility(
return Math.sqrt(yangZhangVariance * annualizationFactor);
}
/**
* Parkinson volatility estimator
*/
export function parkinsonVolatility(
ohlcv: OHLCVData[],
annualizationFactor: number = 252
): number {
if (ohlcv.length < 2) return 0;
const sum = ohlcv
.slice(1)
.reduce((acc, curr) => {
const range = Math.log(curr.high / curr.low);
return acc + range * range;
}, 0);
return Math.sqrt((sum / (ohlcv.length - 1)) * annualizationFactor);
}
/**
* Calculate Implied Volatility using Black-Scholes model (simplified)
*/
export function calculateImpliedVolatility(
optionPrice: number,
spotPrice: number,
strikePrice: number,
timeToExpiry: number,
riskFreeRate: number,
optionType: 'call' | 'put',
maxIterations: number = 100,
tolerance: number = 1e-6
): number {
// Bisection method for implied volatility calculation
let low = 0.01;
let high = 5.0;
let impliedVol = 0.0;
for (let i = 0; i < maxIterations; i++) {
impliedVol = (low + high) / 2;
const modelPrice = blackScholes(spotPrice, strikePrice, timeToExpiry, impliedVol, riskFreeRate, optionType);
const diff = optionPrice - modelPrice;
if (Math.abs(diff) < tolerance) {
return impliedVol;
}
if (diff > 0) {
low = impliedVol;
} else {
high = impliedVol;
}
}
return impliedVol; // Return best estimate if no convergence
}
/**
* Black-Scholes option pricing model
*/
function blackScholes(
spotPrice: number,
strikePrice: number,
timeToExpiry: number,
volatility: number,
riskFreeRate: number,
optionType: 'call' | 'put'
): number {
const d1 = (Math.log(spotPrice / strikePrice) + (riskFreeRate + 0.5 * volatility * volatility) * timeToExpiry) / (volatility * Math.sqrt(timeToExpiry));
const d2 = d1 - volatility * Math.sqrt(timeToExpiry);
if (optionType === 'call') {
return spotPrice * normalCDF(d1) - strikePrice * Math.exp(-riskFreeRate * timeToExpiry) * normalCDF(d2);
} else {
return strikePrice * Math.exp(-riskFreeRate * timeToExpiry) * normalCDF(-d2) - spotPrice * normalCDF(-d1);
}
}
/**
* Normal cumulative distribution function
*/
function normalCDF(x: number): number {
const a1 = 0.254829592;
const a2 = -0.284496736;
const a3 = 1.421060743;
const a4 = -1.453152027;
const a5 = 1.061405429;
const p = 0.3275911;
const sign = x < 0 ? -1 : 1;
const absX = Math.abs(x);
const t = 1 / (1 + p * absX);
const y = 1 - (a1 * t + a2 * t * t + a3 * t * t * t + a4 * t * t * t * t + a5 * t * t * t * t * t) * Math.exp(-absX * absX / 2);
return 0.5 * (1 + sign * y);
}
/**
* Forecast volatility using EWMA
*/
export function forecastVolatilityEWMA(
volatilities: number[],
lambda: number = 0.94,
forecastHorizon: number = 1
): number {
if (volatilities.length === 0) {
return 0;
}
let forecast = volatilities[volatilities.length - 1];
for (let i = 0; i < forecastHorizon; i++) {
forecast = lambda * forecast + (1 - lambda) * forecast; // Using the last value as the long-term average
}
return forecast;
}