/** * Creates and manages all indicator-related logic for the chart. * @param {Object} chart - The Lightweight Charts instance. * @param {Array} baseCandleData - A reference to the array holding the chart's BASE 1m candle data. * @returns {Object} A manager object with public methods to control indicators. */ function createIndicatorManager(chart, baseCandleData) { // This holds the candle data currently displayed on the chart (e.g., 5m, 10m) let currentAggregatedData = []; // Defines the 4 slots available in the UI for indicators. 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: {} }, ]; // Pre-defined colors for the indicator lines. const colors = { bb1: { upper: '#FF9800', lower: '#FF9800' }, // Orange bb2: { upper: '#2196F3', lower: '#2196F3' }, // Blue bb3: { upper: '#9C27B0', lower: '#9C27B0' }, // Purple default: ['#FF5722', '#03A9F4', '#8BC34A', '#F44336'] // Fallback colors for other indicators }; /** * Populates the dropdown menus in each indicator cell. */ 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 = ''; // Clear previous content cell.appendChild(select); cell.appendChild(controlsContainer); select.addEventListener('change', (e) => { const indicatorName = e.target.value; loadIndicator(slot.id, indicatorName); }); }); } /** * Loads a new indicator into a specified slot. * @param {number} slotId - The ID of the slot (1-4). * @param {string} indicatorName - The name of the indicator to load (e.g., 'SMA'). */ function loadIndicator(slotId, indicatorName) { const slot = indicatorSlots.find(s => s.id === slotId); if (!slot) return; // Clean up any previous indicator series in this slot 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; // Create UI controls for the indicator's parameters 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'; input.placeholder = param.name; 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); } /** * Recalculates and redraws the lines for a specific indicator. * @param {number} slotId - The ID of the slot to update. */ function updateIndicator(slotId) { const slot = indicatorSlots.find(s => s.id === slotId); const candleDataForCalc = (slot.definition.usesBaseData) ? baseCandleData : currentAggregatedData; if (!slot || !slot.definition || candleDataForCalc.length === 0) { return; } // Clean up previous series before creating new ones slot.series.forEach(s => chart.removeSeries(s)); slot.series = []; console.log(`Recalculating ${slot.definition.name} for slot ${slot.id} on ${candleDataForCalc.length} candles.`); const indicatorResult = slot.definition.calculateFull(candleDataForCalc, slot.params); // Handle multi-line indicators like Bollinger Bands if (typeof indicatorResult === 'object' && !Array.isArray(indicatorResult)) { Object.keys(indicatorResult).forEach(key => { const seriesData = indicatorResult[key]; const bandName = key.split('_')[0]; const bandType = key.split('_')[1]; const series = chart.addLineSeries({ color: colors[bandName] ? colors[bandName][bandType] : colors.default[slot.id - 1], lineWidth: 2, title: `${slot.definition.label} - ${key}`, lastValueVisible: false, priceLineVisible: false, }); series.setData(seriesData); slot.series.push(series); }); } else { // Handle single-line indicators like SMA/EMA const series = chart.addLineSeries({ color: colors.default[slot.id - 1], lineWidth: 2, title: slot.definition.label, }); series.setData(indicatorResult); slot.series.push(series); } } /** * Internal function to recalculate all active indicators. */ function recalculateAllIndicators() { indicatorSlots.forEach(slot => { if (slot.definition) { updateIndicator(slot.id); } }); } /** * Sets the candle data for indicators and triggers a full recalculation. * @param {Array} aggregatedCandleData - The candle data for the currently selected timeframe. */ function recalculateAllAfterHistory(aggregatedCandleData) { currentAggregatedData = aggregatedCandleData; recalculateAllIndicators(); } /** * Updates all indicators in response to a new candle closing. * @param {Array} aggregatedCandleData - The latest candle data for the currently selected timeframe. */ function updateIndicatorsOnNewCandle(aggregatedCandleData) { currentAggregatedData = aggregatedCandleData; recalculateAllIndicators(); } // Public API for the manager return { populateDropdowns, recalculateAllAfterHistory, updateIndicatorsOnNewCandle }; }