From bde7945a1b77757eed8482129b44af14c85d4cbb Mon Sep 17 00:00:00 2001 From: DiTus Date: Mon, 23 Mar 2026 09:47:07 +0100 Subject: [PATCH] feat: add chart type selector with candlestick, line, and bar charts - Add chart type selector with 3 chart types: candlestick (default), line, and bar - Fix data format conversion when switching between OHLC and simple {time,value} formats - Fix line chart refresh to use update() instead of setData() to preserve chart data - Remove area chart type and AI Insight/refresh buttons - Improve data handling in loadData, loadNewData, loadHistoricalData, and switchChartType methods --- index.html | 70 ++++-- js/ui/chart.js | 671 +++++++++++++++++++++++++++++++++---------------- 2 files changed, 501 insertions(+), 240 deletions(-) diff --git a/index.html b/index.html index 4bc2320..55acc36 100644 --- a/index.html +++ b/index.html @@ -34,13 +34,24 @@ } /* Hide scrollbar for clean UI */ - .no-scrollbar::-webkit-scrollbar { - display: none; - } - .no-scrollbar { - -ms-overflow-style: none; - scrollbar-width: none; - } + .no-scrollbar::-webkit-scrollbar { + display: none; + } + .no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + } + + /* Chart type button active state */ + .chart-type-btn.active { + background-color: #2d3a4f; + color: #3b82f6; + } + + /* Chart type button hover effect */ + .chart-type-btn:hover { + background-color: #2d3a4f; + } @@ -73,8 +84,37 @@ Live -
- + +
+ + +
+ +
+ +
+ + + +
+ + +
+ + +
+ +
@@ -336,15 +376,9 @@ Technical Analysis 1D -
- - - -
+
+ +
diff --git a/js/ui/chart.js b/js/ui/chart.js index 891beef..f694a0d 100644 --- a/js/ui/chart.js +++ b/js/ui/chart.js @@ -3,55 +3,7 @@ import { calculateAllIndicatorSignals, calculateSummarySignal } from './signals- import { calculateSignalMarkers } from './signal-markers.js'; import { updateIndicatorCandles } from './indicators-panel-new.js'; import { TimezoneConfig } from '../config/timezone.js'; - -export class SeriesMarkersPrimitive { - constructor(markers) { - this._markers = markers || []; - this._paneViews = [new MarkersPaneView(this)]; - } - - setMarkers(markers) { - this._markers = markers; - if (this._requestUpdate) { - 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 MarkersPaneView { - constructor(source) { - this._source = source; - } - - renderer() { - return new MarkersRenderer(this._source); - } - - zOrder() { - return 'top'; - } -} +import { DrawingManager } from './drawing-tools.js'; class MarkersRenderer { constructor(source) { @@ -67,7 +19,6 @@ class MarkersRenderer { const chart = this._source._chart; const markers = this._source._markers; - // Adjust coordinates to bitmap space based on pixel ratio const ratio = scope.horizontalPixelRatio; ctx.save(); @@ -76,10 +27,8 @@ class MarkersRenderer { const timeCoordinate = chart.timeScale().timeToCoordinate(marker.time); if (timeCoordinate === null) continue; - // Figure out price coordinate let price = marker.price || marker.value; - // If price wasn't specified but we have the series data, grab the candle high/low if (!price && window.dashboard && window.dashboard.allData) { const data = window.dashboard.allData.get(window.dashboard.currentInterval); if (data) { @@ -132,6 +81,55 @@ class MarkersRenderer { } } +class MarkersPaneView { + constructor(source) { + this._source = source; + } + + renderer() { + return new MarkersRenderer(this._source); + } + + zOrder() { + return 'top'; + } +} + +export class SeriesMarkersPrimitive { + constructor(markers) { + this._markers = markers || []; + this._paneViews = [new MarkersPaneView(this)]; + } + + setMarkers(markers) { + this._markers = markers; + if (this._requestUpdate) { + 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; + } +} + function formatDate(timestamp) { return TimezoneConfig.formatDate(timestamp); } @@ -149,13 +147,12 @@ function throttle(func, limit) { } } -import { DrawingManager } from './drawing-tools.js'; - export class TradingDashboard { constructor() { this.chart = null; this.candleSeries = null; - // Load settings from local storage or defaults + this.currentChartType = localStorage.getItem('winterfail_chart_type') || 'candlestick'; + this.symbol = localStorage.getItem('winterfail_symbol') || 'BTC'; this.currentInterval = localStorage.getItem('winterfail_interval') || '1d'; @@ -169,18 +166,17 @@ export class TradingDashboard { this.lastCandleTimestamp = null; this.simulationMarkers = []; this.avgPriceSeries = null; - this.dailyMAData = new Map(); // timestamp -> { ma44, ma125, price } + this.dailyMAData = new Map(); this.currentMouseTime = null; this.drawingManager = null; + this.seriesMap = {}; - // Throttled versions of heavy functions this.throttledOnVisibleRangeChange = throttle(this.onVisibleRangeChange.bind(this), 150); this.init(); } async loadDailyMAData() { try { - // Use 1d interval for this calculation const interval = '1d'; let candles = this.allData.get(interval); @@ -243,7 +239,6 @@ export class TradingDashboard { this.chart.removeSeries(this.avgPriceSeries); } - // Recreate series to apply custom colors per point via LineSeries data this.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, { lineWidth: 2, lineStyle: LightweightCharts.LineStyle.Solid, @@ -264,6 +259,7 @@ export class TradingDashboard { init() { this.createTimeframeButtons(); + this.createChartTypeButtons(); this.initChart(); this.initEventListeners(); this.loadInitialData(); @@ -302,6 +298,32 @@ export class TradingDashboard { }); } + createChartTypeButtons() { + const container = document.querySelector('.flex.space-x-1:not(#timeframeContainer)'); + if (!container) return; + + const chartTypes = [ + { type: 'candlestick', icon: 'show_chart', name: 'Candlestick' }, + { type: 'line', icon: 'insert_chart', name: 'Line' }, + { type: 'bar', icon: 'scatter_plot', name: 'Bar' } + ]; + + container.innerHTML = ''; + chartTypes.forEach(chartType => { + const btn = document.createElement('button'); + btn.className = 'chart-type-btn w-10 h-10 flex items-center justify-center text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded transition-colors'; + btn.dataset.chartType = chartType.type; + btn.innerHTML = `${chartType.icon}`; + btn.title = chartType.name; + if (chartType.type === this.currentChartType) { + btn.style.backgroundColor = '#2d3a4f'; + btn.classList.add('text-blue-400'); + } + btn.addEventListener('click', () => this.switchChartType(chartType.type)); + container.appendChild(btn); + }); + } + initChart() { const chartContainer = document.getElementById('chart'); @@ -324,7 +346,6 @@ export class TradingDashboard { borderColor: COLORS.tvBorder, autoScale: true, mode: 0, - // Explicitly enable pinch/scale behavior on the price scale scaleMargins: { top: 0.1, bottom: 0.1, @@ -340,30 +361,28 @@ export class TradingDashboard { return TimezoneConfig.formatTickMark(time); }, }, - localization: { - timeFormatter: (timestamp) => { - return TimezoneConfig.formatDate(timestamp * 1000); - }, - }, - handleScroll: { - mouseWheel: true, - pressedMouseMove: true, - horzTouchDrag: true, - vertTouchDrag: true, // Enabled to allow chart-internal vertical scrolling + localization: { + timeFormatter: (timestamp) => { + return TimezoneConfig.formatDate(timestamp * 1000); + }, }, - handleScale: { - axisPressedMouseMove: true, + handleScroll: { mouseWheel: true, - pinch: true, // This enables pinch-to-zoom on touch devices - }, + pressedMouseMove: true, + horzTouchDrag: true, + vertTouchDrag: true, + }, + handleScale: { + axisPressedMouseMove: true, + mouseWheel: true, + pinch: true, + }, crosshair: { mode: LightweightCharts.CrosshairMode.Normal, }, }); - // Setup price format selector change handler const priceInput = document.getElementById("priceFormatInput"); - // Load saved precision let savedPrecision = parseInt(localStorage.getItem('winterfail_price_precision')); if (isNaN(savedPrecision)) savedPrecision = 2; @@ -386,87 +405,63 @@ export class TradingDashboard { }); } - // Load candle colors from storage or default const savedUpColor = localStorage.getItem('winterfail_candle_up') || '#ff9800'; const savedDownColor = localStorage.getItem('winterfail_candle_down') || '#ff9800'; const candleUpInput = document.getElementById('candleUpColor'); const candleDownInput = document.getElementById('candleDownColor'); - if (candleUpInput) candleUpInput.value = savedUpColor; - if (candleDownInput) candleDownInput.value = savedDownColor; + if (candleUpInput && this.currentChartType === 'candlestick') candleUpInput.value = savedUpColor; + if (candleDownInput && this.currentChartType === 'candlestick') candleDownInput.value = savedDownColor; - // Calculate initial minMove based on saved precision - const initialMinMove = savedPrecision === 0 ? 1 : Number((1 / Math.pow(10, savedPrecision)).toFixed(precision)); + const initialMinMove = savedPrecision === 0 ? 1 : Number((1 / Math.pow(10, savedPrecision)).toFixed(savedPrecision)); - this.candleSeries = this.chart.addSeries(LightweightCharts.CandlestickSeries, { - upColor: savedUpColor, - downColor: savedDownColor, - borderUpColor: savedUpColor, - borderDownColor: savedDownColor, - wickUpColor: savedUpColor, - wickDownColor: savedDownColor, - lastValueVisible: false, - priceLineVisible: false, - priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove } - }, 0); + this.candleSeries = this.addSeriesByType(this.currentChartType); + + if (this.currentChartType === 'line') { + this.candleSeries.setData([]); + } - // Color change listeners - if (candleUpInput) { - candleUpInput.addEventListener('input', (e) => { - const color = e.target.value; - localStorage.setItem('winterfail_candle_up', color); - this.candleSeries.applyOptions({ - upColor: color, - borderUpColor: color, - wickUpColor: color + if (this.currentChartType === 'candlestick' || this.currentChartType === 'bar') { + if (candleUpInput) { + candleUpInput.addEventListener('input', (e) => { + const color = e.target.value; + localStorage.setItem('winterfail_candle_up', color); + this.applyColorToChartType(color, 'up'); }); - }); + } + + if (candleDownInput) { + candleDownInput.addEventListener('input', (e) => { + const color = e.target.value; + localStorage.setItem('winterfail_candle_down', color); + this.applyColorToChartType(color, 'down'); + }); + } + + if (this.candleSeries) { + this.currentPriceLine = this.candleSeries.createPriceLine({ + price: 0, + color: '#26a69a', + lineWidth: 1, + lineStyle: LightweightCharts.LineStyle.Dotted, + axisLabelVisible: true, + title: '', + }); + } } - if (candleDownInput) { - candleDownInput.addEventListener('input', (e) => { - const color = e.target.value; - localStorage.setItem('winterfail_candle_down', color); - this.candleSeries.applyOptions({ - downColor: color, - borderDownColor: color, - wickDownColor: color - }); - }); - } - - this.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, { - color: '#00bcd4', - lineWidth: 1, - lineStyle: LightweightCharts.LineStyle.Solid, - lastValueVisible: true, - priceLineVisible: false, - crosshairMarkerVisible: false, - title: '', - priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove } -}); - - this.currentPriceLine = this.candleSeries.createPriceLine({ - price: 0, - color: '#26a69a', - lineWidth: 1, - lineStyle: LightweightCharts.LineStyle.Dotted, - axisLabelVisible: true, - title: '', - }); - + this.addAvgPriceSeries(); + this.initPriceScaleControls(); this.initNavigationControls(); - // Initialize Drawing Manager this.drawingManager = new DrawingManager(this, chartContainer); window.activateDrawingTool = (tool, event) => { const e = event || window.event; this.drawingManager.setTool(tool, e); }; - // Setup price format selector change handler document.addEventListener("DOMContentLoaded", () => { const priceSelect = document.getElementById("priceFormatSelect"); if (priceSelect) { @@ -481,7 +476,6 @@ export class TradingDashboard { this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.onVisibleRangeChange.bind(this)); - // Subscribe to crosshair movement for Best Moving Averages updates this.chart.subscribeCrosshairMove(param => { if (param.time) { this.currentMouseTime = param.time; @@ -492,7 +486,6 @@ export class TradingDashboard { } }); - // Hide indicators panel when clicking on chart this.chart.subscribeClick(param => { window.hideAllPanels?.(); }); @@ -520,7 +513,6 @@ export class TradingDashboard { const btnSettings = document.getElementById('btnSettings'); const settingsPopup = document.getElementById('settingsPopup'); - // Settings Popup Toggle and Outside Click if (btnSettings && settingsPopup) { btnSettings.addEventListener('click', (e) => { e.stopPropagation(); @@ -540,14 +532,12 @@ export class TradingDashboard { } } - // Initialize state from storage this.scaleState = { autoScale: localStorage.getItem('winterfail_scale_auto') !== 'false', invertScale: localStorage.getItem('winterfail_scale_invert') === 'true', scaleMode: parseInt(localStorage.getItem('winterfail_scale_mode')) || 0 }; - // UI Helpers const updateCheckmark = (id, active) => { const el = document.getElementById(id); if (el) el.textContent = active ? '✓' : ''; @@ -561,7 +551,6 @@ export class TradingDashboard { updateCheckmark('modePercentCheck', this.scaleState.scaleMode === 2); updateCheckmark('modeIndexedCheck', this.scaleState.scaleMode === 3); - // Apply state to chart this.candleSeries.priceScale().applyOptions({ autoScale: this.scaleState.autoScale, invertScale: this.scaleState.invertScale, @@ -583,14 +572,12 @@ export class TradingDashboard { updateUI(); }; - // Add keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return; if (e.key.toLowerCase() === 'a') { window.toggleScaleOption('autoScale'); } else if (e.key.toLowerCase() === 'l') { - // Toggle between Normal (0) and Log (1) const newMode = this.scaleState.scaleMode === 1 ? 0 : 1; window.setScaleMode(newMode); } @@ -649,6 +636,8 @@ export class TradingDashboard { } initEventListeners() { + this.initChartTypeListeners(); + document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return; @@ -669,18 +658,27 @@ export class TradingDashboard { } else if (e.key === 'ArrowUp') { this.navigateToRecent(); } -}); + }); + } + + initChartTypeListeners() { + document.addEventListener('click', (e) => { + const btn = e.target.closest('.chart-type-btn'); + if (!btn) return; + + const chartType = btn.dataset.chartType; + if (chartType) { + this.switchChartType(chartType); + } + }); } clearIndicatorCaches(clearSignalState = false) { const activeIndicators = window.getActiveIndicators?.() || []; activeIndicators.forEach(indicator => { - // Always clear calculation caches indicator.cachedResults = null; indicator.cachedMeta = null; - // Only clear signal state if explicitly requested (e.g., timeframe change) - // Do not clear on new candle completion - preserve signal change tracking if (clearSignalState) { indicator.lastSignalTimestamp = null; indicator.lastSignalType = null; @@ -709,7 +707,7 @@ export class TradingDashboard { const response = await fetch(`${window.APP_CONFIG.API_BASE_URL}/candles?symbol=BTC&interval=${this.currentInterval}&limit=${limit}`); const data = await response.json(); -if (data.candles && data.candles.length > 0) { + if (data.candles && data.candles.length > 0) { const chartData = data.candles.reverse().map(c => ({ time: Math.floor(new Date(c.time).getTime() / 1000), open: parseFloat(c.open), @@ -723,7 +721,10 @@ if (data.candles && data.candles.length > 0) { const mergedData = this.mergeData(existingData, chartData); this.allData.set(this.currentInterval, mergedData); - this.candleSeries.setData(mergedData); + if (!this.candleSeries) { + console.error('[Chart] Candle series not initialized'); + return; + } if (fitToContent) { this.chart.timeScale().scrollToRealTime(); @@ -731,6 +732,28 @@ if (data.candles && data.candles.length > 0) { this.chart.timeScale().setVisibleLogicalRange(visibleRange); } + if ((this.currentChartType === 'candlestick' || this.currentChartType === 'bar') && + mergedData.length > 0 && + mergedData[0].hasOwnProperty('open')) { + this.candleSeries.setData(mergedData); + } else if (this.currentChartType === 'line' && + mergedData.length > 0 && + mergedData[0].hasOwnProperty('close')) { + const closePrices = mergedData.map(c => ({ + time: c.time, + value: c.close + })); + this.candleSeries.setData(closePrices); + } else if (mergedData.length > 0 && mergedData[0].hasOwnProperty('value')) { + this.candleSeries.setData(mergedData); + } else if (mergedData.length > 0) { + const closePrices = mergedData.map(c => ({ + time: c.time, + value: c.close || c.value + })); + this.candleSeries.setData(closePrices); + } + const latest = mergedData[mergedData.length - 1]; this.updateStats(latest); } @@ -743,14 +766,19 @@ if (data.candles && data.candles.length > 0) { } } -async loadNewData() { + async loadNewData() { if (!this.hasInitialLoad || this.isLoading) return; try { const response = await fetch(`${window.APP_CONFIG.API_BASE_URL}/candles?symbol=BTC&interval=${this.currentInterval}&limit=50`); const data = await response.json(); - if (data.candles && data.candles.length > 0) { + if (!this.candleSeries) { + console.error('[Chart] Candle series not initialized'); + return; + } + + if (data.candles && data.candles.length > 0) { const atEdge = this.isAtRightEdge(); const currentSeriesData = this.candleSeries.data(); @@ -769,43 +797,53 @@ async loadNewData() { const latest = chartData[chartData.length - 1]; - // Check if new candle detected const isNewCandle = this.lastCandleTimestamp !== null && latest.time > this.lastCandleTimestamp; if (isNewCandle) { console.log(`[NewData Load] New candle detected: ${this.lastCandleTimestamp} -> ${latest.time}`); - // Clear indicator caches but preserve signal state this.clearIndicatorCaches(false); } this.lastCandleTimestamp = latest.time; - chartData.forEach(candle => { - if (candle.time >= lastTimestamp && - !Number.isNaN(candle.time) && - !Number.isNaN(candle.open) && - !Number.isNaN(candle.high) && - !Number.isNaN(candle.low) && - !Number.isNaN(candle.close)) { - this.candleSeries.update(candle); - } - }); + if (this.currentChartType === 'candlestick' || this.currentChartType === 'bar') { + chartData.forEach(candle => { + if (candle.time >= lastTimestamp && + !Number.isNaN(candle.time) && + !Number.isNaN(candle.open) && + !Number.isNaN(candle.high) && + !Number.isNaN(candle.low) && + !Number.isNaN(candle.close)) { + this.candleSeries.update(candle); + } + }); + } else if (this.currentChartType === 'line') { + const closePrices = chartData.map(c => ({ + time: c.time, + value: c.close + })); + + const existingData = this.candleSeries.data(); + const existingTimeSet = new Set(existingData.map(d => d.time)); + + const newDataToAppend = closePrices.filter(c => !existingTimeSet.has(c.time)); + + if (newDataToAppend.length > 0) { + if (existingData.length === 0) { + this.candleSeries.setData(closePrices); + } else { + newDataToAppend.forEach(point => { + this.candleSeries.update(point); + }); + } + } + } const existingData = this.allData.get(this.currentInterval) || []; this.allData.set(this.currentInterval, this.mergeData(existingData, chartData)); - //console.log(`[NewData Load] Added ${chartData.length} new candles, total in dataset: ${this.allData.get(this.currentInterval).length}`); - - // Auto-scrolling disabled per user request - /* - if (atEdge) { - this.chart.timeScale().scrollToRealTime(); - } - */ - this.updateStats(latest); - //console.log('[Chart] Calling drawIndicatorsOnChart after new data'); window.drawIndicatorsOnChart?.(); window.updateIndicatorCandles?.(); @@ -824,7 +862,7 @@ async loadNewData() { return Array.from(dataMap.values()).sort((a, b) => a.time - b.time); } -onVisibleRangeChange() { + onVisibleRangeChange() { if (!this.hasInitialLoad || this.isLoading) { return; } @@ -859,7 +897,6 @@ onVisibleRangeChange() { } } - // Recalculate indicators when data changes if (data.length !== allData?.length) { console.log(`[VisibleRange] Chart data (${data.length}) vs dataset (${allData?.length || 0}) differ, redrawing indicators...`); } @@ -867,7 +904,7 @@ onVisibleRangeChange() { this.loadSignals().catch(e => console.error('Error loading signals:', e)); } -async loadHistoricalData(beforeTime, limit = 1000) { + async loadHistoricalData(beforeTime, limit = 1000) { if (this.isLoading) { return; } @@ -899,17 +936,39 @@ async loadHistoricalData(beforeTime, limit = 1000) { })); const existingData = this.allData.get(this.currentInterval) || []; - const mergedData = this.mergeData(existingData, chartData); - this.allData.set(this.currentInterval, mergedData); + const mergedData = this.mergeData(existingData, chartData); + this.allData.set(this.currentInterval, mergedData); + + console.log(`[Historical] SUCCESS: Added ${chartData.length} candles`); + console.log(`[Historical] Total candles in dataset: ${mergedData.length}`); + console.log(`[Historical] Oldest: ${new Date(mergedData[0]?.time * 1000).toLocaleDateString()}`); + console.log(`[Historical] Newest: ${new Date(mergedData[mergedData.length - 1]?.time * 1000).toLocaleDateString()}`); + + if (this.currentChartType === 'candlestick' || this.currentChartType === 'bar') { + if (mergedData.length > 0 && mergedData[0].hasOwnProperty('open')) { + this.candleSeries.setData(mergedData); + } else { + const ohlcData = mergedData.map(c => ({ + time: c.time, + open: c.value, + high: c.value, + low: c.value, + close: c.value + })); + this.candleSeries.setData(ohlcData); + } + } else { + if (mergedData.length > 0 && mergedData[0].hasOwnProperty('close')) { + const closePrices = mergedData.map(c => ({ + time: c.time, + value: c.close + })); + this.candleSeries.setData(closePrices); + } else if (mergedData.length > 0) { + this.candleSeries.setData(mergedData); + } + } - console.log(`[Historical] SUCCESS: Added ${chartData.length} candles`); - console.log(`[Historical] Total candles in dataset: ${mergedData.length}`); - console.log(`[Historical] Oldest: ${new Date(mergedData[0]?.time * 1000).toLocaleDateString()}`); - console.log(`[Historical] Newest: ${new Date(mergedData[mergedData.length - 1]?.time * 1000).toLocaleDateString()}`); - - this.candleSeries.setData(mergedData); - - // Recalculate indicators and signals with the expanded dataset console.log(`[Historical] Recalculating indicators...`); window.drawIndicatorsOnChart?.(); await this.loadSignals(); @@ -925,7 +984,7 @@ async loadHistoricalData(beforeTime, limit = 1000) { } } -async loadTA() { + async loadTA() { if (!this.hasInitialLoad) { const time = new Date().toLocaleTimeString(); document.getElementById('taContent').innerHTML = `
Loading technical analysis... ${time}
`; @@ -950,7 +1009,7 @@ async loadTA() { } } -async loadSignals() { + async loadSignals() { try { this.indicatorSignals = calculateAllIndicatorSignals(); this.summarySignal = calculateSummarySignal(this.indicatorSignals); @@ -968,18 +1027,14 @@ async loadSignals() { let markers = calculateSignalMarkers(candles); - // Merge simulation markers if present if (this.simulationMarkers && this.simulationMarkers.length > 0) { markers = [...markers, ...this.simulationMarkers]; } - // CRITICAL: Filter out any markers with invalid timestamps before passing to chart markers = markers.filter(m => m && m.time !== null && m.time !== undefined && !isNaN(m.time)); - // Re-sort combined markers by time markers.sort((a, b) => a.time - b.time); - // Use custom primitive for markers in v5 try { if (!this.markerPrimitive) { this.markerPrimitive = new SeriesMarkersPrimitive(); @@ -1012,7 +1067,6 @@ async loadSignals() { const signalColor = indSignal.signal === 'buy' ? '#26a69a' : indSignal.signal === 'sell' ? '#ef5350' : '#787b86'; const lastSignalDate = indSignal.lastSignalDate ? formatDate(indSignal.lastSignalDate * 1000) : '-'; - // Format params as "MA(44)" style let paramsStr = ''; if (indSignal.params !== null && indSignal.params !== undefined) { paramsStr = `(${indSignal.params})`; @@ -1031,16 +1085,13 @@ async loadSignals() { const summaryBadge = ''; - // Best Moving Averages Logic (1D based) let displayMA = { ma44: null, ma125: null, price: null, time: null }; if (this.currentMouseTime && this.dailyMAData.size > 0) { - // Find the 1D candle that includes this mouse time const dayTimestamp = Math.floor(this.currentMouseTime / 86400) * 86400; if (this.dailyMAData.has(dayTimestamp)) { displayMA = { ...this.dailyMAData.get(dayTimestamp), time: dayTimestamp }; } else { - // Fallback to latest if specific day not found const keys = Array.from(this.dailyMAData.keys()).sort((a, b) => b - a); const latestKey = keys[0]; displayMA = { ...this.dailyMAData.get(latestKey), time: latestKey }; @@ -1089,27 +1140,27 @@ async loadSignals() {
-
-
Support / Resistance
-
- Resistance - ${data.levels.resistance.toFixed(2)} -
-
- Support - ${data.levels.support.toFixed(2)} -
-
+
+
Support / Resistance
+
+ Resistance + ${data.levels ? data.levels.resistance.toFixed(2) : 'N/A'} +
+
+ Support + ${data.levels ? data.levels.support.toFixed(2) : 'N/A'} +
+
-
-
Price Position
-
-
-
-
- ${data.levels.position_in_range.toFixed(0)}% in range -
-
+
+
Price Position
+
+
+
+
+ ${data.levels ? data.levels.position_in_range.toFixed(0) : '--'}% in range +
+
`; } @@ -1150,22 +1201,20 @@ async loadSignals() { } } -switchTimeframe(interval) { + switchTimeframe(interval) { if (!this.intervals.includes(interval) || interval === this.currentInterval) return; const oldInterval = this.currentInterval; this.currentInterval = interval; - localStorage.setItem('winterfail_interval', interval); // Save setting + localStorage.setItem('winterfail_interval', interval); this.hasInitialLoad = false; document.querySelectorAll('.timeframe-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.interval === interval); }); - // Clear indicator caches and signal state before switching timeframe this.clearIndicatorCaches(true); - // Clear old interval data, not new interval this.allData.delete(oldInterval); this.lastCandleTimestamp = null; @@ -1174,9 +1223,187 @@ switchTimeframe(interval) { window.clearSimulationResults?.(); window.updateTimeframeDisplay?.(); - // Notify indicators of timeframe change for recalculation window.onTimeframeChange?.(interval); } + + switchChartType(chartType) { + if (chartType === this.currentChartType) return; + + this.currentChartType = chartType; + localStorage.setItem('winterfail_chart_type', chartType); + + const allData = this.allData.get(this.currentInterval) || []; + const currentData = this.candleSeries ? this.candleSeries.data() : allData; + + this.chart.removeSeries(this.candleSeries); + delete this.seriesMap.candlestick; + + if (this.avgPriceSeries) { + this.chart.removeSeries(this.avgPriceSeries); + this.avgPriceSeries = null; + } + + if (this.currentPriceLine) { + this.currentPriceLine.applyOptions({ + visible: false + }); + } + + const newSeries = this.addSeriesByType(chartType); + if (!newSeries) { + console.error('[Chart] Failed to create series for type:', chartType); + return; + } + this.candleSeries = newSeries; + + this.updateChartTypeButtons(); + + if (currentData && currentData.length > 0) { + const chartData = this.allData.get(this.currentInterval) || currentData; + const hasOHLC = chartData.length > 0 && chartData[0].hasOwnProperty('open'); + + if (chartType === 'candlestick' || chartType === 'bar') { + if (hasOHLC) { + this.candleSeries.setData(chartData); + } else { + const ohlcData = chartData.map(c => ({ + time: c.time, + open: c.value, + high: c.value, + low: c.value, + close: c.value + })); + this.candleSeries.setData(ohlcData); + } + } else if (chartType === 'line') { + const closePrices = chartData.length > 0 && chartData[0].hasOwnProperty('close') + ? chartData.map(c => ({ + time: c.time, + value: c.close + })) + : chartData; + this.candleSeries.setData(closePrices); + } + } + + window.drawIndicatorsOnChart?.(); + } + + addSeriesByType(chartType) { + let series; + const savedPrecision = parseInt(localStorage.getItem('winterfail_price_precision')) || 2; + const initialMinMove = savedPrecision === 0 ? 1 : Number((1 / Math.pow(10, savedPrecision)).toFixed(savedPrecision)); + + switch (chartType) { + case 'candlestick': + const candleUpColor = localStorage.getItem('winterfail_candle_up') || '#ff9800'; + const candleDownColor = localStorage.getItem('winterfail_candle_down') || '#ff9800'; + series = this.chart.addSeries(LightweightCharts.CandlestickSeries, { + upColor: candleUpColor, + downColor: candleDownColor, + borderUpColor: candleUpColor, + borderDownColor: candleDownColor, + wickUpColor: candleUpColor, + wickDownColor: candleDownColor, + lastValueVisible: false, + priceLineVisible: false, + priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove } + }); + break; + case 'line': + series = this.chart.addSeries(LightweightCharts.LineSeries, { + color: '#2196f3', + lineWidth: 2, + lineStyle: LightweightCharts.LineStyle.Solid, + lastValueVisible: true, + priceLineVisible: false, + crosshairMarkerVisible: true, + priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove } + }); + break; + case 'bar': + const barUpColor = localStorage.getItem('winterfail_candle_up') || '#ff9800'; + const barDownColor = localStorage.getItem('winterfail_candle_down') || '#ff9800'; + series = this.chart.addSeries(LightweightCharts.BarSeries, { + upColor: barUpColor, + downColor: barDownColor, + barColors: { + up: barUpColor, + down: barDownColor + }, + lastValueVisible: false, + priceLineVisible: false, + priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove } + }); + break; + } + + this.seriesMap[chartType] = series; + return series; + } + + addAvgPriceSeries() { + const savedPrecision = parseInt(localStorage.getItem('winterfail_price_precision')) || 2; + const initialMinMove = savedPrecision === 0 ? 1 : Number((1 / Math.pow(10, savedPrecision)).toFixed(savedPrecision)); + + this.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, { + color: '#00bcd4', + lineWidth: 1, + lineStyle: LightweightCharts.LineStyle.Solid, + lastValueVisible: true, + priceLineVisible: false, + crosshairMarkerVisible: false, + title: '', + priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove } + }); + } + + updateChartTypeButtons() { + const buttons = document.querySelectorAll('.chart-type-btn'); + buttons.forEach(btn => { + btn.classList.remove('text-blue-400'); + btn.style.backgroundColor = ''; + if (btn.dataset.chartType === this.currentChartType) { + btn.style.backgroundColor = '#2d3a4f'; + btn.classList.add('text-blue-400'); + } + }); + } + + getChartTypeData() { + if (!this.candleSeries) return []; + return this.candleSeries.data(); + } + + applyColorToChartType(color, direction) { + if (!this.candleSeries) return; + + if (this.currentChartType === 'candlestick') { + const options = {}; + if (direction === 'up') { + options.upColor = color; + options.borderUpColor = color; + options.wickUpColor = color; + } else { + options.downColor = color; + options.borderDownColor = color; + options.wickDownColor = color; + } + this.candleSeries.applyOptions(options); + } else if (this.currentChartType === 'bar') { + const options = { + barColors: {} + }; + if (direction === 'up') { + options.upColor = color; + options.barColors.up = color; + } else { + options.downColor = color; + options.barColors.down = color; + } + this.candleSeries.applyOptions(options); + } + } } export function refreshTA() {