// Self-contained RSI indicator // Includes math, metadata, signal calculation, and base class // Signal constants (defined in each indicator file) const SIGNAL_TYPES = { BUY: 'buy', SELL: 'sell', HOLD: 'hold' }; const SIGNAL_COLORS = { buy: '#26a69a', hold: '#787b86', sell: '#ef5350' }; // Base class (inline replacement for BaseIndicator) class BaseIndicator { constructor(config) { this.id = config.id; this.type = config.type; this.name = config.name; this.params = config.params || {}; this.timeframe = config.timeframe || '1m'; this.series = []; this.visible = config.visible !== false; this.cachedResults = null; this.cachedMeta = null; this.lastSignalTimestamp = null; this.lastSignalType = null; } } // Signal calculation for RSI function calculateRSISignal(indicator, lastCandle, prevCandle, values, prevValues) { const rsi = values?.rsi; const prevRsi = prevValues?.rsi; const overbought = indicator.params?.overbought || 70; const oversold = indicator.params?.oversold || 30; if (rsi === undefined || rsi === null || prevRsi === undefined || prevRsi === null) { return null; } // BUY when RSI crosses UP through oversold level if (prevRsi < oversold && rsi >= oversold) { return { type: SIGNAL_TYPES.BUY, strength: 75, value: rsi, reasoning: `RSI crossed UP through oversold level (${oversold})` }; } // SELL when RSI crosses DOWN through overbought level else if (prevRsi > overbought && rsi <= overbought) { return { type: SIGNAL_TYPES.SELL, strength: 75, value: rsi, reasoning: `RSI crossed DOWN through overbought level (${overbought})` }; } return null; } // RSI Indicator class export class RSIIndicator extends BaseIndicator { constructor(config) { super(config); this.lastSignalTimestamp = null; this.lastSignalType = null; } calculate(candles) { const period = this.params.period || 14; const overbought = this.params.overbought || 70; const oversold = this.params.oversold || 30; // 1. Calculate RSI using RMA (Wilder's Smoothing) let rsiValues = new Array(candles.length).fill(null); let upSum = 0; let downSum = 0; const rmaAlpha = 1 / period; for (let i = 1; i < candles.length; i++) { const diff = candles[i].close - candles[i-1].close; const up = diff > 0 ? diff : 0; const down = diff < 0 ? -diff : 0; if (i < period) { upSum += up; downSum += down; } else if (i === period) { upSum += up; downSum += down; const avgUp = upSum / period; const avgDown = downSum / period; rsiValues[i] = avgDown === 0 ? 100 : (avgUp === 0 ? 0 : 100 - (100 / (1 + avgUp / avgDown))); upSum = avgUp; downSum = avgDown; } else { upSum = (up - upSum) * rmaAlpha + upSum; downSum = (down - downSum) * rmaAlpha + downSum; rsiValues[i] = downSum === 0 ? 100 : (upSum === 0 ? 0 : 100 - (100 / (1 + upSum / downSum))); } } // Combine results return rsiValues.map((rsi, i) => { return { paneBg: 80, rsi: rsi, overboughtBand: overbought, oversoldBand: oversold }; }); } getMetadata() { return { name: 'RSI', description: 'Relative Strength Index', inputs: [ { name: 'period', label: 'RSI Length', type: 'number', default: 14, min: 1, max: 100 }, { name: 'overbought', label: 'Overbought Level', type: 'number', default: 70, min: 50, max: 95 }, { name: 'oversold', label: 'Oversold Level', type: 'number', default: 30, min: 5, max: 50 } ], plots: [ { id: 'rsi', color: '#7E57C2', title: '', style: 'solid', width: 1, lastValueVisible: true }, { id: 'overboughtBand', color: '#787B86', title: '', style: 'dashed', width: 1, lastValueVisible: false }, { id: 'oversoldBand', color: '#787B86', title: '', style: 'dashed', width: 1, lastValueVisible: false } ], displayMode: 'pane', paneMin: 0, paneMax: 100 }; } } export { calculateRSISignal };