/**
* Volume Profile Indicator
* Analyzes volume distribution across price levels to identify institutional activity
*/
export interface VolumeProfileData {
// Price levels with volume data
priceLevels: Array<{
price: number
volume: number
percentage: number // % of total volume
}>
// Key volume levels
valueAreaHigh: number // 70% of volume (VAH)
valueAreaLow: number // 70% of volume (VAL)
pointOfControl: number // Price level with maximum volume (POC)
// Volume profile analysis
totalVolume: number
averageVolumePerLevel: number
// Current price position vs profile
position: 'above_vah' | 'in_value_area' | 'below_val' | 'at_poc'
// Institutional activity signals
highVolumeNodes: number[] // Price levels with > 2x average volume
lowVolumeNodes: number[] // Price levels with < 0.5x average volume
// Profile imbalance
imbalanceRatio: number // Ratio of volume above vs below POC
// Support/resistance strength
vahStrength: number // Strength of VAH (0-100)
valStrength: number // Strength of VAL (0-100)
pocStrength: number // Strength of POC (0-100)
}
/**
* Calculate Volume Profile
* @param highs Array of high prices
* @param lows Array of low prices
* @param closes Array of close prices
* @param volumes Array of volume data
* @param priceBins Number of price bins to create (default 20)
* @returns VolumeProfileData object
*/
export function calculateVolumeProfile(
highs: number[],
lows: number[],
closes: number[],
volumes: number[],
priceBins: number = 20
): VolumeProfileData | null {
if (highs.length !== lows.length || highs.length !== closes.length || highs.length !== volumes.length) {
return null
}
if (highs.length < 10) {
return null
}
// Find price range
const allPrices = [...highs, ...lows, ...closes]
const minPrice = Math.min(...allPrices)
const maxPrice = Math.max(...allPrices)
const priceRange = maxPrice - minPrice
if (priceRange === 0) {
return null
}
// Create price bins
const binSize = priceRange / priceBins
const priceLevels: Array<{ price: number; volume: number; count: number }> = []
// Initialize bins
for (let i = 0; i < priceBins; i++) {
const binPrice = minPrice + (i * binSize) + (binSize / 2) // Center of bin
priceLevels.push({ price: binPrice, volume: 0, count: 0 })
}
// Distribute volume across price levels
for (let i = 0; i < highs.length; i++) {
const high = highs[i]
const low = lows[i]
const close = closes[i]
const volume = volumes[i]
// Distribute volume across the price range of this candle
const candleRange = high - low
if (candleRange === 0) {
// Doji candle - put all volume at close price
const binIndex = Math.floor((close - minPrice) / binSize)
if (binIndex >= 0 && binIndex < priceBins) {
priceLevels[binIndex].volume += volume
priceLevels[binIndex].count++
}
} else {
// Distribute volume proportionally across the candle range
const volumePerPriceUnit = volume / candleRange
const startBin = Math.floor((low - minPrice) / binSize)
const endBin = Math.floor((high - minPrice) / binSize)
for (let bin = Math.max(0, startBin); bin <= Math.min(priceBins - 1, endBin); bin++) {
const binStart = minPrice + (bin * binSize)
const binEnd = binStart + binSize
// Calculate overlap with candle range
const overlapStart = Math.max(binStart, low)
const overlapEnd = Math.min(binEnd, high)
const overlap = Math.max(0, overlapEnd - overlapStart)
if (overlap > 0) {
const binVolume = volumePerPriceUnit * overlap
priceLevels[bin].volume += binVolume
priceLevels[bin].count++
}
}
}
}
// Calculate total volume and percentages
const totalVolume = priceLevels.reduce((sum, level) => sum + level.volume, 0)
const averageVolumePerLevel = totalVolume / priceBins
const levelsWithPercentage = priceLevels
.map(level => ({
...level,
percentage: totalVolume > 0 ? (level.volume / totalVolume) * 100 : 0
}))
.sort((a, b) => b.volume - a.volume) // Sort by volume descending
// Find Point of Control (POC) - price level with maximum volume
const pocLevel = levelsWithPercentage[0]
const pointOfControl = pocLevel.price
// Calculate Value Area (70% of volume around POC)
const targetVolume = totalVolume * 0.7
let accumulatedVolume = 0
let valueAreaLow = pointOfControl
let valueAreaHigh = pointOfControl
// Sort by price for value area calculation
const sortedByPrice = [...priceLevels].sort((a, b) => a.price - b.price)
// Find levels around POC that contain 70% of volume
for (const level of sortedByPrice) {
if (level.price <= pointOfControl) {
accumulatedVolume += level.volume
if (accumulatedVolume <= targetVolume) {
valueAreaLow = level.price
}
}
}
accumulatedVolume = 0
for (let i = sortedByPrice.length - 1; i >= 0; i--) {
const level = sortedByPrice[i]
if (level.price >= pointOfControl) {
accumulatedVolume += level.volume
if (accumulatedVolume <= targetVolume) {
valueAreaHigh = level.price
}
}
}
// Get current price
const currentPrice = closes[closes.length - 1]
// Determine position
let position: 'above_vah' | 'in_value_area' | 'below_val' | 'at_poc'
if (Math.abs(currentPrice - pointOfControl) / pointOfControl < 0.001) {
position = 'at_poc'
} else if (currentPrice > valueAreaHigh) {
position = 'above_vah'
} else if (currentPrice < valueAreaLow) {
position = 'below_val'
} else {
position = 'in_value_area'
}
// Find high and low volume nodes
const highVolumeNodes = priceLevels
.filter(level => level.volume > averageVolumePerLevel * 2)
.map(level => level.price)
.sort((a, b) => a - b)
const lowVolumeNodes = priceLevels
.filter(level => level.volume > 0 && level.volume < averageVolumePerLevel * 0.5)
.map(level => level.price)
.sort((a, b) => a - b)
// Calculate imbalance ratio (volume above POC vs below POC)
const volumeAbovePOC = priceLevels
.filter(level => level.price > pointOfControl)
.reduce((sum, level) => sum + level.volume, 0)
const volumeBelowPOC = priceLevels
.filter(level => level.price < pointOfControl)
.reduce((sum, level) => sum + level.volume, 0)
const imbalanceRatio = volumeBelowPOC > 0 ? volumeAbovePOC / volumeBelowPOC : volumeAbovePOC > 0 ? 2 : 1
// Calculate strength of key levels based on volume concentration
const pocStrength = Math.min(100, (pocLevel.volume / averageVolumePerLevel) * 10)
const vahLevel = priceLevels.find(l => Math.abs(l.price - valueAreaHigh) < binSize / 2)
const valLevel = priceLevels.find(l => Math.abs(l.price - valueAreaLow) < binSize / 2)
const vahStrength = vahLevel ? Math.min(100, (vahLevel.volume / averageVolumePerLevel) * 10) : 50
const valStrength = valLevel ? Math.min(100, (valLevel.volume / averageVolumePerLevel) * 10) : 50
return {
priceLevels: levelsWithPercentage.map(({ count, ...level }) => level),
valueAreaHigh,
valueAreaLow,
pointOfControl,
totalVolume,
averageVolumePerLevel,
position,
highVolumeNodes,
lowVolumeNodes,
imbalanceRatio,
vahStrength,
valStrength,
pocStrength
}
}
/**
* Get Volume Profile signal
* @param profile VolumeProfileData object
* @returns Trading signal based on volume profile analysis
*/
export function getVolumeProfileSignal(profile: VolumeProfileData): 'buy' | 'sell' | 'neutral' {
const { position, imbalanceRatio, pocStrength, vahStrength, valStrength } = profile
// Strong signals based on position and strength
if (position === 'above_vah' && vahStrength > 70) {
return 'sell' // Rejecting high volume area
}
if (position === 'below_val' && valStrength > 70) {
return 'buy' // Rejecting low volume area
}
if (position === 'at_poc' && pocStrength > 80) {
// At POC - direction depends on imbalance
return imbalanceRatio > 1.5 ? 'buy' : imbalanceRatio < 0.67 ? 'sell' : 'neutral'
}
// Moderate signals
if (position === 'in_value_area') {
return imbalanceRatio > 1.2 ? 'buy' : imbalanceRatio < 0.8 ? 'sell' : 'neutral'
}
return 'neutral'
}
/**
* Get institutional activity level
* @param profile VolumeProfileData object
* @returns Activity level description
*/
export function getInstitutionalActivityLevel(profile: VolumeProfileData): string {
const { pocStrength, vahStrength, valStrength, highVolumeNodes } = profile
const avgStrength = (pocStrength + vahStrength + valStrength) / 3
const nodeCount = highVolumeNodes.length
if (avgStrength > 80 && nodeCount > 3) return 'Very High'
if (avgStrength > 60 && nodeCount > 2) return 'High'
if (avgStrength > 40 && nodeCount > 1) return 'Moderate'
if (avgStrength > 20) return 'Low'
return 'Very Low'
}