/** * 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) { const indicatorSlots = [ { id: 1, cellId: 'indicator-cell-1', series: [], definition: null, params: {} }, { id: 2, cellId: 'indicator-cell-2', series: [], definition: null, params: {} }, { id: 3, cellId: 'indicator-cell-3', series: [], definition: null, params: {} }, { id: 4, cellId: 'indicator-cell-4', series: [], definition: null, params: {} }, ]; const colors = { bb1: { upper: '#FF9800', lower: '#FF9800' }, bb2: { upper: '#2196F3', lower: '#2196F3' }, bb3: { upper: '#9C27B0', lower: '#9C27B0' }, hurst: { topBand: '#673ab7', bottomBand: '#673ab7' }, default: ['#FF5722', '#03A9F4', '#8BC34A', '#F44336'] }; 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; slot.series.forEach(s => chart.removeSeries(s)); slot.series = []; slot.definition = null; slot.params = {}; 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; let debounceTimer; input.addEventListener('input', () => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value; updateIndicator(slot.id); }, 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); } function updateIndicator(slotId) { const slot = indicatorSlots.find(s => s.id === slotId); const candleDataForCalc = (slot.definition.usesBaseData) ? baseCandleDataRef : displayedCandleDataRef; if (!slot || !slot.definition || candleDataForCalc.length === 0) { return; } 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: indicatorNameLower === 'hurst' ? 1 : 2, title: `${slot.definition.label} - ${key}`, lastValueVisible: false, priceLineVisible: false, }); series.setData(seriesData); slot.series.push(series); }); } else { const series = chart.addLineSeries({ color: colors.default[slot.id - 1], lineWidth: 2, title: slot.definition.label, }); series.setData(indicatorResult); slot.series.push(series); } } function recalculateAllAfterHistory(baseData, displayedData) { baseCandleDataRef = baseData; displayedCandleDataRef = displayedData; indicatorSlots.forEach(slot => { if (slot.definition) updateIndicator(slot.id); }); } return { populateDropdowns, recalculateAllAfterHistory, }; }