UI tweaks
This commit is contained in:
@@ -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']
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user