/**
* Chande Momentum Oscillator (CMO)
* Alternative momentum oscillator developed by Tushar Chande
*/
export interface ChandeMomentumData {
// CMO value (-100 to +100)
cmo: number
// Sum of up moves over period
sumUp: number
// Sum of down moves over period
sumDown: number
// Trend direction
trend: 'bullish' | 'bearish' | 'neutral'
// Overbought/oversold levels
overbought: boolean // CMO > 50
oversold: boolean // CMO < -50
// Signal strength (0-100)
strength: number
// Divergence detection
divergence: 'bullish' | 'bearish' | 'none'
// Trading signal
signal: 'buy' | 'sell' | 'neutral'
}
/**
* Calculate Chande Momentum Oscillator
* @param prices Array of closing prices
* @param period Period for calculation (default 14)
* @returns ChandeMomentumData object
*/
export function calculateChandeMomentum(
prices: number[],
period: number = 14
): ChandeMomentumData | null {
// Minimum 5 prices required
if (prices.length < 5) {
return null
}
// Adjust period if not enough data - use adaptive period
const effectivePeriod = Math.min(period, prices.length - 1)
if (effectivePeriod < 3) {
return null
}
// Calculate price changes
const changes: number[] = []
for (let i = 1; i < prices.length; i++) {
changes.push(prices[i] - prices[i - 1])
}
// Use effective period for calculation
const usePeriod = Math.min(effectivePeriod, changes.length)
// Calculate sum of up moves and down moves over the period
const recentChanges = changes.slice(-usePeriod)
let sumUp = 0
let sumDown = 0
for (const change of recentChanges) {
if (change > 0) {
sumUp += change
} else {
sumDown += Math.abs(change)
}
}
// Calculate CMO
// CMO = 100 * (sumUp - sumDown) / (sumUp + sumDown)
const total = sumUp + sumDown
const cmo = total > 0 ? 100 * (sumUp - sumDown) / total : 0
// Determine trend
let trend: 'bullish' | 'bearish' | 'neutral' = 'neutral'
if (cmo > 0) {
trend = 'bullish'
} else if (cmo < 0) {
trend = 'bearish'
}
// Check overbought/oversold levels
const overbought = cmo > 50
const oversold = cmo < -50
// Calculate signal strength based on CMO magnitude
const strength = Math.min(100, Math.abs(cmo) * 2)
// Simple divergence detection (requires more historical data)
let divergence: 'bullish' | 'bearish' | 'none' = 'none'
if (prices.length >= period * 2 && changes.length >= period * 2) {
// Compare recent CMO with previous period
const prevChanges = changes.slice(-period * 2, -period)
let prevSumUp = 0
let prevSumDown = 0
for (const change of prevChanges) {
if (change > 0) {
prevSumUp += change
} else {
prevSumDown += Math.abs(change)
}
}
const prevTotal = prevSumUp + prevSumDown
const prevCmo = prevTotal > 0 ? 100 * (prevSumUp - prevSumDown) / prevTotal : 0
const recentPrice = prices[prices.length - 1]
const prevPrice = prices[prices.length - period - 1]
// Bullish divergence: price makes lower low but CMO makes higher low
if (recentPrice < prevPrice && cmo > prevCmo && cmo < 0) {
divergence = 'bullish'
}
// Bearish divergence: price makes higher high but CMO makes lower high
else if (recentPrice > prevPrice && cmo < prevCmo && cmo > 0) {
divergence = 'bearish'
}
}
// Generate trading signal
let signal: 'buy' | 'sell' | 'neutral' = 'neutral'
if (oversold && divergence === 'bullish') {
signal = 'buy'
} else if (overbought && divergence === 'bearish') {
signal = 'sell'
} else if (oversold && trend === 'bullish') {
signal = 'buy'
} else if (overbought && trend === 'bearish') {
signal = 'sell'
}
return {
cmo,
sumUp,
sumDown,
trend,
overbought,
oversold,
strength,
divergence,
signal
}
}
/**
* Calculate Chande Momentum Oscillator for multiple periods
* @param prices Array of closing prices
* @param periods Array of periods to calculate CMO for
* @returns Array of ChandeMomentumData objects
*/
export function calculateMultipleChandeMomentum(
prices: number[],
periods: number[] = [9, 14, 21]
): ChandeMomentumData[] {
return periods
.map(period => calculateChandeMomentum(prices, period))
.filter((cmo): cmo is ChandeMomentumData => cmo !== null)
}
/**
* Get CMO interpretation
* @param cmo ChandeMomentumData object
* @returns Human-readable interpretation
*/
export function getCMOInterpretation(cmo: ChandeMomentumData): string {
const { cmo: value, overbought, oversold, divergence } = cmo
if (overbought) {
return `Overbought (${value.toFixed(2)}) - potential reversal down`
}
if (oversold) {
return `Oversold (${value.toFixed(2)}) - potential reversal up`
}
if (divergence === 'bullish') {
return `Bullish divergence (${value.toFixed(2)}) - potential buy signal`
}
if (divergence === 'bearish') {
return `Bearish divergence (${value.toFixed(2)}) - potential sell signal`
}
if (value > 30) {
return `Strong bullish momentum (${value.toFixed(2)})`
}
if (value < -30) {
return `Strong bearish momentum (${value.toFixed(2)})`
}
if (value > 0) {
return `Bullish momentum (${value.toFixed(2)})`
}
if (value < 0) {
return `Bearish momentum (${value.toFixed(2)})`
}
return `Neutral (${value.toFixed(2)})`
}
/**
* Calculate CMO momentum strength rating
* @param cmo ChandeMomentumData object
* @returns Momentum strength rating (0-100)
*/
export function getCMOMomentumStrength(cmo: ChandeMomentumData): number {
const { cmo: value, sumUp, sumDown } = cmo
// Strength based on CMO value and the ratio of up vs down moves
const cmoStrength = Math.abs(value)
const ratioStrength = sumUp > 0 && sumDown > 0 ? Math.min(sumUp / sumDown, sumDown / sumUp) * 25 : 0
return Math.min(100, cmoStrength + ratioStrength)
}