// Self-contained Hurst Bands indicator // Based on J.M. Hurst's cyclic price channel theory // Using RMA + ATR displacement method const SIGNAL_TYPES = { BUY: 'buy', SELL: 'sell' }; const SIGNAL_COLORS = { buy: '#9e9e9e', sell: '#9e9e9e' }; 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; } } // Calculate RMA (Rolling Moving Average - Wilder's method) // Recreates Pine Script's ta.rma() exactly function calculateRMA(sourceArray, length) { const rma = new Array(sourceArray.length).fill(null); let sum = 0; const alpha = 1 / length; // PineScript implicitly rounds float lengths for SMA initialization const smaLength = Math.round(length); for (let i = 0; i < sourceArray.length; i++) { if (i < smaLength - 1) { // Accumulate first N-1 bars sum += sourceArray[i]; } else if (i === smaLength - 1) { // On the Nth bar, the first RMA value is the SMA sum += sourceArray[i]; rma[i] = sum / smaLength; } else { // Subsequent bars use the RMA formula const prevRMA = rma[i - 1]; rma[i] = (prevRMA === null || isNaN(prevRMA)) ? alpha * sourceArray[i] : alpha * sourceArray[i] + (1 - alpha) * prevRMA; } } return rma; } function calculateHurstSignal(indicator, lastCandle, prevCandle, values, prevValues) { const close = lastCandle.close; const prevClose = prevCandle?.close; const upper = values?.upper; const lower = values?.lower; if (!upper || !lower || prevClose === undefined) { return null; } if (prevClose > lower && close < lower) { return { type: 'buy', strength: 75, value: close, reasoning: `Price crossed down below lower Hurst Band (${lower.toFixed(2)}), expect bounce` }; } if (prevClose > upper && close < upper) { return { type: 'sell', strength: 75, value: close, reasoning: `Price crossed down below upper Hurst Band (${upper.toFixed(2)}), expect reversal` }; } return null; } export class HurstBandsIndicator extends BaseIndicator { constructor(config) { super(config); this.lastSignalTimestamp = null; this.lastSignalType = null; if (!this.params.markerBuyShape) this.params.markerBuyShape = 'custom'; if (!this.params.markerSellShape) this.params.markerSellShape = 'custom'; if (!this.params.markerBuyColor) this.params.markerBuyColor = '#9e9e9e'; if (!this.params.markerSellColor) this.params.markerSellColor = '#9e9e9e'; if (!this.params.markerBuyCustom) this.params.markerBuyCustom = '▲'; if (!this.params.markerSellCustom) this.params.markerSellCustom = '▼'; } calculate(candles) { const mcl_t = this.params.period || 30; const mcm = this.params.multiplier || 1.8; const mcl = mcl_t / 2; // FIX: PineScript rounds implicit floats for history references []. // 15/2 = 7.5. Pine rounds this to 8. Math.floor gives 7. const mcl_2 = Math.round(mcl / 2); const results = new Array(candles.length).fill(null); const closes = candles.map(c => c.close); const trArray = candles.map((d, i) => { const prevClose = i > 0 ? candles[i - 1].close : null; const high = d.high; const low = d.low; if (prevClose === null || prevClose === undefined || isNaN(prevClose)) { return high - low; } return Math.max( high - low, Math.abs(high - prevClose), Math.abs(low - prevClose) ); }); const ma_mcl = calculateRMA(closes, mcl); const atr = calculateRMA(trArray, mcl); for (let i = 0; i < candles.length; i++) { const src = closes[i]; const mcm_off = mcm * (atr[i] || 0); const historicalIndex = i - mcl_2; const historical_ma = historicalIndex >= 0 ? ma_mcl[historicalIndex] : null; const centerLine = (historical_ma === null || historical_ma === undefined || isNaN(historical_ma)) ? src : historical_ma; results[i] = { upper: centerLine + mcm_off, lower: centerLine - mcm_off }; } return results; } getMetadata() { return { name: 'Hurst Bands', description: 'Cyclic price channels based on Hurst theory', inputs: [ { name: 'period', label: 'Hurst Cycle Length (mcl_t)', type: 'number', default: 30, min: 5, max: 200 }, { name: 'multiplier', label: 'Multiplier (mcm)', type: 'number', default: 1.8, min: 0.5, max: 10, step: 0.1 } ], plots: [ { id: 'upper', color: '#808080', title: 'Upper', lineWidth: 1 }, { id: 'lower', color: '#808080', title: 'Lower', lineWidth: 1 } ], bands: [ { topId: 'upper', bottomId: 'lower', color: 'rgba(128, 128, 128, 0.05)' } ], displayMode: 'overlay' }; } } export { calculateHurstSignal };