added calcs
This commit is contained in:
parent
ef12c9d308
commit
7886b7cfa5
10 changed files with 4331 additions and 0 deletions
521
libs/utils/src/calculations/market-statistics.ts
Normal file
521
libs/utils/src/calculations/market-statistics.ts
Normal file
|
|
@ -0,0 +1,521 @@
|
|||
/**
|
||||
* 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';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Volume Weighted Average Price (VWAP)
|
||||
*/
|
||||
export function calculateVWAP(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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Time Weighted Average Price (TWAP)
|
||||
*/
|
||||
export function calculateTWAP(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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate market impact of trades
|
||||
*/
|
||||
export function calculateMarketImpact(
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate liquidity metrics
|
||||
*/
|
||||
export function calculateLiquidityMetrics(
|
||||
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);
|
||||
|
||||
// Calculate 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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate order book imbalance
|
||||
*/
|
||||
export function calculateOrderBookImbalance(
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate intraday patterns
|
||||
*/
|
||||
export function calculateIntradayPatterns(
|
||||
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);
|
||||
}
|
||||
|
||||
// Calculate 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;
|
||||
}
|
||||
|
||||
// Calculate 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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate price discovery metrics
|
||||
*/
|
||||
export function calculatePriceDiscovery(
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate 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]);
|
||||
}
|
||||
|
||||
// Calculate 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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate market stress indicators
|
||||
*/
|
||||
export function calculateMarketStress(
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue