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

1412 lines
56 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 = [];
// Persistence Logic
function saveActiveIndicators() {
try {
const toSave = activeIndicators.map(ind => ({
id: ind.id,
type: ind.type,
name: ind.name,
params: ind.params,
visible: ind.visible,
paneHeight: ind.paneHeight
}));
console.log('[Persistence] Saving indicators:', toSave.length);
localStorage.setItem('winterfail_active_indicators', JSON.stringify(toSave));
} catch (e) {
console.error('Failed to save active indicators:', e);
}
}
function loadActiveIndicators() {
try {
const saved = localStorage.getItem('winterfail_active_indicators');
console.log('[Persistence] Loading from storage:', saved ? 'data found' : 'empty');
if (!saved) return;
const parsed = JSON.parse(saved);
if (!Array.isArray(parsed)) return;
const restored = [];
parsed.forEach(savedInd => {
const IndicatorClass = IR?.[savedInd.type];
if (!IndicatorClass) {
console.warn(`[Persistence] Unknown indicator type: ${savedInd.type}`);
return;
}
const instance = new IndicatorClass({ type: savedInd.type, params: savedInd.params, name: savedInd.name });
const metadata = instance.getMetadata();
restored.push({
id: savedInd.id,
type: savedInd.type,
name: savedInd.name || metadata.name,
params: savedInd.params,
plots: metadata.plots,
series: [],
visible: savedInd.visible !== undefined ? savedInd.visible : true,
paneHeight: savedInd.paneHeight || 120,
cachedResults: null,
cachedMeta: null
});
const parts = savedInd.id.split('_');
const idNum = parseInt(parts[parts.length - 1]);
if (!isNaN(idNum) && idNum >= nextInstanceId) {
nextInstanceId = idNum + 1;
}
});
activeIndicators = restored;
console.log(`[Persistence] Successfully restored ${activeIndicators.length} indicators`);
} catch (e) {
console.error('Failed to load active indicators:', e);
}
}
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() {
loadActiveIndicators(); // Load persisted indicators
renderIndicatorPanel();
// Also trigger initial draw if dashboard exists (it might be too early, but safe to try)
if (window.dashboard && window.dashboard.hasInitialLoad) {
drawIndicatorsOnChart();
}
}
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;
saveActiveIndicators();
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">
<div class="indicator-info">
<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>
</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 description = meta?.description || '';
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>
<div class="indicator-info">
<span class="indicator-name">${label}</span>
<span class="indicator-desc">${description}</span>
</div>
${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;
saveActiveIndicators();
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
saveActiveIndicators();
drawIndicatorsOnChart();
};
window.clearAllIndicators = function() {
activeIndicators.forEach(ind => {
ind.series?.forEach(s => {
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
});
});
activeIndicators = [];
configuringId = null;
saveActiveIndicators();
renderIndicatorPanel();
drawIndicatorsOnChart();
}
window.toggleAllIndicatorsVisibility = function() {
const allVisible = activeIndicators.every(ind => ind.visible !== false);
activeIndicators.forEach(ind => {
ind.visible = !allVisible;
});
saveActiveIndicators();
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;
}
saveActiveIndicators();
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
});
saveActiveIndicators();
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;
// Optimization: Get visible range to avoid iterating over thousands of historical points
const timeScale = chart.timeScale();
const visibleRange = timeScale.getVisibleLogicalRange();
if (!visibleRange) return;
ctx.save();
ctx.beginPath();
let started = false;
// Find start and end indices in data based on visible range for massive performance boost
// Since data is sorted by time, we could use binary search, but even a linear scan
// with visibility check is better than drawing everything.
// Draw top line (upper) forward
for (let i = 0; i < data.length; i++) {
const point = data[i];
const timeCoordinate = timeScale.timeToCoordinate(point.time);
// Skip points far outside the visible area
if (timeCoordinate === null) {
if (started) break; // We've passed the visible range
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 = timeScale.timeToCoordinate(point.time);
if (timeCoordinate === null) {
if (started && i < data.length / 2) break;
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}: Recalculating... (${candles.length} candles)`);
results = instance.calculate(candles);
indicator.cachedResults = results;
}
if (!results || !Array.isArray(results)) {
return;
}
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);
if (!firstNonNull && meta.plots && meta.plots.length > 1) {
isObjectResult = true;
}
if (!firstNonNull && meta.plots && meta.plots.length === 1 && meta.plots[0].id !== 'value') {
isObjectResult = true;
}
indicator.series = indicator.series || [];
let seriesIdx = 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];
if (isFirstHurst && results[i]) {
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 = indicator.series[seriesIdx];
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;
const seriesOptions = {
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 }
};
if (!series) {
// Create new series if it doesn't exist
if (plot.type === 'histogram') {
series = window.dashboard.chart.addSeries(LightweightCharts.HistogramSeries, seriesOptions, paneIndex);
} else if (plot.type === 'baseline') {
series = window.dashboard.chart.addSeries(LightweightCharts.BaselineSeries, {
...seriesOptions,
baseValue: { type: 'price', price: plot.baseValue || 0 },
topLineColor: plot.topLineColor || plotColor,
topFillColor1: plot.topFillColor1 || plotColor,
topFillColor2: '#00000000',
bottomFillColor1: '#00000000',
bottomColor: plot.bottomColor || '#00000000',
}, paneIndex);
} else {
series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, seriesOptions, paneIndex);
}
indicator.series[seriesIdx] = series;
} else {
// Update existing series options
series.applyOptions(seriesOptions);
}
series.setData(data);
seriesIdx++;
// 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: ''
}));
}
});
// Cleanup extra series if any
while (indicator.series.length > seriesIdx) {
const extra = indicator.series.pop();
try { window.dashboard.chart.removeSeries(extra); } catch(e) {}
}
// Attach Hurst Fill Primitive
if (isFirstHurst && hurstFillData.length > 0 && indicator.series.length > 0) {
const validFillData = hurstFillData.filter(d => d && d.time && d.upper !== undefined && d.lower !== undefined);
if (!indicator.fillPrimitive) {
indicator.fillPrimitive = new SeriesAreaFillPrimitive(validFillData, 'rgba(128, 128, 128, 0.05)');
indicator.series[0].attachPrimitive(indicator.fillPrimitive);
} else {
indicator.fillPrimitive.setData(validFillData);
}
}
}
// 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 - OPTIMIZATION: Removed aggressive clearing to allow reuse
// 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') {
// Hide existing series if they exist by setting empty data
if (ind.series && ind.series.length > 0) {
ind.series.forEach(s => s.setData([]));
}
if (ind.fillPrimitive) {
ind.fillPrimitive.setData([]);
}
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;
});
saveActiveIndicators();
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;
saveActiveIndicators();
// 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
};