From 47840a5f8e308aea9845dfb198f68326fd52a8ef Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 22 Nov 2025 02:00:26 +0200 Subject: [PATCH] UI tweaks --- ANNOTATE/core/real_training_adapter.py | 8 +- ANNOTATE/web/static/js/chart_manager.js | 213 ++++++++++++++++-------- 2 files changed, 153 insertions(+), 68 deletions(-) diff --git a/ANNOTATE/core/real_training_adapter.py b/ANNOTATE/core/real_training_adapter.py index edaf649..fbb6798 100644 --- a/ANNOTATE/core/real_training_adapter.py +++ b/ANNOTATE/core/real_training_adapter.py @@ -2550,12 +2550,18 @@ class RealTrainingAdapter: # This would need separate denormalization based on reference price pass - return { + result_dict = { 'action': action, 'confidence': confidence, 'predicted_price': predicted_price, 'predicted_candle': predicted_candles_denorm } + + # Include trend vector if available + if 'trend_vector' in outputs: + result_dict['trend_vector'] = outputs['trend_vector'] + + return result_dict return None except Exception as e: diff --git a/ANNOTATE/web/static/js/chart_manager.js b/ANNOTATE/web/static/js/chart_manager.js index fdc4ad9..3aeae6a 100644 --- a/ANNOTATE/web/static/js/chart_manager.js +++ b/ANNOTATE/web/static/js/chart_manager.js @@ -123,17 +123,26 @@ class ChartManager { } try { - // Get last timestamp from current data - const lastTimestamp = chart.data.timestamps[chart.data.timestamps.length - 1]; + const lastIdx = chart.data.timestamps.length - 1; + const lastTimestamp = chart.data.timestamps[lastIdx]; - // Fetch only data AFTER last timestamp + // Request overlap to ensure we capture updates to the last candle + // Go back 2 intervals to be safe + const lastTimeMs = new Date(lastTimestamp).getTime(); + let lookbackMs = 2000; // Default 2s + if (timeframe === '1m') lookbackMs = 120000; + if (timeframe === '1h') lookbackMs = 7200000; + + const queryTime = new Date(lastTimeMs - lookbackMs).toISOString(); + + // Fetch data starting from overlap point const response = await fetch('/api/chart-data', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ symbol: window.appState?.currentSymbol || 'ETH/USDT', timeframes: [timeframe], - start_time: lastTimestamp, + start_time: queryTime, limit: 50, // Small limit for incremental update direction: 'after' }) @@ -148,74 +157,72 @@ class ChartManager { if (result.success && result.chart_data && result.chart_data[timeframe]) { const newData = result.chart_data[timeframe]; - // If we got new data if (newData.timestamps.length > 0) { - // Filter out duplicates just in case - const uniqueIndices = []; - const lastTime = new Date(lastTimestamp).getTime(); + // Smart Merge: + // We want to update any existing candles that have changed (live candle) + // and append any new ones. + // 1. Create map of new data for quick lookup + const newMap = new Map(); newData.timestamps.forEach((ts, i) => { - if (new Date(ts).getTime() > lastTime) { - uniqueIndices.push(i); + newMap.set(ts, { + open: newData.open[i], + high: newData.high[i], + low: newData.low[i], + close: newData.close[i], + volume: newData.volume[i] + }); + }); + + // 2. Update existing candles in place if they exist in new data + // Iterate backwards to optimize for recent updates + let updatesCount = 0; + for (let i = chart.data.timestamps.length - 1; i >= 0; i--) { + const ts = chart.data.timestamps[i]; + if (newMap.has(ts)) { + const val = newMap.get(ts); + chart.data.open[i] = val.open; + chart.data.high[i] = val.high; + chart.data.low[i] = val.low; + chart.data.close[i] = val.close; + chart.data.volume[i] = val.volume; + newMap.delete(ts); // Remove from map so we know what remains is truly new + updatesCount++; + } else { + // If we went back past the overlap window, stop + if (new Date(ts).getTime() < new Date(newData.timestamps[0]).getTime()) break; } - }); - - if (uniqueIndices.length === 0) return; - - const uniqueData = { - timestamps: uniqueIndices.map(i => newData.timestamps[i]), - open: uniqueIndices.map(i => newData.open[i]), - high: uniqueIndices.map(i => newData.high[i]), - low: uniqueIndices.map(i => newData.low[i]), - close: uniqueIndices.map(i => newData.close[i]), - volume: uniqueIndices.map(i => newData.volume[i]) - }; - - // Update chart using extendTraces - const plotId = chart.plotId; - - Plotly.extendTraces(plotId, { - x: [uniqueData.timestamps], - open: [uniqueData.open], - high: [uniqueData.high], - low: [uniqueData.low], - close: [uniqueData.close] - }, [0]); - - // Update volume - const volumeColors = uniqueData.close.map((close, i) => { - return close >= uniqueData.open[i] ? '#10b981' : '#ef4444'; - }); - - Plotly.extendTraces(plotId, { - x: [uniqueData.timestamps], - y: [uniqueData.volume], - 'marker.color': [volumeColors] - }, [1]); - - // Update local data cache - chart.data.timestamps.push(...uniqueData.timestamps); - chart.data.open.push(...uniqueData.open); - chart.data.high.push(...uniqueData.high); - chart.data.low.push(...uniqueData.low); - chart.data.close.push(...uniqueData.close); - chart.data.volume.push(...uniqueData.volume); - - // Keep memory usage in check (limit to 5000 candles) - const MAX_CANDLES = 5000; - if (chart.data.timestamps.length > MAX_CANDLES) { - const dropCount = chart.data.timestamps.length - MAX_CANDLES; - chart.data.timestamps.splice(0, dropCount); - chart.data.open.splice(0, dropCount); - chart.data.high.splice(0, dropCount); - chart.data.low.splice(0, dropCount); - chart.data.close.splice(0, dropCount); - chart.data.volume.splice(0, dropCount); - - // Note: Plotly.relayout could be used to shift window, but extending is fine for visual updates } - console.log(`Appended ${uniqueData.timestamps.length} new candles to ${timeframe} chart`); + // 3. Append remaining new candles + // Convert map keys back to sorted arrays + const remainingTimestamps = Array.from(newMap.keys()).sort(); + + if (remainingTimestamps.length > 0) { + remainingTimestamps.forEach(ts => { + const val = newMap.get(ts); + chart.data.timestamps.push(ts); + chart.data.open.push(val.open); + chart.data.high.push(val.high); + chart.data.low.push(val.low); + chart.data.close.push(val.close); + chart.data.volume.push(val.volume); + }); + } + + // 4. Recalculate and Redraw + if (updatesCount > 0 || remainingTimestamps.length > 0) { + this.recalculatePivots(timeframe, chart.data); + this.updateSingleChart(timeframe, chart.data); + + window.liveUpdateCount = (window.liveUpdateCount || 0) + 1; + const counterEl = document.getElementById('live-updates-count') || document.getElementById('live-update-count'); + if (counterEl) { + counterEl.textContent = window.liveUpdateCount + ' updates'; + } + + console.debug(`Incrementally updated ${timeframe} chart`); + } } } } catch (error) { @@ -1882,6 +1889,11 @@ class ChartManager { if (predictions.transformer) { this._addTransformerPrediction(predictions.transformer, predictionShapes, predictionAnnotations); + // Add trend vector visualization + if (predictions.transformer.trend_vector) { + this._addTrendPrediction(predictions.transformer.trend_vector, predictionShapes, predictionAnnotations); + } + // Add ghost candle if available if (predictions.transformer.predicted_candle) { // Check if we have prediction for this timeframe @@ -1924,6 +1936,73 @@ class ChartManager { } } + _addTrendPrediction(trendVector, shapes, annotations) { + // trendVector contains: angle_degrees, steepness, direction, price_delta + // We visualize this as a ray from current price + + // Need current candle close and timestamp + const timeframe = '1m'; // Default to 1m for now + const chart = this.charts[timeframe]; + if (!chart || !chart.data) return; + + const lastIdx = chart.data.timestamps.length - 1; + const lastTimestamp = new Date(chart.data.timestamps[lastIdx]); + const currentPrice = chart.data.close[lastIdx]; + + // Calculate target point + // steepness is [0, 1], angle is in degrees + // We project ahead by e.g. 5 minutes + const projectionMinutes = 5; + const targetTime = new Date(lastTimestamp.getTime() + projectionMinutes * 60000); + + let targetPrice = currentPrice; + + if (trendVector.price_delta) { + // If model provided explicit price delta (denormalized ideally) + // Note: backend sends price_delta as normalized value usually? + // But trend_vector dict constructed in model usually has raw value if we didn't normalize? + // Actually, checking model code, it returns raw tensor value. + // If normalized, it's small. If real price, it's big. + // Heuristic: if delta is < 1.0 and price is > 100, it's likely normalized or percentage. + + // Safer to use angle/steepness if delta is ambiguous, but let's try to interpret direction + const direction = trendVector.direction === 'up' ? 1 : (trendVector.direction === 'down' ? -1 : 0); + const steepness = trendVector.steepness || 0; // 0 to 1 + + // Estimate price change based on steepness (max 2% move in 5 mins) + const maxChange = 0.02 * currentPrice; + const projectedChange = maxChange * steepness * direction; + targetPrice = currentPrice + projectedChange; + } + + // Draw trend ray + shapes.push({ + type: 'line', + x0: lastTimestamp, + y0: currentPrice, + x1: targetTime, + y1: targetPrice, + line: { + color: 'rgba(255, 255, 0, 0.6)', // Yellow for trend + width: 2, + dash: 'dot' + } + }); + + // Add target annotation + annotations.push({ + x: targetTime, + y: targetPrice, + text: `Target
${targetPrice.toFixed(2)}`, + showarrow: true, + arrowhead: 2, + ax: 0, + ay: -20, + font: { size: 10, color: '#fbbf24' }, + bgcolor: 'rgba(0,0,0,0.5)' + }); + } + _addGhostCandlePrediction(candleData, timeframe, traces) { // candleData is [Open, High, Low, Close, Volume] // We need to determine the timestamp for this ghost candle @@ -1971,7 +2050,7 @@ class ChartManager { line: { color: color, width: 1 }, fillcolor: color }, - opacity: 0.3, // 30% transparent + opacity: 0.6, // 60% transparent hoverinfo: 'x+y+text', text: ['Predicted Next Candle'] };