Spaces:
Running
Running
| /** | |
| * @module indicators | |
| * Technical indicators computed as pure functions. | |
| * All functions operate on arrays of numbers (prices/volumes). | |
| * Returns arrays of the same length, using NaN for insufficient data points. | |
| * NO external libraries. | |
| */ | |
| /** | |
| * Simple Moving Average. | |
| * @param {number[]} values - Array of numeric values. | |
| * @param {number} period - Lookback period. | |
| * @returns {number[]} Array of SMA values (NaN where insufficient data). | |
| */ | |
| export function sma(values, period) { | |
| if (!values || values.length === 0 || period <= 0) return []; | |
| if (period > values.length) return new Array(values.length).fill(NaN); | |
| const result = new Array(values.length).fill(NaN); | |
| 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) { | |
| result[i] = sum / period; | |
| } | |
| } | |
| return result; | |
| } | |
| /** | |
| * Exponential Moving Average. | |
| * @param {number[]} values - Array of numeric values. | |
| * @param {number} period - Lookback period. | |
| * @returns {number[]} Array of EMA values (NaN where insufficient data). | |
| */ | |
| export function ema(values, period) { | |
| if (!values || values.length === 0 || period <= 0) return []; | |
| if (period > values.length) return new Array(values.length).fill(NaN); | |
| const result = new Array(values.length).fill(NaN); | |
| const k = 2 / (period + 1); | |
| // Seed with SMA of first `period` values | |
| let sum = 0; | |
| for (let i = 0; i < period; i++) { | |
| sum += values[i]; | |
| } | |
| result[period - 1] = sum / period; | |
| // Calculate EMA from period onward | |
| for (let i = period; i < values.length; i++) { | |
| result[i] = values[i] * k + result[i - 1] * (1 - k); | |
| } | |
| return result; | |
| } | |
| /** | |
| * Relative Strength Index. | |
| * @param {number[]} closes - Array of close prices. | |
| * @param {number} [period=14] - RSI period. | |
| * @returns {number[]} Array of RSI values (0-100, NaN where insufficient data). | |
| */ | |
| export function rsi(closes, period = 14) { | |
| if (!closes || closes.length === 0 || period <= 0) return []; | |
| if (closes.length < period + 1) return new Array(closes.length).fill(NaN); | |
| const result = new Array(closes.length).fill(NaN); | |
| // Step 1: price changes | |
| const changes = new Array(closes.length).fill(0); | |
| for (let i = 1; i < closes.length; i++) { | |
| changes[i] = closes[i] - closes[i - 1]; | |
| } | |
| // Step 2: separate gains and losses | |
| const gains = changes.map(c => (c > 0 ? c : 0)); | |
| const losses = changes.map(c => (c < 0 ? Math.abs(c) : 0)); | |
| // Step 3: first average (SMA over first `period` changes, starting at index 1) | |
| let avgGain = 0; | |
| let avgLoss = 0; | |
| for (let i = 1; i <= period; i++) { | |
| avgGain += gains[i]; | |
| avgLoss += losses[i]; | |
| } | |
| avgGain /= period; | |
| avgLoss /= period; | |
| // First RSI value at index = period | |
| if (avgLoss === 0) { | |
| result[period] = 100; | |
| } else { | |
| const rs = avgGain / avgLoss; | |
| result[period] = 100 - 100 / (1 + rs); | |
| } | |
| // Step 4: smoothed averages for subsequent values | |
| for (let i = period + 1; i < closes.length; i++) { | |
| avgGain = (avgGain * (period - 1) + gains[i]) / period; | |
| avgLoss = (avgLoss * (period - 1) + losses[i]) / period; | |
| if (avgLoss === 0) { | |
| result[i] = 100; | |
| } else { | |
| const rs = avgGain / avgLoss; | |
| result[i] = 100 - 100 / (1 + rs); | |
| } | |
| } | |
| return result; | |
| } | |
| /** | |
| * Moving Average Convergence Divergence. | |
| * @param {number[]} closes - Array of close prices. | |
| * @param {number} [fast=12] - Fast EMA period. | |
| * @param {number} [slow=26] - Slow EMA period. | |
| * @param {number} [signal=9] - Signal line EMA period. | |
| * @returns {{ macdLine: number[], signalLine: number[], histogram: number[] }} | |
| */ | |
| export function macd(closes, fast = 12, slow = 26, signal = 9) { | |
| if (!closes || closes.length === 0) { | |
| return { macdLine: [], signalLine: [], histogram: [] }; | |
| } | |
| const emaFast = ema(closes, fast); | |
| const emaSlow = ema(closes, slow); | |
| // MACD line = fast EMA - slow EMA | |
| const macdLine = new Array(closes.length).fill(NaN); | |
| for (let i = 0; i < closes.length; i++) { | |
| if (!isNaN(emaFast[i]) && !isNaN(emaSlow[i])) { | |
| macdLine[i] = emaFast[i] - emaSlow[i]; | |
| } | |
| } | |
| // Extract valid MACD values for signal line calculation | |
| const validMacdStart = macdLine.findIndex(v => !isNaN(v)); | |
| let signalLine = new Array(closes.length).fill(NaN); | |
| if (validMacdStart !== -1) { | |
| const validMacd = macdLine.slice(validMacdStart); | |
| const signalEma = ema(validMacd, signal); | |
| for (let i = 0; i < signalEma.length; i++) { | |
| signalLine[validMacdStart + i] = signalEma[i]; | |
| } | |
| } | |
| // Histogram = MACD - Signal | |
| const histogram = new Array(closes.length).fill(NaN); | |
| for (let i = 0; i < closes.length; i++) { | |
| if (!isNaN(macdLine[i]) && !isNaN(signalLine[i])) { | |
| histogram[i] = macdLine[i] - signalLine[i]; | |
| } | |
| } | |
| return { macdLine, signalLine, histogram }; | |
| } | |
| /** | |
| * Average True Range. | |
| * @param {{ high: number, low: number, close: number }[]} candles - Candle data. | |
| * @param {number} [period=14] - ATR period. | |
| * @returns {number[]} Array of ATR values (NaN where insufficient data). | |
| */ | |
| export function atr(candles, period = 14) { | |
| if (!candles || candles.length === 0 || period <= 0) return []; | |
| if (candles.length < 2) return [NaN]; | |
| const result = new Array(candles.length).fill(NaN); | |
| // Step 1: calculate True Range for each candle | |
| const tr = new Array(candles.length).fill(0); | |
| tr[0] = candles[0].high - candles[0].low; // No previous close for first candle | |
| for (let i = 1; i < candles.length; i++) { | |
| const highLow = candles[i].high - candles[i].low; | |
| const highPrevClose = Math.abs(candles[i].high - candles[i - 1].close); | |
| const lowPrevClose = Math.abs(candles[i].low - candles[i - 1].close); | |
| tr[i] = Math.max(highLow, highPrevClose, lowPrevClose); | |
| } | |
| // Step 2: first ATR = SMA of first `period` true ranges | |
| if (candles.length < period) return result; | |
| let sum = 0; | |
| for (let i = 0; i < period; i++) { | |
| sum += tr[i]; | |
| } | |
| result[period - 1] = sum / period; | |
| // Step 3: smoothed ATR for subsequent values | |
| for (let i = period; i < candles.length; i++) { | |
| result[i] = (result[i - 1] * (period - 1) + tr[i]) / period; | |
| } | |
| return result; | |
| } | |
| /** | |
| * Bollinger Bands. | |
| * @param {number[]} closes - Array of close prices. | |
| * @param {number} [period=20] - SMA period for the middle band. | |
| * @param {number} [stdDevMult=2] - Standard deviation multiplier. | |
| * @returns {{ upper: number[], middle: number[], lower: number[] }} | |
| */ | |
| export function bollingerBands(closes, period = 20, stdDevMult = 2) { | |
| if (!closes || closes.length === 0) { | |
| return { upper: [], middle: [], lower: [] }; | |
| } | |
| const middle = sma(closes, period); | |
| const upper = new Array(closes.length).fill(NaN); | |
| const lower = new Array(closes.length).fill(NaN); | |
| for (let i = period - 1; i < closes.length; i++) { | |
| // Calculate standard deviation over the window | |
| let sumSqDiff = 0; | |
| for (let j = i - period + 1; j <= i; j++) { | |
| const diff = closes[j] - middle[i]; | |
| sumSqDiff += diff * diff; | |
| } | |
| const sd = Math.sqrt(sumSqDiff / period); | |
| upper[i] = middle[i] + stdDevMult * sd; | |
| lower[i] = middle[i] - stdDevMult * sd; | |
| } | |
| return { upper, middle, lower }; | |
| } | |
| /** | |
| * Volume Simple Moving Average. | |
| * @param {number[]} volumes - Array of volume values. | |
| * @param {number} [period=20] - SMA period. | |
| * @returns {number[]} Array of volume SMA values. | |
| */ | |
| export function volumeSMA(volumes, period = 20) { | |
| return sma(volumes, period); | |
| } | |