985 lines
27 KiB
TypeScript
985 lines
27 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|