diff --git a/src/static/brighterStyles.css b/src/static/brighterStyles.css index 7abb73c..cc6b23a 100644 --- a/src/static/brighterStyles.css +++ b/src/static/brighterStyles.css @@ -398,7 +398,7 @@ height: 500px; text-align: left; font-size: 12px; color: white; - z-index: 99; + z-index: 150; /* Above formation overlay (z-index: 100) */ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } diff --git a/src/static/charts.js b/src/static/charts.js index 73f8209..3443fc6 100644 --- a/src/static/charts.js +++ b/src/static/charts.js @@ -13,8 +13,12 @@ class Charts { /* A list of bound charts this is necessary for maintaining a dynamic number of charts with their position and zoom factors bound.*/ this.bound_charts=[]; + // Store unsubscribe functions for cleanup when rebinding + this._syncUnsubscribes = []; // Debounce timestamp to prevent infinite loop in chart synchronization this._lastSyncTime = 0; + // Flag to prevent recursive sync + this._isSyncing = false; // Only the main chart is created by default. this.create_main_chart(); } @@ -34,9 +38,11 @@ class Charts { // Initialize the candlestick series if price_history is available if (this.price_history && this.price_history.length > 0) { this.candleSeries.setData(this.price_history); - console.log('Candle series init:', this.price_history); + console.log(`Candle series initialized with ${this.price_history.length} candles`); + console.log('First candle:', this.price_history[0]); + console.log('Last candle:', this.price_history[this.price_history.length - 1]); } else { - console.error('Price history is not available or is empty.'); + console.error('Price history is not available or is empty. Received:', this.price_history); } this.bind_charts(this.chart_1); } @@ -158,147 +164,73 @@ class Charts { bind_charts(chart){ - // keep a list of charts and bind all their position and spacing. - // Add (arg1) to bound_charts - this.add_to_list(chart); - // Get the number of objects in bound_charts - let bcl = Object.keys(this.bound_charts).length; - // if bound_charts has two element in it bind them - if (bcl == 2) { this.bind2charts(); } - - // if bound_charts has three elements in it bind them - if (bcl == 3) { this.bind3charts(); } - - // if bound_charts has four elements in it bind them - if (bcl == 4) { this.bind4charts(); } - - // if bound_charts has five elements in it bind them - if (bcl == 5) { this.bind5charts(); } - - return; - } - add_to_list(chart){ - // If the chart isn't already included in the list, add it. - if ( !this.bound_charts.includes(chart) ){ + // Add chart to list if not already present + if (!this.bound_charts.includes(chart)) { this.bound_charts.push(chart); } + + // Only need to sync when we have 2+ charts + if (this.bound_charts.length >= 2) { + this._rebindAllCharts(); + } } - bind2charts(){ - //On change in chart 1 change chart 2 - let syncHandler1 = (e) => { - const now = Date.now(); - if (now - this._lastSyncTime < 50) return; - this._lastSyncTime = now; - // Get the barSpacing(zoom) and position of 1st chart (v5 API: options().barSpacing) - let barSpacing1 = this.bound_charts[0].timeScale().options().barSpacing; - let scrollPosition1 = this.bound_charts[0].timeScale().scrollPosition(); - // Apply barSpacing(zoom) and position to 2nd chart. - this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 }); - } - this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncHandler1); - - //On change in chart 2 change chart 1 - let syncHandler2 = (e) => { - const now = Date.now(); - if (now - this._lastSyncTime < 50) return; - this._lastSyncTime = now; - // Get the barSpacing(zoom) and position of chart 2 (v5 API: options().barSpacing) - let barSpacing2 = this.bound_charts[1].timeScale().options().barSpacing; - let scrollPosition2 = this.bound_charts[1].timeScale().scrollPosition(); - // Apply barSpacing(zoom) and position to chart 1 - this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 }); - } - this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncHandler2); - } - bind3charts(){ - - //On change to chart 1 change chart 2 and 3 - let syncHandler = (e) => { - const now = Date.now(); - if (now - this._lastSyncTime < 50) return; - this._lastSyncTime = now; - // Get the barSpacing(zoom) and position of chart 1 (v5 API) - let barSpacing1 = this.bound_charts[0].timeScale().options().barSpacing; - let scrollPosition1 = this.bound_charts[0].timeScale().scrollPosition(); - // Apply barSpacing(zoom) and position to new chart - this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 }); - this.bound_charts[2].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 }); - } - this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncHandler); - - //On change to chart 2 change chart 1 and 3 - let syncHandler2 = (e) => { - const now = Date.now(); - if (now - this._lastSyncTime < 50) return; - this._lastSyncTime = now; - // Get the barSpacing(zoom) and position of chart 2 (v5 API) - let barSpacing2 = this.bound_charts[1].timeScale().options().barSpacing; - let scrollPosition2 = this.bound_charts[1].timeScale().scrollPosition(); - // Apply barSpacing(zoom) and position to chart 1 and 3 - this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 }); - this.bound_charts[2].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 }); - } - this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncHandler2); - - //On change to chart 3 change chart 1 and 2 - let syncHandler3 = (e) => { - const now = Date.now(); - if (now - this._lastSyncTime < 50) return; - this._lastSyncTime = now; - // Get the barSpacing(zoom) and position of new chart (v5 API) - let barSpacing2 = this.bound_charts[2].timeScale().options().barSpacing; - let scrollPosition2 = this.bound_charts[2].timeScale().scrollPosition(); - // Apply barSpacing(zoom) and position to parent chart - this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 }); - this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 }); - } - this.bound_charts[2].timeScale().subscribeVisibleTimeRangeChange(syncHandler3); - } - - bind4charts(){ - // Sync all 4 charts together (v5 API: options().barSpacing) - let syncFromChart = (sourceIndex) => { - return (e) => { - const now = Date.now(); - if (now - this._lastSyncTime < 50) return; - this._lastSyncTime = now; - let barSpacing = this.bound_charts[sourceIndex].timeScale().options().barSpacing; - let scrollPosition = this.bound_charts[sourceIndex].timeScale().scrollPosition(); - for (let i = 0; i < 4; i++) { - if (i !== sourceIndex) { - this.bound_charts[i].timeScale().applyOptions({ rightOffset: scrollPosition, barSpacing: barSpacing }); - } - } + /** + * Unsubscribe all existing sync handlers and set up fresh ones. + * This prevents cumulative handlers from conflicting. + */ + _rebindAllCharts() { + // Unsubscribe all existing handlers using v5 API + for (const {chart, handler} of this._syncUnsubscribes) { + try { + chart.timeScale().unsubscribeVisibleTimeRangeChange(handler); + } catch (e) { + // Ignore errors during cleanup } } - this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(0)); - this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(1)); - this.bound_charts[2].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(2)); - this.bound_charts[3].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(3)); - } + this._syncUnsubscribes = []; + + // Create a single sync handler that works with all current charts + const chartCount = this.bound_charts.length; + + for (let sourceIndex = 0; sourceIndex < chartCount; sourceIndex++) { + const sourceChart = this.bound_charts[sourceIndex]; + + const syncHandler = () => { + // Prevent recursive sync (when we apply options, it triggers another event) + if (this._isSyncing) return; - bind5charts(){ - // Sync all 5 charts together (main + RSI + MACD + %B + Patterns) (v5 API) - let syncFromChart = (sourceIndex) => { - return (e) => { const now = Date.now(); if (now - this._lastSyncTime < 50) return; + + this._isSyncing = true; this._lastSyncTime = now; - let barSpacing = this.bound_charts[sourceIndex].timeScale().options().barSpacing; - let scrollPosition = this.bound_charts[sourceIndex].timeScale().scrollPosition(); - for (let i = 0; i < 5; i++) { - if (i !== sourceIndex) { - this.bound_charts[i].timeScale().applyOptions({ rightOffset: scrollPosition, barSpacing: barSpacing }); + + try { + const barSpacing = sourceChart.timeScale().options().barSpacing; + const scrollPosition = sourceChart.timeScale().scrollPosition(); + + // Apply to all other charts + for (let i = 0; i < this.bound_charts.length; i++) { + if (i !== sourceIndex) { + this.bound_charts[i].timeScale().applyOptions({ + rightOffset: scrollPosition, + barSpacing: barSpacing + }); + } } + } finally { + // Use setTimeout to reset flag after current event loop + setTimeout(() => { + this._isSyncing = false; + }, 0); } - } + }; + + // Subscribe and store chart+handler for later unsubscribe + sourceChart.timeScale().subscribeVisibleTimeRangeChange(syncHandler); + this._syncUnsubscribes.push({chart: sourceChart, handler: syncHandler}); } - this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(0)); - this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(1)); - this.bound_charts[2].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(2)); - this.bound_charts[3].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(3)); - this.bound_charts[4].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(4)); } // Set trade markers on chart for all trades in backtest results diff --git a/src/static/indicators.js b/src/static/indicators.js index 68913a5..7879d3d 100644 --- a/src/static/indicators.js +++ b/src/static/indicators.js @@ -101,10 +101,10 @@ class Indicator { priceScaleId: 'volume_ps', }); // v5: scaleMargins must be set on the price scale, not series options - // Volume should only take up bottom 30% of the chart + // Volume should only take up bottom 15% of the chart chart.priceScale('volume_ps').applyOptions({ scaleMargins: { - top: 0.7, + top: 0.85, bottom: 0, }, }); @@ -481,7 +481,7 @@ class Volume extends Indicator { constructor(name, chart) { super(name); this.addHist(name, chart); - this.hist[name].applyOptions({ scaleMargins: { top: 0.95, bottom: 0.0 } }); + // Note: scaleMargins are set in addHist() on the price scale (v5 API) this.outputs = ['value']; }