import { getAvailableIndicators, IndicatorRegistry as IR } from '../indicators/index.js'; // State management let activeIndicators = []; console.log('[Module] indicators-panel-new.js loaded - activeIndicators count:', activeIndicators?.length || 0); 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() { renderIndicatorPanel(); } export function getActiveIndicators() { return activeIndicators; } export function setActiveIndicators(indicators) { console.warn('setActiveIndicators() called with', indicators.length, 'indicators - this will replace activeIndicators array!'); console.trace('Call stack:'); activeIndicators = indicators; renderIndicatorPanel(); } window.getActiveIndicators = getActiveIndicators; // 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 || []); const allVisible = activeIndicators.length > 0 ? activeIndicators.every(ind => ind.visible !== false) : false; const visibilityBtnText = allVisible ? 'Hide All' : 'Show All'; 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.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) { 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 `
`.trim() + ''; }).join('')} ${indicator.type !== 'rsi' ? `
${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] === (p.values?.[input.name] ?? input.default)) ); return `
${p.name}
`; }).join('')}
` : '
No saved presets
'; } // Event listeners function setupEventListeners() { const container = document.getElementById('indicatorPanel'); if (!container) return; container.addEventListener('click', (e) => { e.stopPropagation(); // Add button const addBtn = e.target.closest('.indicator-btn.add'); if (addBtn) { e.stopPropagation(); const type = addBtn.dataset.type; if (type && window.addIndicator) { window.addIndicator(type); } 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; } // Clear all button const clearAllBtn = e.target.closest('.clear-all'); if (clearAllBtn) { if (window.clearAllIndicators) { window.clearAllIndicators(); } return; } // Visibility toggle (Hide All / Show All) button const visibilityToggleBtn = e.target.closest('.visibility-toggle'); if (visibilityToggleBtn) { if (window.toggleAllIndicatorsVisibility) { window.toggleAllIndicatorsVisibility(); } 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; } // Visibility button (eye) const visibleBtn = e.target.closest('.indicator-btn.visible'); if (visibleBtn) { e.stopPropagation(); const id = visibleBtn.dataset.id; if (id && window.toggleIndicatorVisibility) { window.toggleIndicatorVisibility(id); } 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(); }); }); } // Actions window.toggleIndicatorExpand = function(id) { configuringId = configuringId === id ? null : id; renderIndicatorPanel(); }; window.clearSearch = function() { searchQuery = ''; renderIndicatorPanel(); }; window.updateIndicatorColor = function(id, index, color) { const indicator = activeIndicators.find(a => a.id === id); if (!indicator) return; indicator.params[`_color_${index}`] = color; drawIndicatorsOnChart(); }; window.updateIndicatorSetting = function(id, key, value) { const indicator = activeIndicators.find(a => a.id === id); if (!indicator) return; indicator.params[key] = value; indicator.lastSignalTimestamp = null; indicator.lastSignalType = null; drawIndicatorsOnChart(); }; window.clearAllIndicators = function() { activeIndicators.forEach(ind => { ind.series?.forEach(s => { try { window.dashboard?.chart?.removeSeries(s); } catch(e) {} }); }); activeIndicators = []; configuringId = null; renderIndicatorPanel(); drawIndicatorsOnChart(); } window.toggleAllIndicatorsVisibility = function() { const allVisible = activeIndicators.every(ind => ind.visible !== false); activeIndicators.forEach(ind => { ind.visible = !allVisible; }); drawIndicatorsOnChart(); renderIndicatorPanel(); } function removeIndicatorById(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) { 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 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, paneHeight: 120 // default 120px }); // Don't set configuringId so indicators are NOT expanded by default renderIndicatorPanel(); drawIndicatorsOnChart(); }; function saveUserPresets() { localStorage.setItem('indicator_presets', JSON.stringify(userPresets)); } function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) { // Recalculate with current TF candles console.log(`[renderIndicatorOnPane] ${indicator.name}: START`); console.log(`[renderIndicatorOnPane] ${indicator.name}: Input candles = ${candles.length}`); console.log(`[renderIndicatorOnPane] ${indicator.name}: Calling instance.calculate()...`); const results = instance.calculate(candles); console.log(`[renderIndicatorOnPane] ${indicator.name}: calculate() returned ${results?.length || 0} results`); console.log(`[renderIndicatorOnPane] ${indicator.name}: Expected ${candles.length} results, got ${results?.length || 0}`); if (results.length !== candles.length) { console.error(`[renderIndicatorOnPane] ${indicator.name}: MISMATCH! Expected ${candles.length} results but got ${results.length}`); console.error(`[renderIndicatorOnPane] ${indicator.name}: This means instance.calculate() is not returning the correct number of results!`); } // Clear previous series for this indicator if (indicator.series && indicator.series.length > 0) { indicator.series.forEach(s => { try { window.dashboard.chart.removeSeries(s); } catch(e) {} }); } 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'; let plotsCreated = 0; let dataPointsAdded = 0; 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 = []; let firstDataIndex = -1; 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) { if (firstDataIndex === -1) { firstDataIndex = i; } data.push({ time: candles[i].time, value: value }); } } console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: ${data.length} data points created, first data at index ${firstDataIndex}/${candles.length}`); if (data.length === 0) { console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: No data to render`); return; } console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: Creating series with ${data.length} data points [${data[0].time} to ${data[data.length - 1].time}]`); 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 || indicator.params._lineWidth || 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 || indicator.params._lineWidth || lineWidth, lineStyle: plotLineStyle, title: plot.title || '', priceLineVisible: false, lastValueVisible: plot.lastValueVisible !== false }, paneIndex); } series.setData(data); indicator.series.push(series); plotsCreated++; console.log(`Created series for ${indicator.id}, plot=${plot.id}, total series now=${indicator.series.length}`); // Create horizontal band lines for RSI if (meta.name === 'RSI' && indicator.series.length > 0) { const mainSeries = indicator.series[0]; const overbought = indicator.params.overbought || 70; const oversold = indicator.params.oversold || 30; // Remove existing price lines first while (indicator.bands && indicator.bands.length > 0) { try { indicator.bands.pop(); } catch(e) {} } indicator.bands = indicator.bands || []; // Create overbought band line indicator.bands.push(mainSeries.createPriceLine({ price: overbought, color: '#787B86', lineWidth: 1, lineStyle: LightweightCharts.LineStyle.Dashed, axisLabelVisible: false, title: '' })); // Create oversold band line indicator.bands.push(mainSeries.createPriceLine({ price: oversold, color: '#787B86', lineWidth: 1, lineStyle: LightweightCharts.LineStyle.Dashed, axisLabelVisible: false, title: '' })); } }); } // Completely redraw indicators (works for both overlay and pane) export function updateIndicatorCandles() { console.log('[UpdateIndicators] Removing and recreating all indicator series'); // Remove all existing series const activeIndicators = getActiveIndicators(); activeIndicators.forEach(indicator => { indicator.series?.forEach(s => { try { window.dashboard.chart.removeSeries(s); } catch(e) { console.warn('[UpdateIndicators] Error removing series:', e); } }); indicator.series = []; }); // Clear pane mappings indicatorPanes.clear(); nextPaneIndex = 1; // Now call drawIndicatorsOnChart to recreate everything drawIndicatorsOnChart(); console.log(`[UpdateIndicators] Recreated ${activeIndicators.length} indicators`); } // Chart drawing export function drawIndicatorsOnChart() { try { if (!window.dashboard || !window.dashboard.chart) { return; } const currentInterval = window.dashboard.currentInterval; const candles = window.dashboard?.allData?.get(currentInterval); if (!candles || candles.length === 0) { console.log('[Indicators] No candles available'); return; } console.log(`[Indicators] ========== drawIndicatorsOnChart START ==========`); console.log(`[Indicators] Candles from allData: ${candles.length}`); console.log(`[Indicators] First candle time: ${candles[0]?.time} (${new Date(candles[0]?.time * 1000).toLocaleDateString()})`); console.log(`[Indicators] Last candle time: ${candles[candles.length - 1]?.time} (${new Date(candles[candles.length - 1]?.time * 1000).toLocaleDateString()})`); const oldestTime = candles[0]?.time; const newestTime = candles[candles.length - 1]?.time; const oldestDate = oldestTime ? new Date(oldestTime * 1000).toLocaleDateString() : 'N/A'; const newestDate = newestTime ? new Date(newestTime * 1000).toLocaleDateString() : 'N/A'; console.log(`[Indicators] ========== Redrawing ==========`); console.log(`[Indicators] Candles: ${candles.length} | Time range: ${oldestDate} (${oldestTime}) to ${newestDate} (${newestTime})`); const activeIndicators = getActiveIndicators(); // Remove all existing series activeIndicators.forEach(ind => { ind.series?.forEach(s => { try { window.dashboard.chart.removeSeries(s); } catch(e) {} }); ind.series = []; }); const lineStyleMap = { 'solid': LightweightCharts.LineStyle.Solid, 'dotted': LightweightCharts.LineStyle.Dotted, 'dashed': LightweightCharts.LineStyle.Dashed }; // Don't clear indicatorPanes - preserve pane assignments across redraws // Only reset nextPaneIndex to avoid creating duplicate panes const maxExistingPane = Math.max(...indicatorPanes.values(), 0); nextPaneIndex = maxExistingPane + 1; const overlayIndicators = []; const paneIndicators = []; // Process all indicators, filtering by visibility activeIndicators.forEach(ind => { if (ind.visible === false || ind.visible === 'false') { return; } const IndicatorClass = IR?.[ind.type]; if (!IndicatorClass) return; const instance = new IndicatorClass(ind); const meta = instance.getMetadata(); // Store calculated results and metadata for signal calculation const results = instance.calculate(candles); ind.cachedResults = results; ind.cachedMeta = meta; const validResults = results.filter(r => r !== null && r !== undefined); const warmupPeriod = ind.params?.period || 44; console.log(`[Indicators] ${ind.name}: ${validResults.length} valid results (need ${warmupPeriod} candles warmup)`); if (meta.displayMode === 'pane') { paneIndicators.push({ indicator: ind, meta, instance }); } else { overlayIndicators.push({ indicator: ind, meta, instance }); } }); // Set main pane height (60% if indicator panes exist, 100% otherwise) const totalPanes = 1 + paneIndicators.length; const mainPaneHeight = paneIndicators.length > 0 ? 60 : 100; window.dashboard.chart.panes()[0]?.setHeight(mainPaneHeight); console.log(`[Indicators] ========== Rendering Indicators ==========`); console.log(`[Indicators] Input candles: ${candles.length} | Panel count: ${totalPanes}`); overlayIndicators.forEach(({ indicator, meta, instance }) => { console.log(`[Indicators] Processing overlay: ${indicator.name}`); console.log(`[Indicators] Before renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`); renderIndicatorOnPane(indicator, meta, instance, candles, 0, lineStyleMap); console.log(`[Indicators] After renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`); }); paneIndicators.forEach(({ indicator, meta, instance }, idx) => { // Use existing pane index if already assigned, otherwise create new one let paneIndex = indicatorPanes.get(indicator.id); if (paneIndex === undefined) { paneIndex = nextPaneIndex++; indicatorPanes.set(indicator.id, paneIndex); } console.log(`[Indicators] Processing pane: ${indicator.name} (pane ${paneIndex})`); console.log(`[Indicators] Before renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`); renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap); console.log(`[Indicators] After renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`); const pane = window.dashboard.chart.panes()[paneIndex]; if (pane) { // Use stored height, localStorage, or default 120px const storedHeight = indicator.paneHeight || parseInt(localStorage.getItem(`pane_height_${indicator.type}`)) || 120; pane.setHeight(storedHeight); // Subscribe to pane height changes to save to localStorage const originalHeight = storedHeight; const paneElement = pane.getHTMLElement && pane.getHTMLElement(); if (paneElement) { const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const newHeight = Math.round(entry.contentRect.height); if (newHeight !== originalHeight && newHeight > 50) { indicator.paneHeight = newHeight; localStorage.setItem(`pane_height_${indicator.type}`, newHeight); console.log(`[Indicators] Saved pane height for ${indicator.type}: ${newHeight}px`); } } }); resizeObserver.observe(paneElement); } } }); console.log(`[Indicators] ========== drawIndicatorsOnChart END ==========`); } catch (error) { console.error('[Indicators] Error drawing indicators:', error); } } function resetIndicator(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: '' }); const meta = instance.getMetadata(); if (!meta || !meta.inputs) return; meta.inputs.forEach(input => { indicator.params[input.name] = input.default; }); renderIndicatorPanel(); drawIndicatorsOnChart(); } function removeIndicator(id) { removeIndicatorById(id); } function toggleIndicatorVisibility(id) { const indicator = activeIndicators.find(a => a.id === id); if (!indicator) { return; } indicator.visible = indicator.visible === false; // Full redraw to ensure all indicators render correctly if (typeof drawIndicatorsOnChart === 'function') { drawIndicatorsOnChart(); } renderIndicatorPanel(); } // Export functions for module access export { addIndicator, removeIndicatorById, toggleIndicatorVisibility }; // Legacy compatibility functions window.renderIndicatorList = renderIndicatorPanel; window.resetIndicator = resetIndicator; window.removeIndicator = removeIndicator; window.toggleIndicator = addIndicator; window.toggleIndicatorVisibility = toggleIndicatorVisibility; 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 };