/** * 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 = Volatility(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] = Volatility(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[0].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 = Correlation(returns1, returns2); const correlation1 = returns1.length > 1 ? Correlation(returns1.slice(1), returns2.slice(0, -1)) : 0; const correlationMinus1 = returns1.length > 1 ? Correlation(returns1.slice(0, -1), returns2.slice(1)) : 0; // Price lead-lag (simplified) const priceLeadLag = correlation1 - correlationMinus1; // Information shares (simplified Hasbrouck methodology) const variance1 = Variance(returns1); const variance2 = Variance(returns2); const covariance = Covariance(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 = Volatility(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 = Volatility(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 }; } // Helper functions function Volatility(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 Correlation(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 Variance(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 Covariance(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); }