diff --git a/js/ui/chart.js b/js/ui/chart.js
index d8b6771..41258ad 100644
--- a/js/ui/chart.js
+++ b/js/ui/chart.js
@@ -136,6 +136,19 @@ function formatDate(timestamp) {
return TimezoneConfig.formatDate(timestamp);
}
+function throttle(func, limit) {
+ let inThrottle;
+ return function() {
+ const args = arguments;
+ const context = this;
+ if (!inThrottle) {
+ func.apply(context, args);
+ inThrottle = true;
+ setTimeout(() => inThrottle = false, limit);
+ }
+ }
+}
+
export class TradingDashboard {
constructor() {
this.chart = null;
@@ -157,6 +170,9 @@ constructor() {
this.dailyMAData = new Map(); // timestamp -> { ma44, ma125, price }
this.currentMouseTime = null;
+ // Throttled versions of heavy functions
+ this.throttledOnVisibleRangeChange = throttle(this.onVisibleRangeChange.bind(this), 150);
+
this.init();
}
@@ -756,7 +772,6 @@ onVisibleRangeChange() {
console.log(`[VisibleRange] Chart data (${data.length}) vs dataset (${allData?.length || 0}) differ, redrawing indicators...`);
}
- window.drawIndicatorsOnChart?.();
this.loadSignals().catch(e => console.error('Error loading signals:', e));
}
diff --git a/js/ui/indicators-panel-new.js b/js/ui/indicators-panel-new.js
index 6eb9abd..ed064b5 100644
--- a/js/ui/indicators-panel-new.js
+++ b/js/ui/indicators-panel-new.js
@@ -966,16 +966,30 @@ class SeriesAreaFillRenderer {
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 = chart.timeScale().timeToCoordinate(point.time);
- if (timeCoordinate === null) continue;
+ 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;
@@ -994,8 +1008,12 @@ class SeriesAreaFillRenderer {
// 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 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;
@@ -1021,30 +1039,15 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
// 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()...`);
+ // console.log(`[renderIndicatorOnPane] ${indicator.name}: Recalculating... (${candles.length} candles)`);
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;
@@ -1052,21 +1055,19 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
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;
+ 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;
+ const isFirstHurst = indicator.type === 'hurst' && activeIndicators.filter(ind => ind.type === 'hurst')[0]?.id === indicator.id;
meta.plots.forEach((plot, plotIdx) => {
if (isObjectResult) {
@@ -1075,17 +1076,13 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
}
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;
@@ -1095,60 +1092,55 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
}
if (value !== null && value !== undefined && typeof value === 'number' && Number.isFinite(value)) {
- data.push({
- time: candles[i].time,
- value: value
- });
+ data.push({ time: candles[i].time, value: value });
}
}
if (data.length === 0) return;
- let series;
+ 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;
- 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);
+ 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 {
- 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);
+ // Update existing series options
+ series.applyOptions(seriesOptions);
}
series.setData(data);
- indicator.series.push(series);
- plotsCreated++;
+ seriesIdx++;
- // Attach RSI bands
+ // RSI bands
if (meta.name === 'RSI' && indicator.series.length > 0) {
const mainSeries = indicator.series[0];
const overbought = indicator.params.overbought || 70;
@@ -1160,32 +1152,31 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
indicator.bands = indicator.bands || [];
indicator.bands.push(mainSeries.createPriceLine({
- price: overbought,
- color: '#787B86',
- lineWidth: 1,
- lineStyle: LightweightCharts.LineStyle.Dashed,
- axisLabelVisible: false,
- title: ''
+ 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: ''
+ 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) {
- // 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);
+ 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);
+ }
}
}
@@ -1246,13 +1237,13 @@ export function drawIndicatorsOnChart() {
const activeIndicators = getActiveIndicators();
- // Remove all existing series
- activeIndicators.forEach(ind => {
- ind.series?.forEach(s => {
- try { window.dashboard.chart.removeSeries(s); } catch(e) {}
- });
- ind.series = [];
- });
+ // 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,
@@ -1271,6 +1262,13 @@ export function drawIndicatorsOnChart() {
// 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;
}