From cca9ac03dddaf0fca5c02a3e67f77bb4b28a25c1 Mon Sep 17 00:00:00 2001 From: Bojan Kucera Date: Wed, 4 Jun 2025 20:01:39 -0400 Subject: [PATCH] added more functions --- .../src/calculations/basic-calculations.ts | 167 ++++++++++ .../src/calculations/correlation-analysis.ts | 272 ++++++++++++++++- .../src/calculations/market-statistics.ts | 287 +++++++++++++++++- .../utils/src/calculations/options-pricing.ts | 214 +++++++++++++ .../src/calculations/performance-metrics.ts | 227 ++++++++++++++ libs/utils/src/calculations/risk-metrics.ts | 92 ++++++ .../src/calculations/technical-indicators.ts | 193 ++++++++++++ .../src/calculations/volatility-models.ts | 113 +++++++ 8 files changed, 1563 insertions(+), 2 deletions(-) diff --git a/libs/utils/src/calculations/basic-calculations.ts b/libs/utils/src/calculations/basic-calculations.ts index e7e7c6f..51c09fe 100644 --- a/libs/utils/src/calculations/basic-calculations.ts +++ b/libs/utils/src/calculations/basic-calculations.ts @@ -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); +} diff --git a/libs/utils/src/calculations/correlation-analysis.ts b/libs/utils/src/calculations/correlation-analysis.ts index 214a9bd..77d9d98 100644 --- a/libs/utils/src/calculations/correlation-analysis.ts +++ b/libs/utils/src/calculations/correlation-analysis.ts @@ -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(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); diff --git a/libs/utils/src/calculations/market-statistics.ts b/libs/utils/src/calculations/market-statistics.ts index 5efb214..5bed776 100644 --- a/libs/utils/src/calculations/market-statistics.ts +++ b/libs/utils/src/calculations/market-statistics.ts @@ -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; diff --git a/libs/utils/src/calculations/options-pricing.ts b/libs/utils/src/calculations/options-pricing.ts index 14c5aba..210ae38 100644 --- a/libs/utils/src/calculations/options-pricing.ts +++ b/libs/utils/src/calculations/options-pricing.ts @@ -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 { + // …Newton–Raphson 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); +} \ No newline at end of file diff --git a/libs/utils/src/calculations/performance-metrics.ts b/libs/utils/src/calculations/performance-metrics.ts index afae53e..ceef4f6 100644 --- a/libs/utils/src/calculations/performance-metrics.ts +++ b/libs/utils/src/calculations/performance-metrics.ts @@ -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 { diff --git a/libs/utils/src/calculations/risk-metrics.ts b/libs/utils/src/calculations/risk-metrics.ts index 62da045..11e85f8 100644 --- a/libs/utils/src/calculations/risk-metrics.ts +++ b/libs/utils/src/calculations/risk-metrics.ts @@ -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; +} \ No newline at end of file diff --git a/libs/utils/src/calculations/technical-indicators.ts b/libs/utils/src/calculations/technical-indicators.ts index e78526b..bdccb06 100644 --- a/libs/utils/src/calculations/technical-indicators.ts +++ b/libs/utils/src/calculations/technical-indicators.ts @@ -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; +} \ No newline at end of file diff --git a/libs/utils/src/calculations/volatility-models.ts b/libs/utils/src/calculations/volatility-models.ts index fd3dff1..70a8000 100644 --- a/libs/utils/src/calculations/volatility-models.ts +++ b/libs/utils/src/calculations/volatility-models.ts @@ -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; +} \ No newline at end of file