- Include visual settings (line width, color, markers) in saved presets - Change default line width to 1px for indicators - Update Hurst Bands to use grey markers and custom shapes by default - Add robust userPresets initialization and error handling in savePreset
175 lines
5.7 KiB
JavaScript
175 lines
5.7 KiB
JavaScript
// 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'
|
|
};
|
|
|
|
const SIGNAL_COLORS = {
|
|
buy: '#9e9e9e',
|
|
sell: '#9e9e9e'
|
|
};
|
|
|
|
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 RMA (Rolling Moving Average - Wilder's method)
|
|
// Recreates Pine Script's ta.rma() exactly
|
|
function calculateRMA(sourceArray, length) {
|
|
const rma = new Array(sourceArray.length).fill(null);
|
|
let sum = 0;
|
|
const alpha = 1 / length;
|
|
|
|
// PineScript implicitly rounds float lengths for SMA initialization
|
|
const smaLength = Math.round(length);
|
|
|
|
for (let i = 0; i < sourceArray.length; i++) {
|
|
if (i < smaLength - 1) {
|
|
// Accumulate first N-1 bars
|
|
sum += sourceArray[i];
|
|
} else if (i === smaLength - 1) {
|
|
// On the Nth bar, the first RMA value is the SMA
|
|
sum += sourceArray[i];
|
|
rma[i] = sum / smaLength;
|
|
} else {
|
|
// Subsequent bars use the RMA formula
|
|
const prevRMA = rma[i - 1];
|
|
rma[i] = (prevRMA === null || isNaN(prevRMA))
|
|
? alpha * sourceArray[i]
|
|
: alpha * sourceArray[i] + (1 - alpha) * prevRMA;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
if (!this.params.markerBuyShape) this.params.markerBuyShape = 'custom';
|
|
if (!this.params.markerSellShape) this.params.markerSellShape = 'custom';
|
|
if (!this.params.markerBuyColor) this.params.markerBuyColor = '#9e9e9e';
|
|
if (!this.params.markerSellColor) this.params.markerSellColor = '#9e9e9e';
|
|
if (!this.params.markerBuyCustom) this.params.markerBuyCustom = '▲';
|
|
if (!this.params.markerSellCustom) this.params.markerSellCustom = '▼';
|
|
}
|
|
|
|
calculate(candles) {
|
|
const mcl_t = this.params.period || 30;
|
|
const mcm = this.params.multiplier || 1.8;
|
|
|
|
const mcl = mcl_t / 2;
|
|
|
|
// FIX: PineScript rounds implicit floats for history references [].
|
|
// 15/2 = 7.5. Pine rounds this to 8. Math.floor gives 7.
|
|
const mcl_2 = Math.round(mcl / 2);
|
|
|
|
const results = new Array(candles.length).fill(null);
|
|
const closes = candles.map(c => c.close);
|
|
|
|
const trArray = candles.map((d, i) => {
|
|
const prevClose = i > 0 ? candles[i - 1].close : null;
|
|
const high = d.high;
|
|
const low = d.low;
|
|
|
|
if (prevClose === null || prevClose === undefined || isNaN(prevClose)) {
|
|
return high - low;
|
|
}
|
|
return Math.max(
|
|
high - low,
|
|
Math.abs(high - prevClose),
|
|
Math.abs(low - prevClose)
|
|
);
|
|
});
|
|
|
|
const ma_mcl = calculateRMA(closes, mcl);
|
|
const atr = calculateRMA(trArray, mcl);
|
|
|
|
for (let i = 0; i < candles.length; i++) {
|
|
const src = closes[i];
|
|
const mcm_off = mcm * (atr[i] || 0);
|
|
|
|
const historicalIndex = i - mcl_2;
|
|
const historical_ma = historicalIndex >= 0 ? ma_mcl[historicalIndex] : null;
|
|
|
|
const centerLine = (historical_ma === null || historical_ma === undefined || isNaN(historical_ma)) ? src : historical_ma;
|
|
|
|
results[i] = {
|
|
upper: centerLine + mcm_off,
|
|
lower: centerLine - mcm_off
|
|
};
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
getMetadata() {
|
|
return {
|
|
name: 'Hurst Bands',
|
|
description: 'Cyclic price channels based on Hurst theory',
|
|
inputs: [
|
|
{ name: 'period', label: 'Hurst Cycle Length (mcl_t)', 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: '#808080', title: 'Upper', lineWidth: 1 },
|
|
{ id: 'lower', color: '#808080', title: 'Lower', lineWidth: 1 }
|
|
],
|
|
bands: [
|
|
{ topId: 'upper', bottomId: 'lower', color: 'rgba(128, 128, 128, 0.05)' }
|
|
],
|
|
displayMode: 'overlay'
|
|
};
|
|
}
|
|
}
|
|
|
|
export { calculateHurstSignal }; |