224 lines
9.6 KiB
JavaScript
224 lines
9.6 KiB
JavaScript
/**
|
|
* Creates and manages all indicator-related logic for the chart.
|
|
* @param {Object} chart - The Lightweight Charts instance.
|
|
* @param {Array<Object>} baseCandleDataRef - A reference to the array holding the chart's BASE 1m candle data.
|
|
* @param {Array<Object>} displayedCandleDataRef - A reference to the array with currently visible candles.
|
|
* @returns {Object} A manager object with public methods to control indicators.
|
|
*/
|
|
function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef) {
|
|
// --- FIX: --- Added `debounceTimerId` to each slot object to track pending updates.
|
|
const indicatorSlots = [
|
|
{ id: 1, cellId: 'indicator-cell-1', series: [], definition: null, params: {}, calculator: null, debounceTimerId: null },
|
|
{ id: 2, cellId: 'indicator-cell-2', series: [], definition: null, params: {}, calculator: null, debounceTimerId: null },
|
|
{ id: 3, cellId: 'indicator-cell-3', series: [], definition: null, params: {}, calculator: null, debounceTimerId: null },
|
|
{ id: 4, cellId: 'indicator-cell-4', series: [], definition: null, params: {}, calculator: null, debounceTimerId: null },
|
|
];
|
|
|
|
const colors = {
|
|
bb: {
|
|
bb1_upper: 'rgba(128, 25, 34, 0.5)',
|
|
bb2_upper: 'rgba(128, 25, 34, 0.75)',
|
|
bb3_upper: 'rgba(128, 25, 34, 1)',
|
|
bb1_lower: 'rgba(6, 95, 6, 0.5)',
|
|
bb2_lower: 'rgba(6, 95, 6, 0.75)',
|
|
bb3_lower: 'rgba(6, 95, 6, 1.0)',
|
|
},
|
|
hurst: { topBand: '#787b86', bottomBand: '#787b86', topBand_h: '#673ab7', bottomBand_h: '#673ab7' },
|
|
hts: {
|
|
fastSMA: '#00bcd4', // Cyan blue for Fast SMA
|
|
slowSMA: '#ff5252' // Red for Slow SMA
|
|
},
|
|
fast_sma: '#00bcd4',
|
|
slow_sma: '#ff5252',
|
|
default: ['#00BCD4', '#FFEB3B', '#4CAF50', '#E91E63']
|
|
};
|
|
|
|
function populateDropdowns() {
|
|
indicatorSlots.forEach(slot => {
|
|
const cell = document.getElementById(slot.cellId);
|
|
if (!cell) return;
|
|
|
|
const select = document.createElement('select');
|
|
select.innerHTML = `<option value="">-- Select Indicator --</option>`;
|
|
AVAILABLE_INDICATORS.forEach(ind => {
|
|
select.innerHTML += `<option value="${ind.name}">${ind.label}</option>`;
|
|
});
|
|
|
|
const controlsContainer = document.createElement('div');
|
|
controlsContainer.className = 'indicator-controls';
|
|
|
|
cell.innerHTML = '';
|
|
cell.appendChild(select);
|
|
cell.appendChild(controlsContainer);
|
|
|
|
select.addEventListener('change', (e) => loadIndicator(slot.id, e.target.value));
|
|
});
|
|
}
|
|
|
|
function loadIndicator(slotId, indicatorName) {
|
|
const slot = indicatorSlots.find(s => s.id === slotId);
|
|
if (!slot) return;
|
|
|
|
// --- FIX: --- Cancel any pending debounced update from the previous indicator's controls.
|
|
// This is the core of the fix, preventing the race condition.
|
|
if (slot.debounceTimerId) {
|
|
clearTimeout(slot.debounceTimerId);
|
|
slot.debounceTimerId = null;
|
|
}
|
|
|
|
slot.series.forEach(s => chart.removeSeries(s));
|
|
slot.series = [];
|
|
slot.definition = null;
|
|
slot.params = {};
|
|
slot.calculator = null;
|
|
|
|
const controlsContainer = document.querySelector(`#${slot.cellId} .indicator-controls`);
|
|
controlsContainer.innerHTML = '';
|
|
|
|
if (!indicatorName) return;
|
|
|
|
const definition = AVAILABLE_INDICATORS.find(ind => ind.name === indicatorName);
|
|
if (!definition) return;
|
|
|
|
slot.definition = definition;
|
|
|
|
definition.params.forEach(param => {
|
|
const label = document.createElement('label');
|
|
label.textContent = param.label || param.name;
|
|
label.style.fontSize = '12px';
|
|
|
|
const input = document.createElement('input');
|
|
input.type = param.type;
|
|
input.value = param.defaultValue;
|
|
if (param.min !== undefined) input.min = param.min;
|
|
if (param.step !== undefined) input.step = param.step;
|
|
input.className = 'input-field';
|
|
slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
|
|
|
|
input.addEventListener('input', () => {
|
|
// --- FIX: --- Use the slot's `debounceTimerId` property to manage the timeout.
|
|
clearTimeout(slot.debounceTimerId);
|
|
slot.debounceTimerId = setTimeout(() => {
|
|
slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
|
|
updateIndicator(slot.id, true);
|
|
slot.debounceTimerId = null; // Clear the ID after the function has run.
|
|
}, 500);
|
|
});
|
|
const controlGroup = document.createElement('div');
|
|
controlGroup.style.display = 'flex';
|
|
controlGroup.style.flexDirection = 'column';
|
|
controlGroup.appendChild(label);
|
|
controlGroup.appendChild(input);
|
|
controlsContainer.appendChild(controlGroup);
|
|
});
|
|
|
|
updateIndicator(slot.id, true);
|
|
}
|
|
|
|
function updateIndicator(slotId, isFullRecalculation = false) {
|
|
const slot = indicatorSlots.find(s => s.id === slotId);
|
|
if (!slot || !slot.definition) return;
|
|
|
|
const candleDataForCalc = (slot.definition.usesBaseData) ? baseCandleDataRef : displayedCandleDataRef;
|
|
if (candleDataForCalc.length === 0) return;
|
|
|
|
|
|
if (isFullRecalculation) {
|
|
slot.series.forEach(s => chart.removeSeries(s));
|
|
slot.series = [];
|
|
|
|
const indicatorResult = slot.definition.calculateFull(candleDataForCalc, slot.params);
|
|
|
|
if (typeof indicatorResult === 'object' && !Array.isArray(indicatorResult)) {
|
|
Object.keys(indicatorResult).forEach(key => {
|
|
const seriesData = indicatorResult[key];
|
|
const indicatorNameLower = slot.definition.name.toLowerCase();
|
|
const series = chart.addLineSeries({
|
|
color: (colors[indicatorNameLower] && colors[indicatorNameLower][key]) ? colors[indicatorNameLower][key] : colors.default[slot.id - 1],
|
|
lineWidth: 1,
|
|
title: '',
|
|
lastValueVisible: false,
|
|
priceLineVisible: false,
|
|
});
|
|
series.setData(seriesData);
|
|
slot.series.push(series);
|
|
});
|
|
} else {
|
|
const indicatorNameLower = slot.definition.name.toLowerCase();
|
|
const indicatorColor = colors[indicatorNameLower] || slot.definition.color || colors.default[slot.id - 1];
|
|
const series = chart.addLineSeries({
|
|
color: indicatorColor,
|
|
lineWidth: 1,
|
|
title: '',
|
|
lastValueVisible: false,
|
|
priceLineVisible: false,
|
|
});
|
|
series.setData(indicatorResult);
|
|
slot.series.push(series);
|
|
}
|
|
|
|
if (slot.definition.createRealtime) {
|
|
slot.calculator = slot.definition.createRealtime(slot.params);
|
|
slot.calculator.prime(candleDataForCalc);
|
|
}
|
|
} else if (slot.calculator) {
|
|
const lastCandle = candleDataForCalc[candleDataForCalc.length - 1];
|
|
if (!lastCandle) return;
|
|
|
|
const newPoint = slot.calculator.update(lastCandle);
|
|
|
|
if (newPoint && typeof newPoint === 'object') {
|
|
if (slot.series.length > 1) { // Multi-line indicator
|
|
Object.keys(newPoint).forEach((key, index) => {
|
|
if (slot.series[index] && newPoint[key]) {
|
|
slot.series[index].update(newPoint[key]);
|
|
}
|
|
});
|
|
} else if (slot.series.length === 1) { // Single-line indicator
|
|
slot.series[0].update(newPoint);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function recalculateAllAfterHistory(baseData, displayedData) {
|
|
baseCandleDataRef = baseData;
|
|
displayedCandleDataRef = displayedData;
|
|
|
|
// --- FIX: --- Clear any pending debounced updates from parameter changes.
|
|
// This prevents a stale update from a parameter input from running after
|
|
// the chart has already been reset for a new timeframe.
|
|
indicatorSlots.forEach(slot => {
|
|
if (slot.debounceTimerId) {
|
|
clearTimeout(slot.debounceTimerId);
|
|
slot.debounceTimerId = null;
|
|
}
|
|
});
|
|
|
|
// --- FIX: --- Defer the full recalculation to the next frame.
|
|
// This prevents a race condition where indicators are removed/added while the chart
|
|
// is still processing the main series' `setData` operation from a timeframe change.
|
|
setTimeout(() => {
|
|
indicatorSlots.forEach(slot => {
|
|
if (slot.definition) {
|
|
updateIndicator(slot.id, true);
|
|
}
|
|
});
|
|
}, 0);
|
|
}
|
|
|
|
function updateAllOnNewCandle() {
|
|
indicatorSlots.forEach(slot => {
|
|
if (slot.definition) {
|
|
updateIndicator(slot.id, false);
|
|
}
|
|
});
|
|
}
|
|
|
|
return {
|
|
populateDropdowns,
|
|
recalculateAllAfterHistory,
|
|
updateAllOnNewCandle,
|
|
};
|
|
}
|