// Self-contained Stochastic Oscillator 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 Stochastic function calculateStochSignal(indicator, lastCandle, prevCandle, values, prevValues) { const k = values?.k; const d = values?.d; const prevK = prevValues?.k; const prevD = prevValues?.d; const overbought = indicator.params?.overbought || 80; const oversold = indicator.params?.oversold || 20; if (k === undefined || d === undefined || prevK === undefined || prevD === undefined) { return null; } // BUY: %K crosses UP through %D while both are oversold if (prevK <= prevD && k > d && k < oversold) { return { type: SIGNAL_TYPES.BUY, strength: 80, value: k, reasoning: `Stochastic %K crossed UP through %D in oversold zone` }; } // SELL: %K crosses DOWN through %D while both are overbought else if (prevK >= prevD && k < d && k > overbought) { return { type: SIGNAL_TYPES.SELL, strength: 80, value: k, reasoning: `Stochastic %K crossed DOWN through %D in overbought zone` }; } return null; } // Stochastic Oscillator Indicator class export class StochasticIndicator extends BaseIndicator { constructor(config) { super(config); this.lastSignalTimestamp = null; this.lastSignalType = null; } calculate(candles) { const kPeriod = this.params.kPeriod || 14; const dPeriod = this.params.dPeriod || 3; const results = new Array(candles.length).fill(null); const kValues = new Array(candles.length).fill(null); for (let i = kPeriod - 1; i < candles.length; i++) { let lowest = Infinity; let highest = -Infinity; for (let j = 0; j < kPeriod; j++) { lowest = Math.min(lowest, candles[i-j].low); highest = Math.max(highest, candles[i-j].high); } const diff = highest - lowest; kValues[i] = diff === 0 ? 50 : ((candles[i].close - lowest) / diff) * 100; } for (let i = kPeriod + dPeriod - 2; i < candles.length; i++) { let sum = 0; for (let j = 0; j < dPeriod; j++) sum += kValues[i-j]; results[i] = { k: kValues[i], d: sum / dPeriod }; } return results; } getMetadata() { return { name: 'Stochastic', description: 'Stochastic Oscillator - compares close to high-low range', inputs: [ { name: 'kPeriod', label: '%K Period', type: 'number', default: 14, min: 1, max: 100, description: 'Lookback period for %K calculation' }, { name: 'dPeriod', label: '%D Period', type: 'number', default: 3, min: 1, max: 20, description: 'Smoothing period for %D (SMA of %K)' } ], plots: [ { id: 'k', color: '#3f51b5', title: '%K', style: 'solid', width: 1 }, { id: 'd', color: '#ff9800', title: '%D', style: 'solid', width: 1 } ], displayMode: 'pane', paneMin: 0, paneMax: 100 }; } } export { calculateStochSignal };