Fix indicator preset saving and update Hurst Bands defaults

- 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
This commit is contained in:
DiTus
2026-03-03 12:19:13 +01:00
parent cf1aca8855
commit 73f325ce19
3 changed files with 188 additions and 112 deletions

View File

@ -4,14 +4,12 @@
const SIGNAL_TYPES = {
BUY: 'buy',
SELL: 'sell',
HOLD: 'hold'
SELL: 'sell'
};
const SIGNAL_COLORS = {
buy: '#26a69a',
hold: '#787b86',
sell: '#ef5350'
buy: '#9e9e9e',
sell: '#9e9e9e'
};
class BaseIndicator {
@ -30,62 +28,30 @@ class BaseIndicator {
}
}
// 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);
// Recreates Pine Script's ta.rma() exactly
function calculateRMA(sourceArray, length) {
const rma = new Array(sourceArray.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 / length;
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]);
// 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;
}
}
@ -128,36 +94,58 @@ export class HurstBandsIndicator extends BaseIndicator {
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 = this.params.period || 30;
const mcl_t = this.params.period || 30;
const mcm = this.params.multiplier || 1.8;
const results = new Array(candles.length).fill(null);
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 = calculateATR(candles, mcl);
const atr = calculateRMA(trArray, 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];
for (let i = 0; i < candles.length; i++) {
const src = closes[i];
const mcm_off = mcm * (atr[i] || 0);
if (ma_val === null || atr_val === null) continue;
const historicalIndex = i - mcl_2;
const historical_ma = historicalIndex >= 0 ? ma_mcl[historicalIndex] : null;
const displacedIdx = i - mcl_2;
const displacedRMA = displacedIdx >= 0 ? ma_mcl[displacedIdx] : null;
if (displacedRMA === null) continue;
const mcm_off = mcm * atr_val;
const centerLine = (historical_ma === null || historical_ma === undefined || isNaN(historical_ma)) ? src : historical_ma;
results[i] = {
upper: displacedRMA + mcm_off,
lower: displacedRMA - mcm_off
upper: centerLine + mcm_off,
lower: centerLine - mcm_off
};
}
@ -169,16 +157,19 @@ export class HurstBandsIndicator extends BaseIndicator {
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: '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: '#ff6b6b', title: 'Upper' },
{ id: 'lower', color: '#4caf50', title: 'Lower' }
{ 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 };
export { calculateHurstSignal };