/** * Market Statistics and Microstructure Analysis * Tools for analyzing market behavior, liquidity, and trading patterns */ // Local interface definition to avoid circular dependency interface OHLCVData { open: number; high: number; low: number; close: number; volume: number; timestamp: Date; } export interface LiquidityMetrics { bidAskSpread: number; relativeSpread: number; effectiveSpread: number; priceImpact: number; marketDepth: number; turnoverRatio: number; volumeWeightedSpread: number; } export interface MarketMicrostructure { tickSize: number; averageTradeSize: number; tradingFrequency: number; marketImpactCoefficient: number; informationShare: number; orderImbalance: number; } export interface TradingSessionStats { openPrice: number; closePrice: number; highPrice: number; lowPrice: number; volume: number; vwap: number; numberOfTrades: number; averageTradeSize: number; volatility: number; } export interface MarketRegime { regime: 'trending' | 'ranging' | 'volatile' | 'quiet'; confidence: number; trendDirection?: 'up' | 'down'; volatilityLevel: 'low' | 'medium' | 'high'; } /** * Volume Weighted Average Price (VWAP) */ export function VWAP(ohlcv: OHLCVData[]): number[] { if (ohlcv.length === 0) return []; const vwap: number[] = []; let cumulativeVolumePrice = 0; let cumulativeVolume = 0; for (const candle of ohlcv) { const typicalPrice = (candle.high + candle.low + candle.close) / 3; cumulativeVolumePrice += typicalPrice * candle.volume; cumulativeVolume += candle.volume; vwap.push(cumulativeVolume > 0 ? cumulativeVolumePrice / cumulativeVolume : typicalPrice); } return vwap; } /** * Time Weighted Average Price (TWAP) */ export function TWAP(prices: number[], timeWeights?: number[]): number { if (prices.length === 0) return 0; if (!timeWeights) { return prices.reduce((sum, price) => sum + price, 0) / prices.length; } if (prices.length !== timeWeights.length) { throw new Error('Prices and time weights arrays must have the same length'); } const totalWeight = timeWeights.reduce((sum, weight) => sum + weight, 0); const weightedSum = prices.reduce((sum, price, index) => sum + price * timeWeights[index], 0); return totalWeight > 0 ? weightedSum / totalWeight : 0; } /** * market impact of trades */ export function MarketImpact( trades: Array<{ price: number; volume: number; side: 'buy' | 'sell'; timestamp: Date }>, benchmarkPrice: number ): { temporaryImpact: number; permanentImpact: number; totalImpact: number; priceImprovement: number; } { if (trades.length === 0) { return { temporaryImpact: 0, permanentImpact: 0, totalImpact: 0, priceImprovement: 0 }; } const volumeWeightedPrice = trades.reduce((sum, trade) => sum + trade.price * trade.volume, 0) / trades.reduce((sum, trade) => sum + trade.volume, 0); const totalImpact = (volumeWeightedPrice - benchmarkPrice) / benchmarkPrice; // Simplified impact calculation const temporaryImpact = totalImpact * 0.6; // Temporary component const permanentImpact = totalImpact * 0.4; // Permanent component const priceImprovement = trades.reduce((sum, trade) => { const improvement = trade.side === 'buy' ? Math.max(0, benchmarkPrice - trade.price) : Math.max(0, trade.price - benchmarkPrice); return sum + improvement * trade.volume; }, 0) / trades.reduce((sum, trade) => sum + trade.volume, 0); return { temporaryImpact, permanentImpact, totalImpact, priceImprovement }; } /** * liquidity metrics */ export function LiquidityMetrics( ohlcv: OHLCVData[], bidPrices: number[], askPrices: number[], bidSizes: number[], askSizes: number[] ): LiquidityMetrics { if (ohlcv.length === 0 || bidPrices.length === 0) { return { bidAskSpread: 0, relativeSpread: 0, effectiveSpread: 0, priceImpact: 0, marketDepth: 0, turnoverRatio: 0, volumeWeightedSpread: 0 }; } // Average bid-ask spread const spreads = bidPrices.map((bid, index) => askPrices[index] - bid); const bidAskSpread = spreads.reduce((sum, spread) => sum + spread, 0) / spreads.length; // Relative spread const midPrices = bidPrices.map((bid, index) => (bid + askPrices[index]) / 2); const averageMidPrice = midPrices.reduce((sum, mid) => sum + mid, 0) / midPrices.length; const relativeSpread = averageMidPrice > 0 ? bidAskSpread / averageMidPrice : 0; // Market depth const averageBidSize = bidSizes.reduce((sum, size) => sum + size, 0) / bidSizes.length; const averageAskSize = askSizes.reduce((sum, size) => sum + size, 0) / askSizes.length; const marketDepth = (averageBidSize + averageAskSize) / 2; // Turnover ratio const averageVolume = ohlcv.reduce((sum, candle) => sum + candle.volume, 0) / ohlcv.length; const averagePrice = ohlcv.reduce((sum, candle) => sum + candle.close, 0) / ohlcv.length; const marketCap = averagePrice * 1000000; // Simplified market cap const turnoverRatio = marketCap > 0 ? (averageVolume * averagePrice) / marketCap : 0; return { bidAskSpread, relativeSpread: relativeSpread * 100, // Convert to percentage effectiveSpread: bidAskSpread * 0.8, // Simplified effective spread priceImpact: relativeSpread * 2, // Simplified price impact marketDepth, turnoverRatio: turnoverRatio * 100, // Convert to percentage volumeWeightedSpread: bidAskSpread // Simplified }; } /** * Identify market regimes */ export function identifyMarketRegime( ohlcv: OHLCVData[], lookbackPeriod: number = 20 ): MarketRegime { if (ohlcv.length < lookbackPeriod) { return { regime: 'quiet', confidence: 0, volatilityLevel: 'low' }; } const recentData = ohlcv.slice(-lookbackPeriod); const prices = recentData.map(candle => candle.close); const volumes = recentData.map(candle => candle.volume); // returns and volatility const returns = []; for (let i = 1; i < prices.length; i++) { returns.push((prices[i] - prices[i - 1]) / prices[i - 1]); } const volatility = calculateVolatility(returns); const averageVolume = volumes.reduce((sum, vol) => sum + vol, 0) / volumes.length; // Trend analysis const firstPrice = prices[0]; const lastPrice = prices[prices.length - 1]; const trendStrength = Math.abs((lastPrice - firstPrice) / firstPrice); // Determine volatility level let volatilityLevel: 'low' | 'medium' | 'high'; if (volatility < 0.01) volatilityLevel = 'low'; else if (volatility < 0.03) volatilityLevel = 'medium'; else volatilityLevel = 'high'; // Determine regime let regime: 'trending' | 'ranging' | 'volatile' | 'quiet'; let confidence = 0; let trendDirection: 'up' | 'down' | undefined; if (volatility < 0.005) { regime = 'quiet'; confidence = 0.8; } else if (volatility > 0.04) { regime = 'volatile'; confidence = 0.7; } else if (trendStrength > 0.05) { regime = 'trending'; trendDirection = lastPrice > firstPrice ? 'up' : 'down'; confidence = Math.min(0.9, trendStrength * 10); } else { regime = 'ranging'; confidence = 0.6; } return { regime, confidence, trendDirection, volatilityLevel }; } /** * order book imbalance */ export function OrderBookImbalance( bidPrices: number[], askPrices: number[], bidSizes: number[], askSizes: number[], levels: number = 5 ): number { const levelsToAnalyze = Math.min(levels, bidPrices.length, askPrices.length); let totalBidVolume = 0; let totalAskVolume = 0; for (let i = 0; i < levelsToAnalyze; i++) { totalBidVolume += bidSizes[i]; totalAskVolume += askSizes[i]; } const totalVolume = totalBidVolume + totalAskVolume; if (totalVolume === 0) return 0; return (totalBidVolume - totalAskVolume) / totalVolume; } /** * intraday patterns */ export function IntradayPatterns( ohlcv: OHLCVData[] ): { hourlyReturns: { [hour: number]: number }; hourlyVolatility: { [hour: number]: number }; hourlyVolume: { [hour: number]: number }; openingGap: number; closingDrift: number; } { const hourlyData: { [hour: number]: { returns: number[]; volumes: number[] } } = {}; // Initialize hourly buckets for (let hour = 0; hour < 24; hour++) { hourlyData[hour] = { returns: [], volumes: [] }; } // Aggregate data by hour for (let i = 1; i < ohlcv.length; i++) { const hour = ohlcv[i].timestamp.getHours(); const return_ = (ohlcv[i].close - ohlcv[i - 1].close) / ohlcv[i - 1].close; hourlyData[hour].returns.push(return_); hourlyData[hour].volumes.push(ohlcv[i].volume); } // statistics for each hour const hourlyReturns: { [hour: number]: number } = {}; const hourlyVolatility: { [hour: number]: number } = {}; const hourlyVolume: { [hour: number]: number } = {}; for (let hour = 0; hour < 24; hour++) { const data = hourlyData[hour]; hourlyReturns[hour] = data.returns.length > 0 ? data.returns.reduce((sum, ret) => sum + ret, 0) / data.returns.length : 0; hourlyVolatility[hour] = calculateVolatility(data.returns); hourlyVolume[hour] = data.volumes.length > 0 ? data.volumes.reduce((sum, vol) => sum + vol, 0) / data.volumes.length : 0; } // opening gap and closing drift const openingGap = ohlcv.length > 1 ? (ohlcv[1].open - ohlcv[0].close) / ohlcv[0].close : 0; const lastCandle = ohlcv[ohlcv.length - 1]; const closingDrift = (lastCandle.close - lastCandle.open) / lastCandle.open; return { hourlyReturns, hourlyVolatility, hourlyVolume, openingGap, closingDrift }; } /** * price discovery metrics */ export function PriceDiscovery( prices1: number[], // Prices from market 1 prices2: number[] // Prices from market 2 ): { informationShare1: number; informationShare2: number; priceLeadLag: number; // Positive if market 1 leads cointegrationStrength: number; } { if (prices1.length !== prices2.length || prices1.length < 2) { return { informationShare1: 0.5, informationShare2: 0.5, priceLeadLag: 0, cointegrationStrength: 0 }; } // returns const returns1 = []; const returns2 = []; for (let i = 1; i < prices1.length; i++) { returns1.push((prices1[i] - prices1[i - 1]) / prices1[i - 1]); returns2.push((prices2[i] - prices2[i - 1]) / prices2[i - 1]); } // correlations with lags const correlation0 = calculateCorrelation(returns1, returns2); const correlation1 = returns1.length > 1 ? calculateCorrelation(returns1.slice(1), returns2.slice(0, -1)) : 0; const correlationMinus1 = returns1.length > 1 ? calculateCorrelation(returns1.slice(0, -1), returns2.slice(1)) : 0; // Price lead-lag (simplified) const priceLeadLag = correlation1 - correlationMinus1; // Information shares (simplified Hasbrouck methodology) const variance1 = calculateVariance(returns1); const variance2 = calculateVariance(returns2); const covariance = calculateCovariance(returns1, returns2); const totalVariance = variance1 + variance2 + 2 * covariance; const informationShare1 = totalVariance > 0 ? (variance1 + covariance) / totalVariance : 0.5; const informationShare2 = 1 - informationShare1; // Cointegration strength (simplified) const cointegrationStrength = Math.abs(correlation0); return { informationShare1, informationShare2, priceLeadLag, cointegrationStrength }; } /** * market stress indicators */ export function MarketStress( ohlcv: OHLCVData[], lookbackPeriod: number = 20 ): { stressLevel: 'low' | 'medium' | 'high' | 'extreme'; volatilityStress: number; liquidityStress: number; correlationStress: number; overallStress: number; } { if (ohlcv.length < lookbackPeriod) { return { stressLevel: 'low', volatilityStress: 0, liquidityStress: 0, correlationStress: 0, overallStress: 0 }; } const recentData = ohlcv.slice(-lookbackPeriod); const returns = []; const volumes = []; for (let i = 1; i < recentData.length; i++) { returns.push((recentData[i].close - recentData[i - 1].close) / recentData[i - 1].close); volumes.push(recentData[i].volume); } // Volatility stress const volatility = calculateVolatility(returns); const volatilityStress = Math.min(1, volatility / 0.05); // Normalize to 5% daily vol // Liquidity stress (volume-based) const averageVolume = volumes.reduce((sum, vol) => sum + vol, 0) / volumes.length; const volumeVariability = calculateVolatility(volumes.map(vol => vol / averageVolume)); const liquidityStress = Math.min(1, volumeVariability); // Correlation stress (simplified - would need multiple assets) const correlationStress = 0.3; // Placeholder // Overall stress const overallStress = (volatilityStress * 0.4 + liquidityStress * 0.3 + correlationStress * 0.3); let stressLevel: 'low' | 'medium' | 'high' | 'extreme'; if (overallStress < 0.25) stressLevel = 'low'; else if (overallStress < 0.5) stressLevel = 'medium'; else if (overallStress < 0.75) stressLevel = 'high'; else stressLevel = 'extreme'; return { stressLevel, volatilityStress, liquidityStress, correlationStress, overallStress }; } /** * realized spread */ export function RealizedSpread( trades: Array<{ price: number; side: 'buy' | 'sell'; timestamp: Date }>, midPrices: number[], timeWindow: number = 5 // minutes ): number { if (trades.length === 0 || midPrices.length === 0) return 0; let totalSpread = 0; let count = 0; for (const trade of trades) { // Find corresponding mid price const midPrice = midPrices[0]; // Simplified - should match by timestamp const spread = trade.side === 'buy' ? 2 * (trade.price - midPrice) : 2 * (midPrice - trade.price); totalSpread += spread; count++; } return count > 0 ? totalSpread / count : 0; } /** * implementation shortfall */ export function ImplementationShortfall( decisionPrice: number, executionPrices: number[], volumes: number[], commissions: number[], marketImpact: number[] ): { totalShortfall: number; delayComponent: number; marketImpactComponent: number; timingComponent: number; commissionComponent: number; } { if (executionPrices.length !== volumes.length) { throw new Error('Execution prices and volumes must have same length'); } const totalVolume = volumes.reduce((sum, vol) => sum + vol, 0); const weightedExecutionPrice = executionPrices.reduce((sum, price, i) => sum + price * volumes[i], 0) / totalVolume; const totalCommissions = commissions.reduce((sum, comm) => sum + comm, 0); const totalMarketImpact = marketImpact.reduce((sum, impact, i) => sum + impact * volumes[i], 0); const delayComponent = weightedExecutionPrice - decisionPrice; const marketImpactComponent = totalMarketImpact / totalVolume; const timingComponent = 0; // Simplified - would need benchmark price evolution const commissionComponent = totalCommissions / totalVolume; const totalShortfall = delayComponent + marketImpactComponent + timingComponent + commissionComponent; return { totalShortfall, delayComponent, marketImpactComponent, timingComponent, commissionComponent }; } /** * Amihud Illiquidity Measure (price impact per unit of volume) */ export function amihudIlliquidity( ohlcv: OHLCVData[], lookbackPeriod: number = 252 ): number { if (ohlcv.length < lookbackPeriod) return 0; const recentData = ohlcv.slice(-lookbackPeriod); let illiquiditySum = 0; let validDays = 0; for (const candle of recentData) { if (candle.volume > 0) { const dailyReturn = Math.abs((candle.close - candle.open) / candle.open); const dollarVolume = candle.volume * candle.close; if (dollarVolume > 0) { illiquiditySum += dailyReturn / dollarVolume; validDays++; } } } return validDays > 0 ? (illiquiditySum / validDays) * 1000000 : 0; // Scale to millions } /** * Roll's Spread Estimator (effective spread from serial covariance) */ export function rollSpreadEstimator(prices: number[]): number { if (prices.length < 3) return 0; // Calculate price changes const priceChanges: number[] = []; for (let i = 1; i < prices.length; i++) { priceChanges.push(prices[i] - prices[i - 1]); } // Calculate serial covariance let covariance = 0; for (let i = 1; i < priceChanges.length; i++) { covariance += priceChanges[i] * priceChanges[i - 1]; } covariance /= (priceChanges.length - 1); // Roll's estimator: spread = 2 * sqrt(-covariance) const spread = covariance < 0 ? 2 * Math.sqrt(-covariance) : 0; return spread; } /** * Kyle's Lambda (price impact coefficient) */ export function kyleLambda( priceChanges: number[], orderFlow: number[] // Signed order flow (positive for buys, negative for sells) ): number { if (priceChanges.length !== orderFlow.length || priceChanges.length < 2) return 0; // Calculate regression: priceChange = lambda * orderFlow + error const n = priceChanges.length; const meanPrice = priceChanges.reduce((sum, p) => sum + p, 0) / n; const meanFlow = orderFlow.reduce((sum, f) => sum + f, 0) / n; let numerator = 0; let denominator = 0; for (let i = 0; i < n; i++) { const priceDeviation = priceChanges[i] - meanPrice; const flowDeviation = orderFlow[i] - meanFlow; numerator += priceDeviation * flowDeviation; denominator += flowDeviation * flowDeviation; } return denominator > 0 ? numerator / denominator : 0; } /** * Probability of Informed Trading (PIN) - simplified version */ export function probabilityInformedTrading( buyVolumes: number[], sellVolumes: number[], period: number = 20 ): number { if (buyVolumes.length !== sellVolumes.length || buyVolumes.length < period) return 0; const recentBuys = buyVolumes.slice(-period); const recentSells = sellVolumes.slice(-period); let totalImbalance = 0; let totalVolume = 0; for (let i = 0; i < period; i++) { const imbalance = Math.abs(recentBuys[i] - recentSells[i]); const volume = recentBuys[i] + recentSells[i]; totalImbalance += imbalance; totalVolume += volume; } // Simplified PIN estimate based on order imbalance return totalVolume > 0 ? totalImbalance / totalVolume : 0; } /** * Herfindahl-Hirschman Index for Volume Concentration */ export function volumeConcentrationHHI( exchanges: Array<{ name: string; volume: number }> ): number { if (exchanges.length === 0) return 0; const totalVolume = exchanges.reduce((sum, exchange) => sum + exchange.volume, 0); if (totalVolume === 0) return 0; let hhi = 0; for (const exchange of exchanges) { const marketShare = exchange.volume / totalVolume; hhi += marketShare * marketShare; } 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 } = {}; 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; } /** * 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; const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / (returns.length - 1); return Math.sqrt(variance); } function calculateCorrelation(x: number[], y: number[]): number { if (x.length !== y.length || x.length < 2) return 0; const n = x.length; const meanX = x.reduce((sum, val) => sum + val, 0) / n; const meanY = y.reduce((sum, val) => sum + val, 0) / n; let numerator = 0; let sumXSquared = 0; let sumYSquared = 0; for (let i = 0; i < n; i++) { const xDiff = x[i] - meanX; const yDiff = y[i] - meanY; numerator += xDiff * yDiff; sumXSquared += xDiff * xDiff; sumYSquared += yDiff * yDiff; } const denominator = Math.sqrt(sumXSquared * sumYSquared); return denominator > 0 ? numerator / denominator : 0; } function calculateVariance(values: number[]): number { if (values.length < 2) return 0; const mean = values.reduce((sum, val) => sum + val, 0) / values.length; return values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / (values.length - 1); } function calculateCovariance(x: number[], y: number[]): number { if (x.length !== y.length || x.length < 2) return 0; const n = x.length; const meanX = x.reduce((sum, val) => sum + val, 0) / n; const meanY = y.reduce((sum, val) => sum + val, 0) / n; return x.reduce((sum, val, i) => sum + (val - meanX) * (y[i] - meanY), 0) / (n - 1); }