import { getAvailableIndicators, IndicatorRegistry as IR } from '../indicators/index.js'; // State management let activeIndicators = []; // Persistence Logic function saveActiveIndicators() { try { const toSave = activeIndicators.map(ind => ({ id: ind.id, type: ind.type, name: ind.name, params: ind.params, visible: ind.visible, paneHeight: ind.paneHeight })); console.log('[Persistence] Saving indicators:', toSave.length); localStorage.setItem('winterfail_active_indicators', JSON.stringify(toSave)); } catch (e) { console.error('Failed to save active indicators:', e); } } function loadActiveIndicators() { try { const saved = localStorage.getItem('winterfail_active_indicators'); console.log('[Persistence] Loading from storage:', saved ? 'data found' : 'empty'); if (!saved) return; const parsed = JSON.parse(saved); if (!Array.isArray(parsed)) return; const restored = []; parsed.forEach(savedInd => { const IndicatorClass = IR?.[savedInd.type]; if (!IndicatorClass) { console.warn(`[Persistence] Unknown indicator type: ${savedInd.type}`); return; } const instance = new IndicatorClass({ type: savedInd.type, params: savedInd.params, name: savedInd.name }); const metadata = instance.getMetadata(); restored.push({ id: savedInd.id, type: savedInd.type, name: savedInd.name || metadata.name, params: savedInd.params, plots: metadata.plots, series: [], visible: savedInd.visible !== undefined ? savedInd.visible : true, paneHeight: savedInd.paneHeight || 120, cachedResults: null, cachedMeta: null }); const parts = savedInd.id.split('_'); const idNum = parseInt(parts[parts.length - 1]); if (!isNaN(idNum) && idNum >= nextInstanceId) { nextInstanceId = idNum + 1; } }); activeIndicators = restored; console.log(`[Persistence] Successfully restored ${activeIndicators.length} indicators`); } catch (e) { console.error('Failed to load active indicators:', e); } } 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; try { userPresets = JSON.parse(localStorage.getItem('indicator_presets') || '{}'); if (!userPresets || typeof userPresets !== 'object') { userPresets = { presets: [] }; } } catch (e) { console.warn('Failed to parse presets:', e); userPresets = { 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; // Always show params in parentheses (e.g., "MA(44)" or "MA(SMA,44)") const paramParts = meta.inputs.map(input => { const val = indicator.params[input.name]; return val !== undefined ? val : input.default; }); 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() { loadActiveIndicators(); // Load persisted indicators renderIndicatorPanel(); // Also trigger initial draw if dashboard exists (it might be too early, but safe to try) if (window.dashboard && window.dashboard.hasInitialLoad) { drawIndicatorsOnChart(); } } 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; saveActiveIndicators(); renderIndicatorPanel(); } window.getActiveIndicators = getActiveIndicators; async function onTimeframeChange(newInterval) { const indicators = getActiveIndicators(); for (const indicator of indicators) { if (indicator.params.timeframe === 'chart' && typeof indicator.shouldRecalculate === 'function') { if (indicator.shouldRecalculate()) { try { await window.renderIndicator(indicator.id); } catch (err) { console.error(`[onTimeframeChange] Failed to recalculate ${indicator.name}:`, err); } } } } } window.onTimeframeChange = onTimeframeChange; // 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('')}
` : ''}
Signals
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; saveActiveIndicators(); 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; indicator.cachedResults = null; // Clear cache when params change saveActiveIndicators(); drawIndicatorsOnChart(); }; window.clearAllIndicators = function() { activeIndicators.forEach(ind => { ind.series?.forEach(s => { try { window.dashboard?.chart?.removeSeries(s); } catch(e) {} }); }); activeIndicators = []; configuringId = null; saveActiveIndicators(); renderIndicatorPanel(); drawIndicatorsOnChart(); } window.toggleAllIndicatorsVisibility = function() { const allVisible = activeIndicators.every(ind => ind.visible !== false); activeIndicators.forEach(ind => { ind.visible = !allVisible; }); saveActiveIndicators(); 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; } saveActiveIndicators(); renderIndicatorPanel(); drawIndicatorsOnChart(); } // Presets function getPresetsForIndicator(indicatorName) { if (!userPresets || !userPresets.presets) return []; return userPresets.presets.filter(p => p.indicatorName === indicatorName); } window.savePreset = function(id) { console.log('[savePreset] Attempting to save preset for id:', id); const indicator = activeIndicators.find(a => a.id === id); if (!indicator) { console.error('[savePreset] Indicator not found for id:', id); return; } const presetName = prompt('Enter preset name:'); if (!presetName) return; const IndicatorClass = IR?.[indicator.type]; if (!IndicatorClass) { console.error('[savePreset] Indicator class not found for type:', indicator.type); return; } try { 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: {} }; // Save standard inputs if (meta.inputs && Array.isArray(meta.inputs)) { meta.inputs.forEach(input => { preset.values[input.name] = indicator.params[input.name]; }); } // Save visual settings (line width, type, colors) preset.values._lineWidth = indicator.params._lineWidth; preset.values._lineType = indicator.params._lineType; // Save colors for each plot if (meta.plots && Array.isArray(meta.plots)) { meta.plots.forEach((plot, idx) => { const colorKey = `_color_${idx}`; if (indicator.params[colorKey]) { preset.values[colorKey] = indicator.params[colorKey]; } }); } // Save marker settings const markerKeys = [ 'showMarkers', 'markerBuyShape', 'markerBuyColor', 'markerBuyCustom', 'markerSellShape', 'markerSellColor', 'markerSellCustom' ]; markerKeys.forEach(key => { if (indicator.params[key] !== undefined) { preset.values[key] = indicator.params[key]; } }); if (!userPresets) userPresets = { presets: [] }; if (!userPresets.presets) userPresets.presets = []; userPresets.presets.push(preset); saveUserPresets(); renderIndicatorPanel(); alert(`Preset "${presetName}" saved!`); } catch (error) { console.error('[savePreset] Error saving preset:', error); alert('Error saving preset. See console for details.'); } }; 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: 1, showMarkers: true, markerBuyShape: 'custom', markerBuyColor: '#9e9e9e', markerBuyCustom: '▲', markerSellShape: 'custom', markerSellColor: '#9e9e9e', markerSellCustom: '▼' }; // Override with Hurst-specific defaults if (type === 'hurst') { const hurstCount = activeIndicators.filter(ind => ind.type === 'hurst').length; const color = hurstCount > 0 ? '#ff9800' : '#9e9e9e'; params._lineWidth = 1; params.timeframe = 'chart'; params.markerBuyShape = 'custom'; params.markerSellShape = 'custom'; params.markerBuyColor = color; params.markerSellColor = color; params.markerBuyCustom = '▲'; params.markerSellCustom = '▼'; } metadata.plots.forEach((plot, idx) => { if (type === 'hurst' && activeIndicators.filter(ind => ind.type === 'hurst').length > 0) { params[`_color_${idx}`] = '#ff9800'; } else { 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 }); saveActiveIndicators(); renderIndicatorPanel(); drawIndicatorsOnChart(); }; function saveUserPresets() { localStorage.setItem('indicator_presets', JSON.stringify(userPresets)); } // Custom Primitive for filling area between two lines class SeriesAreaFillPrimitive { constructor(data, color) { this._data = data || []; this._color = color || 'rgba(128, 128, 128, 0.05)'; this._paneViews = [new SeriesAreaFillPaneView(this)]; } setData(data) { this._data = data; this._requestUpdate?.(); } setColor(color) { this._color = color; this._requestUpdate?.(); } attached(param) { this._chart = param.chart; this._series = param.series; this._requestUpdate = param.requestUpdate; this._requestUpdate(); } detached() { this._chart = undefined; this._series = undefined; this._requestUpdate = undefined; } updateAllViews() { this._requestUpdate?.(); } paneViews() { return this._paneViews; } } class SeriesAreaFillPaneView { constructor(source) { this._source = source; } renderer() { return new SeriesAreaFillRenderer(this._source); } } class SeriesAreaFillRenderer { constructor(source) { this._source = source; } draw(target) { if (!this._source._chart || !this._source._series || this._source._data.length === 0) return; target.useBitmapCoordinateSpace((scope) => { const ctx = scope.context; const series = this._source._series; const chart = this._source._chart; const data = this._source._data; const color = this._source._color; const ratio = scope.horizontalPixelRatio; ctx.save(); ctx.beginPath(); let started = false; // Draw top line (upper) forward for (let i = 0; i < data.length; i++) { const point = data[i]; const timeCoordinate = chart.timeScale().timeToCoordinate(point.time); if (timeCoordinate === null) continue; const upperY = series.priceToCoordinate(point.upper); if (upperY === null) continue; const x = timeCoordinate * ratio; const y = upperY * ratio; if (!started) { ctx.moveTo(x, y); started = true; } else { ctx.lineTo(x, y); } } // Draw bottom line (lower) backward for (let i = data.length - 1; i >= 0; i--) { const point = data[i]; const timeCoordinate = chart.timeScale().timeToCoordinate(point.time); if (timeCoordinate === null) continue; const lowerY = series.priceToCoordinate(point.lower); if (lowerY === null) continue; const x = timeCoordinate * ratio; const y = lowerY * ratio; ctx.lineTo(x, y); } if (started) { ctx.closePath(); ctx.fillStyle = color; ctx.fill(); } ctx.restore(); }); } } function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) { // Recalculate with current TF candles (or use cached if they exist and are the correct length) let results = indicator.cachedResults; if (!results || !Array.isArray(results) || results.length !== candles.length) { console.log(`[renderIndicatorOnPane] ${indicator.name}: Calling instance.calculate()...`); results = instance.calculate(candles); indicator.cachedResults = results; } if (!results || !Array.isArray(results)) { console.error(`[renderIndicatorOnPane] ${indicator.name}: Failed to get valid results (got ${typeof results})`); return; } if (results.length !== candles.length) { console.error(`[renderIndicatorOnPane] ${indicator.name}: MISMATCH! Expected ${candles.length} results but got ${results.length}`); } // 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 || 1; // Improved detection of object-based results (multiple plots) const firstNonNull = Array.isArray(results) ? results.find(r => r !== null && r !== undefined) : null; let isObjectResult = firstNonNull && typeof firstNonNull === 'object' && !Array.isArray(firstNonNull); // Fallback: If results are all null (e.g. during warmup or MTF fetch), // use metadata to determine if it SHOULD be an object result if (!firstNonNull && meta.plots && meta.plots.length > 1) { isObjectResult = true; } // Also check if the only plot has a specific ID that isn't just a number if (!firstNonNull && meta.plots && meta.plots.length === 1 && meta.plots[0].id !== 'value') { isObjectResult = true; } let plotsCreated = 0; // Special logic for Hurst fill let hurstFillData = []; const isFirstHurst = indicator.type === 'hurst' && activeIndicators.filter(ind => ind.type === 'hurst')[0].id === indicator.id; 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]; // Collect fill data if this is Hurst if (isFirstHurst && results[i]) { // Ensure we only add once per index if (!hurstFillData[i]) hurstFillData[i] = { time: candles[i].time }; if (plot.id === 'upper') hurstFillData[i].upper = value; if (plot.id === 'lower') hurstFillData[i].lower = value; } } else { value = results[i]; } if (value !== null && value !== undefined && typeof value === 'number' && Number.isFinite(value)) { 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: 0, minMove: 1 }, 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, priceFormat: { type: 'price', precision: 0, minMove: 1 } }, paneIndex); } else { series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, { color: plotColor, lineWidth: plot.width || indicator.params._lineWidth || lineWidth, lineStyle: plotLineStyle, title: '', priceLineVisible: false, lastValueVisible: plot.lastValueVisible !== false, priceFormat: { type: 'price', precision: 0, minMove: 1 } }, paneIndex); } series.setData(data); indicator.series.push(series); plotsCreated++; // Attach RSI bands 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; while (indicator.bands && indicator.bands.length > 0) { try { indicator.bands.pop(); } catch(e) {} } indicator.bands = indicator.bands || []; indicator.bands.push(mainSeries.createPriceLine({ price: overbought, color: '#787B86', lineWidth: 1, lineStyle: LightweightCharts.LineStyle.Dashed, axisLabelVisible: false, title: '' })); indicator.bands.push(mainSeries.createPriceLine({ price: oversold, color: '#787B86', lineWidth: 1, lineStyle: LightweightCharts.LineStyle.Dashed, axisLabelVisible: false, title: '' })); } }); // Attach Hurst Fill Primitive if (isFirstHurst && hurstFillData.length > 0 && indicator.series.length > 0) { // Filter out incomplete data points const validFillData = hurstFillData.filter(d => d && d.time && d.upper !== undefined && d.lower !== undefined); // Attach to the first series (usually upper or lower band) const fillPrimitive = new SeriesAreaFillPrimitive(validFillData, 'rgba(128, 128, 128, 0.05)'); indicator.series[0].attachPrimitive(fillPrimitive); } } // 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 let results = ind.cachedResults; if (!results || !Array.isArray(results) || results.length !== candles.length) { try { results = instance.calculate(candles); ind.cachedResults = results; } catch (err) { console.error(`[Indicators] Failed to calculate ${ind.name}:`, err); results = []; } } ind.cachedMeta = meta; const validResults = Array.isArray(results) ? 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); } }); //console.log(`[Indicators] ========== drawIndicatorsOnChart END ==========`); } catch (error) { console.error('[Indicators] Error drawing indicators:', error); } // Update signal markers after indicators are drawn if (window.dashboard && typeof window.dashboard.updateSignalMarkers === 'function') { window.dashboard.updateSignalMarkers(); } } 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; }); saveActiveIndicators(); 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; saveActiveIndicators(); // 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 };