/**
* Positive Volume Index (PVI)
* Accumulates price changes on days when volume increases from the previous day
*/
export interface PositiveVolumeIndexData {
// Positive Volume Index value
pvi: number
// Current volume change (%)
volumeChange: number
// Price change on positive volume day
priceChange: number
// Trend direction
trend: 'bullish' | 'bearish' | 'neutral'
// Signal strength (0-100)
strength: number
// PVI slope (rate of change)
slope: number
// Trading signal
signal: 'buy' | 'sell' | 'neutral'
// Volume confirmation
volumeIncreasing: boolean
}
/**
* Calculate Positive Volume Index
* @param closes Array of closing prices
* @param volumes Array of volume data
* @param initialPVI Initial PVI value (default 1000)
* @returns PositiveVolumeIndexData object
*/
export function calculatePositiveVolumeIndex(
closes: number[],
volumes: number[],
initialPVI: number = 1000
): PositiveVolumeIndexData | null {
if (closes.length !== volumes.length || closes.length < 2) {
return null
}
let pvi = initialPVI
let lastVolume = volumes[0]
// Calculate PVI up to the current period
for (let i = 1; i < closes.length; i++) {
const currentVolume = volumes[i]
const volumeIncreasing = currentVolume > lastVolume
if (volumeIncreasing) {
const priceChangePercent = ((closes[i] - closes[i - 1]) / closes[i - 1]) * 100
pvi = pvi + (pvi * priceChangePercent / 100)
}
lastVolume = currentVolume
}
// Calculate current period data
const currentVolume = volumes[volumes.length - 1]
const previousVolume = volumes[volumes.length - 2]
const volumeIncreasing = currentVolume > previousVolume
const volumeChange = ((currentVolume - previousVolume) / previousVolume) * 100
const priceChange = ((closes[closes.length - 1] - closes[closes.length - 2]) / closes[closes.length - 2]) * 100
// Determine trend based on PVI slope
let trend: 'bullish' | 'bearish' | 'neutral' = 'neutral'
let slope = 0
if (closes.length >= 5) {
// Calculate slope over last 5 periods
const recentPVI = calculatePVIHistory(closes.slice(-5), volumes.slice(-5), initialPVI)
if (recentPVI.length >= 2) {
slope = (recentPVI[recentPVI.length - 1] - recentPVI[0]) / (recentPVI.length - 1)
if (slope > 1) {
trend = 'bullish'
} else if (slope < -1) {
trend = 'bearish'
}
}
}
// Calculate signal strength based on slope magnitude
const strength = Math.min(100, Math.abs(slope) * 10)
// Generate trading signal
let signal: 'buy' | 'sell' | 'neutral' = 'neutral'
if (volumeIncreasing && trend === 'bullish' && strength > 20) {
signal = 'buy'
} else if (volumeIncreasing && trend === 'bearish' && strength > 20) {
signal = 'sell'
}
return {
pvi,
volumeChange,
priceChange,
trend,
strength,
slope,
signal,
volumeIncreasing
}
}
/**
* Calculate Negative Volume Index
* @param closes Array of closing prices
* @param volumes Array of volume data
* @param initialNVI Initial NVI value (default 1000)
* @returns NegativeVolumeIndexData object
*/
export function calculateNegativeVolumeIndex(
closes: number[],
volumes: number[],
initialNVI: number = 1000
): {
nvi: number
volumeChange: number
priceChange: number
trend: 'bullish' | 'bearish' | 'neutral'
strength: number
slope: number
signal: 'buy' | 'sell' | 'neutral'
volumeDecreasing: boolean
} | null {
if (closes.length !== volumes.length || closes.length < 2) {
return null
}
let nvi = initialNVI
let lastVolume = volumes[0]
// Calculate NVI up to the current period
for (let i = 1; i < closes.length; i++) {
const currentVolume = volumes[i]
const volumeDecreasing = currentVolume < lastVolume
if (volumeDecreasing) {
const priceChangePercent = ((closes[i] - closes[i - 1]) / closes[i - 1]) * 100
nvi = nvi + (nvi * priceChangePercent / 100)
}
lastVolume = currentVolume
}
// Calculate current period data
const currentVolume = volumes[volumes.length - 1]
const previousVolume = volumes[volumes.length - 2]
const volumeDecreasing = currentVolume < previousVolume
const volumeChange = ((currentVolume - previousVolume) / previousVolume) * 100
const priceChange = ((closes[closes.length - 1] - closes[closes.length - 2]) / closes[closes.length - 2]) * 100
// Determine trend based on NVI slope
let trend: 'bullish' | 'bearish' | 'neutral' = 'neutral'
let slope = 0
if (closes.length >= 5) {
const recentNVI = calculateNVIHistory(closes.slice(-5), volumes.slice(-5), initialNVI)
if (recentNVI.length >= 2) {
slope = (recentNVI[recentNVI.length - 1] - recentNVI[0]) / (recentNVI.length - 1)
if (slope > 1) {
trend = 'bullish'
} else if (slope < -1) {
trend = 'bearish'
}
}
}
const strength = Math.min(100, Math.abs(slope) * 10)
let signal: 'buy' | 'sell' | 'neutral' = 'neutral'
if (volumeDecreasing && trend === 'bullish' && strength > 20) {
signal = 'buy'
} else if (volumeDecreasing && trend === 'bearish' && strength > 20) {
signal = 'sell'
}
return {
nvi,
volumeChange,
priceChange,
trend,
strength,
slope,
signal,
volumeDecreasing
}
}
/**
* Helper function to calculate PVI history
*/
function calculatePVIHistory(closes: number[], volumes: number[], initialPVI: number): number[] {
const pviValues: number[] = []
let pvi = initialPVI
let lastVolume = volumes[0]
pviValues.push(pvi)
for (let i = 1; i < closes.length; i++) {
const currentVolume = volumes[i]
const volumeIncreasing = currentVolume > lastVolume
if (volumeIncreasing) {
const priceChangePercent = ((closes[i] - closes[i - 1]) / closes[i - 1]) * 100
pvi = pvi + (pvi * priceChangePercent / 100)
}
pviValues.push(pvi)
lastVolume = currentVolume
}
return pviValues
}
/**
* Helper function to calculate NVI history
*/
function calculateNVIHistory(closes: number[], volumes: number[], initialNVI: number): number[] {
const nviValues: number[] = []
let nvi = initialNVI
let lastVolume = volumes[0]
nviValues.push(nvi)
for (let i = 1; i < closes.length; i++) {
const currentVolume = volumes[i]
const volumeDecreasing = currentVolume < lastVolume
if (volumeDecreasing) {
const priceChangePercent = ((closes[i] - closes[i - 1]) / closes[i - 1]) * 100
nvi = nvi + (nvi * priceChangePercent / 100)
}
nviValues.push(nvi)
lastVolume = currentVolume
}
return nviValues
}
/**
* Get PVI interpretation
* @param pvi PositiveVolumeIndexData object
* @returns Human-readable interpretation
*/
export function getPVIInterpretation(pvi: PositiveVolumeIndexData): string {
const { trend, volumeIncreasing, strength, signal } = pvi
let interpretation = `PVI: ${pvi.pvi.toFixed(2)}`
if (volumeIncreasing) {
interpretation += ' (Volume ↑)'
} else {
interpretation += ' (Volume ↓)'
}
interpretation += ` - ${trend} trend`
if (strength > 50) {
interpretation += ' (Strong)'
} else if (strength > 20) {
interpretation += ' (Moderate)'
} else {
interpretation += ' (Weak)'
}
if (signal !== 'neutral') {
interpretation += ` - ${signal.toUpperCase()} signal`
}
return interpretation
}
/**
* Get NVI interpretation
* @param nvi Negative volume index data
* @returns Human-readable interpretation
*/
export function getNVIInterpretation(nvi: {
nvi: number
volumeChange: number
priceChange: number
trend: 'bullish' | 'bearish' | 'neutral'
strength: number
slope: number
signal: 'buy' | 'sell' | 'neutral'
volumeDecreasing: boolean
}): string {
const { trend, volumeDecreasing, strength, signal } = nvi
let interpretation = `NVI: ${nvi.nvi.toFixed(2)}`
if (volumeDecreasing) {
interpretation += ' (Volume ↓)'
} else {
interpretation += ' (Volume ↑)'
}
interpretation += ` - ${trend} trend`
if (strength > 50) {
interpretation += ' (Strong)'
} else if (strength > 20) {
interpretation += ' (Moderate)'
} else {
interpretation += ' (Weak)'
}
if (signal !== 'neutral') {
interpretation += ` - ${signal.toUpperCase()} signal`
}
return interpretation
}
/**
* Analyze smart money vs public participation
* @param pvi PositiveVolumeIndexData object
* @param nvi Negative volume index data
* @returns Analysis of market participation
*/
export function analyzeSmartMoneyParticipation(
pvi: PositiveVolumeIndexData,
nvi: {
nvi: number
volumeChange: number
priceChange: number
trend: 'bullish' | 'bearish' | 'neutral'
strength: number
slope: number
signal: 'buy' | 'sell' | 'neutral'
volumeDecreasing: boolean
}
): {
dominantForce: 'smart_money' | 'public' | 'balanced'
confidence: number
interpretation: string
} {
const pviStrength = pvi.strength
const nviStrength = nvi.strength
let dominantForce: 'smart_money' | 'public' | 'balanced'
let confidence: number
let interpretation: string
if (nviStrength > pviStrength * 1.5) {
dominantForce = 'smart_money'
confidence = Math.min(100, nviStrength)
interpretation = 'Smart money (institutional investors) are more active on down volume days'
} else if (pviStrength > nviStrength * 1.5) {
dominantForce = 'public'
confidence = Math.min(100, pviStrength)
interpretation = 'Public participation is higher on up volume days'
} else {
dominantForce = 'balanced'
confidence = Math.min(100, (pviStrength + nviStrength) / 2)
interpretation = 'Balanced participation between smart money and public'
}
return { dominantForce, confidence, interpretation }
}