/**
* Comprehensive Volume-Based Analysis
* Volume confirmation, liquidity zones, footprint charts, and volume-based trading signals
*/
/**
* Calculate Cumulative Volume Delta (CVD)
* CVD tracks the cumulative difference between buying and selling volume
* Positive CVD = More buying pressure
* Negative CVD = More selling pressure
*/
export function calculateCVD(historicalData) {
if (!historicalData || historicalData.length < 10) {
return null;
}
const cvdValues = [];
let cumulativeDelta = 0;
historicalData.forEach((candle) => {
const open = candle.open || 0;
const close = candle.close || 0;
const high = candle.high || close;
const low = candle.low || close;
const volume = candle.volume || 0;
// Estimate buy/sell volume based on price action
// Method: Use close position within the candle range
const priceRange = high - low;
let buyRatio = 0.5; // Default 50/50
if (priceRange > 0) {
// Where did price close within the range? (0 = low, 1 = high)
const closePosition = (close - low) / priceRange;
buyRatio = closePosition;
}
else {
// No range - use direction
buyRatio = close >= open ? 0.6 : 0.4;
}
const buyVolume = volume * buyRatio;
const sellVolume = volume * (1 - buyRatio);
const delta = buyVolume - sellVolume;
cumulativeDelta += delta;
cvdValues.push(cumulativeDelta);
});
// Calculate CVD trend (compare recent CVD to older CVD)
const recentPeriod = Math.min(10, Math.floor(cvdValues.length / 2));
const recentCVD = cvdValues.slice(-recentPeriod);
const olderCVD = cvdValues.slice(-recentPeriod * 2, -recentPeriod);
const recentAvg = recentCVD.length > 0 ? recentCVD.reduce((a, b) => a + b, 0) / recentCVD.length : 0;
const olderAvg = olderCVD.length > 0 ? olderCVD.reduce((a, b) => a + b, 0) / olderCVD.length : 0;
let cvdTrend = 'neutral';
const trendThreshold = Math.abs(cumulativeDelta) * 0.05; // 5% threshold
if (recentAvg > olderAvg + trendThreshold) {
cvdTrend = 'bullish';
}
else if (recentAvg < olderAvg - trendThreshold) {
cvdTrend = 'bearish';
}
// Calculate delta change (most recent vs previous)
const cvdDelta = cvdValues.length >= 2
? cvdValues[cvdValues.length - 1] - cvdValues[cvdValues.length - 2]
: 0;
// Check for divergence (price going up but CVD going down, or vice versa)
let cvdDivergence = false;
if (historicalData.length >= 10) {
const recentPrices = historicalData.slice(-10).map(c => c.close || 0);
const priceDirection = recentPrices[recentPrices.length - 1] > recentPrices[0] ? 'up' : 'down';
const cvdDirection = recentCVD[recentCVD.length - 1] > recentCVD[0] ? 'up' : 'down';
cvdDivergence = priceDirection !== cvdDirection;
}
return {
cvd: cumulativeDelta,
cvdTrend,
cvdDelta,
cvdDivergence,
cvdValues: cvdValues.slice(-20) // Return last 20 values
};
}
/**
* 1. Volume Confirmation for Breakout
* Validates breakouts with volume analysis
*/
export function confirmBreakoutWithVolume(historicalData, breakoutPrice, currentPrice, direction, volumeProfile) {
if (!historicalData || historicalData.length < 20) {
return {
isValid: false,
strength: 'weak',
volumeRatio: 0,
volumeChange: 0,
reason: 'Insufficient data for volume confirmation'
};
}
// Get recent candles for volume analysis
const recentCandles = historicalData.slice(-20);
const avgVolume = recentCandles.reduce((sum, candle) => sum + (candle.volume || 0), 0) / recentCandles.length;
const currentVolume = historicalData[historicalData.length - 1]?.volume || 0;
// CRITICAL FIX: Check for division by zero - avgVolume can be 0
if (avgVolume <= 0) {
return {
isValid: false,
strength: 'weak',
volumeRatio: 0,
volumeChange: 0,
reason: 'Average volume is zero - cannot calculate volume confirmation'
};
}
// Calculate volume change
const volumeChange = ((currentVolume - avgVolume) / avgVolume) * 100;
const volumeRatio = currentVolume / avgVolume;
// Determine breakout type
const isBreakout = direction === 'up' && currentPrice > breakoutPrice;
const isBreakdown = direction === 'down' && currentPrice < breakoutPrice;
if (!isBreakout && !isBreakdown) {
return {
isValid: false,
strength: 'false',
volumeRatio,
volumeChange,
reason: 'Price has not broken the level yet',
breakoutType: direction === 'up' ? 'breakout' : 'breakdown'
};
}
// Volume confirmation rules
let strength = 'weak';
let isValid = false;
let reason = '';
if (volumeRatio >= 2.0 && volumeChange >= 100) {
// Strong volume confirmation
strength = 'strong';
isValid = true;
reason = `Strong volume confirmation: ${volumeChange.toFixed(1)}% above average (${volumeRatio.toFixed(2)}x)`;
}
else if (volumeRatio >= 1.5 && volumeChange >= 50) {
// Moderate volume confirmation
strength = 'moderate';
isValid = true;
reason = `Moderate volume confirmation: ${volumeChange.toFixed(1)}% above average (${volumeRatio.toFixed(2)}x)`;
}
else if (volumeRatio >= 1.2 && volumeChange >= 20) {
// Weak volume confirmation
strength = 'weak';
isValid = true;
reason = `Weak volume confirmation: ${volumeChange.toFixed(1)}% above average (${volumeRatio.toFixed(2)}x)`;
}
else {
// False breakout - low volume
strength = 'false';
isValid = false;
reason = `False breakout detected: Volume only ${volumeChange.toFixed(1)}% above average (${volumeRatio.toFixed(2)}x). Low volume suggests lack of conviction.`;
}
// Check volume profile context if available
if (volumeProfile) {
const poc = volumeProfile.poc || 0;
const distanceFromPoc = Math.abs(currentPrice - poc) / poc * 100;
if (distanceFromPoc < 1 && isValid) {
reason += ` Price near POC (${poc.toFixed(2)}), high volume expected.`;
}
else if (distanceFromPoc > 5 && isValid) {
reason += ` Price far from POC, confirm with additional volume.`;
}
}
return {
isValid,
strength,
volumeRatio,
volumeChange,
reason,
breakoutType: direction === 'up' ? 'breakout' : 'breakdown',
priceLevel: breakoutPrice
};
}
/**
* 2. Liquidity Zones Analysis
* Identifies high liquidity areas based on volume and open interest
*/
export function identifyLiquidityZones(historicalData, openInterestData, _volumeProfile) {
if (!historicalData || historicalData.length < 50) {
return [];
}
const zones = [];
const priceVolumeMap = new Map();
// Aggregate volume by price levels (rounded to nearest 0.1%)
historicalData.forEach(candle => {
const price = candle.close || 0;
const volume = candle.volume || 0;
const roundedPrice = Math.round(price * 1000) / 1000; // Round to 0.001
if (priceVolumeMap.has(roundedPrice)) {
const existing = priceVolumeMap.get(roundedPrice);
existing.volume += volume;
existing.count += 1;
}
else {
priceVolumeMap.set(roundedPrice, { volume, count: 1 });
}
});
// Convert to array and sort by volume
const priceVolumeArray = Array.from(priceVolumeMap.entries())
.map(([price, data]) => ({
price,
volume: data.volume,
count: data.count,
avgVolume: data.volume / data.count
}))
.sort((a, b) => b.volume - a.volume);
// Get top 20% of price levels by volume
const topCount = Math.max(5, Math.floor(priceVolumeArray.length * 0.2));
const topPriceLevels = priceVolumeArray.slice(0, topCount);
// Group nearby price levels into zones
const zoneThreshold = 0.005; // 0.5% price range for zone grouping
const processedPrices = new Set();
topPriceLevels.forEach(({ price }) => {
if (processedPrices.has(price))
return;
// Find nearby prices to form a zone
const zonePrices = priceVolumeArray.filter(p => !processedPrices.has(p.price) && Math.abs(p.price - price) / price <= zoneThreshold);
if (zonePrices.length === 0)
return;
// Calculate zone boundaries
const prices = zonePrices.map(p => p.price).sort((a, b) => a - b);
const minPrice = prices[0];
const maxPrice = prices[prices.length - 1];
const zoneVolume = zonePrices.reduce((sum, p) => sum + p.volume, 0);
const zoneContracts = zonePrices.reduce((sum, p) => sum + p.count, 0);
// Get open interest for this zone if available
let zoneOI = 0;
if (openInterestData) {
zoneOI = openInterestData
.filter(oi => oi.price >= minPrice && oi.price <= maxPrice)
.reduce((sum, oi) => sum + oi.openInterest, 0);
}
// Calculate liquidity score (volume + OI weighted)
const maxVolume = priceVolumeArray[0]?.volume || 1;
const volumeScore = maxVolume > 0 ? zoneVolume / maxVolume : 0;
// CRITICAL FIX: Check for empty array before using Math.max to avoid -Infinity
let oiScore = 0;
if (openInterestData && openInterestData.length > 0) {
const maxOI = Math.max(...openInterestData.map(oi => oi.openInterest || 0));
if (maxOI > 0) {
oiScore = zoneOI / maxOI;
}
}
const liquidityScore = (volumeScore * 0.7) + (oiScore * 0.3);
// Determine zone type and strength
const currentPrice = historicalData[historicalData.length - 1]?.close || 0;
const zoneCenter = (minPrice + maxPrice) / 2;
const isSupport = currentPrice > zoneCenter;
const isResistance = currentPrice < zoneCenter;
let type = 'neutral';
if (isSupport && currentPrice - zoneCenter > (maxPrice - minPrice) * 0.5) {
type = 'support';
}
else if (isResistance && zoneCenter - currentPrice > (maxPrice - minPrice) * 0.5) {
type = 'resistance';
}
let strength = 'low';
if (liquidityScore >= 0.7) {
strength = 'high';
}
else if (liquidityScore >= 0.4) {
strength = 'medium';
}
zones.push({
priceRange: [minPrice, maxPrice],
volume: zoneVolume,
openInterest: zoneOI,
liquidityScore,
type,
strength,
contracts: zoneContracts
});
// Mark prices as processed
zonePrices.forEach(p => processedPrices.add(p.price));
});
// Sort zones by liquidity score
zones.sort((a, b) => b.liquidityScore - a.liquidityScore);
return zones;
}
/**
* 3. Footprint Charts Analysis
* Real-time buying vs selling pressure analysis
*/
export function analyzeFootprint(historicalData, currentPrice) {
if (!historicalData || historicalData.length < 10) {
return {
currentPrice,
footprints: [],
totalBuyVolume: 0,
totalSellVolume: 0,
netDelta: 0,
buyPressure: 0,
sellPressure: 0,
dominantSide: 'neutral',
imbalanceRatio: 0,
significantLevels: []
};
}
// Analyze recent candles (last 50 for footprint)
const recentCandles = historicalData.slice(-50);
const footprints = [];
let totalBuyVolume = 0;
let totalSellVolume = 0;
recentCandles.forEach((candle) => {
const price = candle.close || 0;
const volume = candle.volume || 0;
const open = candle.open || price;
const high = candle.high || price;
const low = candle.low || price;
// Estimate buy/sell volume based on price action
// If close > open: more buying pressure
// If close < open: more selling pressure
const priceChange = price - open;
const priceRange = high - low || 1;
// Calculate buy/sell ratio based on where price closed in the range
const closePosition = priceRange > 0 ? (price - low) / priceRange : 0.5;
const buyRatio = closePosition; // Higher close = more buying
// const sellRatio = 1 - closePosition // Lower close = more selling
// Adjust based on price change direction
const directionalBias = priceChange > 0 ? 0.1 : priceChange < 0 ? -0.1 : 0;
const adjustedBuyRatio = Math.max(0, Math.min(1, buyRatio + directionalBias));
const adjustedSellRatio = 1 - adjustedBuyRatio;
const buyVolume = volume * adjustedBuyRatio;
const sellVolume = volume * adjustedSellRatio;
const delta = buyVolume - sellVolume;
totalBuyVolume += buyVolume;
totalSellVolume += sellVolume;
footprints.push({
price,
buyVolume,
sellVolume,
delta,
totalVolume: volume,
buyPressure: adjustedBuyRatio * 100,
sellPressure: adjustedSellRatio * 100,
imbalance: delta
});
});
// Calculate aggregate metrics
const netDelta = totalBuyVolume - totalSellVolume;
const totalVolume = totalBuyVolume + totalSellVolume;
const buyPressure = totalVolume > 0 ? (totalBuyVolume / totalVolume) * 100 : 50;
const sellPressure = totalVolume > 0 ? (totalSellVolume / totalVolume) * 100 : 50;
const imbalanceRatio = totalVolume > 0 ? netDelta / totalVolume : 0;
// Determine dominant side
let dominantSide = 'neutral';
if (imbalanceRatio > 0.1) {
dominantSide = 'buy';
}
else if (imbalanceRatio < -0.1) {
dominantSide = 'sell';
}
// Identify significant price levels (high volume, high delta)
const significantLevels = footprints
.filter(f => Math.abs(f.delta) > totalVolume * 0.05) // At least 5% of total volume
.map(f => ({
price: f.price,
buyVolume: f.buyVolume,
sellVolume: f.sellVolume,
delta: f.delta,
significance: Math.abs(f.delta) > totalVolume * 0.1 ? 'high' :
Math.abs(f.delta) > totalVolume * 0.07 ? 'medium' : 'low'
}))
.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta))
.slice(0, 10); // Top 10 significant levels
return {
currentPrice,
footprints,
totalBuyVolume,
totalSellVolume,
netDelta,
buyPressure,
sellPressure,
dominantSide,
imbalanceRatio,
significantLevels
};
}
export function performComprehensiveVolumeAnalysis(historicalData, currentPrice, breakoutLevel, breakoutDirection, volumeProfile, cvd, openInterestData) {
// 1. Volume confirmation for breakout
let volumeConfirmation;
if (breakoutLevel && breakoutDirection) {
volumeConfirmation = confirmBreakoutWithVolume(historicalData, breakoutLevel, currentPrice, breakoutDirection, volumeProfile);
}
// 2. Liquidity zones
const liquidityZones = identifyLiquidityZones(historicalData, openInterestData, volumeProfile);
// 3. Footprint analysis
const footprint = analyzeFootprint(historicalData, currentPrice);
// 4. Generate recommendations
let action = 'hold';
let reason = '';
let confidence = 0.5;
let riskLevel = 'medium';
// Analyze footprint dominance
if (footprint.dominantSide === 'buy' && footprint.buyPressure > 60) {
action = 'enter';
reason = `Strong buying pressure (${footprint.buyPressure.toFixed(1)}%). Net delta: +${(footprint.netDelta / 1000).toFixed(2)}k`;
confidence = Math.min(0.8, 0.5 + (footprint.buyPressure - 50) / 100);
riskLevel = footprint.buyPressure > 70 ? 'low' : 'medium';
}
else if (footprint.dominantSide === 'sell' && footprint.sellPressure > 60) {
action = 'exit';
reason = `Strong selling pressure (${footprint.sellPressure.toFixed(1)}%). Net delta: ${(footprint.netDelta / 1000).toFixed(2)}k`;
confidence = Math.min(0.8, 0.5 + (footprint.sellPressure - 50) / 100);
riskLevel = footprint.sellPressure > 70 ? 'low' : 'medium';
}
// Check volume confirmation
if (volumeConfirmation) {
if (volumeConfirmation.isValid && volumeConfirmation.strength === 'strong') {
action = 'enter';
reason = `${reason ? reason + '. ' : ''}Strong volume confirmation: ${volumeConfirmation.reason}`;
confidence = Math.min(0.9, confidence + 0.2);
riskLevel = 'low';
}
else if (volumeConfirmation.strength === 'false') {
action = 'wait';
reason = `False breakout detected: ${volumeConfirmation.reason}`;
confidence = 0.3;
riskLevel = 'high';
}
}
// Check liquidity zones
const nearbyZone = liquidityZones.find(zone => currentPrice >= zone.priceRange[0] && currentPrice <= zone.priceRange[1]);
if (nearbyZone && nearbyZone.strength === 'high') {
if (nearbyZone.type === 'support' && action === 'enter') {
reason = `${reason ? reason + '. ' : ''}Near high-liquidity support zone`;
confidence = Math.min(0.9, confidence + 0.1);
}
else if (nearbyZone.type === 'resistance' && action === 'enter') {
action = 'wait';
reason = `${reason ? reason + '. ' : ''}Near high-liquidity resistance zone - wait for breakout`;
confidence = 0.4;
riskLevel = 'high';
}
}
return {
volumeConfirmation,
liquidityZones,
footprint,
volumeProfile,
cvd,
recommendations: {
action,
reason: reason || 'Neutral volume conditions - monitor for clearer signals',
confidence,
riskLevel
}
};
}