Files
btc-trading/src/api/dashboard/static/js/ui/indicators-panel-new.js

1043 lines
40 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 = [];
console.log('[Module] indicators-panel-new.js loaded - activeIndicators count:', activeIndicators?.length || 0);
let configuringId = null;
let searchQuery = '';
let selectedCategory = 'all';
let nextInstanceId = 1;
let listenersAttached = false; // Single flag to track if any listeners are attached
// Chart pane management
let indicatorPanes = new Map();
let nextPaneIndex = 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'];
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 initIndicatorPanel() {
renderIndicatorPanel();
}
export function getActiveIndicators() {
return activeIndicators;
}
export function setActiveIndicators(indicators) {
console.warn('setActiveIndicators() called with', indicators.length, 'indicators - this will replace activeIndicators array!');
console.trace('Call stack:');
activeIndicators = indicators;
renderIndicatorPanel();
}
window.getActiveIndicators = getActiveIndicators;
// 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 || []);
const allVisible = activeIndicators.length > 0 ? activeIndicators.every(ind => ind.visible !== false) : false;
const visibilityBtnText = allVisible ? 'Hide All' : 'Show All';
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">×</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].length > 0 ? `
<div class="indicator-section favorites">
<div class="section-title">★ Favorites</div>
${[...favoriteIds].map(id => {
const ind = available.find(a => {
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="visibility-toggle">${visibilityBtnText}</button>` : ''}
${activeIndicators.length > 0 ? `<button class="clear-all">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>
`;
// Only setup event listeners once
if (!listenersAttached) {
setupEventListeners();
listenersAttached = true;
}
}
function renderIndicatorItem(indicator, isFavorite) {
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 class="indicator-actions">
<button class="indicator-btn add" data-type="${indicator.type}" title="Add to chart">+</button>
${isFavorite ? '' : `
<button class="indicator-btn favorite" data-type="${indicator.type}" title="Add to favorites">
${userPresets.favorites?.includes(indicator.type) ? '★' : '☆'}
</button>
`}
</div>
</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;
const showPresets = meta.name && function() {
const hasPresets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : [];
if (!hasPresets || hasPresets.length === 0) return '';
return `<div class="indicator-presets">
<button class="preset-indicator" title="${hasPresets.length} saved presets">💾</button>
</div>`;
}();
return `
<div class="indicator-item active ${isExpanded ? 'expanded' : ''}" data-id="${indicator.id}">
<div class="indicator-item-main" onclick="window.toggleIndicatorExpand && window.toggleIndicatorExpand('${indicator.id}');">
<div class="drag-handle" title="Drag to reorder">⋮⋮</div>
<button class="indicator-btn visible" onclick="event.stopPropagation(); window.toggleIndicatorVisibility && window.toggleIndicatorVisibility('${indicator.id}')" title="${indicator.visible !== false ? 'Hide' : 'Show'}">
${indicator.visible !== false ? '👁' : '👁‍🗨'}
</button>
<span class="indicator-name">${label}</span>
${showPresets}
<button class="indicator-btn favorite" onclick="event.stopPropagation(); window.toggleFavorite && window.toggleFavorite('${indicator.type}')" title="Add to favorites">
${isFavorite ? '★' : '☆'}
</button>
<button class="indicator-btn expand ${isExpanded ? 'rotated' : ''}" data-id="${indicator.id}" onclick="event.stopPropagation(); window.toggleIndicatorExpand && window.toggleIndicatorExpand('${indicator.id}')" title="Show settings">
${isExpanded ? '▼' : '▶'}
</button>
</div>
${isExpanded ? `
<div class="indicator-config">
${typeof renderIndicatorConfig === 'function' ? renderIndicatorConfig(indicator, meta) : ''}
</div>
` : ''}
</div>
`;
}
function renderPresetIndicatorIndicator(meta, indicator) {
const hasPresets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : [];
if (!hasPresets || hasPresets.length === 0) return '';
return `<button class="preset-indicator" title="${hasPresets.length} saved presets" onclick="event.stopPropagation(); window.showPresets && 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 && window.updateIndicatorColor('${indicator.id}', ${firstIdx}, this.value)">
<span class="color-preview" style="background: ${color};"></span>
</div>
</div>
`.trim() + '';
}).join('')}
${indicator.type !== 'rsi' ? `
<div class="config-row">
<label>Line Type</label>
<select onchange="window.updateIndicatorSetting && 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="this.nextElementSibling.textContent = this.value; window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '_lineWidth', parseInt(this.value))">
<span class="range-value">${indicator.params._lineWidth || 2}</span>
</div>
` : ''}
</div>
${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 && 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>` :
`<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 && window.updateIndicatorSetting('${indicator.id}', '${input.name}', parseFloat(this.value))"
>`
}
</div>
`).join('')}
</div>
` : ''}
<div class="config-section">
<div class="section-subtitle">
Presets
<button class="preset-action-btn" onclick="window.savePreset && window.savePreset('${indicator.id}')">+ Save Preset</button>
</div>
${typeof renderIndicatorPresets === 'function' ? renderIndicatorPresets(indicator, meta) : ''}
</div>
<div class="config-actions">
<button class="btn-secondary" onclick="window.resetIndicator && window.resetIndicator('${indicator.id}')">Reset to Defaults</button>
<button class="btn-danger" onclick="window.removeIndicator && window.removeIndicator('${indicator.id}')">Remove</button>
</div>
</div>
`;
}
function renderIndicatorPresets(indicator, meta) {
const presets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : [];
return presets.length > 0 ? `
<div class="presets-list">
${presets.map(p => {
const isApplied = meta.inputs.every(input =>
(indicator.params[input.name] === (p.values?.[input.name] ?? input.default))
);
return `
<div class="preset-item ${isApplied ? 'applied' : ''}" data-preset="${p.id}">
<span class="preset-label" onclick="window.applyPreset && window.applyPreset('${indicator.id}', '${p.id}')">${p.name}</span>
<button class="preset-delete" onclick="window.deletePreset && window.deletePreset('${p.id}')">×</button>
</div>
`;
}).join('')}
</div>
` : '<div class="no-presets">No saved presets</div>';
}
// Event listeners
function setupEventListeners() {
const container = document.getElementById('indicatorPanel');
if (!container) return;
container.addEventListener('click', (e) => {
e.stopPropagation();
// Add button
const addBtn = e.target.closest('.indicator-btn.add');
if (addBtn) {
e.stopPropagation();
const type = addBtn.dataset.type;
if (type && window.addIndicator) {
window.addIndicator(type);
}
return;
}
// Remove button
const removeBtn = e.target.closest('.indicator-btn.remove');
if (removeBtn) {
e.stopPropagation();
const id = removeBtn.dataset.id;
if (id && window.removeIndicatorById) {
window.removeIndicatorById(id);
}
return;
}
// Clear all button
const clearAllBtn = e.target.closest('.clear-all');
if (clearAllBtn) {
if (window.clearAllIndicators) {
window.clearAllIndicators();
}
return;
}
// Visibility toggle (Hide All / Show All) button
const visibilityToggleBtn = e.target.closest('.visibility-toggle');
if (visibilityToggleBtn) {
if (window.toggleAllIndicatorsVisibility) {
window.toggleAllIndicatorsVisibility();
}
return;
}
// Expand/collapse button
const expandBtn = e.target.closest('.indicator-btn.expand');
if (expandBtn) {
e.stopPropagation();
const id = expandBtn.dataset.id;
if (id && window.toggleIndicatorExpand) {
window.toggleIndicatorExpand(id);
}
return;
}
// Visibility button (eye)
const visibleBtn = e.target.closest('.indicator-btn.visible');
if (visibleBtn) {
e.stopPropagation();
const id = visibleBtn.dataset.id;
if (id && window.toggleIndicatorVisibility) {
window.toggleIndicatorVisibility(id);
}
return;
}
});
// Search input
const searchInput = document.getElementById('indicatorSearch');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
searchQuery = e.target.value;
renderIndicatorPanel();
});
}
// Search clear button
const searchClear = container.querySelector('.search-clear');
if (searchClear) {
searchClear.addEventListener('click', (e) => {
searchQuery = '';
renderIndicatorPanel();
});
}
// Category tabs
document.querySelectorAll('.category-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
selectedCategory = tab.dataset.category;
renderIndicatorPanel();
});
});
}
// Actions
window.toggleIndicatorExpand = function(id) {
configuringId = configuringId === id ? null : id;
renderIndicatorPanel();
};
window.clearSearch = function() {
searchQuery = '';
renderIndicatorPanel();
};
window.updateIndicatorColor = function(id, index, color) {
const indicator = activeIndicators.find(a => a.id === id);
if (!indicator) return;
indicator.params[`_color_${index}`] = color;
drawIndicatorsOnChart();
};
window.updateIndicatorSetting = function(id, key, value) {
const indicator = activeIndicators.find(a => a.id === id);
if (!indicator) return;
indicator.params[key] = value;
indicator.lastSignalTimestamp = null;
indicator.lastSignalType = null;
drawIndicatorsOnChart();
};
window.clearAllIndicators = function() {
activeIndicators.forEach(ind => {
ind.series?.forEach(s => {
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
});
});
activeIndicators = [];
configuringId = null;
renderIndicatorPanel();
drawIndicatorsOnChart();
}
window.toggleAllIndicatorsVisibility = function() {
const allVisible = activeIndicators.every(ind => ind.visible !== false);
activeIndicators.forEach(ind => {
ind.visible = !allVisible;
});
drawIndicatorsOnChart();
renderIndicatorPanel();
}
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(idx, 1);
if (configuringId === id) {
configuringId = null;
}
renderIndicatorPanel();
drawIndicatorsOnChart();
}
// Presets
function getPresetsForIndicator(indicatorName) {
if (!userPresets || !userPresets.presets) return [];
return userPresets.presets.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 = (userPresets?.presets || []).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 addIndicator(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,
paneHeight: 120 // default 120px
});
// Don't set configuringId so indicators are NOT expanded by default
renderIndicatorPanel();
drawIndicatorsOnChart();
};
function saveUserPresets() {
localStorage.setItem('indicator_presets', JSON.stringify(userPresets));
}
function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) {
// Recalculate with current TF candles
console.log(`[renderIndicatorOnPane] ${indicator.name}: START`);
console.log(`[renderIndicatorOnPane] ${indicator.name}: Input candles = ${candles.length}`);
console.log(`[renderIndicatorOnPane] ${indicator.name}: Calling instance.calculate()...`);
const results = instance.calculate(candles);
console.log(`[renderIndicatorOnPane] ${indicator.name}: calculate() returned ${results?.length || 0} results`);
console.log(`[renderIndicatorOnPane] ${indicator.name}: Expected ${candles.length} results, got ${results?.length || 0}`);
if (results.length !== candles.length) {
console.error(`[renderIndicatorOnPane] ${indicator.name}: MISMATCH! Expected ${candles.length} results but got ${results.length}`);
console.error(`[renderIndicatorOnPane] ${indicator.name}: This means instance.calculate() is not returning the correct number of results!`);
}
// Clear previous series for this indicator
if (indicator.series && indicator.series.length > 0) {
indicator.series.forEach(s => {
try {
window.dashboard.chart.removeSeries(s);
} catch(e) {}
});
}
indicator.series = [];
const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid;
const lineWidth = indicator.params._lineWidth || 2;
const firstNonNull = results?.find(r => r !== null && r !== undefined);
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
let plotsCreated = 0;
let dataPointsAdded = 0;
meta.plots.forEach((plot, plotIdx) => {
if (isObjectResult) {
const hasData = results.some(r => r && r[plot.id] !== undefined && r[plot.id] !== null);
if (!hasData) return;
}
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
const data = [];
let firstDataIndex = -1;
for (let i = 0; i < candles.length; i++) {
let value;
if (isObjectResult) {
value = results[i]?.[plot.id];
} else {
value = results[i];
}
if (value !== null && value !== undefined) {
if (firstDataIndex === -1) {
firstDataIndex = i;
}
data.push({
time: candles[i].time,
value: value
});
}
}
console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: ${data.length} data points created, first data at index ${firstDataIndex}/${candles.length}`);
if (data.length === 0) {
console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: No data to render`);
return;
}
console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: Creating series with ${data.length} data points [${data[0].time} to ${data[data.length - 1].time}]`);
let series;
let plotLineStyle = lineStyle;
if (plot.style === 'dashed') plotLineStyle = LightweightCharts.LineStyle.Dashed;
else if (plot.style === 'dotted') plotLineStyle = LightweightCharts.LineStyle.Dotted;
else if (plot.style === 'solid') plotLineStyle = LightweightCharts.LineStyle.Solid;
if (plot.type === 'histogram') {
series = window.dashboard.chart.addSeries(LightweightCharts.HistogramSeries, {
color: plotColor,
priceFormat: { type: 'price', precision: 4, minMove: 0.0001 },
priceLineVisible: false,
lastValueVisible: false
}, paneIndex);
} else if (plot.type === 'baseline') {
series = window.dashboard.chart.addSeries(LightweightCharts.BaselineSeries, {
baseValue: { type: 'price', price: plot.baseValue || 0 },
topLineColor: plot.topLineColor || plotColor,
topFillColor1: plot.topFillColor1 || plotColor,
topFillColor2: '#00000000',
bottomFillColor1: '#00000000',
bottomColor: plot.bottomColor || '#00000000',
lineWidth: plot.width || indicator.params._lineWidth || lineWidth,
lineStyle: plotLineStyle,
title: plot.title || '',
priceLineVisible: false,
lastValueVisible: plot.lastValueVisible !== false
}, paneIndex);
} else {
series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
color: plotColor,
lineWidth: plot.width || indicator.params._lineWidth || lineWidth,
lineStyle: plotLineStyle,
title: plot.title || '',
priceLineVisible: false,
lastValueVisible: plot.lastValueVisible !== false
}, paneIndex);
}
series.setData(data);
indicator.series.push(series);
plotsCreated++;
console.log(`Created series for ${indicator.id}, plot=${plot.id}, total series now=${indicator.series.length}`);
// Create horizontal band lines for RSI
if (meta.name === 'RSI' && indicator.series.length > 0) {
const mainSeries = indicator.series[0];
const overbought = indicator.params.overbought || 70;
const oversold = indicator.params.oversold || 30;
// Remove existing price lines first
while (indicator.bands && indicator.bands.length > 0) {
try {
indicator.bands.pop();
} catch(e) {}
}
indicator.bands = indicator.bands || [];
// Create overbought band line
indicator.bands.push(mainSeries.createPriceLine({
price: overbought,
color: '#787B86',
lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dashed,
axisLabelVisible: false,
title: ''
}));
// Create oversold band line
indicator.bands.push(mainSeries.createPriceLine({
price: oversold,
color: '#787B86',
lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dashed,
axisLabelVisible: false,
title: ''
}));
}
});
}
// Completely redraw indicators (works for both overlay and pane)
export function updateIndicatorCandles() {
console.log('[UpdateIndicators] Removing and recreating all indicator series');
// Remove all existing series
const activeIndicators = getActiveIndicators();
activeIndicators.forEach(indicator => {
indicator.series?.forEach(s => {
try {
window.dashboard.chart.removeSeries(s);
} catch(e) {
console.warn('[UpdateIndicators] Error removing series:', e);
}
});
indicator.series = [];
});
// Clear pane mappings
indicatorPanes.clear();
nextPaneIndex = 1;
// Now call drawIndicatorsOnChart to recreate everything
drawIndicatorsOnChart();
console.log(`[UpdateIndicators] Recreated ${activeIndicators.length} indicators`);
}
// Chart drawing
export function drawIndicatorsOnChart() {
try {
if (!window.dashboard || !window.dashboard.chart) {
return;
}
const currentInterval = window.dashboard.currentInterval;
const candles = window.dashboard?.allData?.get(currentInterval);
if (!candles || candles.length === 0) {
console.log('[Indicators] No candles available');
return;
}
console.log(`[Indicators] ========== drawIndicatorsOnChart START ==========`);
console.log(`[Indicators] Candles from allData: ${candles.length}`);
console.log(`[Indicators] First candle time: ${candles[0]?.time} (${new Date(candles[0]?.time * 1000).toLocaleDateString()})`);
console.log(`[Indicators] Last candle time: ${candles[candles.length - 1]?.time} (${new Date(candles[candles.length - 1]?.time * 1000).toLocaleDateString()})`);
const oldestTime = candles[0]?.time;
const newestTime = candles[candles.length - 1]?.time;
const oldestDate = oldestTime ? new Date(oldestTime * 1000).toLocaleDateString() : 'N/A';
const newestDate = newestTime ? new Date(newestTime * 1000).toLocaleDateString() : 'N/A';
console.log(`[Indicators] ========== Redrawing ==========`);
console.log(`[Indicators] Candles: ${candles.length} | Time range: ${oldestDate} (${oldestTime}) to ${newestDate} (${newestTime})`);
const activeIndicators = getActiveIndicators();
// Remove all existing series
activeIndicators.forEach(ind => {
ind.series?.forEach(s => {
try { window.dashboard.chart.removeSeries(s); } catch(e) {}
});
ind.series = [];
});
const lineStyleMap = {
'solid': LightweightCharts.LineStyle.Solid,
'dotted': LightweightCharts.LineStyle.Dotted,
'dashed': LightweightCharts.LineStyle.Dashed
};
// Don't clear indicatorPanes - preserve pane assignments across redraws
// Only reset nextPaneIndex to avoid creating duplicate panes
const maxExistingPane = Math.max(...indicatorPanes.values(), 0);
nextPaneIndex = maxExistingPane + 1;
const overlayIndicators = [];
const paneIndicators = [];
// Process all indicators, filtering by visibility
activeIndicators.forEach(ind => {
if (ind.visible === false || ind.visible === 'false') {
return;
}
const IndicatorClass = IR?.[ind.type];
if (!IndicatorClass) return;
const instance = new IndicatorClass(ind);
const meta = instance.getMetadata();
// Store calculated results and metadata for signal calculation
const results = instance.calculate(candles);
ind.cachedResults = results;
ind.cachedMeta = meta;
const validResults = results.filter(r => r !== null && r !== undefined);
const warmupPeriod = ind.params?.period || 44;
console.log(`[Indicators] ${ind.name}: ${validResults.length} valid results (need ${warmupPeriod} candles warmup)`);
if (meta.displayMode === 'pane') {
paneIndicators.push({ indicator: ind, meta, instance });
} else {
overlayIndicators.push({ indicator: ind, meta, instance });
}
});
// Set main pane height (60% if indicator panes exist, 100% otherwise)
const totalPanes = 1 + paneIndicators.length;
const mainPaneHeight = paneIndicators.length > 0 ? 60 : 100;
window.dashboard.chart.panes()[0]?.setHeight(mainPaneHeight);
console.log(`[Indicators] ========== Rendering Indicators ==========`);
console.log(`[Indicators] Input candles: ${candles.length} | Panel count: ${totalPanes}`);
overlayIndicators.forEach(({ indicator, meta, instance }) => {
console.log(`[Indicators] Processing overlay: ${indicator.name}`);
console.log(`[Indicators] Before renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`);
renderIndicatorOnPane(indicator, meta, instance, candles, 0, lineStyleMap);
console.log(`[Indicators] After renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`);
});
paneIndicators.forEach(({ indicator, meta, instance }, idx) => {
// Use existing pane index if already assigned, otherwise create new one
let paneIndex = indicatorPanes.get(indicator.id);
if (paneIndex === undefined) {
paneIndex = nextPaneIndex++;
indicatorPanes.set(indicator.id, paneIndex);
}
console.log(`[Indicators] Processing pane: ${indicator.name} (pane ${paneIndex})`);
console.log(`[Indicators] Before renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`);
renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap);
console.log(`[Indicators] After renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`);
const pane = window.dashboard.chart.panes()[paneIndex];
if (pane) {
// Use stored height, localStorage, or default 120px
const storedHeight = indicator.paneHeight ||
parseInt(localStorage.getItem(`pane_height_${indicator.type}`)) ||
120;
pane.setHeight(storedHeight);
// Subscribe to pane height changes to save to localStorage
const originalHeight = storedHeight;
const paneElement = pane.getHTMLElement && pane.getHTMLElement();
if (paneElement) {
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const newHeight = Math.round(entry.contentRect.height);
if (newHeight !== originalHeight && newHeight > 50) {
indicator.paneHeight = newHeight;
localStorage.setItem(`pane_height_${indicator.type}`, newHeight);
console.log(`[Indicators] Saved pane height for ${indicator.type}: ${newHeight}px`);
}
}
});
resizeObserver.observe(paneElement);
}
}
});
console.log(`[Indicators] ========== drawIndicatorsOnChart END ==========`);
} catch (error) {
console.error('[Indicators] Error drawing indicators:', error);
}
}
function resetIndicator(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: '' });
const meta = instance.getMetadata();
if (!meta || !meta.inputs) return;
meta.inputs.forEach(input => {
indicator.params[input.name] = input.default;
});
renderIndicatorPanel();
drawIndicatorsOnChart();
}
function removeIndicator(id) {
removeIndicatorById(id);
}
function toggleIndicatorVisibility(id) {
const indicator = activeIndicators.find(a => a.id === id);
if (!indicator) {
return;
}
indicator.visible = indicator.visible === false;
// Full redraw to ensure all indicators render correctly
if (typeof drawIndicatorsOnChart === 'function') {
drawIndicatorsOnChart();
}
renderIndicatorPanel();
}
// Export functions for module access
export { addIndicator, removeIndicatorById, toggleIndicatorVisibility };
// Legacy compatibility functions
window.renderIndicatorList = renderIndicatorPanel;
window.resetIndicator = resetIndicator;
window.removeIndicator = removeIndicator;
window.toggleIndicator = addIndicator;
window.toggleIndicatorVisibility = toggleIndicatorVisibility;
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
};