/**
* Momentum Indicators
* RSI, MACD, Stochastic, CCI, Williams %R calculations
*/
import { calculateEMA } from './moving-averages'
export function calculateRSI(closes: number[], period: number = 14): number[] {
// Minimum 5 data points required
if (closes.length < 5) return []
// Use adaptive period
const effectivePeriod = Math.min(period, closes.length - 1)
const rsi: number[] = []
const gains: number[] = []
const losses: number[] = []
// Calculate price changes
for (let i = 1; i < closes.length; i++) {
const change = closes[i] - closes[i - 1]
gains.push(change > 0 ? change : 0)
losses.push(change < 0 ? Math.abs(change) : 0)
}
// Calculate initial average gain and loss using effective period
let avgGain = gains.slice(0, effectivePeriod).reduce((a, b) => a + b, 0) / effectivePeriod
let avgLoss = losses.slice(0, effectivePeriod).reduce((a, b) => a + b, 0) / effectivePeriod
// Calculate RSI
for (let i = effectivePeriod; i < gains.length; i++) {
if (avgLoss === 0) {
rsi.push(100)
} else {
const rs = avgGain / avgLoss
const currentRSI = 100 - (100 / (1 + rs))
rsi.push(currentRSI)
}
// Update averages
avgGain = (avgGain * (effectivePeriod - 1) + gains[i]) / effectivePeriod
avgLoss = (avgLoss * (effectivePeriod - 1) + losses[i]) / effectivePeriod
}
return rsi
}
export interface MACDResult {
MACD: number
signal: number
histogram: number
}
export function calculateMACD(
closes: number[],
fastPeriod: number = 12,
slowPeriod: number = 26,
signalPeriod: number = 9
): MACDResult[] {
// Minimum 10 data points required
if (closes.length < 10) return []
// Use adaptive periods
const dataRatio = Math.min(1, closes.length / 35)
const effectiveSlowPeriod = Math.max(5, Math.floor(slowPeriod * dataRatio))
const effectiveFastPeriod = Math.max(3, Math.floor(fastPeriod * dataRatio))
const effectiveSignalPeriod = Math.max(3, Math.floor(signalPeriod * dataRatio))
const fastEMA = calculateEMA(closes, effectiveFastPeriod)
const slowEMA = calculateEMA(closes, effectiveSlowPeriod)
if (fastEMA.length === 0 || slowEMA.length === 0) return []
// Align arrays (slowEMA starts later)
const macdLine: number[] = []
const startIdx = Math.max(0, effectiveSlowPeriod - effectiveFastPeriod)
for (let i = 0; i < slowEMA.length; i++) {
if (fastEMA[startIdx + i] !== undefined) {
macdLine.push(fastEMA[startIdx + i] - slowEMA[i])
}
}
if (macdLine.length === 0) return []
// Calculate signal line (EMA of MACD line) using effective period
const signalLine = calculateEMA(macdLine, effectiveSignalPeriod)
if (signalLine.length === 0) {
// Fallback: return MACD without signal
return macdLine.map(m => ({ MACD: m, signal: m, histogram: 0 }))
}
// Calculate histogram
const histogram: MACDResult[] = []
const signalStartIdx = Math.max(0, effectiveSignalPeriod - 1)
for (let i = 0; i < signalLine.length; i++) {
if (macdLine[signalStartIdx + i] !== undefined) {
histogram.push({
MACD: macdLine[signalStartIdx + i],
signal: signalLine[i],
histogram: macdLine[signalStartIdx + i] - signalLine[i]
})
}
}
return histogram
}
export function calculateStochastic(
highs: number[],
lows: number[],
closes: number[],
kPeriod: number = 14,
dPeriod: number = 3
): { k: number[]; d: number[] } {
// Minimum 5 data points required
if (highs.length < 5 || lows.length < 5 || closes.length < 5) {
return { k: [], d: [] }
}
// Use adaptive periods
const effectiveKPeriod = Math.min(kPeriod, highs.length)
const effectiveDPeriod = Math.min(dPeriod, Math.floor(effectiveKPeriod / 3))
const stochK: number[] = []
const stochD: number[] = []
// Calculate %K (Stochastic %K)
for (let i = effectiveKPeriod - 1; i < closes.length; i++) {
const periodHighs = highs.slice(i - effectiveKPeriod + 1, i + 1)
const periodLows = lows.slice(i - effectiveKPeriod + 1, i + 1)
const highestHigh = Math.max(...periodHighs)
const lowestLow = Math.min(...periodLows)
const currentClose = closes[i]
if (highestHigh !== lowestLow) {
const k = ((currentClose - lowestLow) / (highestHigh - lowestLow)) * 100
stochK.push(k)
} else {
stochK.push(50) // Neutral if no range
}
}
// Calculate %D (Stochastic %D) as SMA of %K
const useDPeriod = Math.max(2, effectiveDPeriod)
if (stochK.length >= useDPeriod) {
for (let i = useDPeriod - 1; i < stochK.length; i++) {
const sum = stochK.slice(i - useDPeriod + 1, i + 1).reduce((a, b) => a + b, 0)
stochD.push(sum / useDPeriod)
}
}
return {
k: stochK,
d: stochD
}
}
export function calculateCCI(
highs: number[],
lows: number[],
closes: number[],
period: number = 20
): number[] {
// Minimum 5 data points required
if (highs.length < 5 || lows.length < 5 || closes.length < 5) {
return []
}
// Use adaptive period
const effectivePeriod = Math.min(period, highs.length)
const cci: number[] = []
for (let i = effectivePeriod - 1; i < closes.length; i++) {
const typicalPrices: number[] = []
for (let j = i - effectivePeriod + 1; j <= i; j++) {
const tp = (highs[j] + lows[j] + closes[j]) / 3
typicalPrices.push(tp)
}
const sma = typicalPrices.reduce((a, b) => a + b, 0) / effectivePeriod
const currentTP = (highs[i] + lows[i] + closes[i]) / 3
// Calculate Mean Deviation
const meanDeviation = typicalPrices.reduce((sum, tp) => sum + Math.abs(tp - sma), 0) / effectivePeriod
if (meanDeviation !== 0) {
const cciValue = (currentTP - sma) / (0.015 * meanDeviation)
cci.push(cciValue)
} else {
cci.push(0)
}
}
return cci
}
export function calculateWilliamsR(
highs: number[],
lows: number[],
closes: number[],
period: number = 14
): number[] {
// Minimum 5 data points required
if (highs.length < 5 || lows.length < 5 || closes.length < 5) {
return []
}
// Use adaptive period
const effectivePeriod = Math.min(period, highs.length)
const williamsR: number[] = []
for (let i = effectivePeriod - 1; i < closes.length; i++) {
const periodHighs = highs.slice(i - effectivePeriod + 1, i + 1)
const periodLows = lows.slice(i - effectivePeriod + 1, i + 1)
const highestHigh = Math.max(...periodHighs)
const lowestLow = Math.min(...periodLows)
const currentClose = closes[i]
if (highestHigh !== lowestLow) {
const wr = ((highestHigh - currentClose) / (highestHigh - lowestLow)) * -100
williamsR.push(wr)
} else {
williamsR.push(-50) // Neutral if no range
}
}
return williamsR
}