1. Removed configuringId setting from addIndicator so indicators are NOT expanded by default 2. Added window.updateIndicatorColor function to handle color picker changes 3. Added onchange handler to color picker input to call updateIndicatorColor 4. Added window.updateIndicatorSetting function to update indicator parameters Users can now: - Not have indicators auto-expanded when added (default collapsed) - Click indicator name to manually expand settings - Change colors and settings and they will update on the chart
800 lines
30 KiB
JavaScript
800 lines
30 KiB
JavaScript
import { getAvailableIndicators, IndicatorRegistry as IR } from '../indicators/index.js';
|
||
|
||
// State management
|
||
let activeIndicators = [];
|
||
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() {
|
||
console.log('[IndicatorPanel] Initializing...');
|
||
renderIndicatorPanel();
|
||
console.log('[IndicatorPanel] Initialized');
|
||
}
|
||
|
||
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) {
|
||
console.error('[IndicatorPanel] Container #indicatorPanel not found!');
|
||
return;
|
||
}
|
||
|
||
console.log('[IndicatorPanel] Rendering panel, searchQuery:', searchQuery, 'selectedCategory:', selectedCategory);
|
||
|
||
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;
|
||
});
|
||
|
||
console.log("[IndicatorPanel] Total indicators:", available.length, "Filtered to:", catalog.length);
|
||
|
||
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">×</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="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) {
|
||
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" 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>
|
||
`;
|
||
}
|
||
|
||
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' : ''}" 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('')}
|
||
|
||
<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="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] === (preset.values?.[input.name] ?? input.default))
|
||
);
|
||
|
||
return `
|
||
<div class="preset-item ${isApplied ? 'applied' : ''}" data-preset="${preset.id}">
|
||
<span class="preset-label" onclick="window.applyPreset && window.applyPreset('${indicator.id}', '${preset.id}')">${preset.name}</span>
|
||
<button class="preset-delete" onclick="window.deletePreset && window.deletePreset('${preset.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;
|
||
|
||
console.log('[IndicatorPanel] Setting up event listeners...');
|
||
|
||
// Single event delegation handler for add button
|
||
container.addEventListener('click', (e) => {
|
||
const addBtn = e.target.closest('.indicator-btn.add');
|
||
if (addBtn) {
|
||
e.stopPropagation();
|
||
const type = addBtn.dataset.type;
|
||
if (type && window.addIndicator) {
|
||
console.log('[IndicatorPanel] Add button clicked for type:', type);
|
||
window.addIndicator(type);
|
||
}
|
||
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;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// Favorite button
|
||
const favoriteBtn = e.target.closest('.indicator-btn.favorite');
|
||
if (favoriteBtn) {
|
||
e.stopPropagation();
|
||
const type = favoriteBtn.dataset.type;
|
||
if (type && window.toggleFavorite) {
|
||
window.toggleFavorite(type);
|
||
}
|
||
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();
|
||
});
|
||
});
|
||
|
||
// Clear all button
|
||
const clearAllBtn = container.querySelector('.clear-all');
|
||
if (clearAllBtn) {
|
||
clearAllBtn.addEventListener('click', () => {
|
||
window.clearAllIndicators();
|
||
});
|
||
}
|
||
|
||
console.log('[IndicatorPanel] Event listeners setup complete');
|
||
}
|
||
|
||
// Actions
|
||
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;
|
||
drawIndicatorsOnChart();
|
||
};
|
||
|
||
window.clearAllIndicators = function() {
|
||
activeIndicators.forEach(ind => {
|
||
ind.series?.forEach(s => {
|
||
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
|
||
});
|
||
});
|
||
activeIndicators = [];
|
||
configuringId = null;
|
||
renderIndicatorPanel();
|
||
drawIndicatorsOnChart();
|
||
}
|
||
|
||
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
|
||
});
|
||
// 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) {
|
||
const results = instance.calculate(candles);
|
||
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';
|
||
|
||
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 = [];
|
||
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) {
|
||
data.push({
|
||
time: candles[i].time,
|
||
value: value
|
||
});
|
||
}
|
||
}
|
||
|
||
if (data.length === 0) return;
|
||
|
||
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 !== undefined ? plot.width : 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 !== undefined ? plot.width : lineWidth,
|
||
lineStyle: plotLineStyle,
|
||
title: plot.title || '',
|
||
priceLineVisible: false,
|
||
lastValueVisible: plot.lastValueVisible !== false
|
||
}, paneIndex);
|
||
}
|
||
|
||
series.setData(data);
|
||
indicator.series.push(series);
|
||
});
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Export functions for module access
|
||
export { addIndicator, removeIndicatorById };
|
||
|
||
// 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
|
||
}; |