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:
@ -770,57 +770,152 @@
|
|||||||
.indicator-list {
|
.indicator-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 2px;
|
||||||
margin-top: 8px;
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
justify-content: space-between;
|
||||||
padding: 6px 8px;
|
padding: 5px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
transition: background 0.15s;
|
||||||
|
user-select: none;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator-checkbox-item:hover {
|
.indicator-catalog-item:hover {
|
||||||
background: var(--tv-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);
|
background: rgba(41, 98, 255, 0.1);
|
||||||
border-color: var(--tv-blue);
|
border-color: var(--tv-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator-checkbox {
|
.indicator-active-eye {
|
||||||
width: 14px;
|
font-size: 11px;
|
||||||
height: 14px;
|
|
||||||
accent-color: var(--tv-blue);
|
|
||||||
cursor: pointer;
|
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;
|
flex: 1;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
|
||||||
color: var(--tv-text);
|
color: var(--tv-text);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator-config-btn {
|
.indicator-config-btn {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid var(--tv-border);
|
border: 1px solid var(--tv-border);
|
||||||
color: var(--tv-text-secondary);
|
color: var(--tv-text-secondary);
|
||||||
width: 22px;
|
width: 20px;
|
||||||
height: 22px;
|
height: 20px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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 {
|
.indicator-config-btn:hover {
|
||||||
@ -833,18 +928,128 @@
|
|||||||
background: var(--tv-blue);
|
background: var(--tv-blue);
|
||||||
border-color: var(--tv-blue);
|
border-color: var(--tv-blue);
|
||||||
color: white;
|
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 {
|
.indicator-color-dot {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 10px;
|
width: 8px;
|
||||||
height: 10px;
|
height: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-left: 8px;
|
margin-left: 2px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
border: 1px solid rgba(255,255,255,0.2);
|
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 {
|
.ta-level {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@ -22,10 +22,12 @@ import {
|
|||||||
} from './ui/strategies-panel.js';
|
} from './ui/strategies-panel.js';
|
||||||
import {
|
import {
|
||||||
renderIndicatorList,
|
renderIndicatorList,
|
||||||
|
addIndicator,
|
||||||
toggleIndicator,
|
toggleIndicator,
|
||||||
showIndicatorConfig,
|
showIndicatorConfig,
|
||||||
applyIndicatorConfig,
|
applyIndicatorConfig,
|
||||||
removeIndicator,
|
removeIndicator,
|
||||||
|
removeIndicatorById,
|
||||||
removeIndicatorByIndex,
|
removeIndicatorByIndex,
|
||||||
drawIndicatorsOnChart
|
drawIndicatorsOnChart
|
||||||
} from './ui/indicators-panel.js';
|
} from './ui/indicators-panel.js';
|
||||||
@ -65,6 +67,7 @@ window.deleteSavedSimulation = deleteSavedSimulation;
|
|||||||
window.clearSimulationResults = clearSimulationResults;
|
window.clearSimulationResults = clearSimulationResults;
|
||||||
window.updateTimeframeDisplay = updateTimeframeDisplay;
|
window.updateTimeframeDisplay = updateTimeframeDisplay;
|
||||||
window.renderIndicatorList = renderIndicatorList;
|
window.renderIndicatorList = renderIndicatorList;
|
||||||
|
window.addIndicator = addIndicator;
|
||||||
window.toggleIndicator = toggleIndicator;
|
window.toggleIndicator = toggleIndicator;
|
||||||
window.showIndicatorConfig = showIndicatorConfig;
|
window.showIndicatorConfig = showIndicatorConfig;
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export class ATRIndicator extends BaseIndicator {
|
|||||||
getMetadata() {
|
getMetadata() {
|
||||||
return {
|
return {
|
||||||
name: 'ATR',
|
name: 'ATR',
|
||||||
|
description: 'Average True Range - measures market volatility',
|
||||||
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }],
|
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }],
|
||||||
plots: [{ id: 'value', color: '#795548', title: 'ATR' }]
|
plots: [{ id: 'value', color: '#795548', title: 'ATR' }]
|
||||||
};
|
};
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export class BollingerBandsIndicator extends BaseIndicator {
|
|||||||
getMetadata() {
|
getMetadata() {
|
||||||
return {
|
return {
|
||||||
name: 'Bollinger Bands',
|
name: 'Bollinger Bands',
|
||||||
|
description: 'Volatility bands around a moving average',
|
||||||
inputs: [
|
inputs: [
|
||||||
{ name: 'period', label: 'Period', type: 'number', default: 20, min: 1, max: 100 },
|
{ 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 }
|
{ name: 'stdDev', label: 'Std Dev', type: 'number', default: 2, min: 0.5, max: 5, step: 0.5 }
|
||||||
|
|||||||
@ -25,3 +25,19 @@ export const IndicatorRegistry = {
|
|||||||
stoch: StochasticIndicator,
|
stoch: StochasticIndicator,
|
||||||
atr: ATRIndicator
|
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 || ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export class MAIndicator extends BaseIndicator {
|
|||||||
getMetadata() {
|
getMetadata() {
|
||||||
return {
|
return {
|
||||||
name: 'MA',
|
name: 'MA',
|
||||||
|
description: 'Moving Average (SMA/EMA/RMA/WMA/VWMA)',
|
||||||
inputs: [
|
inputs: [
|
||||||
{ name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 },
|
{ 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' }
|
{ name: 'maType', label: 'MA Type', type: 'select', options: ['SMA', 'EMA', 'RMA', 'WMA', 'VWMA'], default: 'SMA' }
|
||||||
|
|||||||
@ -43,6 +43,7 @@ export class MACDIndicator extends BaseIndicator {
|
|||||||
getMetadata() {
|
getMetadata() {
|
||||||
return {
|
return {
|
||||||
name: 'MACD',
|
name: 'MACD',
|
||||||
|
description: 'Moving Average Convergence Divergence - trend & momentum',
|
||||||
inputs: [
|
inputs: [
|
||||||
{ name: 'fast', label: 'Fast Period', type: 'number', default: 12 },
|
{ name: 'fast', label: 'Fast Period', type: 'number', default: 12 },
|
||||||
{ name: 'slow', label: 'Slow Period', type: 'number', default: 26 },
|
{ name: 'slow', label: 'Slow Period', type: 'number', default: 26 },
|
||||||
|
|||||||
@ -30,6 +30,7 @@ export class RSIIndicator extends BaseIndicator {
|
|||||||
getMetadata() {
|
getMetadata() {
|
||||||
return {
|
return {
|
||||||
name: 'RSI',
|
name: 'RSI',
|
||||||
|
description: 'Relative Strength Index - momentum oscillator (0-100)',
|
||||||
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }],
|
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }],
|
||||||
plots: [{ id: 'value', color: '#9c27b0', title: 'RSI' }]
|
plots: [{ id: 'value', color: '#9c27b0', title: 'RSI' }]
|
||||||
};
|
};
|
||||||
|
|||||||
@ -31,6 +31,7 @@ export class StochasticIndicator extends BaseIndicator {
|
|||||||
getMetadata() {
|
getMetadata() {
|
||||||
return {
|
return {
|
||||||
name: 'Stochastic',
|
name: 'Stochastic',
|
||||||
|
description: 'Stochastic Oscillator - compares close to high-low range',
|
||||||
inputs: [
|
inputs: [
|
||||||
{ name: 'kPeriod', label: 'K Period', type: 'number', default: 14 },
|
{ name: 'kPeriod', label: 'K Period', type: 'number', default: 14 },
|
||||||
{ name: 'dPeriod', label: 'D Period', type: 'number', default: 3 }
|
{ name: 'dPeriod', label: 'D Period', type: 'number', default: 3 }
|
||||||
|
|||||||
@ -3,13 +3,3 @@ export const StrategyParams = {
|
|||||||
{ name: 'period', label: 'MA Period', type: 'number', default: 44, min: 5, max: 500 }
|
{ 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' }
|
|
||||||
];
|
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
export { StrategyParams, AVAILABLE_INDICATORS } from './config.js';
|
export { StrategyParams } from './config.js';
|
||||||
export { RiskManager } from './risk-manager.js';
|
export { RiskManager } from './risk-manager.js';
|
||||||
export { ClientStrategyEngine } from './engine.js';
|
export { ClientStrategyEngine } from './engine.js';
|
||||||
|
|||||||
@ -444,6 +444,9 @@ export class TradingDashboard {
|
|||||||
|
|
||||||
this.candleSeries.setData(mergedData);
|
this.candleSeries.setData(mergedData);
|
||||||
|
|
||||||
|
// Recalculate indicators with the expanded dataset
|
||||||
|
window.drawIndicatorsOnChart?.();
|
||||||
|
|
||||||
console.log(`Prefetched ${chartData.length} candles, total: ${mergedData.length}`);
|
console.log(`Prefetched ${chartData.length} candles, total: ${mergedData.length}`);
|
||||||
} else {
|
} else {
|
||||||
console.log('No more historical data available');
|
console.log('No more historical data available');
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { AVAILABLE_INDICATORS } from '../strategies/config.js';
|
import { getAvailableIndicators, IndicatorRegistry as IR } from '../indicators/index.js';
|
||||||
import { IndicatorRegistry as IR } from '../indicators/index.js';
|
|
||||||
|
|
||||||
let activeIndicators = [];
|
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 DEFAULT_COLORS = ['#2962ff', '#26a69a', '#ef5350', '#ff9800', '#9c27b0', '#00bcd4', '#ffeb3b', '#e91e63'];
|
||||||
const LINE_TYPES = ['solid', 'dotted', 'dashed'];
|
const LINE_TYPES = ['solid', 'dotted', 'dashed'];
|
||||||
@ -37,6 +38,31 @@ function groupPlotsByColor(plots) {
|
|||||||
return Object.values(groups);
|
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() {
|
export function getActiveIndicators() {
|
||||||
return activeIndicators;
|
return activeIndicators;
|
||||||
}
|
}
|
||||||
@ -45,47 +71,95 @@ export function setActiveIndicators(indicators) {
|
|||||||
activeIndicators = 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() {
|
export function renderIndicatorList() {
|
||||||
const container = document.getElementById('indicatorList');
|
const container = document.getElementById('indicatorList');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
container.innerHTML = AVAILABLE_INDICATORS.map((ind, idx) => {
|
const available = getAvailableIndicators();
|
||||||
const activeIdx = activeIndicators.findIndex(a => a.type === ind.type);
|
|
||||||
const isActive = activeIdx >= 0;
|
container.innerHTML = `
|
||||||
const isConfiguring = activeIdx === configuringIndex;
|
<div class="indicator-catalog">
|
||||||
|
${available.map(ind => `
|
||||||
let colorDots = '';
|
<div class="indicator-catalog-item ${previewingType === ind.type ? 'previewing' : ''}"
|
||||||
if (isActive) {
|
title="${ind.description || ''}"
|
||||||
const indicator = activeIndicators[activeIdx];
|
data-type="${ind.type}">
|
||||||
const plotGroups = groupPlotsByColor(indicator.plots || []);
|
<span class="indicator-catalog-name">${ind.name}</span>
|
||||||
colorDots = plotGroups.map(group => {
|
<span class="indicator-catalog-add" data-type="${ind.type}">+</span>
|
||||||
const firstIdx = group.indices[0];
|
</div>
|
||||||
const color = indicator.params[`_color_${firstIdx}`] || '#2962ff';
|
`).join('')}
|
||||||
return `<span class="indicator-color-dot" style="background: ${color};"></span>`;
|
</div>
|
||||||
}).join('');
|
${activeIndicators.length > 0 ? `
|
||||||
}
|
<div class="indicator-active-divider">Active</div>
|
||||||
|
<div class="indicator-active-list">
|
||||||
return `
|
${activeIndicators.map(ind => {
|
||||||
<div class="indicator-checkbox-item ${isConfiguring ? 'configuring' : ''}"
|
const isConfiguring = ind.id === configuringId;
|
||||||
onclick="toggleIndicator('${ind.type}')"
|
const plotGroups = groupPlotsByColor(ind.plots || []);
|
||||||
title="${ind.description}">
|
const colorDots = plotGroups.map(group => {
|
||||||
<input type="checkbox"
|
const firstIdx = group.indices[0];
|
||||||
id="ind_${ind.type}"
|
const color = ind.params[`_color_${firstIdx}`] || '#2962ff';
|
||||||
${isActive ? 'checked' : ''}
|
return `<span class="indicator-color-dot" style="background: ${color};"></span>`;
|
||||||
onclick="event.stopPropagation(); toggleIndicator('${ind.type}')"
|
}).join('');
|
||||||
class="indicator-checkbox">
|
const label = getIndicatorLabel(ind);
|
||||||
<label for="ind_${ind.type}" onclick="event.stopPropagation(); toggleIndicator('${ind.type}')" style="flex: 1;">
|
|
||||||
${ind.name}
|
return `
|
||||||
${colorDots}
|
<div class="indicator-active-item ${isConfiguring ? 'configuring' : ''}"
|
||||||
</label>
|
data-id="${ind.id}">
|
||||||
${isActive ? `<button class="indicator-config-btn ${isConfiguring ? 'active' : ''}"
|
<span class="indicator-active-eye" data-id="${ind.id}"
|
||||||
onclick="event.stopPropagation(); showIndicatorConfig(${activeIdx})"
|
title="${ind.visible !== false ? 'Hide' : 'Show'}">
|
||||||
title="Configure ${ind.name}">⚙</button>` : ''}
|
${ind.visible !== false ? '👁' : '👁🗨'}
|
||||||
|
</span>
|
||||||
|
<span class="indicator-active-name" data-id="${ind.id}">${label}</span>
|
||||||
|
${colorDots}
|
||||||
|
<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('')}
|
||||||
</div>
|
</div>
|
||||||
`;
|
` : ''}
|
||||||
}).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();
|
updateConfigPanel();
|
||||||
|
updateChartLegend();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateConfigPanel() {
|
function updateConfigPanel() {
|
||||||
@ -95,95 +169,126 @@ function updateConfigPanel() {
|
|||||||
|
|
||||||
configPanel.style.display = 'block';
|
configPanel.style.display = 'block';
|
||||||
|
|
||||||
if (configuringIndex >= 0 && configuringIndex < activeIndicators.length) {
|
// Active indicator config takes priority over preview
|
||||||
renderIndicatorConfig(configuringIndex);
|
const indicator = configuringId ? activeIndicators.find(a => a.id === configuringId) : null;
|
||||||
|
|
||||||
|
if (indicator) {
|
||||||
|
renderIndicatorConfig(indicator);
|
||||||
if (configButtons) configButtons.style.display = 'flex';
|
if (configButtons) configButtons.style.display = 'flex';
|
||||||
|
} else if (previewingType) {
|
||||||
|
renderPreviewConfig(previewingType);
|
||||||
|
if (configButtons) configButtons.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
const container = document.getElementById('configForm');
|
const container = document.getElementById('configForm');
|
||||||
if (container) {
|
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';
|
if (configButtons) configButtons.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleIndicator(type) {
|
/** Single-click: preview config for a catalog indicator type (read-only) */
|
||||||
const existingIdx = activeIndicators.findIndex(a => a.type === type);
|
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) {
|
const IndicatorClass = IR?.[type];
|
||||||
activeIndicators[existingIdx].series?.forEach(s => {
|
if (!IndicatorClass) return;
|
||||||
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
|
|
||||||
});
|
const instance = new IndicatorClass({ type, params: {}, name: '' });
|
||||||
activeIndicators.splice(existingIdx, 1);
|
const meta = instance.getMetadata();
|
||||||
if (configuringIndex >= activeIndicators.length) {
|
|
||||||
configuringIndex = -1;
|
container.innerHTML = `
|
||||||
} else if (configuringIndex === existingIdx) {
|
<div style="font-size: 11px; color: var(--tv-blue); margin-bottom: 4px; font-weight: 600;">${meta.name}</div>
|
||||||
configuringIndex = -1;
|
<div style="font-size: 11px; color: var(--tv-text-secondary); margin-bottom: 10px;">${meta.description || ''}</div>
|
||||||
} else if (configuringIndex > existingIdx) {
|
|
||||||
configuringIndex--;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const indicatorDef = AVAILABLE_INDICATORS.find(i => i.type === type);
|
|
||||||
if (!indicatorDef) return;
|
|
||||||
|
|
||||||
const IndicatorClass = IR?.[type];
|
${meta.inputs.map(input => `
|
||||||
if (!IndicatorClass) return;
|
<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('')}
|
||||||
|
|
||||||
const instance = new IndicatorClass({ type, params: {}, name: indicatorDef.name });
|
<div style="font-size: 10px; color: var(--tv-text-secondary); margin-top: 8px; text-align: center;">Double-click to add to chart</div>
|
||||||
const metadata = instance.getMetadata();
|
`;
|
||||||
|
}
|
||||||
const params = {
|
|
||||||
_lineType: 'solid',
|
/** Add a new instance of an indicator type */
|
||||||
_lineWidth: 2
|
export function addIndicator(type) {
|
||||||
};
|
const IndicatorClass = IR?.[type];
|
||||||
metadata.plots.forEach((plot, idx) => {
|
if (!IndicatorClass) return;
|
||||||
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
|
|
||||||
});
|
previewingType = null;
|
||||||
metadata.inputs.forEach(input => {
|
const id = `${type}_${nextInstanceId++}`;
|
||||||
params[input.name] = input.default;
|
const instance = new IndicatorClass({ type, params: {}, name: '' });
|
||||||
});
|
const metadata = instance.getMetadata();
|
||||||
|
|
||||||
activeIndicators.push({
|
const params = {
|
||||||
type: type,
|
_lineType: 'solid',
|
||||||
name: metadata.name,
|
_lineWidth: 2
|
||||||
params: params,
|
};
|
||||||
plots: metadata.plots,
|
metadata.plots.forEach((plot, idx) => {
|
||||||
series: []
|
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
|
||||||
});
|
});
|
||||||
|
metadata.inputs.forEach(input => {
|
||||||
configuringIndex = activeIndicators.length - 1;
|
params[input.name] = input.default;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
activeIndicators.push({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
name: metadata.name,
|
||||||
|
params,
|
||||||
|
plots: metadata.plots,
|
||||||
|
series: [],
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
|
||||||
|
configuringId = id;
|
||||||
|
|
||||||
renderIndicatorList();
|
renderIndicatorList();
|
||||||
drawIndicatorsOnChart();
|
drawIndicatorsOnChart();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showIndicatorConfig(index) {
|
function selectIndicatorConfig(id) {
|
||||||
if (configuringIndex === index) {
|
previewingType = null;
|
||||||
configuringIndex = -1;
|
if (configuringId === id) {
|
||||||
|
configuringId = null;
|
||||||
} else {
|
} else {
|
||||||
configuringIndex = index;
|
configuringId = id;
|
||||||
}
|
}
|
||||||
renderIndicatorList();
|
renderIndicatorList();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showIndicatorConfigByType(type) {
|
function toggleVisibility(id) {
|
||||||
const idx = activeIndicators.findIndex(a => a.type === type);
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
if (idx >= 0) {
|
if (!indicator) return;
|
||||||
configuringIndex = idx;
|
|
||||||
renderIndicatorList();
|
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');
|
const container = document.getElementById('configForm');
|
||||||
if (!container) return;
|
if (!container || !indicator) return;
|
||||||
|
|
||||||
const indicator = activeIndicators[index];
|
|
||||||
if (!indicator) {
|
|
||||||
container.innerHTML = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const IndicatorClass = IR?.[indicator.type];
|
const IndicatorClass = IR?.[indicator.type];
|
||||||
if (!IndicatorClass) {
|
if (!IndicatorClass) {
|
||||||
@ -208,7 +313,7 @@ export function renderIndicatorConfig(index) {
|
|||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
container.innerHTML = `
|
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}
|
${colorInputs}
|
||||||
|
|
||||||
@ -237,9 +342,9 @@ export function renderIndicatorConfig(index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function applyIndicatorConfig() {
|
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];
|
const IndicatorClass = IR?.[indicator.type];
|
||||||
if (!IndicatorClass) return;
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
@ -276,14 +381,23 @@ export function applyIndicatorConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function removeIndicator() {
|
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) {}
|
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
activeIndicators.splice(configuringIndex, 1);
|
activeIndicators.splice(idx, 1);
|
||||||
configuringIndex = -1;
|
|
||||||
|
if (configuringId === id) {
|
||||||
|
configuringId = null;
|
||||||
|
}
|
||||||
|
|
||||||
renderIndicatorList();
|
renderIndicatorList();
|
||||||
drawIndicatorsOnChart();
|
drawIndicatorsOnChart();
|
||||||
@ -291,20 +405,7 @@ export function removeIndicator() {
|
|||||||
|
|
||||||
export function removeIndicatorByIndex(index) {
|
export function removeIndicatorByIndex(index) {
|
||||||
if (index < 0 || index >= activeIndicators.length) return;
|
if (index < 0 || index >= activeIndicators.length) return;
|
||||||
|
removeIndicatorById(activeIndicators[index].id);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function drawIndicatorsOnChart() {
|
export function drawIndicatorsOnChart() {
|
||||||
@ -321,7 +422,12 @@ export function drawIndicatorsOnChart() {
|
|||||||
|
|
||||||
const lineStyleMap = { 'solid': 0, 'dotted': 1, 'dashed': 2 };
|
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];
|
const IndicatorClass = IR?.[indicator.type];
|
||||||
if (!IndicatorClass) return;
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
@ -350,7 +456,7 @@ export function drawIndicatorsOnChart() {
|
|||||||
color: plotColor,
|
color: plotColor,
|
||||||
lineWidth: plot.width || lineWidth,
|
lineWidth: plot.width || lineWidth,
|
||||||
lineStyle: lineStyle,
|
lineStyle: lineStyle,
|
||||||
title: plot.title,
|
title: '',
|
||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
lastValueVisible: true
|
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">×</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.toggleIndicator = toggleIndicator;
|
||||||
window.showIndicatorConfig = showIndicatorConfig;
|
window.showIndicatorConfig = showIndicatorConfig;
|
||||||
window.applyIndicatorConfig = applyIndicatorConfig;
|
window.applyIndicatorConfig = applyIndicatorConfig;
|
||||||
window.removeIndicator = removeIndicator;
|
window.removeIndicator = removeIndicator;
|
||||||
|
window.removeIndicatorById = removeIndicatorById;
|
||||||
window.removeIndicatorByIndex = removeIndicatorByIndex;
|
window.removeIndicatorByIndex = removeIndicatorByIndex;
|
||||||
|
window.drawIndicatorsOnChart = drawIndicatorsOnChart;
|
||||||
|
|||||||
Reference in New Issue
Block a user