UI tweaks

This commit is contained in:
Dobromir Popov
2025-11-22 02:00:26 +02:00
parent d4c0483675
commit 47840a5f8e
2 changed files with 153 additions and 68 deletions

View File

@@ -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<br>${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']
};