Files
btc-trading/src/api/dashboard/static/js/ui/indicators-panel-new.js
DiTus 5140e437b0 Fix temporal dead zone error in exports
Changed exports to use export { } syntax after function definitions
to avoid 'cannot access before initialization' error.
Moved export statements to end of file where all functions are defined.
2026-02-25 22:41:56 +01:00

720 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { getAvailableIndicators, IndicatorRegistry as IR } from '../indicators/index.js';
// State management
let activeIndicators = [];
let configuringId = null;
let searchQuery = '';
let selectedCategory = 'all';
let nextInstanceId = 1;
// Presets storage
let userPresets = JSON.parse(localStorage.getItem('indicator_presets') || '{}');
// Categories
const CATEGORIES = [
{ id: 'all', name: 'All Indicators', icon: '📊' },
{ id: 'trend', name: 'Trend', icon: '📊' },
{ id: 'momentum', name: 'Momentum', icon: '📈' },
{ id: 'volatility', name: 'Volatility', icon: '📉' },
{ id: 'volume', name: 'Volume', icon: '🔀' },
{ id: 'favorites', name: 'Favorites', icon: '★' }
];
const CATEGORY_MAP = {
sma: 'trend', ema: 'trend', hts: 'trend',
rsi: 'momentum', macd: 'momentum', stoch: 'momentum',
bb: 'volatility', atr: 'volatility',
others: 'volume'
};
const DEFAULT_COLORS = ['#2962ff', '#26a69a', '#ef5350', '#ff9800', '#9c27b0', '#00bcd4', '#ffeb3b', '#e91e63'];
const LINE_TYPES = ['solid', 'dotted', 'dashed'];
// Initialize
export function initIndicatorPanel() {
renderIndicatorPanel();
setupEventListeners();
}
function getDefaultColor(index) {
return DEFAULT_COLORS[index % DEFAULT_COLORS.length];
}
function getIndicatorCategory(indicator) {
return CATEGORY_MAP[indicator.type] || 'trend';
}
function getIndicatorLabel(indicator) {
const meta = getIndicatorMeta(indicator);
if (!meta) return indicator.name;
const paramParts = meta.inputs.map(input => {
const val = indicator.params[input.name];
if (val !== undefined && val !== input.default) 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();
}
function groupPlotsByColor(plots) {
const groups = {};
plots.forEach((plot, idx) => {
const groupMap = {
'fast': 'Fast', 'slow': 'Slow', 'upper': 'Upper', 'lower': 'Lower',
'middle': 'Middle', 'basis': 'Middle', 'signal': 'Signal',
'histogram': 'Histogram', 'k': '%K', 'd': '%D'
};
const groupName = Object.entries(groupMap).find(([k, v]) => plot.id.toLowerCase().includes(k))?.[1] || plot.id;
if (!groups[groupName]) {
groups[groupName] = { name: groupName, indices: [], plots: [] };
}
groups[groupName].indices.push(idx);
groups[groupName].plots.push(plot);
});
return Object.values(groups);
}
export function getActiveIndicators() {
return activeIndicators;
}
export function setActiveIndicators(indicators) {
activeIndicators = indicators;
renderIndicatorPanel();
}
// Render main panel
export function renderIndicatorPanel() {
const container = document.getElementById('indicatorPanel');
if (!container) return;
const available = getAvailableIndicators();
const catalog = available.filter(ind => {
if (searchQuery && !ind.name.toLowerCase().includes(searchQuery.toLowerCase())) return false;
if (selectedCategory === 'all') return true;
if (selectedCategory === 'favorites') return false;
const cat = CATEGORY_MAP[ind.type] || 'trend';
return cat === selectedCategory;
});
const favoriteIds = new Set(userPresets.favorites || []);
container.innerHTML = `
<div class="indicator-panel">
<!-- Search Bar -->
<div class="indicator-search">
<span class="search-icon">🔍</span>
<input
type="text"
id="indicatorSearch"
placeholder="Search indicators..."
value="${searchQuery}"
autocomplete="off"
>
${searchQuery ? `<button class="search-clear" onclick="clearSearch()">×</button>` : ''}
</div>
<!-- Categories -->
<div class="category-tabs">
${CATEGORIES.map(cat => `
<button class="category-tab ${selectedCategory === cat.id ? 'active' : ''}" data-category="${cat.id}">
${cat.icon} ${cat.name}
</button>
`).join('')}
</div>
<!-- Favorites (if any) -->
${favoriteIds.size > 0 ? `
<div class="indicator-section favorites">
<div class="section-title">★ Favorites</div>
${favoriteIds.map(id => {
const ind = available.find(a => {
// Find matching indicator by type
return a.type === id || (activeIndicators.find(ai => ai.id === id)?.type === '');
});
if (!ind) return '';
return renderIndicatorItem(ind, true);
}).join('')}
</div>
` : ''}
<!-- Active Indicators -->
${activeIndicators.length > 0 ? `
<div class="indicator-section active">
<div class="section-title">
${activeIndicators.length} Active
${activeIndicators.length > 0 ? `<button class="clear-all" onclick="clearAllIndicators()">Clear All</button>` : ''}
</div>
${activeIndicators.slice().reverse().map(ind => renderActiveIndicator(ind)).join('')}
</div>
` : ''}
<!-- Available Indicators -->
${catalog.length > 0 ? `
<div class="indicator-section catalog">
<div class="section-title">Available Indicators</div>
${catalog.map(ind => renderIndicatorItem(ind, false)).join('')}
</div>
` : `
<div class="no-results">
No indicators found
</div>
`}
</div>
`;
setupEventListeners();
}
function renderIndicatorItem(indicator, isFavorite) {
const colorDots = '';
return `
<div class="indicator-item ${isFavorite ? 'favorite' : ''}" data-type="${indicator.type}">
<div class="indicator-item-main">
<span class="indicator-name">${indicator.name}</span>
<span class="indicator-desc">${indicator.description || ''}</span>
</div>
<div class="indicator-actions">
<button class="indicator-btn add" title="Add to chart" onclick="window.addIndicator('${indicator.type}')">
+
</button>
${isFavorite ? '' : `
<button class="indicator-btn favorite" title="Add to favorites" onclick="window.toggleFavorite('${indicator.type}')">
${userPresets.favorites?.includes(indicator.type) ? '★' : '☆'}
</button>
`}
</div>
</div>
`;
}
function renderActiveIndicator(indicator) {
const isExpanded = configuringId === indicator.id;
const meta = getIndicatorMeta(indicator);
const label = getIndicatorLabel(indicator);
const isFavorite = userPresets.favorites?.includes(indicator.type) || false;
return `
<div class="indicator-item active ${isExpanded ? 'expanded' : ''}" data-id="${indicator.id}">
<div class="indicator-item-main" onclick="window.toggleIndicatorExpand('${indicator.id}')">
<div class="drag-handle" title="Drag to reorder">⋮⋮</div>
<button class="indicator-btn visible" onclick="event.stopPropagation(); window.toggleIndicatorVisibility('${indicator.id}')" title="${indicator.visible !== false ? 'Hide' : 'Show'}">
${indicator.visible !== false ? '👁' : '👁‍🗨'}
</button>
<span class="indicator-name">${label}</span>
<div class="indicator-presets">
${meta.name && renderPresetIndicatorIndicator(meta, indicator)}
</div>
<button class="indicator-btn favorite" onclick="event.stopPropagation(); window.toggleFavorite('${indicator.type}')" title="Add to favorites">
${isFavorite ? '★' : '☆'}
</button>
<button class="indicator-btn expand ${isExpanded ? 'rotated' : ''}" title="Show settings">
${isExpanded ? '▼' : '▶'}
</button>
</div>
${isExpanded ? `
<div class="indicator-config">
${renderIndicatorConfig(indicator, meta)}
</div>
` : ''}
</div>
`;
}
function renderPresetIndicatorIndicator(meta, indicator) {
const hasPresets = getPresetsForIndicator(meta.name);
if (!hasPresets || hasPresets.length === 0) return '';
return `<button class="preset-indicator" title="${hasPresets.length} saved presets" onclick="event.stopPropagation(); window.showPresets('${meta.name}')">💾</button>`;
}
function renderIndicatorConfig(indicator, meta) {
const plotGroups = groupPlotsByColor(meta?.plots || []);
return `
<div class="config-sections">
<!-- Colors -->
<div class="config-section">
<div class="section-subtitle">Visual Settings</div>
${plotGroups.map(group => {
const firstIdx = group.indices[0];
const color = indicator.params[`_color_${firstIdx}`] || meta.plots[firstIdx]?.color || getDefaultColor(activeIndicators.indexOf(indicator));
return `
<div class="config-row">
<label>${group.name} Color</label>
<div class="color-picker">
<input type="color" id="color_${indicator.id}_${firstIdx}" value="${color}" onchange="window.updateIndicatorColor('${indicator.id}', ${firstIdx}, this.value)">
<span class="color-preview" style="background: ${color};"></span>
</div>
</div>
`;
}).join('')}
<div class="config-row">
<label>Line Type</label>
<select onchange="window.updateIndicatorSetting('${indicator.id}', '_lineType', this.value)">
${LINE_TYPES.map(lt => `<option value="${lt}" ${indicator.params._lineType === lt ? 'selected' : ''}>${lt.charAt(0).toUpperCase() + lt.slice(1)}</option>`).join('')}
</select>
</div>
<div class="config-row">
<label>Line Width</label>
<input type="range" min="1" max="5" value="${indicator.params._lineWidth || 2}" onchange="window.updateIndicatorSetting('${indicator.id}', '_lineWidth', parseInt(this.value))">
<span class="range-value">${indicator.params._lineWidth || 2}</span>
</div>
</div>
<!-- Inputs -->
${meta?.inputs && meta.inputs.length > 0 ? `
<div class="config-section">
<div class="section-subtitle">Parameters</div>
${meta.inputs.map(input => `
<div class="config-row">
<label>${input.label}</label>
${input.type === 'select' ?
`<select onchange="window.updateIndicatorSetting('${indicator.id}', '${input.name}', this.value)">
${input.options.map(o => `<option value="${o}" ${indicator.params[input.name] === o ? 'selected' : ''}>${o}</option>`).join('')}
</select>` :
`<div class="input-with-preset">
<input
type="number"
value="${indicator.params[input.name]}"
${input.min !== undefined ? `min="${input.min}"` : ''}
${input.max !== undefined ? `max="${input.max}"` : ''}
${input.step !== undefined ? `step="${input.step}"` : ''}
onchange="window.updateIndicatorSetting('${indicator.id}', '${input.name}', parseFloat(this.value))"
>
<button class="presets-btn" onclick="window.showInputPresets('${indicator.id}', '${input.name}')">⋯</button>
</div>`
}
</div>
`).join('')}
</div>
` : ''}
<!-- Presets -->
<div class="config-section">
<div class="section-subtitle">
Presets
<button class="preset-action-btn" onclick="window.savePreset('${indicator.id}')" title="Save current settings as preset">+ Save Preset</button>
</div>
${renderIndicatorPresets(indicator, meta)}
</div>
<!-- Actions -->
<div class="config-actions">
<button class="btn-secondary" onclick="window.resetIndicator('${indicator.id}')">Reset to Defaults</button>
<button class="btn-danger" onclick="window.removeIndicatorById('${indicator.id}')">Remove</button>
</div>
</div>
`;
}
function renderIndicatorPresets(indicator, meta) {
const presets = getPresetsForIndicator(meta.name);
const instance = new IR[indicator.type]({ type: indicator.type, params: indicator.params, name: indicator.name });
const metadata = instance.getMetadata();
return presets.length > 0 ? `
<div class="presets-list">
${presets.map(preset => {
// Match values against current settings
const isApplied = metadata.inputs.every(input =>
preset.values[input.name] === indicator.params[input.name]
);
return `
<div class="preset-item ${isApplied ? 'applied' : ''}" data-preset="${preset.id}">
<span class="preset-label" onclick="window.applyPreset('${indicator.id}', '${preset.id}')">${preset.name}</span>
<button class="preset-delete" onclick="event.stopPropagation(); window.deletePreset('${preset.id}')" title="Delete preset">×</button>
</div>
`;
}).join('')}
</div>
` : '<div class="no-presets">No saved presets</div>';
}
// Event listeners
function setupEventListeners() {
// Search
const searchInput = document.getElementById('indicatorSearch');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
searchQuery = e.target.value;
renderIndicatorPanel();
});
}
// Category tabs
document.querySelectorAll('.category-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
selectedCategory = tab.dataset.category;
renderIndicatorPanel();
});
});
}
// Actions
window.clearSearch = function() {
searchQuery = '';
renderIndicatorPanel();
};
window.clearAllIndicators = function() {
activeIndicators.forEach(ind => {
ind.series?.forEach(s => {
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
});
});
activeIndicators = [];
configuringId = null;
renderIndicatorPanel();
drawIndicatorsOnChart();
};
window.addIndicator = function(type) {
const IndicatorClass = IR?.[type];
if (!IndicatorClass) return;
const id = `${type}_${nextInstanceId++}`;
const instance = new IndicatorClass({ type, params: {}, name: '' });
const metadata = instance.getMetadata();
const params = {
_lineType: 'solid',
_lineWidth: 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;
renderIndicatorPanel();
drawIndicatorsOnChart();
};
window.toggleIndicatorExpand = function(id) {
configuringId = configuringId === id ? null : id;
renderIndicatorPanel();
};
window.toggleIndicatorVisibility = function(id) {
const indicator = activeIndicators.find(a => a.id === id);
if (!indicator) return;
indicator.visible = indicator.visible === false ? true : false;
indicator.series?.forEach(s => {
try {
s.applyOptions({ visible: indicator.visible });
} catch(e) {}
});
renderIndicatorPanel();
};
window.toggleFavorite = function(type) {
const favorites = userPresets.favorites || [];
const idx = favorites.indexOf(type);
if (idx >= 0) {
favorites.splice(idx, 1);
} else {
favorites.push(type);
}
userPresets.favorites = favorites;
saveUserPresets();
renderIndicatorPanel();
};
window.updateIndicatorColor = function(id, index, color) {
const indicator = activeIndicators.find(a => a.id === id);
if (!indicator) return;
indicator.params[`_color_${index}`] = color;
const preview = document.querySelector(`#color_${id}_${index} + .color-preview`);
if (preview) {
preview.style.background = color;
}
drawIndicatorsOnChart();
};
window.updateIndicatorSetting = function(id, key, value) {
const indicator = activeIndicators.find(a => a.id === id);
if (!indicator) return;
indicator.params[key] = value;
const valueSpan = document.querySelector(`#color_${id}_${index} + .color-preview`);
if (valueSpan) {
valueSpan.textContent = value;
}
drawIndicatorsOnChart();
};
window.resetIndicator = function(id) {
const indicator = activeIndicators.find(a => a.id === id);
if (!indicator) return;
const IndicatorClass = IR?.[indicator.type];
if (!IndicatorClass) return;
const instance = new IndicatorClass({ type: indicator.type, params: {}, name: indicator.name });
const metadata = instance.getMetadata();
metadata.inputs.forEach(input => {
indicator.params[input.name] = input.default;
});
renderIndicatorPanel();
drawIndicatorsOnChart();
};
window.removeIndicatorById = function(id) {
const idx = activeIndicators.findIndex(a => a.id === id);
if (idx < 0) return;
activeIndicators[idx].series?.forEach(s => {
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
});
activeIndicators.splice(idx, 1);
if (configuringId === id) {
configuringId = null;
}
renderIndicatorPanel();
drawIndicatorsOnChart();
};
// Presets
function getPresetsForIndicator(indicatorName) {
const allPresets = Object.values(userPresets).flat().filter(p => typeof p === 'object' && p.name);
return allPresets.filter(p => p.indicatorName === indicatorName);
}
window.savePreset = function(id) {
const indicator = activeIndicators.find(a => a.id === id);
if (!indicator) return;
const presetName = prompt('Enter preset name:');
if (!presetName) return;
const IndicatorClass = IR?.[indicator.type];
if (!IndicatorClass) return;
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
const meta = instance.getMetadata();
const preset = {
id: `preset_${Date.now()}`,
name: presetName,
indicatorName: meta.name,
values: {}
};
meta.inputs.forEach(input => {
preset.values[input.name] = indicator.params[input.name];
});
if (!userPresets.presets) userPresets.presets = [];
userPresets.presets.push(preset);
saveUserPresets();
renderIndicatorPanel();
alert(`Preset "${presetName}" saved!`);
};
window.applyPreset = function(id, presetId) {
const allPresets = Object.values(userPresets).flat().filter(p => typeof p === 'object' && p.id);
const preset = allPresets.find(p => p.id === presetId);
if (!preset) return;
const indicator = activeIndicators.find(a => a.id === id);
if (!indicator) return;
Object.keys(preset.values).forEach(key => {
indicator.params[key] = preset.values[key];
});
renderIndicatorPanel();
drawIndicatorsOnChart();
};
window.deletePreset = function(presetId) {
if (!confirm('Delete this preset?')) return;
if (userPresets.presets) {
userPresets.presets = userPresets.presets.filter(p => p.id !== presetId);
saveUserPresets();
renderIndicatorPanel();
}
};
window.showPresets = function(indicatorName) {
const presets = getPresetsForIndicator(indicatorName);
if (presets.length === 0) {
alert('No saved presets for this indicator');
return;
}
const menu = window.open('', '_blank', 'width=400,height=500');
let htmlContent =
'<html><head><title>Presets - ' + indicatorName + '</title><style>' +
'body { font-family: sans-serif; padding: 20px; background: #1e222d; color: #d1d4dc; }' +
'.preset { padding: 10px; margin: 5px; background: #131722; border-radius: 4px; }' +
'.preset:hover { background: #2a2e39; cursor: pointer; }' +
'</style></head><body>' +
'<h3>' + indicatorName + ' Presets</h3>';
presets.forEach(p => {
htmlContent += '<div class="preset" onclick="opener.applyPresetFromWindow(' + "'" + p.id + "'" + ')">' + p.name + '</div>';
});
htmlContent += '</body></html>';
menu.document.write(htmlContent);
};
window.applyPresetFromWindow = function(presetId) {
const indicator = activeIndicators.find(a => a.id === configuringId);
if (!indicator) return;
applyPreset(indicator.id, presetId);
};
function saveUserPresets() {
localStorage.setItem('indicator_presets', JSON.stringify(userPresets));
}
// Chart drawing
export function drawIndicatorsOnChart() {
if (!window.dashboard || !window.dashboard.chart) return;
activeIndicators.forEach(ind => {
ind.series?.forEach(s => {
try { window.dashboard.chart.removeSeries(s); } catch(e) {}
});
});
const candles = window.dashboard.allData.get(window.dashboard.currentInterval);
if (!candles || candles.length === 0) return;
const lineStyleMap = {
'solid': LightweightCharts.LineStyle.Solid,
'dotted': LightweightCharts.LineStyle.Dotted,
'dashed': LightweightCharts.LineStyle.Dashed
};
indicatorPanes.clear();
nextPaneIndex = 1;
const overlayIndicators = [];
const paneIndicators = [];
activeIndicators.forEach(ind => {
const IndicatorClass = IR?.[ind.type];
if (!IndicatorClass) return;
const instance = new IndicatorClass({ type: ind.type, params: ind.params, name: ind.name });
const meta = instance.getMetadata();
if (meta.displayMode === 'pane') {
paneIndicators.push({ indicator: ind, meta, instance });
} else {
overlayIndicators.push({ indicator: ind, meta, instance });
}
});
const totalPanes = 1 + paneIndicators.length;
const mainPaneHeight = paneIndicators.length > 0 ? 60 : 100;
const paneHeight = paneIndicators.length > 0 ? Math.floor(40 / paneIndicators.length) : 0;
window.dashboard.chart.panes()[0]?.setHeight(mainPaneHeight);
overlayIndicators.forEach(({ indicator, meta, instance }) => {
if (indicator.visible === false) {
indicator.series = [];
return;
}
renderIndicatorOnPane(indicator, meta, instance, candles, 0, lineStyleMap);
});
paneIndicators.forEach(({ indicator, meta, instance }, idx) => {
if (indicator.visible === false) {
indicator.series = [];
return;
}
const paneIndex = nextPaneIndex++;
indicatorPanes.set(indicator.id, paneIndex);
renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap);
const pane = window.dashboard.chart.panes()[paneIndex];
if (pane) {
pane.setHeight(paneHeight);
}
});
}
// Legacy compatibility functions
window.renderIndicatorList = renderIndicatorPanel;
window.toggleIndicator = addIndicator;
window.showIndicatorConfig = function(id) {
const ind = activeIndicators.find(a => a.id === id);
if (ind) configuringId = id;
renderIndicatorPanel();
};
window.applyIndicatorConfig = function() {
// No-op - config is applied immediately
};
window.removeIndicator = function() {
if (!configuringId) return;
removeIndicatorById(configuringId);
};
// Assign to window for backward compatibility
window.toggleIndicator = window.addIndicator;
window.removeIndicatorByIndex = function(index) {
if (index < 0 || index >= activeIndicators.length) return;
removeIndicatorById(activeIndicators[index].id);
};
window.drawIndicatorsOnChart = drawIndicatorsOnChart;
// Export functions for module imports
export { window.addIndicator as addIndicator };
export { window.removeIndicatorById as removeIndicatorById };
export { window.removeIndicatorByIndex as removeIndicatorByIndex };