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 ` -