import { getAvailableIndicators, IndicatorRegistry as IR } from '../indicators/index.js'; // State management let activeIndicators = []; let configuringId = null; let searchQuery = ''; let selectedCategory = 'all'; let nextInstanceId = 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']; // Initialize export function initIndicatorPanel() { renderIndicatorPanel(); setupEventListeners(); } 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 getActiveIndicators() { return activeIndicators; } export function setActiveIndicators(indicators) { activeIndicators = indicators; renderIndicatorPanel(); } // Render main panel export function renderIndicatorPanel() { const container = document.getElementById('indicatorPanel'); if (!container) return; 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; }); const favoriteIds = new Set(userPresets.favorites || []); container.innerHTML = `
${CATEGORIES.map(cat => ` `).join('')}
${favoriteIds.size > 0 ? `
★ Favorites
${favoriteIds.map(id => { const ind = available.find(a => { // Find matching indicator by type 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
`}
`; setupEventListeners(); } 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; return `
⋮⋮
${label}
${meta.name && renderPresetIndicatorIndicator(meta, indicator)}
${isExpanded ? `
${renderIndicatorConfig(indicator, meta)}
` : ''}
`; } function renderPresetIndicatorIndicator(meta, indicator) { const hasPresets = 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
${renderIndicatorPresets(indicator, meta)}
`; } function renderIndicatorPresets(indicator, meta) { const presets = getPresetsForIndicator(meta.name); const instance = new IR[indicator.type]({ type: indicator.type, params: indicator.params, name: indicator.name }); const metadata = instance.getMetadata(); return presets.length > 0 ? `
${presets.map(preset => { // Match values against current settings const isApplied = metadata.inputs.every(input => preset.values[input.name] === indicator.params[input.name] ); return `
${preset.name}
`; }).join('')}
` : '
No saved presets
'; } // Event listeners function setupEventListeners() { // Search const searchInput = document.getElementById('indicatorSearch'); if (searchInput) { searchInput.addEventListener('input', (e) => { searchQuery = e.target.value; renderIndicatorPanel(); }); } // Category tabs document.querySelectorAll('.category-tab').forEach(tab => { tab.addEventListener('click', (e) => { selectedCategory = tab.dataset.category; renderIndicatorPanel(); }); }); } // 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(); }; window.addIndicator = function(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) { 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; const valueSpan = document.querySelector(`#color_${id}_${index} + .color-preview`); if (valueSpan) { valueSpan.textContent = 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 metadata = instance.getMetadata(); metadata.inputs.forEach(input => { indicator.params[input.name] = input.default; }); renderIndicatorPanel(); drawIndicatorsOnChart(); }; 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(); }; // Presets function getPresetsForIndicator(indicatorName) { const allPresets = Object.values(userPresets).flat().filter(p => typeof p === 'object' && p.name); return allPresets.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 = Object.values(userPresets).flat().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)); } // 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); } }); } // 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 }; window.removeIndicator = function() { if (!configuringId) return; removeIndicatorById(configuringId); }; // Assign to window for backward compatibility window.toggleIndicator = window.addIndicator; window.removeIndicatorByIndex = function(index) { if (index < 0 || index >= activeIndicators.length) return; removeIndicatorById(activeIndicators[index].id); }; window.drawIndicatorsOnChart = drawIndicatorsOnChart; // Export functions for module imports export { window.addIndicator as addIndicator }; export { window.removeIndicatorById as removeIndicatorById }; export { window.removeIndicatorByIndex as removeIndicatorByIndex };