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:
@ -4,14 +4,12 @@
|
|||||||
|
|
||||||
const SIGNAL_TYPES = {
|
const SIGNAL_TYPES = {
|
||||||
BUY: 'buy',
|
BUY: 'buy',
|
||||||
SELL: 'sell',
|
SELL: 'sell'
|
||||||
HOLD: 'hold'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SIGNAL_COLORS = {
|
const SIGNAL_COLORS = {
|
||||||
buy: '#26a69a',
|
buy: '#9e9e9e',
|
||||||
hold: '#787b86',
|
sell: '#9e9e9e'
|
||||||
sell: '#ef5350'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class BaseIndicator {
|
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)
|
// Calculate RMA (Rolling Moving Average - Wilder's method)
|
||||||
function calculateRMA(values, period) {
|
// Recreates Pine Script's ta.rma() exactly
|
||||||
const rma = new Array(values.length).fill(null);
|
function calculateRMA(sourceArray, length) {
|
||||||
|
const rma = new Array(sourceArray.length).fill(null);
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
for (let i = 0; i < period && i < values.length; i++) {
|
const alpha = 1 / length;
|
||||||
sum += values[i];
|
|
||||||
if (i === period - 1) {
|
|
||||||
rma[i] = sum / period;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const alpha = 1 / period;
|
// PineScript implicitly rounds float lengths for SMA initialization
|
||||||
for (let i = period; i < values.length; i++) {
|
const smaLength = Math.round(length);
|
||||||
if (rma[i - 1] !== null) {
|
|
||||||
rma[i] = rma[i - 1] + alpha * (values[i] - rma[i - 1]);
|
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);
|
super(config);
|
||||||
this.lastSignalTimestamp = null;
|
this.lastSignalTimestamp = null;
|
||||||
this.lastSignalType = 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) {
|
calculate(candles) {
|
||||||
const mcl = this.params.period || 30;
|
const mcl_t = this.params.period || 30;
|
||||||
const mcm = this.params.multiplier || 1.8;
|
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 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 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 = 0; i < candles.length; i++) {
|
||||||
|
const src = closes[i];
|
||||||
|
const mcm_off = mcm * (atr[i] || 0);
|
||||||
|
|
||||||
for (let i = mcl_2; i < candles.length; i++) {
|
const historicalIndex = i - mcl_2;
|
||||||
const ma_val = ma_mcl[i];
|
const historical_ma = historicalIndex >= 0 ? ma_mcl[historicalIndex] : null;
|
||||||
const atr_val = atr[i];
|
|
||||||
|
|
||||||
if (ma_val === null || atr_val === null) continue;
|
const centerLine = (historical_ma === null || historical_ma === undefined || isNaN(historical_ma)) ? src : historical_ma;
|
||||||
|
|
||||||
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] = {
|
results[i] = {
|
||||||
upper: displacedRMA + mcm_off,
|
upper: centerLine + mcm_off,
|
||||||
lower: displacedRMA - mcm_off
|
lower: centerLine - mcm_off
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,12 +157,15 @@ export class HurstBandsIndicator extends BaseIndicator {
|
|||||||
name: 'Hurst Bands',
|
name: 'Hurst Bands',
|
||||||
description: 'Cyclic price channels based on Hurst theory',
|
description: 'Cyclic price channels based on Hurst theory',
|
||||||
inputs: [
|
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 }
|
{ name: 'multiplier', label: 'Multiplier (mcm)', type: 'number', default: 1.8, min: 0.5, max: 10, step: 0.1 }
|
||||||
],
|
],
|
||||||
plots: [
|
plots: [
|
||||||
{ id: 'upper', color: '#ff6b6b', title: 'Upper' },
|
{ id: 'upper', color: '#808080', title: 'Upper', lineWidth: 1 },
|
||||||
{ id: 'lower', color: '#4caf50', title: 'Lower' }
|
{ id: 'lower', color: '#808080', title: 'Lower', lineWidth: 1 }
|
||||||
|
],
|
||||||
|
bands: [
|
||||||
|
{ topId: 'upper', bottomId: 'lower', color: 'rgba(128, 128, 128, 0.05)' }
|
||||||
],
|
],
|
||||||
displayMode: 'overlay'
|
displayMode: 'overlay'
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,16 @@ let indicatorPanes = new Map();
|
|||||||
let nextPaneIndex = 1;
|
let nextPaneIndex = 1;
|
||||||
|
|
||||||
// Presets storage
|
// Presets storage
|
||||||
let userPresets = JSON.parse(localStorage.getItem('indicator_presets') || '{}');
|
let userPresets;
|
||||||
|
try {
|
||||||
|
userPresets = JSON.parse(localStorage.getItem('indicator_presets') || '{}');
|
||||||
|
if (!userPresets || typeof userPresets !== 'object') {
|
||||||
|
userPresets = { presets: [] };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse presets:', e);
|
||||||
|
userPresets = { presets: [] };
|
||||||
|
}
|
||||||
|
|
||||||
// Categories
|
// Categories
|
||||||
const CATEGORIES = [
|
const CATEGORIES = [
|
||||||
@ -589,35 +598,78 @@ function getPresetsForIndicator(indicatorName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.savePreset = function(id) {
|
window.savePreset = function(id) {
|
||||||
|
console.log('[savePreset] Attempting to save preset for id:', id);
|
||||||
const indicator = activeIndicators.find(a => a.id === id);
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
if (!indicator) return;
|
if (!indicator) {
|
||||||
|
console.error('[savePreset] Indicator not found for id:', id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const presetName = prompt('Enter preset name:');
|
const presetName = prompt('Enter preset name:');
|
||||||
if (!presetName) return;
|
if (!presetName) return;
|
||||||
|
|
||||||
const IndicatorClass = IR?.[indicator.type];
|
const IndicatorClass = IR?.[indicator.type];
|
||||||
if (!IndicatorClass) return;
|
if (!IndicatorClass) {
|
||||||
|
console.error('[savePreset] Indicator class not found for type:', indicator.type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
|
try {
|
||||||
const meta = instance.getMetadata();
|
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
|
||||||
|
const meta = instance.getMetadata();
|
||||||
|
|
||||||
const preset = {
|
const preset = {
|
||||||
id: `preset_${Date.now()}`,
|
id: `preset_${Date.now()}`,
|
||||||
name: presetName,
|
name: presetName,
|
||||||
indicatorName: meta.name,
|
indicatorName: meta.name,
|
||||||
values: {}
|
values: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
meta.inputs.forEach(input => {
|
// Save standard inputs
|
||||||
preset.values[input.name] = indicator.params[input.name];
|
if (meta.inputs && Array.isArray(meta.inputs)) {
|
||||||
});
|
meta.inputs.forEach(input => {
|
||||||
|
preset.values[input.name] = indicator.params[input.name];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!userPresets.presets) userPresets.presets = [];
|
// Save visual settings (line width, type, colors)
|
||||||
userPresets.presets.push(preset);
|
preset.values._lineWidth = indicator.params._lineWidth;
|
||||||
saveUserPresets();
|
preset.values._lineType = indicator.params._lineType;
|
||||||
renderIndicatorPanel();
|
|
||||||
|
|
||||||
alert(`Preset "${presetName}" saved!`);
|
// Save colors for each plot
|
||||||
|
if (meta.plots && Array.isArray(meta.plots)) {
|
||||||
|
meta.plots.forEach((plot, idx) => {
|
||||||
|
const colorKey = `_color_${idx}`;
|
||||||
|
if (indicator.params[colorKey]) {
|
||||||
|
preset.values[colorKey] = indicator.params[colorKey];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save marker settings
|
||||||
|
const markerKeys = [
|
||||||
|
'showMarkers',
|
||||||
|
'markerBuyShape', 'markerBuyColor', 'markerBuyCustom',
|
||||||
|
'markerSellShape', 'markerSellColor', 'markerSellCustom'
|
||||||
|
];
|
||||||
|
markerKeys.forEach(key => {
|
||||||
|
if (indicator.params[key] !== undefined) {
|
||||||
|
preset.values[key] = indicator.params[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userPresets) userPresets = { presets: [] };
|
||||||
|
if (!userPresets.presets) userPresets.presets = [];
|
||||||
|
|
||||||
|
userPresets.presets.push(preset);
|
||||||
|
saveUserPresets();
|
||||||
|
renderIndicatorPanel();
|
||||||
|
|
||||||
|
alert(`Preset "${presetName}" saved!`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[savePreset] Error saving preset:', error);
|
||||||
|
alert('Error saving preset. See console for details.');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.applyPreset = function(id, presetId) {
|
window.applyPreset = function(id, presetId) {
|
||||||
@ -688,15 +740,27 @@ function addIndicator(type) {
|
|||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
_lineType: 'solid',
|
_lineType: 'solid',
|
||||||
_lineWidth: 2,
|
_lineWidth: 1,
|
||||||
showMarkers: true,
|
showMarkers: true,
|
||||||
markerBuyShape: 'arrowUp',
|
markerBuyShape: 'custom',
|
||||||
markerBuyColor: '#26a69a',
|
markerBuyColor: '#9e9e9e',
|
||||||
markerBuyCustom: '◭',
|
markerBuyCustom: '▲',
|
||||||
markerSellShape: 'arrowDown',
|
markerSellShape: 'custom',
|
||||||
markerSellColor: '#ef5350',
|
markerSellColor: '#9e9e9e',
|
||||||
markerSellCustom: '▼'
|
markerSellCustom: '▼'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Override with Hurst-specific defaults
|
||||||
|
if (type === 'hurst') {
|
||||||
|
params._lineWidth = 1;
|
||||||
|
params.markerBuyShape = 'custom';
|
||||||
|
params.markerSellShape = 'custom';
|
||||||
|
params.markerBuyColor = '#9e9e9e';
|
||||||
|
params.markerSellColor = '#9e9e9e';
|
||||||
|
params.markerBuyCustom = '▲';
|
||||||
|
params.markerSellCustom = '▼';
|
||||||
|
}
|
||||||
|
|
||||||
metadata.plots.forEach((plot, idx) => {
|
metadata.plots.forEach((plot, idx) => {
|
||||||
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
|
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
|
||||||
});
|
});
|
||||||
@ -751,7 +815,7 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
|
|||||||
indicator.series = [];
|
indicator.series = [];
|
||||||
|
|
||||||
const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid;
|
const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid;
|
||||||
const lineWidth = indicator.params._lineWidth || 2;
|
const lineWidth = indicator.params._lineWidth || 1;
|
||||||
|
|
||||||
const firstNonNull = results?.find(r => r !== null && r !== undefined);
|
const firstNonNull = results?.find(r => r !== null && r !== undefined);
|
||||||
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
|
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
|
||||||
|
|||||||
@ -235,8 +235,19 @@ export function addIndicator(type) {
|
|||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
_lineType: 'solid',
|
_lineType: 'solid',
|
||||||
_lineWidth: 2
|
_lineWidth: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set Hurst-specific defaults
|
||||||
|
if (type === 'hurst') {
|
||||||
|
params.markerBuyShape = 'custom';
|
||||||
|
params.markerSellShape = 'custom';
|
||||||
|
params.markerBuyColor = '#9e9e9e';
|
||||||
|
params.markerSellColor = '#9e9e9e';
|
||||||
|
params.markerBuyCustom = '▲';
|
||||||
|
params.markerSellCustom = '▼';
|
||||||
|
}
|
||||||
|
|
||||||
metadata.plots.forEach((plot, idx) => {
|
metadata.plots.forEach((plot, idx) => {
|
||||||
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
|
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
|
||||||
});
|
});
|
||||||
@ -326,7 +337,7 @@ export function renderIndicatorConfig(indicator) {
|
|||||||
|
|
||||||
<div style="margin-bottom: 8px;">
|
<div style="margin-bottom: 8px;">
|
||||||
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">Line Width</label>
|
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">Line Width</label>
|
||||||
<input type="number" id="config__lineWidth" class="sim-input" value="${indicator.params._lineWidth || 2}" min="1" max="5" style="font-size: 12px; padding: 6px;">
|
<input type="number" id="config__lineWidth" class="sim-input" value="${indicator.params._lineWidth || 1}" min="1" max="5" style="font-size: 12px; padding: 6px;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${meta.inputs.map(input => `
|
${meta.inputs.map(input => `
|
||||||
@ -485,7 +496,7 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
|
|||||||
indicator.series = [];
|
indicator.series = [];
|
||||||
|
|
||||||
const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid;
|
const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid;
|
||||||
const lineWidth = indicator.params._lineWidth || 2;
|
const lineWidth = indicator.params._lineWidth || 1;
|
||||||
|
|
||||||
const firstNonNull = results?.find(r => r !== null && r !== undefined);
|
const firstNonNull = results?.find(r => r !== null && r !== undefined);
|
||||||
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
|
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
|
||||||
@ -497,6 +508,9 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
|
|||||||
if (!hasData) return;
|
if (!hasData) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip hidden plots
|
||||||
|
if (plot.visible === false) return;
|
||||||
|
|
||||||
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
|
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
|
||||||
|
|
||||||
const data = [];
|
const data = [];
|
||||||
@ -563,6 +577,13 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
|
|||||||
}
|
}
|
||||||
|
|
||||||
series.setData(data);
|
series.setData(data);
|
||||||
|
series.plotId = plot.id;
|
||||||
|
|
||||||
|
// Skip hidden plots (visible: false)
|
||||||
|
if (plot.visible === false) {
|
||||||
|
series.applyOptions({ visible: false });
|
||||||
|
}
|
||||||
|
|
||||||
indicator.series.push(series);
|
indicator.series.push(series);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user