/** * Indicator Definition Object for Hurst Bands (Multi-Timeframe). * 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 (Multi-TF)', params: [ { name: 'cycle', type: 'number', defaultValue: 30, min: 2 }, { name: 'timeframe_mult', type: 'number', defaultValue: 5, min: 2, step: 1 }, ], // The output is { topBand, bottomBand, topBand_h, bottomBand_h } calculateFull: calculateFullHurst, createRealtime: createRealtimeHurstCalculator, }; // --- Helper Functions (private to this file) --- /** * Aggregates candle data into a higher timeframe. * @param {Array} data - The original candle data. * @param {number} multiplier - The timeframe multiplier (e.g., 5 for 5-minute candles from 1-minute data). * @returns {Array} A new array of aggregated candle objects. */ function _aggregateCandles(data, multiplier) { if (multiplier <= 1) return data; const aggregatedData = []; for (let i = 0; i < data.length; i += multiplier) { const chunk = data.slice(i, i + multiplier); if (chunk.length > 0) { const newCandle = { open: chunk[0].open, high: Math.max(...chunk.map(c => c.high)), low: Math.min(...chunk.map(c => c.low)), close: chunk[chunk.length - 1].close, // The timestamp of the new candle corresponds to the end of the period. time: chunk[chunk.length - 1].time, }; aggregatedData.push(newCandle); } } return aggregatedData; } /** * 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) { if (series.length < period) return []; const alpha = 1 / period; let rma = []; let sum = 0; for (let i = 0; i < period; i++) { sum += series[i]; } rma.push(sum / period); 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 = [data[0].high - data[0].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); } return _calculateRMA(tr_series, period); } /** * A generic function to calculate a single set of Hurst Bands. * This is the core calculation logic. * @param {Array} data - An array of candle objects for a specific timeframe. * @param {number} cycle - The cycle length for this calculation. * @param {number} atr_mult - The ATR multiplier for this calculation. * @returns {Object} An object containing two arrays: { topBand: [...], bottomBand: [...] }. */ function _calculateSingleBandSet(data, cycle, atr_mult) { const mcl = Math.floor(cycle / 2); const mcl_2 = Math.floor(mcl / 2); if (data.length < cycle + mcl_2) { return { topBand: [], bottomBand: [] }; } const closePrices = data.map(d => d.close); const ma_mcl_full = _calculateRMA(closePrices, mcl); const atr_full = _calculateATR(data, mcl); const topBand = []; const bottomBand = []; const startIndex = mcl - 1 + mcl_2; for (let i = startIndex; i < data.length; i++) { 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; if (center !== undefined && offset !== undefined) { topBand.push({ time: data[i].time, value: center + offset }); bottomBand.push({ time: data[i].time, value: center - offset }); } } } return { topBand, bottomBand }; } // --- Main Calculation Functions --- /** * Calculates both primary and higher-timeframe Hurst Bands for an entire dataset. * @param {Array} data - An array of candle objects. * @param {Object} params - An object with { cycle, timeframe_mult }. * @returns {Object} An object containing four arrays: { topBand, bottomBand, topBand_h, bottomBand_h }. */ function calculateFullHurst(data, params) { const { cycle, timeframe_mult } = params; // 1. Calculate Primary Bands (e.g., 1-minute) const primaryBands = _calculateSingleBandSet(data, cycle, 1.8); // 2. Aggregate candles to higher timeframe (e.g., 5-minute) const higherTfData = _aggregateCandles(data, timeframe_mult); // 3. Calculate Higher Timeframe Bands const higherTFBandsRaw = _calculateSingleBandSet(higherTfData, cycle, 1.9); // 4. Align higher timeframe results back to the primary timeframe for plotting const higherTfResults = new Map(higherTFBandsRaw.topBand.map((p, i) => [ p.time, { top: p.value, bottom: higherTFBandsRaw.bottomBand[i].value } ])); const topBand_h = []; const bottomBand_h = []; let lastKnownTop = null; let lastKnownBottom = null; for (const candle of data) { if (higherTfResults.has(candle.time)) { const bands = higherTfResults.get(candle.time); lastKnownTop = bands.top; lastKnownBottom = bands.bottom; } // Carry forward the last known value until a new one is calculated if (lastKnownTop !== null) { topBand_h.push({ time: candle.time, value: lastKnownTop }); bottomBand_h.push({ time: candle.time, value: lastKnownBottom }); } } return { topBand: primaryBands.topBand, bottomBand: primaryBands.bottomBand, topBand_h, bottomBand_h, }; } /** * Creates a stateful Hurst calculator for real-time updates. * @param {Object} params - An object with { cycle, timeframe_mult }. * @returns {Object} A calculator object with `update` and `prime` methods. */ function createRealtimeHurstCalculator(params) { const { cycle, timeframe_mult } = params; // Buffer needs to be large enough to contain enough aggregated candles for a valid calculation. const minHigherTfCandles = cycle + Math.floor(Math.floor(cycle / 2) / 2); const bufferSize = minHigherTfCandles * timeframe_mult * 2; // Use a safe buffer size let buffer = []; return { update: function(candle) { buffer.push(candle); if (buffer.length > bufferSize) { buffer.shift(); } // Check if there's enough data for at least one calculation on the higher timeframe. const requiredLength = minHigherTfCandles * timeframe_mult; if (buffer.length < requiredLength) { return null; } const result = calculateFullHurst(buffer, params); if (result.topBand.length > 0 && result.topBand_h.length > 0) { return { topBand: result.topBand[result.topBand.length - 1], bottomBand: result.bottomBand[result.bottomBand.length - 1], topBand_h: result.topBand_h[result.topBand_h.length - 1], bottomBand_h: result.bottomBand_h[result.bottomBand_h.length - 1], }; } return null; }, prime: function(historicalCandles) { buffer = historicalCandles.slice(-bufferSize); } }; }