import { getAvailableIndicators, IndicatorRegistry as IR } from '../indicators/index.js'; // State management let activeIndicators = []; let configuringId = null; let searchQuery = ''; let selectedCategory = 'all'; let nextInstanceId = 1; let listenersAttached = false; // Single flag to track if any listeners are attached // Chart pane management let indicatorPanes = new Map(); let nextPaneIndex = 1; // Presets storage let userPresets = JSON.parse(localStorage.getItem('indicator_presets') || '{}'); // Categories const CATEGORIES = [ { id: 'all', name: 'All Indicators', icon: '📊' }, { id: 'trend', name: 'Trend', icon: '📊' }, { id: 'momentum', name: 'Momentum', icon: '📈' }, { id: 'volatility', name: 'Volatility', icon: '📉' }, { id: 'volume', name: 'Volume', icon: '🔀' }, { id: 'favorites', name: 'Favorites', icon: '★' } ]; const CATEGORY_MAP = { sma: 'trend', ema: 'trend', hts: 'trend', rsi: 'momentum', macd: 'momentum', stoch: 'momentum', bb: 'volatility', atr: 'volatility', others: 'volume' }; const DEFAULT_COLORS = ['#2962ff', '#26a69a', '#ef5350', '#ff9800', '#9c27b0', '#00bcd4', '#ffeb3b', '#e91e63']; const LINE_TYPES = ['solid', 'dotted', 'dashed']; function getDefaultColor(index) { return DEFAULT_COLORS[index % DEFAULT_COLORS.length]; } function getIndicatorCategory(indicator) { return CATEGORY_MAP[indicator.type] || 'trend'; } function getIndicatorLabel(indicator) { const meta = getIndicatorMeta(indicator); if (!meta) return indicator.name; const paramParts = meta.inputs.map(input => { const val = indicator.params[input.name]; if (val !== undefined && val !== input.default) return val; return null; }).filter(v => v !== null); if (paramParts.length > 0) { return `${indicator.name} (${paramParts.join(', ')})`; } return indicator.name; } function getIndicatorMeta(indicator) { const IndicatorClass = IR?.[indicator.type]; if (!IndicatorClass) return null; const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name }); return instance.getMetadata(); } function groupPlotsByColor(plots) { const groups = {}; plots.forEach((plot, idx) => { const groupMap = { 'fast': 'Fast', 'slow': 'Slow', 'upper': 'Upper', 'lower': 'Lower', 'middle': 'Middle', 'basis': 'Middle', 'signal': 'Signal', 'histogram': 'Histogram', 'k': '%K', 'd': '%D' }; const groupName = Object.entries(groupMap).find(([k, v]) => plot.id.toLowerCase().includes(k))?.[1] || plot.id; if (!groups[groupName]) { groups[groupName] = { name: groupName, indices: [], plots: [] }; } groups[groupName].indices.push(idx); groups[groupName].plots.push(plot); }); return Object.values(groups); } export function initIndicatorPanel() { console.log('[IndicatorPanel] Initializing...'); renderIndicatorPanel(); console.log('[IndicatorPanel] Initialized'); } export function getActiveIndicators() { return activeIndicators; } export function setActiveIndicators(indicators) { activeIndicators = indicators; renderIndicatorPanel(); } // Render main panel export function renderIndicatorPanel() { const container = document.getElementById('indicatorPanel'); if (!container) { console.error('[IndicatorPanel] Container #indicatorPanel not found!'); return; } console.log('[IndicatorPanel] Rendering panel, searchQuery:', searchQuery, 'selectedCategory:', selectedCategory); const available = getAvailableIndicators(); const catalog = available.filter(ind => { if (searchQuery && !ind.name.toLowerCase().includes(searchQuery.toLowerCase())) return false; if (selectedCategory === 'all') return true; if (selectedCategory === 'favorites') return false; const cat = CATEGORY_MAP[ind.type] || 'trend'; return cat === selectedCategory; }); console.log("[IndicatorPanel] Total indicators:", available.length, "Filtered to:", catalog.length); const favoriteIds = new Set(userPresets.favorites || []); container.innerHTML = `
${CATEGORIES.map(cat => ` `).join('')}
${[...favoriteIds].length > 0 ? `
★ Favorites
${[...favoriteIds].map(id => { const ind = available.find(a => { return a.type === id || (activeIndicators.find(ai => ai.id === id)?.type === ''); }); if (!ind) return ''; return renderIndicatorItem(ind, true); }).join('')}
` : ''} ${activeIndicators.length > 0 ? `
${activeIndicators.length} Active ${activeIndicators.length > 0 ? `` : ''}
${activeIndicators.slice().reverse().map(ind => renderActiveIndicator(ind)).join('')}
` : ''} ${catalog.length > 0 ? `
Available Indicators
${catalog.map(ind => renderIndicatorItem(ind, false)).join('')}
` : `
No indicators found
`}
`; // Only setup event listeners once if (!listenersAttached) { setupEventListeners(); listenersAttached = true; } } function renderIndicatorItem(indicator, isFavorite) { const colorDots = ''; return `
${indicator.name} ${indicator.description || ''}
${isFavorite ? '' : ` `}
`; } function renderActiveIndicator(indicator) { const isExpanded = configuringId === indicator.id; const meta = getIndicatorMeta(indicator); const label = getIndicatorLabel(indicator); const isFavorite = userPresets.favorites?.includes(indicator.type) || false; const showPresets = meta.name && function() { const hasPresets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : []; if (!hasPresets || hasPresets.length === 0) return ''; return `
`; }(); return `
⋮⋮
${label} ${showPresets}
${isExpanded ? `
${typeof renderIndicatorConfig === 'function' ? renderIndicatorConfig(indicator, meta) : ''}
` : ''}
`; } function renderPresetIndicatorIndicator(meta, indicator) { const hasPresets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : []; if (!hasPresets || hasPresets.length === 0) return ''; return ``; } function renderIndicatorConfig(indicator, meta) { const plotGroups = groupPlotsByColor(meta?.plots || []); return `
Visual Settings
${plotGroups.map(group => { const firstIdx = group.indices[0]; const color = indicator.params[`_color_${firstIdx}`] || meta.plots[firstIdx]?.color || getDefaultColor(activeIndicators.indexOf(indicator)); return `
`; }).join('')}
${indicator.params._lineWidth || 2}
${meta?.inputs && meta.inputs.length > 0 ? `
Parameters
${meta.inputs.map(input => `
${input.type === 'select' ? `` : `` }
`).join('')}
` : ''}
Presets
${typeof renderIndicatorPresets === 'function' ? renderIndicatorPresets(indicator, meta) : ''}
`; } function renderIndicatorPresets(indicator, meta) { const presets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : []; return presets.length > 0 ? `
${presets.map(p => { const isApplied = meta.inputs.every(input => (indicator.params[input.name] === (preset.values?.[input.name] ?? input.default)) ); return `
${preset.name}
`; }).join('')}
` : '
No saved presets
'; } // Event listeners function setupEventListeners() { const container = document.getElementById('indicatorPanel'); if (!container) return; console.log('[IndicatorPanel] Setting up event listeners...'); // Single event delegation handler for add button container.addEventListener('click', (e) => { const addBtn = e.target.closest('.indicator-btn.add'); if (addBtn) { e.stopPropagation(); const type = addBtn.dataset.type; if (type && window.addIndicator) { console.log('[IndicatorPanel] Adding indicator:', type); window.addIndicator(type); } return; } // Expand/collapse button const expandBtn = e.target.closest('.indicator-btn.expand'); if (expandBtn) { e.stopPropagation(); const id = expandBtn.dataset.id; if (id && window.toggleIndicatorExpand) { window.toggleIndicatorExpand(id); } return; } // Remove button const removeBtn = e.target.closest('.indicator-btn.remove'); if (removeBtn) { e.stopPropagation(); const id = removeBtn.dataset.id; if (id && window.removeIndicatorById) { window.removeIndicatorById(id); } return; } // Favorite button const favoriteBtn = e.target.closest('.indicator-btn.favorite'); if (favoriteBtn) { e.stopPropagation(); const type = favoriteBtn.dataset.type; if (type && window.toggleFavorite) { window.toggleFavorite(type); } return; } }); // Search input const searchInput = document.getElementById('indicatorSearch'); if (searchInput) { searchInput.addEventListener('input', (e) => { searchQuery = e.target.value; renderIndicatorPanel(); }); } // Search clear button const searchClear = container.querySelector('.search-clear'); if (searchClear) { searchClear.addEventListener('click', (e) => { searchQuery = ''; renderIndicatorPanel(); }); } // Category tabs document.querySelectorAll('.category-tab').forEach(tab => { tab.addEventListener('click', (e) => { selectedCategory = tab.dataset.category; renderIndicatorPanel(); }); }); // Clear all button const clearAllBtn = container.querySelector('.clear-all'); if (clearAllBtn) { clearAllBtn.addEventListener('click', () => { window.clearAllIndicators(); }); } console.log('[IndicatorPanel] Event listeners setup complete'); } // Actions window.clearSearch = function() { searchQuery = ''; renderIndicatorPanel(); }; window.clearAllIndicators = function() { activeIndicators.forEach(ind => { ind.series?.forEach(s => { try { window.dashboard?.chart?.removeSeries(s); } catch(e) {} }); }); activeIndicators = []; configuringId = null; renderIndicatorPanel(); drawIndicatorsOnChart(); }; function addIndicator(type) { const IndicatorClass = IR?.[type]; if (!IndicatorClass) return; const id = `${type}_${nextInstanceId++}`; const instance = new IndicatorClass({ type, params: {}, name: '' }); const metadata = instance.getMetadata(); const params = { _lineType: 'solid', _lineWidth: 2 }; metadata.plots.forEach((plot, idx) => { params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx); }); metadata.inputs.forEach(input => { params[input.name] = input.default; }); activeIndicators.push({ id, type, name: metadata.name, params, plots: metadata.plots, series: [], visible: true }); configuringId = id; renderIndicatorPanel(); drawIndicatorsOnChart(); } window.toggleIndicatorExpand = function(id) { configuringId = configuringId === id ? null : id; renderIndicatorPanel(); }; window.toggleIndicatorVisibility = function(id) { const indicator = activeIndicators.find(a => a.id === id); if (!indicator) return; indicator.visible = indicator.visible === false ? true : false; indicator.series?.forEach(s => { try { s.applyOptions({ visible: indicator.visible }); } catch(e) {} }); renderIndicatorPanel(); }; window.toggleFavorite = function(type) { if (!userPresets) userPresets = {}; if (!userPresets.favorites) userPresets.favorites = []; const favorites = userPresets.favorites; const idx = favorites.indexOf(type); if (idx >= 0) { favorites.splice(idx, 1); } else { favorites.push(type); } userPresets.favorites = favorites; saveUserPresets(); renderIndicatorPanel(); }; window.updateIndicatorColor = function(id, index, color) { const indicator = activeIndicators.find(a => a.id === id); if (!indicator) return; indicator.params[`_color_${index}`] = color; const preview = document.querySelector(`#color_${id}_${index} + .color-preview`); if (preview) { preview.style.background = color; } drawIndicatorsOnChart(); }; window.updateIndicatorSetting = function(id, key, value) { const indicator = activeIndicators.find(a => a.id === id); if (!indicator) return; indicator.params[key] = value; drawIndicatorsOnChart(); }; window.resetIndicator = function(id) { const indicator = activeIndicators.find(a => a.id === id); if (!indicator) return; const IndicatorClass = IR?.[indicator.type]; if (!IndicatorClass) return; const instance = new IndicatorClass({ type: indicator.type, params: {}, name: indicator.name }); const meta = instance.getMetadata(); meta.inputs.forEach(input => { indicator.params[input.name] = input.default; }); renderIndicatorPanel(); drawIndicatorsOnChart(); }; window.removeIndicator = function() { if (!configuringId) return; removeIndicatorById(configuringId); }; window.removeIndicatorById = function(id) { const idx = activeIndicators.findIndex(a => a.id === id); if (idx < 0) return; activeIndicators[idx].series?.forEach(s => { try { window.dashboard?.chart?.removeSeries(s); } catch(e) {} }); activeIndicators.splice(idx, 1); if (configuringId === id) { configuringId = null; } renderIndicatorPanel(); drawIndicatorsOnChart(); }; function removeIndicatorByIndex(index) { if (index < 0 || index >= activeIndicators.length) return; removeIndicatorById(activeIndicators[index].id); } // Presets function getPresetsForIndicator(indicatorName) { if (!userPresets || !userPresets.presets) return []; return userPresets.presets.filter(p => p.indicatorName === indicatorName); } window.savePreset = function(id) { const indicator = activeIndicators.find(a => a.id === id); if (!indicator) return; const presetName = prompt('Enter preset name:'); if (!presetName) return; const IndicatorClass = IR?.[indicator.type]; if (!IndicatorClass) return; const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name }); const meta = instance.getMetadata(); const preset = { id: `preset_${Date.now()}`, name: presetName, indicatorName: meta.name, values: {} }; meta.inputs.forEach(input => { preset.values[input.name] = indicator.params[input.name]; }); if (!userPresets.presets) userPresets.presets = []; userPresets.presets.push(preset); saveUserPresets(); renderIndicatorPanel(); alert(`Preset "${presetName}" saved!`); }; window.applyPreset = function(id, presetId) { const allPresets = (userPresets?.presets || []).filter(p => typeof p === 'object' && p.id); const preset = allPresets.find(p => p.id === presetId); if (!preset) return; const indicator = activeIndicators.find(a => a.id === id); if (!indicator) return; Object.keys(preset.values).forEach(key => { indicator.params[key] = preset.values[key]; }); renderIndicatorPanel(); drawIndicatorsOnChart(); }; window.deletePreset = function(presetId) { if (!confirm('Delete this preset?')) return; if (userPresets?.presets) { userPresets.presets = userPresets.presets.filter(p => p.id !== presetId); saveUserPresets(); renderIndicatorPanel(); } }; window.showPresets = function(indicatorName) { const presets = getPresetsForIndicator(indicatorName); if (presets.length === 0) { alert('No saved presets for this indicator'); return; } const menu = window.open('', '_blank', 'width=400,height=500'); let htmlContent = 'Presets - ' + indicatorName + '' + '

' + indicatorName + ' Presets

'; presets.forEach(p => { htmlContent += '
' + p.name + '
'; }); htmlContent += ''; menu.document.write(htmlContent); }; window.applyPresetFromWindow = function(presetId) { const indicator = activeIndicators.find(a => a.id === configuringId); if (!indicator) return; applyPreset(indicator.id, presetId); }; function saveUserPresets() { localStorage.setItem('indicator_presets', JSON.stringify(userPresets)); } function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) { const results = instance.calculate(candles); indicator.series = []; const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid; const lineWidth = indicator.params._lineWidth || 2; const firstNonNull = results?.find(r => r !== null && r !== undefined); const isObjectResult = firstNonNull && typeof firstNonNull === 'object'; meta.plots.forEach((plot, plotIdx) => { if (isObjectResult) { const hasData = results.some(r => r && r[plot.id] !== undefined && r[plot.id] !== null); if (!hasData) return; } const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff'; const data = []; for (let i = 0; i < candles.length; i++) { let value; if (isObjectResult) { value = results[i]?.[plot.id]; } else { value = results[i]; } if (value !== null && value !== undefined) { data.push({ time: candles[i].time, value: value }); } } if (data.length === 0) return; let series; let plotLineStyle = lineStyle; if (plot.style === 'dashed') plotLineStyle = LightweightCharts.LineStyle.Dashed; else if (plot.style === 'dotted') plotLineStyle = LightweightCharts.LineStyle.Dotted; else if (plot.style === 'solid') plotLineStyle = LightweightCharts.LineStyle.Solid; if (plot.type === 'histogram') { series = window.dashboard.chart.addSeries(LightweightCharts.HistogramSeries, { color: plotColor, priceFormat: { type: 'price', precision: 4, minMove: 0.0001 }, priceLineVisible: false, lastValueVisible: false }, paneIndex); } else if (plot.type === 'baseline') { series = window.dashboard.chart.addSeries(LightweightCharts.BaselineSeries, { baseValue: { type: 'price', price: plot.baseValue || 0 }, topLineColor: plot.topLineColor || plotColor, topFillColor1: plot.topFillColor1 || plotColor, topFillColor2: '#00000000', bottomFillColor1: '#00000000', bottomColor: plot.bottomColor || '#00000000', lineWidth: plot.width !== undefined ? plot.width : lineWidth, lineStyle: plotLineStyle, title: plot.title || '', priceLineVisible: false, lastValueVisible: plot.lastValueVisible !== false }, paneIndex); } else { series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, { color: plotColor, lineWidth: plot.width !== undefined ? plot.width : lineWidth, lineStyle: plotLineStyle, title: plot.title || '', priceLineVisible: false, lastValueVisible: plot.lastValueVisible !== false }, paneIndex); } series.setData(data); indicator.series.push(series); }); } // Chart drawing export function drawIndicatorsOnChart() { if (!window.dashboard || !window.dashboard.chart) return; activeIndicators.forEach(ind => { ind.series?.forEach(s => { try { window.dashboard.chart.removeSeries(s); } catch(e) {} }); }); const candles = window.dashboard.allData.get(window.dashboard.currentInterval); if (!candles || candles.length === 0) return; const lineStyleMap = { 'solid': LightweightCharts.LineStyle.Solid, 'dotted': LightweightCharts.LineStyle.Dotted, 'dashed': LightweightCharts.LineStyle.Dashed }; indicatorPanes.clear(); nextPaneIndex = 1; const overlayIndicators = []; const paneIndicators = []; activeIndicators.forEach(ind => { const IndicatorClass = IR?.[ind.type]; if (!IndicatorClass) return; const instance = new IndicatorClass({ type: ind.type, params: ind.params, name: ind.name }); const meta = instance.getMetadata(); if (meta.displayMode === 'pane') { paneIndicators.push({ indicator: ind, meta, instance }); } else { overlayIndicators.push({ indicator: ind, meta, instance }); } }); const totalPanes = 1 + paneIndicators.length; const mainPaneHeight = paneIndicators.length > 0 ? 60 : 100; const paneHeight = paneIndicators.length > 0 ? Math.floor(40 / paneIndicators.length) : 0; window.dashboard.chart.panes()[0]?.setHeight(mainPaneHeight); overlayIndicators.forEach(({ indicator, meta, instance }) => { if (indicator.visible === false) { indicator.series = []; return; } renderIndicatorOnPane(indicator, meta, instance, candles, 0, lineStyleMap); }); paneIndicators.forEach(({ indicator, meta, instance }, idx) => { if (indicator.visible === false) { indicator.series = []; return; } const paneIndex = nextPaneIndex++; indicatorPanes.set(indicator.id, paneIndex); renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap); const pane = window.dashboard.chart.panes()[paneIndex]; if (pane) { pane.setHeight(paneHeight); } }); } // Export functions for module access export { addIndicator, removeIndicatorById, removeIndicatorByIndex }; // Legacy compatibility functions window.renderIndicatorList = renderIndicatorPanel; window.toggleIndicator = addIndicator; window.showIndicatorConfig = function(id) { const ind = activeIndicators.find(a => a.id === id); if (ind) configuringId = id; renderIndicatorPanel(); }; window.applyIndicatorConfig = function() { // No-op - config is applied immediately }; // Assign to window for backward compatibility window.addIndicator = addIndicator; window.toggleIndicator = addIndicator; window.removeIndicatorById = removeIndicatorById; window.removeIndicatorByIndex = removeIndicatorByIndexWindow; const removeIndicatorByIndexWindow = function(index) { if (index < 0 || index >= activeIndicators.length) return; removeIndicatorById(activeIndicators[index].id); }; window.removeIndicatorByIndex = removeIndicatorByIndexWindow; window.drawIndicatorsOnChart = drawIndicatorsOnChart;