Add Hurst Bands indicator with buy/sell signal markers
This commit is contained in:
184
src/api/dashboard/static/js/indicators/hurst.js
Normal file
184
src/api/dashboard/static/js/indicators/hurst.js
Normal file
@ -0,0 +1,184 @@
|
||||
// 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',
|
||||
HOLD: 'hold'
|
||||
};
|
||||
|
||||
const SIGNAL_COLORS = {
|
||||
buy: '#26a69a',
|
||||
hold: '#787b86',
|
||||
sell: '#ef5350'
|
||||
};
|
||||
|
||||
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 ATR (Average True Range)
|
||||
function calculateATR(candles, period) {
|
||||
const atr = new Array(candles.length).fill(null);
|
||||
|
||||
for (let i = 1; i < candles.length; i++) {
|
||||
const high = candles[i].high;
|
||||
const low = candles[i].low;
|
||||
const prevClose = candles[i - 1].close;
|
||||
|
||||
const tr = Math.max(
|
||||
high - low,
|
||||
Math.abs(high - prevClose),
|
||||
Math.abs(low - prevClose)
|
||||
);
|
||||
|
||||
if (i >= period) {
|
||||
let sum = 0;
|
||||
for (let j = 0; j < period; j++) {
|
||||
const idx = i - j;
|
||||
const h = candles[idx].high;
|
||||
const l = candles[idx].low;
|
||||
const pc = candles[idx - 1]?.close || h;
|
||||
sum += Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc));
|
||||
}
|
||||
atr[i] = sum / period;
|
||||
} else {
|
||||
let sum = 0;
|
||||
for (let j = 1; j <= i; j++) {
|
||||
const h = candles[j].high;
|
||||
const l = candles[j].low;
|
||||
const pc = candles[j - 1].close;
|
||||
sum += Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc));
|
||||
}
|
||||
atr[i] = sum / (i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return atr;
|
||||
}
|
||||
|
||||
// Calculate RMA (Rolling Moving Average - Wilder's method)
|
||||
function calculateRMA(values, period) {
|
||||
const rma = new Array(values.length).fill(null);
|
||||
|
||||
let sum = 0;
|
||||
for (let i = 0; i < period && i < values.length; i++) {
|
||||
sum += values[i];
|
||||
if (i === period - 1) {
|
||||
rma[i] = sum / period;
|
||||
}
|
||||
}
|
||||
|
||||
const alpha = 1 / period;
|
||||
for (let i = period; i < values.length; i++) {
|
||||
if (rma[i - 1] !== null) {
|
||||
rma[i] = rma[i - 1] + alpha * (values[i] - rma[i - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
calculate(candles) {
|
||||
const mcl = this.params.period || 30;
|
||||
const mcm = this.params.multiplier || 1.8;
|
||||
const results = new Array(candles.length).fill(null);
|
||||
|
||||
const closes = candles.map(c => c.close);
|
||||
|
||||
const ma_mcl = calculateRMA(closes, mcl);
|
||||
const atr = calculateATR(candles, mcl);
|
||||
|
||||
const mcl_2 = Math.floor(mcl / 2);
|
||||
|
||||
for (let i = mcl_2; i < candles.length; i++) {
|
||||
const ma_val = ma_mcl[i];
|
||||
const atr_val = atr[i];
|
||||
|
||||
if (ma_val === null || atr_val === null) continue;
|
||||
|
||||
const displacedIdx = i - mcl_2;
|
||||
const displacedRMA = displacedIdx >= 0 ? ma_mcl[displacedIdx] : null;
|
||||
|
||||
if (displacedRMA === null) continue;
|
||||
|
||||
const mcm_off = mcm * atr_val;
|
||||
|
||||
results[i] = {
|
||||
upper: displacedRMA + mcm_off,
|
||||
lower: displacedRMA - mcm_off
|
||||
};
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'Hurst Bands',
|
||||
description: 'Cyclic price channels based on Hurst theory',
|
||||
inputs: [
|
||||
{ name: 'period', label: 'Period (mcl)', 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: '#ff6b6b', title: 'Upper' },
|
||||
{ id: 'lower', color: '#4caf50', title: 'Lower' }
|
||||
],
|
||||
displayMode: 'overlay'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { calculateHurstSignal };
|
||||
@ -8,6 +8,7 @@ export { RSIIndicator, calculateRSISignal } from './rsi.js';
|
||||
export { BollingerBandsIndicator, calculateBollingerBandsSignal } from './bb.js';
|
||||
export { StochasticIndicator, calculateStochSignal } from './stoch.js';
|
||||
export { ATRIndicator, calculateATRSignal } from './atr.js';
|
||||
export { HurstBandsIndicator, calculateHurstSignal } from './hurst.js';
|
||||
|
||||
// Import for registry
|
||||
import { MAIndicator as MAI, calculateMASignal as CMA } from './moving_average.js';
|
||||
@ -17,6 +18,7 @@ import { RSIIndicator as RSII, calculateRSISignal as CRSI } from './rsi.js';
|
||||
import { BollingerBandsIndicator as BBI, calculateBollingerBandsSignal as CBB } from './bb.js';
|
||||
import { StochasticIndicator as STOCHI, calculateStochSignal as CST } from './stoch.js';
|
||||
import { ATRIndicator as ATRI, calculateATRSignal as CATR } from './atr.js';
|
||||
import { HurstBandsIndicator as HURSTI, calculateHurstSignal as CHURST } from './hurst.js';
|
||||
|
||||
// Signal function registry for easy dispatch
|
||||
export const SignalFunctionRegistry = {
|
||||
@ -26,7 +28,8 @@ export const SignalFunctionRegistry = {
|
||||
rsi: CRSI,
|
||||
bb: CBB,
|
||||
stoch: CST,
|
||||
atr: CATR
|
||||
atr: CATR,
|
||||
hurst: CHURST
|
||||
};
|
||||
|
||||
// Indicator registry for UI
|
||||
@ -37,7 +40,8 @@ export const IndicatorRegistry = {
|
||||
rsi: RSII,
|
||||
bb: BBI,
|
||||
stoch: STOCHI,
|
||||
atr: ATRI
|
||||
atr: ATRI,
|
||||
hurst: HURSTI
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user