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 = `
🔍
${searchQuery ? `` : ''}
${CATEGORIES.map(cat => `
`).join('')}
${[...favoriteIds].length > 0 ? `
★ Favorites
${[...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('')}
` : ''}
${activeIndicators.length > 0 ? `
${activeIndicators.length} Active
${activeIndicators.length > 0 ? `` : ''}
${activeIndicators.length > 0 ? `` : ''}
${activeIndicators.slice().reverse().map(ind => renderActiveIndicator(ind)).join('')}
` : ''}
${catalog.length > 0 ? `
Available Indicators
${catalog.map(ind => renderIndicatorItem(ind, false)).join('')}
` : `
No indicators found
`}
`;
// Only setup event listeners once
if (!listenersAttached) {
setupEventListeners();
listenersAttached = true;
}
}
function renderIndicatorItem(indicator, isFavorite) {
return `
${indicator.name}
${indicator.description || ''}
${isFavorite ? '' : `
`}
`;
}
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 `
`;
}();
return `
⋮⋮
${label}
${showPresets}
${isExpanded ? `
${typeof renderIndicatorConfig === 'function' ? renderIndicatorConfig(indicator, meta) : ''}
` : ''}
`;
}
function renderPresetIndicatorIndicator(meta, indicator) {
const hasPresets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : [];
if (!hasPresets || hasPresets.length === 0) return '';
return ``;
}
function renderIndicatorConfig(indicator, meta) {
const plotGroups = groupPlotsByColor(meta?.plots || []);
return `
Visual Settings
${plotGroups.map(group => {
const firstIdx = group.indices[0];
const color = indicator.params[`_color_${firstIdx}`] || meta.plots[firstIdx]?.color || getDefaultColor(activeIndicators.indexOf(indicator));
return `
`.trim() + '';
}).join('')}
${indicator.type !== 'rsi' ? `
${indicator.params._lineWidth || 2}
` : ''}
${meta?.inputs && meta.inputs.length > 0 ? `
Parameters
${meta.inputs.map(input => `
${input.type === 'select' ?
`` :
``
}
`).join('')}
` : ''}
Presets
${typeof renderIndicatorPresets === 'function' ? renderIndicatorPresets(indicator, meta) : ''}
`;
}
function renderIndicatorPresets(indicator, meta) {
const presets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : [];
return presets.length > 0 ? `
${presets.map(p => {
const isApplied = meta.inputs.every(input =>
(indicator.params[input.name] === (p.values?.[input.name] ?? input.default))
);
return `
${p.name}
`;
}).join('')}
` : 'No saved presets
';
}
// 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 =
'Presets - ' + indicatorName + '' +
'' + indicatorName + ' Presets
';
presets.forEach(p => {
htmlContent += '' + p.name + '
';
});
htmlContent += '';
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;
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;
});
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
};