function mean(values: number[]): number { if (values.length === 0) return 0; return values.reduce((a, b) => a + b, 0) / values.length; } function stddev(values: number[]): number { if (values.length === 0) return 0; const m = mean(values); const v = values.reduce((acc, x) => acc + (x - m) * (x - m), 0) / values.length; return Math.sqrt(v); } export function sma(values: number[], period: number): Array { if (period <= 0) throw new Error('period must be > 0'); const out: Array = new Array(values.length).fill(null); let sum = 0; for (let i = 0; i < values.length; i++) { sum += values[i]; if (i >= period) sum -= values[i - period]; if (i >= period - 1) out[i] = sum / period; } return out; } export function ema(values: number[], period: number): Array { if (period <= 0) throw new Error('period must be > 0'); const out: Array = new Array(values.length).fill(null); const k = 2 / (period + 1); if (values.length < period) return out; const first = mean(values.slice(0, period)); out[period - 1] = first; let prev = first; for (let i = period; i < values.length; i++) { const next = values[i] * k + prev * (1 - k); out[i] = next; prev = next; } return out; } export function rsi(values: number[], period: number): Array { if (period <= 0) throw new Error('period must be > 0'); const out: Array = new Array(values.length).fill(null); if (values.length <= period) return out; let gains = 0; let losses = 0; for (let i = 1; i <= period; i++) { const change = values[i] - values[i - 1]; if (change >= 0) gains += change; else losses -= change; } let avgGain = gains / period; let avgLoss = losses / period; const rs = avgLoss === 0 ? Number.POSITIVE_INFINITY : avgGain / avgLoss; out[period] = 100 - 100 / (1 + rs); for (let i = period + 1; i < values.length; i++) { const change = values[i] - values[i - 1]; const gain = Math.max(change, 0); const loss = Math.max(-change, 0); avgGain = (avgGain * (period - 1) + gain) / period; avgLoss = (avgLoss * (period - 1) + loss) / period; const rs2 = avgLoss === 0 ? Number.POSITIVE_INFINITY : avgGain / avgLoss; out[i] = 100 - 100 / (1 + rs2); } return out; } export function bollingerBands(values: number[], period: number, stdDevMult: number) { if (period <= 0) throw new Error('period must be > 0'); const upper: Array = new Array(values.length).fill(null); const lower: Array = new Array(values.length).fill(null); const mid = sma(values, period); for (let i = period - 1; i < values.length; i++) { const window = values.slice(i - period + 1, i + 1); const sd = stddev(window); const m = mid[i]; if (m == null) continue; upper[i] = m + stdDevMult * sd; lower[i] = m - stdDevMult * sd; } return { upper, lower, mid }; } export function macd(values: number[], fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) { const fast = ema(values, fastPeriod); const slow = ema(values, slowPeriod); const macdLine: Array = values.map((_, i) => { const f = fast[i]; const s = slow[i]; return f == null || s == null ? null : f - s; }); // EMA over a nullable series, aligned by index. const signal: Array = new Array(values.length).fill(null); const k = 2 / (signalPeriod + 1); let seeded = false; let prev = 0; const buf: number[] = []; for (let i = 0; i < macdLine.length; i++) { const v = macdLine[i]; if (v == null) continue; if (!seeded) { buf.push(v); if (buf.length === signalPeriod) { const first = mean(buf); signal[i] = first; prev = first; seeded = true; } continue; } const next = v * k + prev * (1 - k); signal[i] = next; prev = next; } return { macd: macdLine, signal }; } export function lastNonNull(values: Array): number | null { for (let i = values.length - 1; i >= 0; i--) { const v = values[i]; if (v != null && Number.isFinite(v)) return v; } return null; }