UI tweaks
This commit is contained in:
@@ -2550,12 +2550,18 @@ class RealTrainingAdapter:
|
|||||||
# This would need separate denormalization based on reference price
|
# This would need separate denormalization based on reference price
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return {
|
result_dict = {
|
||||||
'action': action,
|
'action': action,
|
||||||
'confidence': confidence,
|
'confidence': confidence,
|
||||||
'predicted_price': predicted_price,
|
'predicted_price': predicted_price,
|
||||||
'predicted_candle': predicted_candles_denorm
|
'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
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -123,17 +123,26 @@ class ChartManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get last timestamp from current data
|
const lastIdx = chart.data.timestamps.length - 1;
|
||||||
const lastTimestamp = chart.data.timestamps[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', {
|
const response = await fetch('/api/chart-data', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
symbol: window.appState?.currentSymbol || 'ETH/USDT',
|
symbol: window.appState?.currentSymbol || 'ETH/USDT',
|
||||||
timeframes: [timeframe],
|
timeframes: [timeframe],
|
||||||
start_time: lastTimestamp,
|
start_time: queryTime,
|
||||||
limit: 50, // Small limit for incremental update
|
limit: 50, // Small limit for incremental update
|
||||||
direction: 'after'
|
direction: 'after'
|
||||||
})
|
})
|
||||||
@@ -148,74 +157,72 @@ class ChartManager {
|
|||||||
if (result.success && result.chart_data && result.chart_data[timeframe]) {
|
if (result.success && result.chart_data && result.chart_data[timeframe]) {
|
||||||
const newData = result.chart_data[timeframe];
|
const newData = result.chart_data[timeframe];
|
||||||
|
|
||||||
// If we got new data
|
|
||||||
if (newData.timestamps.length > 0) {
|
if (newData.timestamps.length > 0) {
|
||||||
// Filter out duplicates just in case
|
// Smart Merge:
|
||||||
const uniqueIndices = [];
|
// We want to update any existing candles that have changed (live candle)
|
||||||
const lastTime = new Date(lastTimestamp).getTime();
|
// and append any new ones.
|
||||||
|
|
||||||
|
// 1. Create map of new data for quick lookup
|
||||||
|
const newMap = new Map();
|
||||||
newData.timestamps.forEach((ts, i) => {
|
newData.timestamps.forEach((ts, i) => {
|
||||||
if (new Date(ts).getTime() > lastTime) {
|
newMap.set(ts, {
|
||||||
uniqueIndices.push(i);
|
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) {
|
} catch (error) {
|
||||||
@@ -1882,6 +1889,11 @@ class ChartManager {
|
|||||||
if (predictions.transformer) {
|
if (predictions.transformer) {
|
||||||
this._addTransformerPrediction(predictions.transformer, predictionShapes, predictionAnnotations);
|
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
|
// Add ghost candle if available
|
||||||
if (predictions.transformer.predicted_candle) {
|
if (predictions.transformer.predicted_candle) {
|
||||||
// Check if we have prediction for this timeframe
|
// 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) {
|
_addGhostCandlePrediction(candleData, timeframe, traces) {
|
||||||
// candleData is [Open, High, Low, Close, Volume]
|
// candleData is [Open, High, Low, Close, Volume]
|
||||||
// We need to determine the timestamp for this ghost candle
|
// We need to determine the timestamp for this ghost candle
|
||||||
@@ -1971,7 +2050,7 @@ class ChartManager {
|
|||||||
line: { color: color, width: 1 },
|
line: { color: color, width: 1 },
|
||||||
fillcolor: color
|
fillcolor: color
|
||||||
},
|
},
|
||||||
opacity: 0.3, // 30% transparent
|
opacity: 0.6, // 60% transparent
|
||||||
hoverinfo: 'x+y+text',
|
hoverinfo: 'x+y+text',
|
||||||
text: ['Predicted Next Candle']
|
text: ['Predicted Next Candle']
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user