Files
winterfail/js/ui/indicators-panel-new.js

1329 lines
52 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;
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') {
const hurstCount = activeIndicators.filter(ind => ind.type === 'hurst').length;
const color = hurstCount > 0 ? '#ff9800' : '#9e9e9e';
params._lineWidth = 1;
params.timeframe = 'chart';
params.markerBuyShape = 'custom';
params.markerSellShape = 'custom';
params.markerBuyColor = color;
params.markerSellColor = color;
params.markerBuyCustom = '▲';
params.markerSellCustom = '▼';
}
metadata.plots.forEach((plot, idx) => {
if (type === 'hurst' && activeIndicators.filter(ind => ind.type === 'hurst').length > 0) {
params[`_color_${idx}`] = '#ff9800';
} else {
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));
}
// Custom Primitive for filling area between two lines
class SeriesAreaFillPrimitive {
constructor(data, color) {
this._data = data || [];
this._color = color || 'rgba(128, 128, 128, 0.05)';
this._paneViews = [new SeriesAreaFillPaneView(this)];
}
setData(data) {
this._data = data;
this._requestUpdate?.();
}
setColor(color) {
this._color = color;
this._requestUpdate?.();
}
attached(param) {
this._chart = param.chart;
this._series = param.series;
this._requestUpdate = param.requestUpdate;
this._requestUpdate();
}
detached() {
this._chart = undefined;
this._series = undefined;
this._requestUpdate = undefined;
}
updateAllViews() {
this._requestUpdate?.();
}
paneViews() {
return this._paneViews;
}
}
class SeriesAreaFillPaneView {
constructor(source) {
this._source = source;
}
renderer() {
return new SeriesAreaFillRenderer(this._source);
}
}
class SeriesAreaFillRenderer {
constructor(source) {
this._source = source;
}
draw(target) {
if (!this._source._chart || !this._source._series || this._source._data.length === 0) return;
target.useBitmapCoordinateSpace((scope) => {
const ctx = scope.context;
const series = this._source._series;
const chart = this._source._chart;
const data = this._source._data;
const color = this._source._color;
const ratio = scope.horizontalPixelRatio;
ctx.save();
ctx.beginPath();
let started = false;
// Draw top line (upper) forward
for (let i = 0; i < data.length; i++) {
const point = data[i];
const timeCoordinate = chart.timeScale().timeToCoordinate(point.time);
if (timeCoordinate === null) continue;
const upperY = series.priceToCoordinate(point.upper);
if (upperY === null) continue;
const x = timeCoordinate * ratio;
const y = upperY * ratio;
if (!started) {
ctx.moveTo(x, y);
started = true;
} else {
ctx.lineTo(x, y);
}
}
// Draw bottom line (lower) backward
for (let i = data.length - 1; i >= 0; i--) {
const point = data[i];
const timeCoordinate = chart.timeScale().timeToCoordinate(point.time);
if (timeCoordinate === null) continue;
const lowerY = series.priceToCoordinate(point.lower);
if (lowerY === null) continue;
const x = timeCoordinate * ratio;
const y = lowerY * ratio;
ctx.lineTo(x, y);
}
if (started) {
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
ctx.restore();
});
}
}
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;
// Special logic for Hurst fill
let hurstFillData = [];
const isFirstHurst = indicator.type === 'hurst' && activeIndicators.filter(ind => ind.type === 'hurst')[0].id === indicator.id;
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];
// Collect fill data if this is Hurst
if (isFirstHurst && results[i]) {
// Ensure we only add once per index
if (!hurstFillData[i]) hurstFillData[i] = { time: candles[i].time };
if (plot.id === 'upper') hurstFillData[i].upper = value;
if (plot.id === 'lower') hurstFillData[i].lower = value;
}
} else {
value = results[i];
}
if (value !== null && value !== undefined && typeof value === 'number' && Number.isFinite(value)) {
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: 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++;
// Attach RSI bands
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;
while (indicator.bands && indicator.bands.length > 0) {
try { indicator.bands.pop(); } catch(e) {}
}
indicator.bands = indicator.bands || [];
indicator.bands.push(mainSeries.createPriceLine({
price: overbought,
color: '#787B86',
lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dashed,
axisLabelVisible: false,
title: ''
}));
indicator.bands.push(mainSeries.createPriceLine({
price: oversold,
color: '#787B86',
lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dashed,
axisLabelVisible: false,
title: ''
}));
}
});
// Attach Hurst Fill Primitive
if (isFirstHurst && hurstFillData.length > 0 && indicator.series.length > 0) {
// Filter out incomplete data points
const validFillData = hurstFillData.filter(d => d && d.time && d.upper !== undefined && d.lower !== undefined);
// Attach to the first series (usually upper or lower band)
const fillPrimitive = new SeriesAreaFillPrimitive(validFillData, 'rgba(128, 128, 128, 0.05)');
indicator.series[0].attachPrimitive(fillPrimitive);
}
}
// 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
};