/** * Indicator Definition Object for Hurst Bands. * This object is used by the indicator manager to create and control the indicator. * It defines the parameters and the calculation functions. */ const HURST_INDICATOR = { name: 'Hurst', label: 'Hurst Bands', params: [ { name: 'cycle', type: 'number', defaultValue: 30, min: 2 }, { name: 'atr_mult', type: 'number', defaultValue: 1.8, min: 0.1, step: 0.1 }, ], // This indicator returns multiple lines, so the manager will need to handle an object of arrays. calculateFull: calculateFullHurst, createRealtime: createRealtimeHurstCalculator, }; // --- Helper Functions (private to this file) --- /** * Calculates RMA (Relative Moving Average), a type of EMA. * @param {number[]} series - An array of numbers. * @param {number} period - The smoothing period. * @returns {number[]} The calculated RMA series. */ function _calculateRMA(series, period) { const alpha = 1 / period; let rma = []; if (series.length < period) return rma; // Initial SMA let sum = 0; for (let i = 0; i < period; i++) { sum += series[i]; } rma.push(sum / period); // Subsequent RMAs for (let i = period; i < series.length; i++) { const val = alpha * series[i] + (1 - alpha) * rma[rma.length - 1]; rma.push(val); } return rma; } /** * Calculates ATR (Average True Range). * @param {Array} data - The full candle data. * @param {number} period - The ATR period. * @returns {number[]} The calculated ATR series. */ function _calculateATR(data, period) { if (data.length < period) return []; let tr_series = []; tr_series.push(data[0].high - data[0].low); // First TR is just High - Low for (let i = 1; i < data.length; i++) { const h = data[i].high; const l = data[i].low; const prev_c = data[i - 1].close; const tr = Math.max(h - l, Math.abs(h - prev_c), Math.abs(l - prev_c)); tr_series.push(tr); } // Smooth the True Range series with RMA to get ATR return _calculateRMA(tr_series, period); } // --- Main Calculation Functions --- /** * Calculates the Hurst Bands for an entire dataset. * @param {Array} data - An array of candle objects. * @param {Object} params - An object with { cycle, atr_mult }. * @returns {Object} An object containing two arrays: { topBand: [...], bottomBand: [...] }. */ function calculateFullHurst(data, params) { const { cycle, atr_mult } = params; const mcl = Math.floor(cycle / 2); const mcl_2 = Math.floor(mcl / 2); // Ensure there's enough data for all calculations, including the lookback. if (data.length < cycle + mcl_2) { return { topBand: [], bottomBand: [] }; } const closePrices = data.map(d => d.close); // 1. Calculate RMA of close prices const ma_mcl_full = _calculateRMA(closePrices, mcl); // 2. Calculate ATR const atr_full = _calculateATR(data, mcl); const topBand = []; const bottomBand = []; // Loop through the data to construct the bands. // We start the loop where the first valid calculation can occur. const startIndex = mcl - 1 + mcl_2; for (let i = startIndex; i < data.length; i++) { // Align indices: the result of RMA/ATR at index `j` corresponds to the original data at index `j + mcl - 1`. const rma_atr_base_index = i - (mcl - 1); const center_ma_index = rma_atr_base_index - mcl_2; if (center_ma_index >= 0 && rma_atr_base_index >= 0) { const center = ma_mcl_full[center_ma_index]; const offset = atr_full[rma_atr_base_index] * atr_mult; topBand.push({ time: data[i].time, value: center + offset }); bottomBand.push({ time: data[i].time, value: center - offset }); } } return { topBand, bottomBand }; } /** * Creates a stateful Hurst calculator for real-time updates. * Note: Due to the lookback (`[mcl_2]`), a truly efficient real-time version is complex. * This version recalculates on a rolling buffer for simplicity and correctness. * @param {Object} params - An object with { cycle, atr_mult }. * @returns {Object} A calculator object with `update` and `prime` methods. */ function createRealtimeHurstCalculator(params) { const bufferSize = params.cycle * 2; // Use a buffer to handle lookbacks let buffer = []; return { update: function(candle) { buffer.push(candle); if (buffer.length > bufferSize) { buffer.shift(); } if (buffer.length < params.cycle + Math.floor(params.cycle / 4)) { return null; } // Recalculate on the small buffer const result = calculateFullHurst(buffer, params); if (result.topBand.length > 0) { // Return the last calculated point for each band const lastTop = result.topBand[result.topBand.length - 1]; const lastBottom = result.bottomBand[result.bottomBand.length - 1]; // The manager will expect an object matching the keys from calculateFull return { topBand: lastTop, bottomBand: lastBottom }; } return null; }, prime: function(historicalCandles) { // Prime the buffer with the last N candles from history buffer = historicalCandles.slice(-bufferSize); } }; }