Redesign indicators panel: dynamic catalog, multi-instance, chart legend

- Build indicator list dynamically from class getMetadata() instead of hardcoded array
- Remove checkboxes; single-click previews config, double-click adds to chart
- Support multiple instances of same indicator type (unique IDs)
- Add TradingView-style column legend overlay on chart (top-left)
- Recalculate indicators when historical data is prefetched on scroll
- Make indicator list and config panels scrollable (hidden scrollbars)
- Remove AVAILABLE_INDICATORS from strategies/config.js
This commit is contained in:
BTC Bot
2026-02-24 11:21:30 +01:00
parent 610911bca0
commit 06b2a4eac4
13 changed files with 558 additions and 155 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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' }]
};

View File

@ -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 }

View File

@ -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 || ''
};
});
}

View File

@ -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' }

View File

@ -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 },

View File

@ -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' }]
};

View File

@ -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 }

View File

@ -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' }
];

View File

@ -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';

View File

@ -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');

View File

@ -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;
const available = getAvailableIndicators();
let colorDots = '';
if (isActive) {
const indicator = activeIndicators[activeIdx];
const plotGroups = groupPlotsByColor(indicator.plots || []);
colorDots = plotGroups.map(group => {
container.innerHTML = `
<div class="indicator-catalog">
${available.map(ind => `
<div class="indicator-catalog-item ${previewingType === ind.type ? 'previewing' : ''}"
title="${ind.description || ''}"
data-type="${ind.type}">
<span class="indicator-catalog-name">${ind.name}</span>
<span class="indicator-catalog-add" data-type="${ind.type}">+</span>
</div>
`).join('')}
</div>
${activeIndicators.length > 0 ? `
<div class="indicator-active-divider">Active</div>
<div class="indicator-active-list">
${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 = indicator.params[`_color_${firstIdx}`] || '#2962ff';
const color = ind.params[`_color_${firstIdx}`] || '#2962ff';
return `<span class="indicator-color-dot" style="background: ${color};"></span>`;
}).join('');
}
const label = getIndicatorLabel(ind);
return `
<div class="indicator-checkbox-item ${isConfiguring ? 'configuring' : ''}"
onclick="toggleIndicator('${ind.type}')"
title="${ind.description}">
<input type="checkbox"
id="ind_${ind.type}"
${isActive ? 'checked' : ''}
onclick="event.stopPropagation(); toggleIndicator('${ind.type}')"
class="indicator-checkbox">
<label for="ind_${ind.type}" onclick="event.stopPropagation(); toggleIndicator('${ind.type}')" style="flex: 1;">
${ind.name}
<div class="indicator-active-item ${isConfiguring ? 'configuring' : ''}"
data-id="${ind.id}">
<span class="indicator-active-eye" data-id="${ind.id}"
title="${ind.visible !== false ? 'Hide' : 'Show'}">
${ind.visible !== false ? '👁' : '👁‍🗨'}
</span>
<span class="indicator-active-name" data-id="${ind.id}">${label}</span>
${colorDots}
</label>
${isActive ? `<button class="indicator-config-btn ${isConfiguring ? 'active' : ''}"
onclick="event.stopPropagation(); showIndicatorConfig(${activeIdx})"
title="Configure ${ind.name}">⚙</button>` : ''}
<button class="indicator-config-btn ${isConfiguring ? 'active' : ''}"
data-id="${ind.id}" title="Configure">⚙</button>
<button class="indicator-remove-btn"
data-id="${ind.id}" title="Remove">×</button>
</div>
`;
}).join('');
}).join('')}
</div>
` : ''}
`;
// 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,41 +169,68 @@ 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 = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 20px; font-size: 12px;">Select an active indicator to configure its settings</div>';
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 20px; font-size: 12px;">Click an indicator to preview its settings</div>';
}
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();
}
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;
/** Render a read-only preview of an indicator's default config */
function renderPreviewConfig(type) {
const container = document.getElementById('configForm');
if (!container) return;
const IndicatorClass = IR?.[type];
if (!IndicatorClass) return;
const instance = new IndicatorClass({ type, params: {}, name: indicatorDef.name });
const instance = new IndicatorClass({ type, params: {}, name: '' });
const meta = instance.getMetadata();
container.innerHTML = `
<div style="font-size: 11px; color: var(--tv-blue); margin-bottom: 4px; font-weight: 600;">${meta.name}</div>
<div style="font-size: 11px; color: var(--tv-text-secondary); margin-bottom: 10px;">${meta.description || ''}</div>
${meta.inputs.map(input => `
<div style="margin-bottom: 8px;">
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">${input.label}</label>
${input.type === 'select' ?
`<select class="sim-input" style="font-size: 12px; padding: 6px;" disabled>${input.options.map(o => `<option ${input.default === o ? 'selected' : ''}>${o}</option>`).join('')}</select>` :
`<input type="number" class="sim-input" value="${input.default}" style="font-size: 12px; padding: 6px;" disabled>`
}
</div>
`).join('')}
<div style="font-size: 10px; color: var(--tv-text-secondary); margin-top: 8px; text-align: center;">Double-click to add to chart</div>
`;
}
/** 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 = {
@ -144,46 +245,50 @@ export function toggleIndicator(type) {
});
activeIndicators.push({
type: type,
id,
type,
name: metadata.name,
params: params,
params,
plots: metadata.plots,
series: []
series: [],
visible: true
});
configuringIndex = activeIndicators.length - 1;
}
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;
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 = `
<div style="font-size: 11px; color: var(--tv-blue); margin-bottom: 8px; font-weight: 600;">${indicator.name}</div>
<div style="font-size: 11px; color: var(--tv-blue); margin-bottom: 8px; font-weight: 600;">${getIndicatorLabel(indicator)}</div>
${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);
}
activeIndicators[configuringIndex].series?.forEach(s => {
export 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(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 `
<div class="legend-item ${dimmed ? 'legend-dimmed' : ''} ${ind.id === configuringId ? 'legend-selected' : ''}"
data-id="${ind.id}">
<span class="legend-dot" style="background: ${firstColor};"></span>
<span class="legend-label">${label}</span>
<span class="legend-close" data-id="${ind.id}" title="Remove">&times;</span>
</div>
`;
}).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;