From 666d5fb0071662c6682b68f790be4385b39038d2 Mon Sep 17 00:00:00 2001 From: DiTus Date: Mon, 14 Jul 2025 20:49:19 +0200 Subject: [PATCH] first hurst --- static/hurst.js | 165 +++++++++++++++++++++++++++++++++++++++++++ static/indicators.js | 4 +- 2 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 static/hurst.js diff --git a/static/hurst.js b/static/hurst.js new file mode 100644 index 0000000..93aab60 --- /dev/null +++ b/static/hurst.js @@ -0,0 +1,165 @@ +/** + * 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); + } + }; +} diff --git a/static/indicators.js b/static/indicators.js index 81c38a2..66ea6ab 100644 --- a/static/indicators.js +++ b/static/indicators.js @@ -9,5 +9,5 @@ const AVAILABLE_INDICATORS = [ SMA_INDICATOR, EMA_INDICATOR, BB_INDICATOR, // Added the new Bollinger Bands indicator - // Add other indicators here, e.g., RSI_INDICATOR -]; \ No newline at end of file + HURST_INDICATOR, // Added the new Hurst Bands indicator + // Add other indicators here as needed]; \ No newline at end of file