diff --git a/src/api/dashboard/static/index.html b/src/api/dashboard/static/index.html index 7f65fe3..4936f9b 100644 --- a/src/api/dashboard/static/index.html +++ b/src/api/dashboard/static/index.html @@ -770,57 +770,152 @@ .indicator-list { display: flex; flex-direction: column; - gap: 4px; + gap: 2px; margin-top: 8px; + max-height: 300px; + overflow-y: auto; + scrollbar-width: none; + } + .indicator-list::-webkit-scrollbar { display: none; } + + /* Indicator Catalog (available indicators) */ + .indicator-catalog { + display: flex; + flex-direction: column; + gap: 2px; } - .indicator-checkbox-item { + .indicator-catalog-item { display: flex; align-items: center; - gap: 8px; - padding: 6px 8px; + justify-content: space-between; + padding: 5px 8px; border-radius: 4px; cursor: pointer; - transition: background 0.2s; + transition: background 0.15s; + user-select: none; border: 1px solid transparent; } - .indicator-checkbox-item:hover { + .indicator-catalog-item:hover { background: var(--tv-hover); } - .indicator-checkbox-item.configuring { + .indicator-catalog-item.previewing { + background: rgba(41, 98, 255, 0.1); + border: 1px solid var(--tv-blue); + } + + .indicator-catalog-name { + font-size: 12px; + color: var(--tv-text-secondary); + } + + .indicator-catalog-item:hover .indicator-catalog-name { + color: var(--tv-text); + } + + .indicator-catalog-add { + font-size: 16px; + color: var(--tv-text-secondary); + cursor: pointer; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + opacity: 0; + transition: opacity 0.15s; + } + + .indicator-catalog-item:hover .indicator-catalog-add { + opacity: 1; + } + + .indicator-catalog-add:hover { + background: var(--tv-blue); + color: white; + } + + /* Active indicators divider */ + .indicator-active-divider { + font-size: 10px; + color: var(--tv-text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 6px 8px 2px; + border-top: 1px solid var(--tv-border); + margin-top: 4px; + } + + /* Active indicator items */ + .indicator-active-list { + display: flex; + flex-direction: column; + gap: 2px; + } + + .indicator-active-item { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 8px; + border-radius: 4px; + border: 1px solid transparent; + transition: background 0.15s; + } + + .indicator-active-item:hover { + background: var(--tv-hover); + } + + .indicator-active-item.configuring { background: rgba(41, 98, 255, 0.1); border-color: var(--tv-blue); } - .indicator-checkbox { - width: 14px; - height: 14px; - accent-color: var(--tv-blue); + .indicator-active-eye { + font-size: 11px; cursor: pointer; + opacity: 0.6; + transition: opacity 0.15s; + flex-shrink: 0; } - .indicator-checkbox-item label { + .indicator-active-eye:hover { + opacity: 1; + } + + .indicator-active-name { flex: 1; font-size: 12px; - cursor: pointer; color: var(--tv-text); + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .indicator-config-btn { background: transparent; border: 1px solid var(--tv-border); color: var(--tv-text-secondary); - width: 22px; - height: 22px; + width: 20px; + height: 20px; border-radius: 4px; cursor: pointer; - font-size: 11px; + font-size: 10px; display: flex; align-items: center; justify-content: center; - transition: all 0.2s; + transition: all 0.15s; + flex-shrink: 0; + opacity: 0; + } + + .indicator-active-item:hover .indicator-config-btn { + opacity: 1; } .indicator-config-btn:hover { @@ -833,18 +928,128 @@ background: var(--tv-blue); border-color: var(--tv-blue); color: white; + opacity: 1; + } + + .indicator-remove-btn { + background: transparent; + border: none; + color: var(--tv-text-secondary); + width: 20px; + height: 20px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s; + flex-shrink: 0; + opacity: 0; + } + + .indicator-active-item:hover .indicator-remove-btn { + opacity: 1; + } + + .indicator-remove-btn:hover { + background: rgba(239, 83, 80, 0.2); + color: var(--tv-red); } .indicator-color-dot { display: inline-block; - width: 10px; - height: 10px; + width: 8px; + height: 8px; border-radius: 50%; - margin-left: 8px; + margin-left: 2px; vertical-align: middle; border: 1px solid rgba(255,255,255,0.2); + flex-shrink: 0; } + /* Chart Legend Overlay */ + .chart-indicator-legend { + position: absolute; + top: 8px; + left: 8px; + z-index: 10; + display: flex; + flex-direction: column; + gap: 3px; + pointer-events: auto; + max-height: calc(100% - 40px); + overflow-y: auto; + scrollbar-width: none; + } + .chart-indicator-legend::-webkit-scrollbar { display: none; } + + .legend-item { + display: flex; + align-items: center; + gap: 4px; + padding: 2px 6px; + background: rgba(30, 33, 40, 0.85); + border: 1px solid var(--tv-border); + border-radius: 3px; + cursor: pointer; + transition: all 0.15s; + font-size: 11px; + white-space: nowrap; + width: fit-content; + } + + .legend-item:hover { + border-color: var(--tv-blue); + background: rgba(30, 33, 40, 0.95); + } + + .legend-item.legend-selected { + border-color: var(--tv-blue); + background: rgba(41, 98, 255, 0.15); + } + + .legend-item.legend-dimmed { + opacity: 0.4; + } + + .legend-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + } + + .legend-label { + color: var(--tv-text); + font-size: 10px; + } + + .legend-close { + color: var(--tv-text-secondary); + font-size: 12px; + line-height: 1; + opacity: 0; + transition: opacity 0.15s; + margin-left: 2px; + } + + .legend-item:hover .legend-close { + opacity: 1; + } + + .legend-close:hover { + color: var(--tv-red); + } + + /* Scrollable config form */ + #configForm { + max-height: 280px; + overflow-y: auto; + scrollbar-width: none; + } + #configForm::-webkit-scrollbar { display: none; } + .ta-level { display: flex; justify-content: space-between; diff --git a/src/api/dashboard/static/js/app.js b/src/api/dashboard/static/js/app.js index 492ab49..ccf5573 100644 --- a/src/api/dashboard/static/js/app.js +++ b/src/api/dashboard/static/js/app.js @@ -22,10 +22,12 @@ import { } from './ui/strategies-panel.js'; import { renderIndicatorList, + addIndicator, toggleIndicator, showIndicatorConfig, applyIndicatorConfig, removeIndicator, + removeIndicatorById, removeIndicatorByIndex, drawIndicatorsOnChart } from './ui/indicators-panel.js'; @@ -65,6 +67,7 @@ window.deleteSavedSimulation = deleteSavedSimulation; window.clearSimulationResults = clearSimulationResults; window.updateTimeframeDisplay = updateTimeframeDisplay; window.renderIndicatorList = renderIndicatorList; +window.addIndicator = addIndicator; window.toggleIndicator = toggleIndicator; window.showIndicatorConfig = showIndicatorConfig; diff --git a/src/api/dashboard/static/js/indicators/atr.js b/src/api/dashboard/static/js/indicators/atr.js index 6cc488f..bba60bb 100644 --- a/src/api/dashboard/static/js/indicators/atr.js +++ b/src/api/dashboard/static/js/indicators/atr.js @@ -29,6 +29,7 @@ export class ATRIndicator extends BaseIndicator { getMetadata() { return { name: 'ATR', + description: 'Average True Range - measures market volatility', inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }], plots: [{ id: 'value', color: '#795548', title: 'ATR' }] }; diff --git a/src/api/dashboard/static/js/indicators/bb.js b/src/api/dashboard/static/js/indicators/bb.js index 698a740..8629562 100644 --- a/src/api/dashboard/static/js/indicators/bb.js +++ b/src/api/dashboard/static/js/indicators/bb.js @@ -27,6 +27,7 @@ export class BollingerBandsIndicator extends BaseIndicator { getMetadata() { return { name: 'Bollinger Bands', + description: 'Volatility bands around a moving average', inputs: [ { name: 'period', label: 'Period', type: 'number', default: 20, min: 1, max: 100 }, { name: 'stdDev', label: 'Std Dev', type: 'number', default: 2, min: 0.5, max: 5, step: 0.5 } diff --git a/src/api/dashboard/static/js/indicators/index.js b/src/api/dashboard/static/js/indicators/index.js index 13b1fe6..da6fb20 100644 --- a/src/api/dashboard/static/js/indicators/index.js +++ b/src/api/dashboard/static/js/indicators/index.js @@ -25,3 +25,19 @@ export const IndicatorRegistry = { stoch: StochasticIndicator, atr: ATRIndicator }; + +/** + * Dynamically build the available indicators list from the registry. + * Each indicator class provides its own name and description via getMetadata(). + */ +export function getAvailableIndicators() { + return Object.entries(IndicatorRegistry).map(([type, IndicatorClass]) => { + const instance = new IndicatorClass({ type, params: {}, name: '' }); + const meta = instance.getMetadata(); + return { + type, + name: meta.name || type.toUpperCase(), + description: meta.description || '' + }; + }); +} diff --git a/src/api/dashboard/static/js/indicators/ma_indicator.js b/src/api/dashboard/static/js/indicators/ma_indicator.js index 6d94b8b..b0c796b 100644 --- a/src/api/dashboard/static/js/indicators/ma_indicator.js +++ b/src/api/dashboard/static/js/indicators/ma_indicator.js @@ -11,6 +11,7 @@ export class MAIndicator extends BaseIndicator { getMetadata() { return { name: 'MA', + description: 'Moving Average (SMA/EMA/RMA/WMA/VWMA)', inputs: [ { name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 }, { name: 'maType', label: 'MA Type', type: 'select', options: ['SMA', 'EMA', 'RMA', 'WMA', 'VWMA'], default: 'SMA' } diff --git a/src/api/dashboard/static/js/indicators/macd.js b/src/api/dashboard/static/js/indicators/macd.js index aa2f9c1..a9ff59d 100644 --- a/src/api/dashboard/static/js/indicators/macd.js +++ b/src/api/dashboard/static/js/indicators/macd.js @@ -43,6 +43,7 @@ export class MACDIndicator extends BaseIndicator { getMetadata() { return { name: 'MACD', + description: 'Moving Average Convergence Divergence - trend & momentum', inputs: [ { name: 'fast', label: 'Fast Period', type: 'number', default: 12 }, { name: 'slow', label: 'Slow Period', type: 'number', default: 26 }, diff --git a/src/api/dashboard/static/js/indicators/rsi.js b/src/api/dashboard/static/js/indicators/rsi.js index 221be5c..e5f2888 100644 --- a/src/api/dashboard/static/js/indicators/rsi.js +++ b/src/api/dashboard/static/js/indicators/rsi.js @@ -30,6 +30,7 @@ export class RSIIndicator extends BaseIndicator { getMetadata() { return { name: 'RSI', + description: 'Relative Strength Index - momentum oscillator (0-100)', inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }], plots: [{ id: 'value', color: '#9c27b0', title: 'RSI' }] }; diff --git a/src/api/dashboard/static/js/indicators/stoch.js b/src/api/dashboard/static/js/indicators/stoch.js index 4702ec4..ed98995 100644 --- a/src/api/dashboard/static/js/indicators/stoch.js +++ b/src/api/dashboard/static/js/indicators/stoch.js @@ -31,6 +31,7 @@ export class StochasticIndicator extends BaseIndicator { getMetadata() { return { name: 'Stochastic', + description: 'Stochastic Oscillator - compares close to high-low range', inputs: [ { name: 'kPeriod', label: 'K Period', type: 'number', default: 14 }, { name: 'dPeriod', label: 'D Period', type: 'number', default: 3 } diff --git a/src/api/dashboard/static/js/strategies/config.js b/src/api/dashboard/static/js/strategies/config.js index 432bb5c..95eb001 100644 --- a/src/api/dashboard/static/js/strategies/config.js +++ b/src/api/dashboard/static/js/strategies/config.js @@ -3,13 +3,3 @@ export const StrategyParams = { { name: 'period', label: 'MA Period', type: 'number', default: 44, min: 5, max: 500 } ] }; - -export const AVAILABLE_INDICATORS = [ - { type: 'hts', name: 'HTS Trend System', description: 'Fast/Slow MAs of High/Low prices' }, - { type: 'ma', name: 'MA', description: 'Moving Average (SMA/EMA/RMA/WMA/VWMA)' }, - { type: 'rsi', name: 'RSI', description: 'Relative Strength Index' }, - { type: 'bb', name: 'Bollinger Bands', description: 'Volatility bands' }, - { type: 'macd', name: 'MACD', description: 'Moving Average Convergence Divergence' }, - { type: 'stoch', name: 'Stochastic', description: 'Stochastic Oscillator' }, - { type: 'atr', name: 'ATR', description: 'Average True Range' } -]; diff --git a/src/api/dashboard/static/js/strategies/index.js b/src/api/dashboard/static/js/strategies/index.js index c7ca41d..a14f5b4 100644 --- a/src/api/dashboard/static/js/strategies/index.js +++ b/src/api/dashboard/static/js/strategies/index.js @@ -1,3 +1,3 @@ -export { StrategyParams, AVAILABLE_INDICATORS } from './config.js'; +export { StrategyParams } from './config.js'; export { RiskManager } from './risk-manager.js'; export { ClientStrategyEngine } from './engine.js'; diff --git a/src/api/dashboard/static/js/ui/chart.js b/src/api/dashboard/static/js/ui/chart.js index 0f32866..33c2c2c 100644 --- a/src/api/dashboard/static/js/ui/chart.js +++ b/src/api/dashboard/static/js/ui/chart.js @@ -444,6 +444,9 @@ export class TradingDashboard { this.candleSeries.setData(mergedData); + // Recalculate indicators with the expanded dataset + window.drawIndicatorsOnChart?.(); + console.log(`Prefetched ${chartData.length} candles, total: ${mergedData.length}`); } else { console.log('No more historical data available'); diff --git a/src/api/dashboard/static/js/ui/indicators-panel.js b/src/api/dashboard/static/js/ui/indicators-panel.js index b246745..1418b39 100644 --- a/src/api/dashboard/static/js/ui/indicators-panel.js +++ b/src/api/dashboard/static/js/ui/indicators-panel.js @@ -1,8 +1,9 @@ -import { AVAILABLE_INDICATORS } from '../strategies/config.js'; -import { IndicatorRegistry as IR } from '../indicators/index.js'; +import { getAvailableIndicators, IndicatorRegistry as IR } from '../indicators/index.js'; let activeIndicators = []; -let configuringIndex = -1; +let configuringId = null; +let previewingType = null; // type being previewed (not yet added) +let nextInstanceId = 1; const DEFAULT_COLORS = ['#2962ff', '#26a69a', '#ef5350', '#ff9800', '#9c27b0', '#00bcd4', '#ffeb3b', '#e91e63']; const LINE_TYPES = ['solid', 'dotted', 'dashed']; @@ -37,6 +38,31 @@ function groupPlotsByColor(plots) { return Object.values(groups); } +/** Generate a short label for an active indicator showing its key params */ +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; + if (val !== undefined) 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(); +} + export function getActiveIndicators() { return activeIndicators; } @@ -45,47 +71,95 @@ export function setActiveIndicators(indicators) { activeIndicators = indicators; } +/** + * Render the indicator catalog (available indicators) and active list. + * Catalog items are added via double-click (multiple instances allowed). + */ export function renderIndicatorList() { const container = document.getElementById('indicatorList'); if (!container) return; - container.innerHTML = AVAILABLE_INDICATORS.map((ind, idx) => { - const activeIdx = activeIndicators.findIndex(a => a.type === ind.type); - const isActive = activeIdx >= 0; - const isConfiguring = activeIdx === configuringIndex; - - let colorDots = ''; - if (isActive) { - const indicator = activeIndicators[activeIdx]; - const plotGroups = groupPlotsByColor(indicator.plots || []); - colorDots = plotGroups.map(group => { - const firstIdx = group.indices[0]; - const color = indicator.params[`_color_${firstIdx}`] || '#2962ff'; - return ``; - }).join(''); - } - - return ` -
- - - ${isActive ? `` : ''} + const available = getAvailableIndicators(); + + container.innerHTML = ` +
+ ${available.map(ind => ` +
+ ${ind.name} + + +
+ `).join('')} +
+ ${activeIndicators.length > 0 ? ` +
Active
+
+ ${activeIndicators.map(ind => { + const isConfiguring = ind.id === configuringId; + const plotGroups = groupPlotsByColor(ind.plots || []); + const colorDots = plotGroups.map(group => { + const firstIdx = group.indices[0]; + const color = ind.params[`_color_${firstIdx}`] || '#2962ff'; + return ``; + }).join(''); + const label = getIndicatorLabel(ind); + + return ` +
+ + ${ind.visible !== false ? '👁' : '👁‍🗨'} + + ${label} + ${colorDots} + + +
+ `; + }).join('')}
- `; - }).join(''); + ` : ''} + `; + + // Bind events via delegation + container.querySelectorAll('.indicator-catalog-item').forEach(el => { + el.addEventListener('click', () => previewIndicator(el.dataset.type)); + el.addEventListener('dblclick', () => addIndicator(el.dataset.type)); + }); + container.querySelectorAll('.indicator-catalog-add').forEach(el => { + el.addEventListener('click', (e) => { + e.stopPropagation(); + addIndicator(el.dataset.type); + }); + }); + container.querySelectorAll('.indicator-active-name').forEach(el => { + el.addEventListener('click', () => selectIndicatorConfig(el.dataset.id)); + }); + container.querySelectorAll('.indicator-config-btn').forEach(el => { + el.addEventListener('click', (e) => { + e.stopPropagation(); + selectIndicatorConfig(el.dataset.id); + }); + }); + container.querySelectorAll('.indicator-remove-btn').forEach(el => { + el.addEventListener('click', (e) => { + e.stopPropagation(); + removeIndicatorById(el.dataset.id); + }); + }); + container.querySelectorAll('.indicator-active-eye').forEach(el => { + el.addEventListener('click', (e) => { + e.stopPropagation(); + toggleVisibility(el.dataset.id); + }); + }); updateConfigPanel(); + updateChartLegend(); } function updateConfigPanel() { @@ -95,95 +169,126 @@ function updateConfigPanel() { configPanel.style.display = 'block'; - if (configuringIndex >= 0 && configuringIndex < activeIndicators.length) { - renderIndicatorConfig(configuringIndex); + // Active indicator config takes priority over preview + const indicator = configuringId ? activeIndicators.find(a => a.id === configuringId) : null; + + if (indicator) { + renderIndicatorConfig(indicator); if (configButtons) configButtons.style.display = 'flex'; + } else if (previewingType) { + renderPreviewConfig(previewingType); + if (configButtons) configButtons.style.display = 'none'; } else { const container = document.getElementById('configForm'); if (container) { - container.innerHTML = '
Select an active indicator to configure its settings
'; + container.innerHTML = '
Click an indicator to preview its settings
'; } if (configButtons) configButtons.style.display = 'none'; } } -export function toggleIndicator(type) { - const existingIdx = activeIndicators.findIndex(a => a.type === type); +/** Single-click: preview config for a catalog indicator type (read-only) */ +function previewIndicator(type) { + configuringId = null; + previewingType = previewingType === type ? null : type; + renderIndicatorList(); +} + +/** Render a read-only preview of an indicator's default config */ +function renderPreviewConfig(type) { + const container = document.getElementById('configForm'); + if (!container) return; - if (existingIdx >= 0) { - activeIndicators[existingIdx].series?.forEach(s => { - try { window.dashboard?.chart?.removeSeries(s); } catch(e) {} - }); - activeIndicators.splice(existingIdx, 1); - if (configuringIndex >= activeIndicators.length) { - configuringIndex = -1; - } else if (configuringIndex === existingIdx) { - configuringIndex = -1; - } else if (configuringIndex > existingIdx) { - configuringIndex--; - } - } else { - const indicatorDef = AVAILABLE_INDICATORS.find(i => i.type === type); - if (!indicatorDef) return; + const IndicatorClass = IR?.[type]; + if (!IndicatorClass) return; + + const instance = new IndicatorClass({ type, params: {}, name: '' }); + const meta = instance.getMetadata(); + + container.innerHTML = ` +
${meta.name}
+
${meta.description || ''}
- const IndicatorClass = IR?.[type]; - if (!IndicatorClass) return; + ${meta.inputs.map(input => ` +
+ + ${input.type === 'select' ? + `` : + `` + } +
+ `).join('')} - const instance = new IndicatorClass({ type, params: {}, name: indicatorDef.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({ - type: type, - name: metadata.name, - params: params, - plots: metadata.plots, - series: [] - }); - - configuringIndex = activeIndicators.length - 1; - } +
Double-click to add to chart
+ `; +} + +/** Add a new instance of an indicator type */ +export function addIndicator(type) { + const IndicatorClass = IR?.[type]; + if (!IndicatorClass) return; + + previewingType = null; + 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; renderIndicatorList(); drawIndicatorsOnChart(); } -export function showIndicatorConfig(index) { - if (configuringIndex === index) { - configuringIndex = -1; +function selectIndicatorConfig(id) { + previewingType = null; + if (configuringId === id) { + configuringId = null; } else { - configuringIndex = index; + configuringId = id; } renderIndicatorList(); } -export function showIndicatorConfigByType(type) { - const idx = activeIndicators.findIndex(a => a.type === type); - if (idx >= 0) { - configuringIndex = idx; - renderIndicatorList(); - } +function toggleVisibility(id) { + const indicator = activeIndicators.find(a => a.id === id); + if (!indicator) return; + + indicator.visible = indicator.visible === false ? true : false; + + // Show/hide all series for this indicator + indicator.series?.forEach(s => { + try { + s.applyOptions({ visible: indicator.visible }); + } catch(e) {} + }); + + renderIndicatorList(); } -export function renderIndicatorConfig(index) { +export function renderIndicatorConfig(indicator) { const container = document.getElementById('configForm'); - if (!container) return; - - const indicator = activeIndicators[index]; - if (!indicator) { - container.innerHTML = ''; - return; - } + if (!container || !indicator) return; const IndicatorClass = IR?.[indicator.type]; if (!IndicatorClass) { @@ -208,7 +313,7 @@ export function renderIndicatorConfig(index) { }).join(''); container.innerHTML = ` -
${indicator.name}
+
${getIndicatorLabel(indicator)}
${colorInputs} @@ -237,9 +342,9 @@ export function renderIndicatorConfig(index) { } export function applyIndicatorConfig() { - if (configuringIndex < 0 || configuringIndex >= activeIndicators.length) return; + const indicator = configuringId ? activeIndicators.find(a => a.id === configuringId) : null; + if (!indicator) return; - const indicator = activeIndicators[configuringIndex]; const IndicatorClass = IR?.[indicator.type]; if (!IndicatorClass) return; @@ -276,14 +381,23 @@ export function applyIndicatorConfig() { } export function removeIndicator() { - if (configuringIndex < 0 || configuringIndex >= activeIndicators.length) return; + if (!configuringId) return; + removeIndicatorById(configuringId); +} + +export function removeIndicatorById(id) { + const idx = activeIndicators.findIndex(a => a.id === id); + if (idx < 0) return; - activeIndicators[configuringIndex].series?.forEach(s => { + activeIndicators[idx].series?.forEach(s => { try { window.dashboard?.chart?.removeSeries(s); } catch(e) {} }); - activeIndicators.splice(configuringIndex, 1); - configuringIndex = -1; + activeIndicators.splice(idx, 1); + + if (configuringId === id) { + configuringId = null; + } renderIndicatorList(); drawIndicatorsOnChart(); @@ -291,20 +405,7 @@ export function removeIndicator() { export function removeIndicatorByIndex(index) { if (index < 0 || index >= activeIndicators.length) return; - - activeIndicators[index].series?.forEach(s => { - try { window.dashboard?.chart?.removeSeries(s); } catch(e) {} - }); - - activeIndicators.splice(index, 1); - if (configuringIndex === index) { - configuringIndex = -1; - } else if (configuringIndex > index) { - configuringIndex--; - } - - renderIndicatorList(); - drawIndicatorsOnChart(); + removeIndicatorById(activeIndicators[index].id); } export function drawIndicatorsOnChart() { @@ -321,7 +422,12 @@ export function drawIndicatorsOnChart() { const lineStyleMap = { 'solid': 0, 'dotted': 1, 'dashed': 2 }; - activeIndicators.forEach((indicator, idx) => { + activeIndicators.forEach((indicator) => { + if (indicator.visible === false) { + indicator.series = []; + return; + } + const IndicatorClass = IR?.[indicator.type]; if (!IndicatorClass) return; @@ -350,7 +456,7 @@ export function drawIndicatorsOnChart() { color: plotColor, lineWidth: plot.width || lineWidth, lineStyle: lineStyle, - title: plot.title, + title: '', priceLineVisible: false, lastValueVisible: true }); @@ -378,10 +484,84 @@ export function drawIndicatorsOnChart() { } }); }); + + updateChartLegend(); } +/** Update the TradingView-style legend overlay on the chart */ +export function updateChartLegend() { + let legend = document.getElementById('chartIndicatorLegend'); + if (!legend) { + const chartWrapper = document.getElementById('chartWrapper'); + if (!chartWrapper) return; + legend = document.createElement('div'); + legend.id = 'chartIndicatorLegend'; + legend.className = 'chart-indicator-legend'; + chartWrapper.appendChild(legend); + } + + if (activeIndicators.length === 0) { + legend.innerHTML = ''; + legend.style.display = 'none'; + return; + } + + legend.style.display = 'flex'; + legend.innerHTML = activeIndicators.map(ind => { + const label = getIndicatorLabel(ind); + const plotGroups = groupPlotsByColor(ind.plots || []); + const firstColor = ind.params['_color_0'] || '#2962ff'; + const dimmed = ind.visible === false; + + return ` +
+ + ${label} + × +
+ `; + }).join(''); + + // Bind legend events + legend.querySelectorAll('.legend-item').forEach(el => { + el.addEventListener('click', (e) => { + if (e.target.classList.contains('legend-close')) return; + selectIndicatorConfig(el.dataset.id); + renderIndicatorList(); + }); + }); + legend.querySelectorAll('.legend-close').forEach(el => { + el.addEventListener('click', (e) => { + e.stopPropagation(); + removeIndicatorById(el.dataset.id); + }); + }); +} + +// Legacy compat: toggleIndicator still works for external callers +export function toggleIndicator(type) { + addIndicator(type); +} + +export function showIndicatorConfig(index) { + if (index >= 0 && index < activeIndicators.length) { + selectIndicatorConfig(activeIndicators[index].id); + } +} + +export function showIndicatorConfigByType(type) { + const ind = activeIndicators.find(a => a.type === type); + if (ind) { + selectIndicatorConfig(ind.id); + } +} + +window.addIndicator = addIndicator; window.toggleIndicator = toggleIndicator; window.showIndicatorConfig = showIndicatorConfig; window.applyIndicatorConfig = applyIndicatorConfig; window.removeIndicator = removeIndicator; +window.removeIndicatorById = removeIndicatorById; window.removeIndicatorByIndex = removeIndicatorByIndex; +window.drawIndicatorsOnChart = drawIndicatorsOnChart;