/** * Creates and manages all indicator-related logic for the chart. * @param {Object} chart - The Lightweight Charts instance. * @param {Array} baseCandleDataRef - A reference to the array holding the chart's BASE 1m candle data. * @param {Array} 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 = ``; AVAILABLE_INDICATORS.forEach(ind => { select.innerHTML += ``; }); 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, }; }