dual hurst + removed labels
This commit is contained in:
170
static/hurst.js
170
static/hurst.js
@ -1,22 +1,50 @@
|
||||
/**
|
||||
* Indicator Definition Object for Hurst Bands.
|
||||
* 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',
|
||||
label: 'Hurst Bands (Multi-TF)',
|
||||
params: [
|
||||
{ name: 'cycle', type: 'number', defaultValue: 30, min: 2 },
|
||||
{ name: 'atr_mult', type: 'number', defaultValue: 1.8, min: 0.1, step: 0.1 },
|
||||
{ name: 'timeframe_mult', type: 'number', defaultValue: 5, min: 2, step: 1 },
|
||||
],
|
||||
// This indicator returns multiple lines, so the manager will need to handle an object of arrays.
|
||||
// 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<Object>} data - The original candle data.
|
||||
* @param {number} multiplier - The timeframe multiplier (e.g., 5 for 5-minute candles from 1-minute data).
|
||||
* @returns {Array<Object>} 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.
|
||||
@ -24,18 +52,14 @@ const HURST_INDICATOR = {
|
||||
* @returns {number[]} The calculated RMA series.
|
||||
*/
|
||||
function _calculateRMA(series, period) {
|
||||
if (series.length < period) return [];
|
||||
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);
|
||||
@ -51,10 +75,7 @@ function _calculateRMA(series, period) {
|
||||
*/
|
||||
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
|
||||
|
||||
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;
|
||||
@ -62,78 +83,112 @@ function _calculateATR(data, period) {
|
||||
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<Object>} data - An array of candle objects.
|
||||
* @param {Object} params - An object with { cycle, atr_mult }.
|
||||
* A generic function to calculate a single set of Hurst Bands.
|
||||
* This is the core calculation logic.
|
||||
* @param {Array<Object>} 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 calculateFullHurst(data, params) {
|
||||
const { cycle, atr_mult } = params;
|
||||
function _calculateSingleBandSet(data, cycle, atr_mult) {
|
||||
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
|
||||
});
|
||||
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<Object>} 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, bottomBand };
|
||||
return {
|
||||
topBand: primaryBands.topBand,
|
||||
bottomBand: primaryBands.bottomBand,
|
||||
topBand_h,
|
||||
bottomBand_h,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 }.
|
||||
* @param {Object} params - An object with { cycle, timeframe_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
|
||||
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 {
|
||||
@ -142,23 +197,26 @@ function createRealtimeHurstCalculator(params) {
|
||||
if (buffer.length > bufferSize) {
|
||||
buffer.shift();
|
||||
}
|
||||
if (buffer.length < params.cycle + Math.floor(params.cycle / 4)) {
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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 };
|
||||
|
||||
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) {
|
||||
// Prime the buffer with the last N candles from history
|
||||
buffer = historicalCandles.slice(-bufferSize);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user