This commit is contained in:
Dobromir Popov
2025-11-22 02:39:23 +02:00
parent 47840a5f8e
commit 7a219b5ebc
5 changed files with 534 additions and 88 deletions

View File

@@ -310,19 +310,16 @@ class HistoricalDataLoader:
}
binance_timeframe = timeframe_map.get(timeframe, '1m')
# Build API parameters
# Build initial API parameters
params = {
'symbol': binance_symbol,
'interval': binance_timeframe,
'limit': min(limit, 1000) # Binance max is 1000
'interval': binance_timeframe
}
# Add time range parameters if specified
if direction == 'before' and end_time:
# Get data ending at end_time
params['endTime'] = int(end_time.timestamp() * 1000)
elif direction == 'after' and start_time:
# Get data starting at start_time
params['startTime'] = int(start_time.timestamp() * 1000)
elif start_time:
params['startTime'] = int(start_time.timestamp() * 1000)
@@ -335,40 +332,91 @@ class HistoricalDataLoader:
logger.info(f"Fetching from Binance: {symbol} {timeframe} (direction={direction}, limit={limit})")
response = rate_limiter.make_request('binance_api', url, 'GET', params=params)
# Pagination variables
all_dfs = []
total_fetched = 0
is_fetching_forward = (direction == 'after')
if response is None or response.status_code != 200:
logger.warning(f"Binance API failed, trying MEXC...")
# Try MEXC as fallback
return self._fetch_from_mexc_with_time_range(
symbol, timeframe, start_time, end_time, limit, direction
)
# Fetch loop
while total_fetched < limit:
# Calculate batch limit (max 1000 per request)
batch_limit = min(limit - total_fetched, 1000)
params['limit'] = batch_limit
response = rate_limiter.make_request('binance_api', url, 'GET', params=params)
if response is None or response.status_code != 200:
if total_fetched == 0:
logger.warning(f"Binance API failed, trying MEXC...")
return self._fetch_from_mexc_with_time_range(
symbol, timeframe, start_time, end_time, limit, direction
)
else:
logger.warning("Binance API failed during pagination, returning partial data")
break
data = response.json()
if not data:
if total_fetched == 0:
logger.warning(f"No data returned from Binance for {symbol} {timeframe}")
return None
else:
break
# Convert to DataFrame
df = pd.DataFrame(data, columns=[
'timestamp', 'open', 'high', 'low', 'close', 'volume',
'close_time', 'quote_volume', 'trades', 'taker_buy_base',
'taker_buy_quote', 'ignore'
])
# Process columns
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True)
for col in ['open', 'high', 'low', 'close', 'volume']:
df[col] = df[col].astype(float)
# Keep only OHLCV columns
df = df[['timestamp', 'open', 'high', 'low', 'close', 'volume']]
df = df.set_index('timestamp')
df = df.sort_index()
if df.empty:
break
all_dfs.append(df)
total_fetched += len(df)
# Prepare for next batch
if total_fetched >= limit:
break
# Update params for next iteration
if is_fetching_forward:
# Next batch starts after the last candle
last_ts = df.index[-1]
params['startTime'] = int(last_ts.value / 10**6) + 1
# Check if we exceeded end_time
if 'endTime' in params and params['startTime'] > params['endTime']:
break
else:
# Next batch ends before the first candle
first_ts = df.index[0]
params['endTime'] = int(first_ts.value / 10**6) - 1
# Check if we exceeded start_time
if 'startTime' in params and params['endTime'] < params['startTime']:
break
data = response.json()
if not data:
logger.warning(f"No data returned from Binance for {symbol} {timeframe}")
# Combine all batches
if not all_dfs:
return None
final_df = pd.concat(all_dfs)
final_df = final_df.sort_index()
final_df = final_df[~final_df.index.duplicated(keep='first')]
# Convert to DataFrame
df = pd.DataFrame(data, columns=[
'timestamp', 'open', 'high', 'low', 'close', 'volume',
'close_time', 'quote_volume', 'trades', 'taker_buy_base',
'taker_buy_quote', 'ignore'
])
# Process columns
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True)
for col in ['open', 'high', 'low', 'close', 'volume']:
df[col] = df[col].astype(float)
# Keep only OHLCV columns
df = df[['timestamp', 'open', 'high', 'low', 'close', 'volume']]
df = df.set_index('timestamp')
df = df.sort_index()
logger.info(f" Fetched {len(df)} candles from Binance for {symbol} {timeframe}")
return df
logger.info(f" Fetched {len(final_df)} candles from Binance for {symbol} {timeframe} (requested {limit})")
return final_df
except Exception as e:
logger.error(f"Error fetching from exchange API: {e}")

View File

@@ -2200,9 +2200,18 @@ class AnnotationDashboard:
signals = self.training_adapter.get_latest_signals()
# Get metrics from active inference sessions
metrics = {'accuracy': 0.0, 'loss': 0.0}
if hasattr(self.training_adapter, 'inference_sessions'):
for session in self.training_adapter.inference_sessions.values():
if 'metrics' in session:
metrics = session['metrics']
break
return jsonify({
'success': True,
'signals': signals
'signals': signals,
'metrics': metrics
})
except Exception as e:

View File

@@ -11,6 +11,7 @@ class ChartManager {
this.syncedTime = null;
this.updateTimers = {}; // Track auto-update timers
this.autoUpdateEnabled = false; // Auto-update state
this.liveMetricsOverlay = null; // Live metrics display overlay
console.log('ChartManager initialized with timeframes:', timeframes);
}
@@ -27,28 +28,19 @@ class ChartManager {
this.autoUpdateEnabled = true;
console.log('Starting chart auto-update...');
// Update 1s chart every 2 seconds (was 20s)
// Update 1s chart every 1 second (was 2s) for live updates
if (this.timeframes.includes('1s')) {
this.updateTimers['1s'] = setInterval(() => {
this.updateChartIncremental('1s');
}, 2000); // 2 seconds
}, 1000); // 1 second
}
// Update 1m chart - sync to whole minutes + every 5s (was 20s)
// Update 1m chart - every 1 second for live candle updates
if (this.timeframes.includes('1m')) {
// Calculate ms until next whole minute
const now = new Date();
const msUntilNextMinute = (60 - now.getSeconds()) * 1000 - now.getMilliseconds();
// Update on next whole minute
setTimeout(() => {
// We can poll every second for live updates
this.updateTimers['1m'] = setInterval(() => {
this.updateChartIncremental('1m');
// Then update every 5s
this.updateTimers['1m'] = setInterval(() => {
this.updateChartIncremental('1m');
}, 5000); // 5 seconds
}, msUntilNextMinute);
}, 1000);
}
console.log('Auto-update enabled for:', Object.keys(this.updateTimers));
@@ -130,6 +122,7 @@ class ChartManager {
// Go back 2 intervals to be safe
const lastTimeMs = new Date(lastTimestamp).getTime();
let lookbackMs = 2000; // Default 2s
if (timeframe === '1s') lookbackMs = 5000; // Increased lookback for 1s to prevent misses
if (timeframe === '1m') lookbackMs = 120000;
if (timeframe === '1h') lookbackMs = 7200000;
@@ -157,6 +150,11 @@ class ChartManager {
if (result.success && result.chart_data && result.chart_data[timeframe]) {
const newData = result.chart_data[timeframe];
console.log(`[${timeframe}] Received ${newData.timestamps.length} candles from API`);
if (newData.timestamps.length > 0) {
console.log(`[${timeframe}] First: ${newData.timestamps[0]}, Last: ${newData.timestamps[newData.timestamps.length - 1]}`);
}
if (newData.timestamps.length > 0) {
// Smart Merge:
// We want to update any existing candles that have changed (live candle)
@@ -212,6 +210,8 @@ class ChartManager {
// 4. Recalculate and Redraw
if (updatesCount > 0 || remainingTimestamps.length > 0) {
console.log(`[${timeframe}] Chart update: ${updatesCount} updated, ${remainingTimestamps.length} new candles`);
this.recalculatePivots(timeframe, chart.data);
this.updateSingleChart(timeframe, chart.data);
@@ -221,7 +221,9 @@ class ChartManager {
counterEl.textContent = window.liveUpdateCount + ' updates';
}
console.debug(`Incrementally updated ${timeframe} chart`);
console.log(`[${timeframe}] Chart updated successfully. Total candles: ${chart.data.timestamps.length}`);
} else {
console.log(`[${timeframe}] No updates needed (no changes detected)`);
}
}
}
@@ -410,7 +412,8 @@ class ChartManager {
gridcolor: '#374151',
color: '#9ca3af',
showgrid: true,
zeroline: false
zeroline: false,
fixedrange: false
},
yaxis: {
title: {
@@ -421,7 +424,8 @@ class ChartManager {
color: '#9ca3af',
showgrid: true,
zeroline: false,
domain: [0.3, 1]
domain: [0.3, 1],
fixedrange: false // Allow vertical scaling
},
yaxis2: {
title: {
@@ -432,7 +436,8 @@ class ChartManager {
color: '#9ca3af',
showgrid: false,
zeroline: false,
domain: [0, 0.25]
domain: [0, 0.25],
fixedrange: false
},
plot_bgcolor: '#1f2937',
paper_bgcolor: '#1f2937',
@@ -448,17 +453,30 @@ class ChartManager {
const config = {
responsive: true,
displayModeBar: true,
modeBarButtonsToRemove: ['lasso2d', 'select2d', 'autoScale2d'],
modeBarButtonsToRemove: ['lasso2d', 'select2d'], // Allow autoScale2d
displaylogo: false,
scrollZoom: true,
// Performance optimizations
doubleClick: false,
showAxisDragHandles: false,
doubleClick: 'reset', // Enable double-click reset
showAxisDragHandles: true, // Enable axis dragging
showAxisRangeEntryBoxes: false
};
// Prepare chart data with pivot bounds
const chartData = [candlestickTrace, volumeTrace];
// Add pivot dots trace (trace index 2)
const pivotDotsTrace = {
x: [],
y: [],
text: [],
marker: { color: [], size: [], symbol: [] },
mode: 'markers',
hoverinfo: 'text',
showlegend: false,
yaxis: 'y'
};
chartData.push(pivotDotsTrace);
// Add pivot markers from chart data
const shapes = [];
@@ -566,13 +584,16 @@ class ChartManager {
}
});
// Add pivot dots trace if we have any
if (pivotDots.x.length > 0) {
chartData.push(pivotDots);
}
console.log(`Added ${shapes.length} pivot levels to ${timeframe} chart`);
}
// Populate pivot dots trace (trace index 2) with data
if (pivotDots.x.length > 0) {
pivotDotsTrace.x = pivotDots.x;
pivotDotsTrace.y = pivotDots.y;
pivotDotsTrace.text = pivotDots.text;
pivotDotsTrace.marker = pivotDots.marker;
}
// Add shapes and annotations to layout
if (shapes.length > 0) {
@@ -1857,8 +1878,9 @@ class ChartManager {
if (!predictions) return;
try {
// Update predictions on 1m chart (primary timeframe for predictions)
const timeframe = '1m';
// Use the currently active timeframe from app state
// This ensures predictions appear on the chart the user is watching (e.g., '1s')
const timeframe = window.appState?.currentTimeframes?.[0] || '1m';
const chart = this.charts[timeframe];
if (!chart) return;
@@ -1894,12 +1916,55 @@ class ChartManager {
this._addTrendPrediction(predictions.transformer.trend_vector, predictionShapes, predictionAnnotations);
}
// Add ghost candle if available
// Handle Predicted Candles
if (predictions.transformer.predicted_candle) {
// Check if we have prediction for this timeframe
const candleData = predictions.transformer.predicted_candle[timeframe];
if (candleData) {
this._addGhostCandlePrediction(candleData, timeframe, predictionTraces);
// Get the prediction timestamp from the model (when inference was made)
const predictionTimestamp = predictions.transformer.timestamp || new Date().toISOString();
// Calculate the target timestamp (when this prediction is for)
// This should be the NEXT candle after the inference time
const inferenceTime = new Date(predictionTimestamp);
let targetTimestamp;
if (timeframe === '1s') {
targetTimestamp = new Date(inferenceTime.getTime() + 1000);
} else if (timeframe === '1m') {
targetTimestamp = new Date(inferenceTime.getTime() + 60000);
} else if (timeframe === '1h') {
targetTimestamp = new Date(inferenceTime.getTime() + 3600000);
} else {
targetTimestamp = new Date(inferenceTime.getTime() + 60000);
}
// 1. Next Candle Prediction (Ghost)
// Show the prediction at its proper timestamp
this._addGhostCandlePrediction(candleData, timeframe, predictionTraces, targetTimestamp);
// 2. Store as "Last Prediction" for this timeframe
// This allows us to visualize the "Shadow" (prediction vs actual) on the next tick
if (!this.lastPredictions) this.lastPredictions = {};
this.lastPredictions[timeframe] = {
timestamp: targetTimestamp.toISOString(),
candle: candleData,
inferenceTime: predictionTimestamp
};
console.log(`[${timeframe}] Ghost candle prediction placed at ${targetTimestamp.toISOString()} (inference at ${predictionTimestamp})`);
}
}
// 3. Render "Shadow Candle" (Previous Prediction for Current Candle)
// If we have a stored prediction that matches the CURRENT candle time, show it
if (this.lastPredictions && this.lastPredictions[timeframe]) {
const lastPred = this.lastPredictions[timeframe];
const currentTimestamp = chart.data.timestamps[chart.data.timestamps.length - 1];
// Compare timestamps (allow small diff for jitter)
if (Math.abs(new Date(lastPred.timestamp).getTime() - new Date(currentTimestamp).getTime()) < 1000) {
this._addShadowCandlePrediction(lastPred.candle, currentTimestamp, predictionTraces);
}
}
}
@@ -1914,12 +1979,12 @@ class ChartManager {
// Add prediction traces (ghost candles)
if (predictionTraces.length > 0) {
// Remove existing ghost traces safely
// We iterate backwards to avoid index shifting issues when deleting
// Remove existing ghost/shadow traces safely
const currentTraces = plotElement.data.length;
const indicesToRemove = [];
for (let i = currentTraces - 1; i >= 0; i--) {
if (plotElement.data[i].name === 'Ghost Prediction') {
const name = plotElement.data[i].name;
if (name === 'Ghost Prediction' || name === 'Shadow Prediction') {
indicesToRemove.push(i);
}
}
@@ -2003,26 +2068,32 @@ class ChartManager {
});
}
_addGhostCandlePrediction(candleData, timeframe, traces) {
_addGhostCandlePrediction(candleData, timeframe, traces, predictionTimestamp = null) {
// candleData is [Open, High, Low, Close, Volume]
// We need to determine the timestamp for this ghost candle
// It should be the NEXT candle after the last one on chart
// predictionTimestamp is when the model made this prediction (optional)
// If not provided, we calculate the next candle time
const chart = this.charts[timeframe];
if (!chart || !chart.data) return;
const lastTimestamp = new Date(chart.data.timestamps[chart.data.timestamps.length - 1]);
let nextTimestamp;
// Calculate next timestamp based on timeframe
if (timeframe === '1s') {
nextTimestamp = new Date(lastTimestamp.getTime() + 1000);
} else if (timeframe === '1m') {
nextTimestamp = new Date(lastTimestamp.getTime() + 60000);
} else if (timeframe === '1h') {
nextTimestamp = new Date(lastTimestamp.getTime() + 3600000);
if (predictionTimestamp) {
// Use the actual prediction timestamp from the model
nextTimestamp = new Date(predictionTimestamp);
} else {
nextTimestamp = new Date(lastTimestamp.getTime() + 60000); // Default 1m
// Fallback: Calculate next timestamp based on timeframe
const lastTimestamp = new Date(chart.data.timestamps[chart.data.timestamps.length - 1]);
if (timeframe === '1s') {
nextTimestamp = new Date(lastTimestamp.getTime() + 1000);
} else if (timeframe === '1m') {
nextTimestamp = new Date(lastTimestamp.getTime() + 60000);
} else if (timeframe === '1h') {
nextTimestamp = new Date(lastTimestamp.getTime() + 3600000);
} else {
nextTimestamp = new Date(lastTimestamp.getTime() + 60000); // Default 1m
}
}
const open = candleData[0];
@@ -2059,6 +2130,42 @@ class ChartManager {
console.log('Added ghost candle prediction:', ghostTrace);
}
_addShadowCandlePrediction(candleData, timestamp, traces) {
// candleData is [Open, High, Low, Close, Volume]
// timestamp is the time where this shadow should appear (matches current candle)
const open = candleData[0];
const high = candleData[1];
const low = candleData[2];
const close = candleData[3];
// Shadow color (purple to distinguish from ghost)
const color = '#8b5cf6'; // Violet
const shadowTrace = {
x: [timestamp],
open: [open],
high: [high],
low: [low],
close: [close],
type: 'candlestick',
name: 'Shadow Prediction',
increasing: {
line: { color: color, width: 1 },
fillcolor: 'rgba(139, 92, 246, 0.0)' // Hollow
},
decreasing: {
line: { color: color, width: 1 },
fillcolor: 'rgba(139, 92, 246, 0.0)' // Hollow
},
opacity: 0.7,
hoverinfo: 'x+y+text',
text: ['Past Prediction']
};
traces.push(shadowTrace);
}
_addDQNPrediction(prediction, shapes, annotations) {
const timestamp = new Date(prediction.timestamp || Date.now());
const price = prediction.current_price || 0;
@@ -2174,4 +2281,98 @@ class ChartManager {
opacity: 0.6 + confidence * 0.4
});
}
/**
* Update live metrics overlay on the active chart
*/
updateLiveMetrics(metrics) {
try {
// Get the active timeframe (first in list)
const activeTimeframe = window.appState?.currentTimeframes?.[0] || '1m';
const chart = this.charts[activeTimeframe];
if (!chart) return;
const plotId = chart.plotId;
const plotElement = document.getElementById(plotId);
if (!plotElement) return;
// Create or update metrics overlay
let overlay = document.getElementById(`metrics-overlay-${activeTimeframe}`);
if (!overlay) {
// Create overlay div
overlay = document.createElement('div');
overlay.id = `metrics-overlay-${activeTimeframe}`;
overlay.style.cssText = `
position: absolute;
top: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 8px 12px;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 12px;
z-index: 1000;
pointer-events: none;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
`;
// Append to plot container
plotElement.parentElement.style.position = 'relative';
plotElement.parentElement.appendChild(overlay);
}
// Format metrics
const accuracy = metrics.accuracy ? (metrics.accuracy * 100).toFixed(1) : '--';
const loss = metrics.loss ? metrics.loss.toFixed(4) : '--';
// Determine color based on loss (lower is better)
let lossColor = '#10b981'; // Green
if (metrics.loss > 0.5) {
lossColor = '#ef4444'; // Red
} else if (metrics.loss > 0.3) {
lossColor = '#f59e0b'; // Yellow
}
// Update content
overlay.innerHTML = `
<div style="font-weight: bold; margin-bottom: 4px; color: #3b82f6;">
📊 Live Inference [${activeTimeframe}]
</div>
<div style="display: flex; gap: 16px;">
<div>
<span style="color: #9ca3af;">Loss:</span>
<span style="color: ${lossColor}; font-weight: bold; margin-left: 4px;">${loss}</span>
</div>
<div>
<span style="color: #9ca3af;">Acc:</span>
<span style="color: #10b981; font-weight: bold; margin-left: 4px;">${accuracy}%</span>
</div>
</div>
`;
// Store reference
this.liveMetricsOverlay = overlay;
} catch (error) {
console.error('Error updating live metrics overlay:', error);
}
}
/**
* Remove live metrics overlay
*/
removeLiveMetrics() {
if (this.liveMetricsOverlay) {
this.liveMetricsOverlay.remove();
this.liveMetricsOverlay = null;
}
// Remove all overlays
document.querySelectorAll('[id^="metrics-overlay-"]').forEach(el => el.remove());
}
}

View File

@@ -71,6 +71,42 @@
</div>
</div>
<!-- Additional Metrics Row -->
<div class="row mt-2">
<div class="col-md-3">
<div class="card bg-dark">
<div class="card-body text-center py-2">
<div class="small text-muted">Simulated PnL ($100/trade)</div>
<div class="h4 mb-0" id="metric-pnl">$0.00</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-dark">
<div class="card-body text-center py-2">
<div class="small text-muted">Win Rate</div>
<div class="h4 mb-0" id="metric-winrate">--</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-dark">
<div class="card-body text-center py-2">
<div class="small text-muted">Total Trades</div>
<div class="h4 mb-0" id="metric-trades">0</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-dark">
<div class="card-body text-center py-2">
<div class="small text-muted">Open Positions</div>
<div class="h4 mb-0" id="metric-positions">0</div>
</div>
</div>
</div>
</div>
<!-- Prediction Timeline -->
<div class="row mt-3">
<div class="col-12">

View File

@@ -517,7 +517,16 @@
// Real-time inference controls
let currentInferenceId = null;
let signalPollInterval = null;
let predictionHistory = []; // Store last 5 predictions
let predictionHistory = []; // Store last 15 predictions
// PnL tracking for simulated trading ($100 position size)
let pnlTracker = {
positions: [], // Open positions: {action, entryPrice, entryTime, size}
closedTrades: [], // Completed trades with PnL
totalPnL: 0,
winRate: 0,
positionSize: 100 // $100 per trade
};
// Prediction steps slider handler
document.getElementById('prediction-steps-slider').addEventListener('input', function() {
@@ -563,9 +572,17 @@
document.getElementById('inference-status').style.display = 'block';
document.getElementById('inference-controls').style.display = 'block';
// Clear prediction history
// Clear prediction history and reset PnL tracker
predictionHistory = [];
pnlTracker = {
positions: [],
closedTrades: [],
totalPnL: 0,
winRate: 0,
positionSize: 100
};
updatePredictionHistory();
updatePnLDisplay();
// Show live mode banner
const banner = document.getElementById('live-mode-banner');
@@ -636,9 +653,10 @@
// Stop polling
stopSignalPolling();
// Stop chart auto-update
// Stop chart auto-update and remove metrics overlay
if (window.appState && window.appState.chartManager) {
window.appState.chartManager.stopAutoUpdate();
window.appState.chartManager.removeLiveMetrics();
}
currentInferenceId = null;
@@ -842,6 +860,109 @@
}
}
function updatePnLTracking(action, currentPrice, timestamp) {
// Simple trading simulation: BUY opens long, SELL opens short, HOLD closes positions
if (action === 'BUY' && pnlTracker.positions.length === 0) {
// Open long position
pnlTracker.positions.push({
action: 'BUY',
entryPrice: currentPrice,
entryTime: timestamp,
size: pnlTracker.positionSize
});
} else if (action === 'SELL' && pnlTracker.positions.length === 0) {
// Open short position
pnlTracker.positions.push({
action: 'SELL',
entryPrice: currentPrice,
entryTime: timestamp,
size: pnlTracker.positionSize
});
} else if (action === 'HOLD' && pnlTracker.positions.length > 0) {
// Close all positions
pnlTracker.positions.forEach(pos => {
let pnl = 0;
if (pos.action === 'BUY') {
// Long: profit if price went up
pnl = (currentPrice - pos.entryPrice) / pos.entryPrice * pos.size;
} else if (pos.action === 'SELL') {
// Short: profit if price went down
pnl = (pos.entryPrice - currentPrice) / pos.entryPrice * pos.size;
}
pnlTracker.closedTrades.push({
entryPrice: pos.entryPrice,
exitPrice: currentPrice,
pnl: pnl,
entryTime: pos.entryTime,
exitTime: timestamp
});
pnlTracker.totalPnL += pnl;
});
pnlTracker.positions = [];
// Calculate win rate
const wins = pnlTracker.closedTrades.filter(t => t.pnl > 0).length;
pnlTracker.winRate = pnlTracker.closedTrades.length > 0
? (wins / pnlTracker.closedTrades.length * 100)
: 0;
}
// Update PnL display
updatePnLDisplay();
}
function updatePnLDisplay() {
const pnlColor = pnlTracker.totalPnL >= 0 ? 'text-success' : 'text-danger';
const pnlSign = pnlTracker.totalPnL >= 0 ? '+' : '';
// Update PnL metric
const pnlElement = document.getElementById('metric-pnl');
if (pnlElement) {
pnlElement.textContent = `${pnlSign}$${pnlTracker.totalPnL.toFixed(2)}`;
pnlElement.className = `h4 mb-0 ${pnlColor}`;
}
// Update Win Rate
const winrateElement = document.getElementById('metric-winrate');
if (winrateElement) {
winrateElement.textContent = pnlTracker.closedTrades.length > 0
? `${pnlTracker.winRate.toFixed(1)}%`
: '--';
}
// Update Total Trades
const tradesElement = document.getElementById('metric-trades');
if (tradesElement) {
tradesElement.textContent = pnlTracker.closedTrades.length;
}
// Update Open Positions
const positionsElement = document.getElementById('metric-positions');
if (positionsElement) {
positionsElement.textContent = pnlTracker.positions.length;
}
// Update in live banner if exists
const banner = document.getElementById('inference-status');
if (banner) {
let pnlDiv = document.getElementById('live-banner-pnl');
if (!pnlDiv) {
const metricsDiv = document.getElementById('live-banner-metrics');
if (metricsDiv) {
pnlDiv = document.createElement('span');
pnlDiv.id = 'live-banner-pnl';
metricsDiv.appendChild(pnlDiv);
}
}
if (pnlDiv) {
pnlDiv.innerHTML = `<span class="${pnlColor}">PnL: ${pnlSign}$${pnlTracker.totalPnL.toFixed(2)}</span>`;
}
}
}
function updatePredictionHistory() {
const historyDiv = document.getElementById('prediction-history');
if (predictionHistory.length === 0) {
@@ -932,22 +1053,48 @@
timestamp = latest.timestamp;
}
// Get current price from signal (backend uses 'price' field)
const currentPrice = latest.price || latest.current_price;
// Add to prediction history (keep last 15)
const newPrediction = {
timestamp: timestamp,
action: latest.action,
confidence: latest.confidence,
predicted_price: latest.predicted_price,
current_price: currentPrice,
timeframe: appState.currentTimeframes ? appState.currentTimeframes[0] : '1m'
};
// Filter out undefined/invalid predictions before adding
if (latest.action && !isNaN(latest.confidence)) {
// Strengthen filter: only add valid signals
const validActions = ['BUY', 'SELL', 'HOLD'];
if (latest.action &&
validActions.includes(latest.action) &&
!isNaN(latest.confidence) &&
latest.confidence > 0 &&
currentPrice &&
!isNaN(currentPrice)) {
// Update PnL tracking
updatePnLTracking(latest.action, currentPrice, timestamp);
predictionHistory.unshift(newPrediction);
if (predictionHistory.length > 15) {
predictionHistory = predictionHistory.slice(0, 15);
}
updatePredictionHistory();
} else {
console.warn('Signal filtered out:', {
action: latest.action,
confidence: latest.confidence,
price: currentPrice,
reason: !latest.action ? 'no action' :
!validActions.includes(latest.action) ? 'invalid action' :
isNaN(latest.confidence) ? 'NaN confidence' :
latest.confidence <= 0 ? 'zero confidence' :
!currentPrice ? 'no price' :
isNaN(currentPrice) ? 'NaN price' : 'unknown'
});
}
// Update chart with signal markers and predictions
@@ -960,6 +1107,11 @@
predictions[modelKey] = latest;
window.appState.chartManager.updatePredictions(predictions);
// Display live metrics on the active chart
if (data.metrics) {
window.appState.chartManager.updateLiveMetrics(data.metrics);
}
}
}
})