// Investor Flow — Pure technical-indicator functions. // Zero external dependencies. Operate on raw number arrays or PriceCandle[]. import type { PriceCandle } from '../cache/CacheRepository.ts'; /** * Exponential Moving Average. * Returns an array aligned to `values`, with `undefined` for indices before the * seed SMA is complete (i.e. before index `period - 1`). At `period - 1` the * SMA of the first `period` values seeds the EMA; subsequent values use the * standard EMA multiplier `2 / (period + 1)`. */ export function ema(values: number[], period: number): (number | undefined)[] { if (period <= 0) throw new Error(`period must be positive, got ${period}`); const result: (number | undefined)[] = new Array(values.length); if (values.length < period) { return result; // all undefined } // Seed: SMA of the first `period` values. let sum = 0; for (let i = 0; i < period; i++) sum += values[i]; const sma = sum / period; result[period - 1] = sma; const multiplier = 2 / (period + 1); let emaVal = sma; for (let i = period; i < values.length; i++) { emaVal = (values[i] - emaVal) * multiplier + emaVal; result[i] = emaVal; } return result; } /** * Relative Strength Index (Wilder's smoothing). * Returns an array aligned to `closes`, with `undefined` for indices before * the period-th close. Computes on price changes (close[i] - close[i-1]). */ export function rsi(closes: number[], period: number = 14): (number | undefined)[] { if (period <= 0) throw new Error(`period must be positive, got ${period}`); const result: (number | undefined)[] = new Array(closes.length); if (closes.length < period + 1) { return result; // all undefined — need at least `period` changes } // Compute price changes. const deltas: number[] = new Array(closes.length - 1); for (let i = 1; i < closes.length; i++) { deltas[i - 1] = closes[i] - closes[i - 1]; } // Wilder's smoothing: average gain / average loss over `period` changes. let avgGain = 0; let avgLoss = 0; for (let i = 0; i < period; i++) { if (deltas[i] > 0) avgGain += deltas[i]; else avgLoss += Math.abs(deltas[i]); } avgGain /= period; avgLoss /= period; // RSIs are defined starting at index `period` in the original closes array // (because we need `period` changes, which starts at delta[0] = closes[1]-closes[0]). result[period] = computeRsiFrom(avgGain, avgLoss); for (let i = period; i < deltas.length; i++) { const gain = deltas[i] > 0 ? deltas[i] : 0; const loss = deltas[i] < 0 ? Math.abs(deltas[i]) : 0; avgGain = (avgGain * (period - 1) + gain) / period; avgLoss = (avgLoss * (period - 1) + loss) / period; result[i + 1] = computeRsiFrom(avgGain, avgLoss); } return result; } function computeRsiFrom(avgGain: number, avgLoss: number): number { if (avgLoss === 0) return 100; const rs = avgGain / avgLoss; return 100 - 100 / (1 + rs); } /** * Relative Volume: current volume divided by the SMA of the previous `period` * volumes. Returns `undefined` before the period is complete. */ export function relativeVolume(volumes: number[], period: number = 20): (number | undefined)[] { if (period <= 0) throw new Error(`period must be positive, got ${period}`); const result: (number | undefined)[] = new Array(volumes.length); if (volumes.length < period + 1) { return result; // all undefined — need `period` prior values to compute the SMA, plus 1 current } // The first `period` values form the initial SMA; the result at index `period` // is volumes[period] / SMA(volumes[0..period-1]). let sum = 0; for (let i = 0; i < period; i++) sum += volumes[i]; result[period] = volumes[period] / (sum / period); for (let i = period + 1; i < volumes.length; i++) { sum += volumes[i] - volumes[i - period]; result[i] = volumes[i] / (sum / period); } return result; } /** * Extract a price series from PriceCandle[] and compute an EMA. * Defaults to the close (`c`) field; pass `"adjClose"` to use adjusted close. */ export function emaFromCandles( candles: PriceCandle[], priceKey: "c" | "adjClose" = "c", period: number, ): (number | undefined)[] { const prices: number[] = new Array(candles.length); for (let i = 0; i < candles.length; i++) { const c = candles[i]; if (priceKey === "adjClose") { prices[i] = c.adjClose ?? c.c; // fall back to close if adjClose is null } else { prices[i] = c.c; } } return ema(prices, period); }