1202 lines
49 KiB
JavaScript
1202 lines
49 KiB
JavaScript
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;
|
||
try {
|
||
userPresets = JSON.parse(localStorage.getItem('indicator_presets') || '{}');
|
||
if (!userPresets || typeof userPresets !== 'object') {
|
||
userPresets = { presets: [] };
|
||
}
|
||
} catch (e) {
|
||
console.warn('Failed to parse presets:', e);
|
||
userPresets = { 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;
|
||
|
||
// Always show params in parentheses (e.g., "MA(44)" or "MA(SMA,44)")
|
||
const paramParts = meta.inputs.map(input => {
|
||
const val = indicator.params[input.name];
|
||
return val !== undefined ? val : input.default;
|
||
});
|
||
|
||
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;
|
||
|
||
async function onTimeframeChange(newInterval) {
|
||
const indicators = getActiveIndicators();
|
||
for (const indicator of indicators) {
|
||
if (indicator.params.timeframe === 'chart' && typeof indicator.shouldRecalculate === 'function') {
|
||
if (indicator.shouldRecalculate()) {
|
||
try {
|
||
await window.renderIndicator(indicator.id);
|
||
} catch (err) {
|
||
console.error(`[onTimeframeChange] Failed to recalculate ${indicator.name}:`, err);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
window.onTimeframeChange = onTimeframeChange;
|
||
|
||
// 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 => {
|
||
const label = input.labels?.[o] || o;
|
||
return `<option value="${o}" ${indicator.params[input.name] === o ? 'selected' : ''}>${label}</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">Signals</div>
|
||
<div class="config-row">
|
||
<label>Show Markers</label>
|
||
<input type="checkbox" ${indicator.params.showMarkers !== false ? 'checked' : ''}
|
||
onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', 'showMarkers', this.checked)">
|
||
</div>
|
||
<div class="config-row">
|
||
<label>Buy Shape</label>
|
||
<select onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', 'markerBuyShape', this.value)">
|
||
<option value="arrowUp" ${indicator.params.markerBuyShape === 'arrowUp' || !indicator.params.markerBuyShape ? 'selected' : ''}>Arrow Up</option>
|
||
<option value="arrowDown" ${indicator.params.markerBuyShape === 'arrowDown' ? 'selected' : ''}>Arrow Down</option>
|
||
<option value="circle" ${indicator.params.markerBuyShape === 'circle' ? 'selected' : ''}>Circle</option>
|
||
<option value="square" ${indicator.params.markerBuyShape === 'square' ? 'selected' : ''}>Square</option>
|
||
<option value="triangleUp" ${indicator.params.markerBuyShape === 'triangleUp' ? 'selected' : ''}>Triangle Up</option>
|
||
<option value="triangleDown" ${indicator.params.markerBuyShape === 'triangleDown' ? 'selected' : ''}>Triangle Down</option>
|
||
<option value="custom" ${indicator.params.markerBuyShape === 'custom' ? 'selected' : ''}>Custom</option>
|
||
</select>
|
||
<input type="text" style="width: 60px; margin-left: 5px;" value="${indicator.params.markerBuyShape === 'custom' ? (indicator.params.markerBuyCustom || '') : ''}"
|
||
placeholder="◭"
|
||
onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', 'markerBuyCustom', this.value)">
|
||
</div>
|
||
<div class="config-row">
|
||
<label>Buy Color</label>
|
||
<div class="color-picker">
|
||
<input type="color" id="markerBuyColor_${indicator.id}" value="${indicator.params.markerBuyColor || '#26a69a'}"
|
||
onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', 'markerBuyColor', this.value)">
|
||
<span class="color-preview" style="background: ${indicator.params.markerBuyColor || '#26a69a'};"></span>
|
||
</div>
|
||
</div>
|
||
<div class="config-row">
|
||
<label>Sell Shape</label>
|
||
<select onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', 'markerSellShape', this.value)">
|
||
<option value="arrowDown" ${indicator.params.markerSellShape === 'arrowDown' || !indicator.params.markerSellShape ? 'selected' : ''}>Arrow Down</option>
|
||
<option value="arrowUp" ${indicator.params.markerSellShape === 'arrowUp' ? 'selected' : ''}>Arrow Up</option>
|
||
<option value="circle" ${indicator.params.markerSellShape === 'circle' ? 'selected' : ''}>Circle</option>
|
||
<option value="square" ${indicator.params.markerSellShape === 'square' ? 'selected' : ''}>Square</option>
|
||
<option value="triangleUp" ${indicator.params.markerSellShape === 'triangleUp' ? 'selected' : ''}>Triangle Up</option>
|
||
<option value="triangleDown" ${indicator.params.markerSellShape === 'triangleDown' ? 'selected' : ''}>Triangle Down</option>
|
||
<option value="custom" ${indicator.params.markerSellShape === 'custom' ? 'selected' : ''}>Custom</option>
|
||
</select>
|
||
<input type="text" style="width: 60px; margin-left: 5px;" value="${indicator.params.markerSellShape === 'custom' ? (indicator.params.markerSellCustom || '') : ''}"
|
||
placeholder="▼"
|
||
onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', 'markerSellCustom', this.value)">
|
||
</div>
|
||
<div class="config-row">
|
||
<label>Sell Color</label>
|
||
<div class="color-picker">
|
||
<input type="color" id="markerSellColor_${indicator.id}" value="${indicator.params.markerSellColor || '#ef5350'}"
|
||
onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', 'markerSellColor', this.value)">
|
||
<span class="color-preview" style="background: ${indicator.params.markerSellColor || '#ef5350'};"></span>
|
||
</div>
|
||
</div>
|
||
</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;
|
||
indicator.cachedResults = null; // Clear cache when params change
|
||
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) {
|
||
console.log('[savePreset] Attempting to save preset for id:', id);
|
||
const indicator = activeIndicators.find(a => a.id === id);
|
||
if (!indicator) {
|
||
console.error('[savePreset] Indicator not found for id:', id);
|
||
return;
|
||
}
|
||
|
||
const presetName = prompt('Enter preset name:');
|
||
if (!presetName) return;
|
||
|
||
const IndicatorClass = IR?.[indicator.type];
|
||
if (!IndicatorClass) {
|
||
console.error('[savePreset] Indicator class not found for type:', indicator.type);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
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: {}
|
||
};
|
||
|
||
// Save standard inputs
|
||
if (meta.inputs && Array.isArray(meta.inputs)) {
|
||
meta.inputs.forEach(input => {
|
||
preset.values[input.name] = indicator.params[input.name];
|
||
});
|
||
}
|
||
|
||
// Save visual settings (line width, type, colors)
|
||
preset.values._lineWidth = indicator.params._lineWidth;
|
||
preset.values._lineType = indicator.params._lineType;
|
||
|
||
// Save colors for each plot
|
||
if (meta.plots && Array.isArray(meta.plots)) {
|
||
meta.plots.forEach((plot, idx) => {
|
||
const colorKey = `_color_${idx}`;
|
||
if (indicator.params[colorKey]) {
|
||
preset.values[colorKey] = indicator.params[colorKey];
|
||
}
|
||
});
|
||
}
|
||
|
||
// Save marker settings
|
||
const markerKeys = [
|
||
'showMarkers',
|
||
'markerBuyShape', 'markerBuyColor', 'markerBuyCustom',
|
||
'markerSellShape', 'markerSellColor', 'markerSellCustom'
|
||
];
|
||
markerKeys.forEach(key => {
|
||
if (indicator.params[key] !== undefined) {
|
||
preset.values[key] = indicator.params[key];
|
||
}
|
||
});
|
||
|
||
if (!userPresets) userPresets = { presets: [] };
|
||
if (!userPresets.presets) userPresets.presets = [];
|
||
|
||
userPresets.presets.push(preset);
|
||
saveUserPresets();
|
||
renderIndicatorPanel();
|
||
|
||
alert(`Preset "${presetName}" saved!`);
|
||
} catch (error) {
|
||
console.error('[savePreset] Error saving preset:', error);
|
||
alert('Error saving preset. See console for details.');
|
||
}
|
||
};
|
||
|
||
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: 1,
|
||
showMarkers: true,
|
||
markerBuyShape: 'custom',
|
||
markerBuyColor: '#9e9e9e',
|
||
markerBuyCustom: '▲',
|
||
markerSellShape: 'custom',
|
||
markerSellColor: '#9e9e9e',
|
||
markerSellCustom: '▼'
|
||
};
|
||
|
||
// Override with Hurst-specific defaults
|
||
if (type === 'hurst') {
|
||
params._lineWidth = 1;
|
||
params.timeframe = 'chart';
|
||
params.markerBuyShape = 'custom';
|
||
params.markerSellShape = 'custom';
|
||
params.markerBuyColor = '#9e9e9e';
|
||
params.markerSellColor = '#9e9e9e';
|
||
params.markerBuyCustom = '▲';
|
||
params.markerSellCustom = '▼';
|
||
}
|
||
|
||
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 (or use cached if they exist and are the correct length)
|
||
let results = indicator.cachedResults;
|
||
if (!results || !Array.isArray(results) || results.length !== candles.length) {
|
||
console.log(`[renderIndicatorOnPane] ${indicator.name}: Calling instance.calculate()...`);
|
||
results = instance.calculate(candles);
|
||
indicator.cachedResults = results;
|
||
}
|
||
|
||
if (!results || !Array.isArray(results)) {
|
||
console.error(`[renderIndicatorOnPane] ${indicator.name}: Failed to get valid results (got ${typeof results})`);
|
||
return;
|
||
}
|
||
|
||
if (results.length !== candles.length) {
|
||
console.error(`[renderIndicatorOnPane] ${indicator.name}: MISMATCH! Expected ${candles.length} results but got ${results.length}`);
|
||
}
|
||
|
||
// 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 || 1;
|
||
|
||
// Improved detection of object-based results (multiple plots)
|
||
const firstNonNull = Array.isArray(results) ? results.find(r => r !== null && r !== undefined) : null;
|
||
let isObjectResult = firstNonNull && typeof firstNonNull === 'object' && !Array.isArray(firstNonNull);
|
||
|
||
// Fallback: If results are all null (e.g. during warmup or MTF fetch),
|
||
// use metadata to determine if it SHOULD be an object result
|
||
if (!firstNonNull && meta.plots && meta.plots.length > 1) {
|
||
isObjectResult = true;
|
||
}
|
||
// Also check if the only plot has a specific ID that isn't just a number
|
||
if (!firstNonNull && meta.plots && meta.plots.length === 1 && meta.plots[0].id !== 'value') {
|
||
isObjectResult = true;
|
||
}
|
||
|
||
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 && typeof value === 'number' && Number.isFinite(value)) {
|
||
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: 0, minMove: 1 },
|
||
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,
|
||
priceFormat: { type: 'price', precision: 0, minMove: 1 }
|
||
}, paneIndex);
|
||
} else {
|
||
series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
|
||
color: plotColor,
|
||
lineWidth: plot.width || indicator.params._lineWidth || lineWidth,
|
||
lineStyle: plotLineStyle,
|
||
title: '',
|
||
priceLineVisible: false,
|
||
lastValueVisible: plot.lastValueVisible !== false,
|
||
priceFormat: { type: 'price', precision: 0, minMove: 1 }
|
||
}, 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
|
||
let results = ind.cachedResults;
|
||
if (!results || !Array.isArray(results) || results.length !== candles.length) {
|
||
try {
|
||
results = instance.calculate(candles);
|
||
ind.cachedResults = results;
|
||
} catch (err) {
|
||
console.error(`[Indicators] Failed to calculate ${ind.name}:`, err);
|
||
results = [];
|
||
}
|
||
}
|
||
ind.cachedMeta = meta;
|
||
|
||
const validResults = Array.isArray(results) ? 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);
|
||
}
|
||
});
|
||
|
||
//console.log(`[Indicators] ========== drawIndicatorsOnChart END ==========`);
|
||
} catch (error) {
|
||
console.error('[Indicators] Error drawing indicators:', error);
|
||
}
|
||
|
||
// Update signal markers after indicators are drawn
|
||
if (window.dashboard && typeof window.dashboard.updateSignalMarkers === 'function') {
|
||
window.dashboard.updateSignalMarkers();
|
||
}
|
||
}
|
||
|
||
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
|
||
}; |