UI
This commit is contained in:
@@ -310,19 +310,16 @@ class HistoricalDataLoader:
|
|||||||
}
|
}
|
||||||
binance_timeframe = timeframe_map.get(timeframe, '1m')
|
binance_timeframe = timeframe_map.get(timeframe, '1m')
|
||||||
|
|
||||||
# Build API parameters
|
# Build initial API parameters
|
||||||
params = {
|
params = {
|
||||||
'symbol': binance_symbol,
|
'symbol': binance_symbol,
|
||||||
'interval': binance_timeframe,
|
'interval': binance_timeframe
|
||||||
'limit': min(limit, 1000) # Binance max is 1000
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add time range parameters if specified
|
# Add time range parameters if specified
|
||||||
if direction == 'before' and end_time:
|
if direction == 'before' and end_time:
|
||||||
# Get data ending at end_time
|
|
||||||
params['endTime'] = int(end_time.timestamp() * 1000)
|
params['endTime'] = int(end_time.timestamp() * 1000)
|
||||||
elif direction == 'after' and start_time:
|
elif direction == 'after' and start_time:
|
||||||
# Get data starting at start_time
|
|
||||||
params['startTime'] = int(start_time.timestamp() * 1000)
|
params['startTime'] = int(start_time.timestamp() * 1000)
|
||||||
elif start_time:
|
elif start_time:
|
||||||
params['startTime'] = int(start_time.timestamp() * 1000)
|
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})")
|
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:
|
# Fetch loop
|
||||||
logger.warning(f"Binance API failed, trying MEXC...")
|
while total_fetched < limit:
|
||||||
# Try MEXC as fallback
|
# Calculate batch limit (max 1000 per request)
|
||||||
return self._fetch_from_mexc_with_time_range(
|
batch_limit = min(limit - total_fetched, 1000)
|
||||||
symbol, timeframe, start_time, end_time, limit, direction
|
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()
|
# Combine all batches
|
||||||
|
if not all_dfs:
|
||||||
if not data:
|
|
||||||
logger.warning(f"No data returned from Binance for {symbol} {timeframe}")
|
|
||||||
return None
|
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
|
logger.info(f" Fetched {len(final_df)} candles from Binance for {symbol} {timeframe} (requested {limit})")
|
||||||
df = pd.DataFrame(data, columns=[
|
return final_df
|
||||||
'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
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching from exchange API: {e}")
|
logger.error(f"Error fetching from exchange API: {e}")
|
||||||
|
|||||||
@@ -2200,9 +2200,18 @@ class AnnotationDashboard:
|
|||||||
|
|
||||||
signals = self.training_adapter.get_latest_signals()
|
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({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'signals': signals
|
'signals': signals,
|
||||||
|
'metrics': metrics
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class ChartManager {
|
|||||||
this.syncedTime = null;
|
this.syncedTime = null;
|
||||||
this.updateTimers = {}; // Track auto-update timers
|
this.updateTimers = {}; // Track auto-update timers
|
||||||
this.autoUpdateEnabled = false; // Auto-update state
|
this.autoUpdateEnabled = false; // Auto-update state
|
||||||
|
this.liveMetricsOverlay = null; // Live metrics display overlay
|
||||||
|
|
||||||
console.log('ChartManager initialized with timeframes:', timeframes);
|
console.log('ChartManager initialized with timeframes:', timeframes);
|
||||||
}
|
}
|
||||||
@@ -27,28 +28,19 @@ class ChartManager {
|
|||||||
this.autoUpdateEnabled = true;
|
this.autoUpdateEnabled = true;
|
||||||
console.log('Starting chart auto-update...');
|
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')) {
|
if (this.timeframes.includes('1s')) {
|
||||||
this.updateTimers['1s'] = setInterval(() => {
|
this.updateTimers['1s'] = setInterval(() => {
|
||||||
this.updateChartIncremental('1s');
|
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')) {
|
if (this.timeframes.includes('1m')) {
|
||||||
// Calculate ms until next whole minute
|
// We can poll every second for live updates
|
||||||
const now = new Date();
|
this.updateTimers['1m'] = setInterval(() => {
|
||||||
const msUntilNextMinute = (60 - now.getSeconds()) * 1000 - now.getMilliseconds();
|
|
||||||
|
|
||||||
// Update on next whole minute
|
|
||||||
setTimeout(() => {
|
|
||||||
this.updateChartIncremental('1m');
|
this.updateChartIncremental('1m');
|
||||||
|
}, 1000);
|
||||||
// Then update every 5s
|
|
||||||
this.updateTimers['1m'] = setInterval(() => {
|
|
||||||
this.updateChartIncremental('1m');
|
|
||||||
}, 5000); // 5 seconds
|
|
||||||
}, msUntilNextMinute);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Auto-update enabled for:', Object.keys(this.updateTimers));
|
console.log('Auto-update enabled for:', Object.keys(this.updateTimers));
|
||||||
@@ -130,6 +122,7 @@ class ChartManager {
|
|||||||
// Go back 2 intervals to be safe
|
// Go back 2 intervals to be safe
|
||||||
const lastTimeMs = new Date(lastTimestamp).getTime();
|
const lastTimeMs = new Date(lastTimestamp).getTime();
|
||||||
let lookbackMs = 2000; // Default 2s
|
let lookbackMs = 2000; // Default 2s
|
||||||
|
if (timeframe === '1s') lookbackMs = 5000; // Increased lookback for 1s to prevent misses
|
||||||
if (timeframe === '1m') lookbackMs = 120000;
|
if (timeframe === '1m') lookbackMs = 120000;
|
||||||
if (timeframe === '1h') lookbackMs = 7200000;
|
if (timeframe === '1h') lookbackMs = 7200000;
|
||||||
|
|
||||||
@@ -157,6 +150,11 @@ 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];
|
||||||
|
|
||||||
|
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) {
|
if (newData.timestamps.length > 0) {
|
||||||
// Smart Merge:
|
// Smart Merge:
|
||||||
// We want to update any existing candles that have changed (live candle)
|
// We want to update any existing candles that have changed (live candle)
|
||||||
@@ -212,6 +210,8 @@ class ChartManager {
|
|||||||
|
|
||||||
// 4. Recalculate and Redraw
|
// 4. Recalculate and Redraw
|
||||||
if (updatesCount > 0 || remainingTimestamps.length > 0) {
|
if (updatesCount > 0 || remainingTimestamps.length > 0) {
|
||||||
|
console.log(`[${timeframe}] Chart update: ${updatesCount} updated, ${remainingTimestamps.length} new candles`);
|
||||||
|
|
||||||
this.recalculatePivots(timeframe, chart.data);
|
this.recalculatePivots(timeframe, chart.data);
|
||||||
this.updateSingleChart(timeframe, chart.data);
|
this.updateSingleChart(timeframe, chart.data);
|
||||||
|
|
||||||
@@ -221,7 +221,9 @@ class ChartManager {
|
|||||||
counterEl.textContent = window.liveUpdateCount + ' updates';
|
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',
|
gridcolor: '#374151',
|
||||||
color: '#9ca3af',
|
color: '#9ca3af',
|
||||||
showgrid: true,
|
showgrid: true,
|
||||||
zeroline: false
|
zeroline: false,
|
||||||
|
fixedrange: false
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
title: {
|
title: {
|
||||||
@@ -421,7 +424,8 @@ class ChartManager {
|
|||||||
color: '#9ca3af',
|
color: '#9ca3af',
|
||||||
showgrid: true,
|
showgrid: true,
|
||||||
zeroline: false,
|
zeroline: false,
|
||||||
domain: [0.3, 1]
|
domain: [0.3, 1],
|
||||||
|
fixedrange: false // Allow vertical scaling
|
||||||
},
|
},
|
||||||
yaxis2: {
|
yaxis2: {
|
||||||
title: {
|
title: {
|
||||||
@@ -432,7 +436,8 @@ class ChartManager {
|
|||||||
color: '#9ca3af',
|
color: '#9ca3af',
|
||||||
showgrid: false,
|
showgrid: false,
|
||||||
zeroline: false,
|
zeroline: false,
|
||||||
domain: [0, 0.25]
|
domain: [0, 0.25],
|
||||||
|
fixedrange: false
|
||||||
},
|
},
|
||||||
plot_bgcolor: '#1f2937',
|
plot_bgcolor: '#1f2937',
|
||||||
paper_bgcolor: '#1f2937',
|
paper_bgcolor: '#1f2937',
|
||||||
@@ -448,17 +453,30 @@ class ChartManager {
|
|||||||
const config = {
|
const config = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
displayModeBar: true,
|
displayModeBar: true,
|
||||||
modeBarButtonsToRemove: ['lasso2d', 'select2d', 'autoScale2d'],
|
modeBarButtonsToRemove: ['lasso2d', 'select2d'], // Allow autoScale2d
|
||||||
displaylogo: false,
|
displaylogo: false,
|
||||||
scrollZoom: true,
|
scrollZoom: true,
|
||||||
// Performance optimizations
|
// Performance optimizations
|
||||||
doubleClick: false,
|
doubleClick: 'reset', // Enable double-click reset
|
||||||
showAxisDragHandles: false,
|
showAxisDragHandles: true, // Enable axis dragging
|
||||||
showAxisRangeEntryBoxes: false
|
showAxisRangeEntryBoxes: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prepare chart data with pivot bounds
|
// Prepare chart data with pivot bounds
|
||||||
const chartData = [candlestickTrace, volumeTrace];
|
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
|
// Add pivot markers from chart data
|
||||||
const shapes = [];
|
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`);
|
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
|
// Add shapes and annotations to layout
|
||||||
if (shapes.length > 0) {
|
if (shapes.length > 0) {
|
||||||
@@ -1857,8 +1878,9 @@ class ChartManager {
|
|||||||
if (!predictions) return;
|
if (!predictions) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update predictions on 1m chart (primary timeframe for predictions)
|
// Use the currently active timeframe from app state
|
||||||
const timeframe = '1m';
|
// 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];
|
const chart = this.charts[timeframe];
|
||||||
if (!chart) return;
|
if (!chart) return;
|
||||||
|
|
||||||
@@ -1894,12 +1916,55 @@ class ChartManager {
|
|||||||
this._addTrendPrediction(predictions.transformer.trend_vector, predictionShapes, predictionAnnotations);
|
this._addTrendPrediction(predictions.transformer.trend_vector, predictionShapes, predictionAnnotations);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add ghost candle if available
|
// Handle Predicted Candles
|
||||||
if (predictions.transformer.predicted_candle) {
|
if (predictions.transformer.predicted_candle) {
|
||||||
// Check if we have prediction for this timeframe
|
|
||||||
const candleData = predictions.transformer.predicted_candle[timeframe];
|
const candleData = predictions.transformer.predicted_candle[timeframe];
|
||||||
if (candleData) {
|
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)
|
// Add prediction traces (ghost candles)
|
||||||
if (predictionTraces.length > 0) {
|
if (predictionTraces.length > 0) {
|
||||||
// Remove existing ghost traces safely
|
// Remove existing ghost/shadow traces safely
|
||||||
// We iterate backwards to avoid index shifting issues when deleting
|
|
||||||
const currentTraces = plotElement.data.length;
|
const currentTraces = plotElement.data.length;
|
||||||
const indicesToRemove = [];
|
const indicesToRemove = [];
|
||||||
for (let i = currentTraces - 1; i >= 0; i--) {
|
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);
|
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]
|
// candleData is [Open, High, Low, Close, Volume]
|
||||||
// We need to determine the timestamp for this ghost candle
|
// predictionTimestamp is when the model made this prediction (optional)
|
||||||
// It should be the NEXT candle after the last one on chart
|
// If not provided, we calculate the next candle time
|
||||||
|
|
||||||
const chart = this.charts[timeframe];
|
const chart = this.charts[timeframe];
|
||||||
if (!chart || !chart.data) return;
|
if (!chart || !chart.data) return;
|
||||||
|
|
||||||
const lastTimestamp = new Date(chart.data.timestamps[chart.data.timestamps.length - 1]);
|
|
||||||
let nextTimestamp;
|
let nextTimestamp;
|
||||||
|
|
||||||
// Calculate next timestamp based on timeframe
|
if (predictionTimestamp) {
|
||||||
if (timeframe === '1s') {
|
// Use the actual prediction timestamp from the model
|
||||||
nextTimestamp = new Date(lastTimestamp.getTime() + 1000);
|
nextTimestamp = new Date(predictionTimestamp);
|
||||||
} else if (timeframe === '1m') {
|
|
||||||
nextTimestamp = new Date(lastTimestamp.getTime() + 60000);
|
|
||||||
} else if (timeframe === '1h') {
|
|
||||||
nextTimestamp = new Date(lastTimestamp.getTime() + 3600000);
|
|
||||||
} else {
|
} 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];
|
const open = candleData[0];
|
||||||
@@ -2059,6 +2130,42 @@ class ChartManager {
|
|||||||
console.log('Added ghost candle prediction:', ghostTrace);
|
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) {
|
_addDQNPrediction(prediction, shapes, annotations) {
|
||||||
const timestamp = new Date(prediction.timestamp || Date.now());
|
const timestamp = new Date(prediction.timestamp || Date.now());
|
||||||
const price = prediction.current_price || 0;
|
const price = prediction.current_price || 0;
|
||||||
@@ -2174,4 +2281,98 @@ class ChartManager {
|
|||||||
opacity: 0.6 + confidence * 0.4
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Prediction Timeline -->
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|||||||
@@ -517,7 +517,16 @@
|
|||||||
// Real-time inference controls
|
// Real-time inference controls
|
||||||
let currentInferenceId = null;
|
let currentInferenceId = null;
|
||||||
let signalPollInterval = 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
|
// Prediction steps slider handler
|
||||||
document.getElementById('prediction-steps-slider').addEventListener('input', function() {
|
document.getElementById('prediction-steps-slider').addEventListener('input', function() {
|
||||||
@@ -563,9 +572,17 @@
|
|||||||
document.getElementById('inference-status').style.display = 'block';
|
document.getElementById('inference-status').style.display = 'block';
|
||||||
document.getElementById('inference-controls').style.display = 'block';
|
document.getElementById('inference-controls').style.display = 'block';
|
||||||
|
|
||||||
// Clear prediction history
|
// Clear prediction history and reset PnL tracker
|
||||||
predictionHistory = [];
|
predictionHistory = [];
|
||||||
|
pnlTracker = {
|
||||||
|
positions: [],
|
||||||
|
closedTrades: [],
|
||||||
|
totalPnL: 0,
|
||||||
|
winRate: 0,
|
||||||
|
positionSize: 100
|
||||||
|
};
|
||||||
updatePredictionHistory();
|
updatePredictionHistory();
|
||||||
|
updatePnLDisplay();
|
||||||
|
|
||||||
// Show live mode banner
|
// Show live mode banner
|
||||||
const banner = document.getElementById('live-mode-banner');
|
const banner = document.getElementById('live-mode-banner');
|
||||||
@@ -636,9 +653,10 @@
|
|||||||
// Stop polling
|
// Stop polling
|
||||||
stopSignalPolling();
|
stopSignalPolling();
|
||||||
|
|
||||||
// Stop chart auto-update
|
// Stop chart auto-update and remove metrics overlay
|
||||||
if (window.appState && window.appState.chartManager) {
|
if (window.appState && window.appState.chartManager) {
|
||||||
window.appState.chartManager.stopAutoUpdate();
|
window.appState.chartManager.stopAutoUpdate();
|
||||||
|
window.appState.chartManager.removeLiveMetrics();
|
||||||
}
|
}
|
||||||
|
|
||||||
currentInferenceId = null;
|
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() {
|
function updatePredictionHistory() {
|
||||||
const historyDiv = document.getElementById('prediction-history');
|
const historyDiv = document.getElementById('prediction-history');
|
||||||
if (predictionHistory.length === 0) {
|
if (predictionHistory.length === 0) {
|
||||||
@@ -932,22 +1053,48 @@
|
|||||||
timestamp = latest.timestamp;
|
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)
|
// Add to prediction history (keep last 15)
|
||||||
const newPrediction = {
|
const newPrediction = {
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
action: latest.action,
|
action: latest.action,
|
||||||
confidence: latest.confidence,
|
confidence: latest.confidence,
|
||||||
predicted_price: latest.predicted_price,
|
predicted_price: latest.predicted_price,
|
||||||
|
current_price: currentPrice,
|
||||||
timeframe: appState.currentTimeframes ? appState.currentTimeframes[0] : '1m'
|
timeframe: appState.currentTimeframes ? appState.currentTimeframes[0] : '1m'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter out undefined/invalid predictions before adding
|
// Strengthen filter: only add valid signals
|
||||||
if (latest.action && !isNaN(latest.confidence)) {
|
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);
|
predictionHistory.unshift(newPrediction);
|
||||||
if (predictionHistory.length > 15) {
|
if (predictionHistory.length > 15) {
|
||||||
predictionHistory = predictionHistory.slice(0, 15);
|
predictionHistory = predictionHistory.slice(0, 15);
|
||||||
}
|
}
|
||||||
updatePredictionHistory();
|
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
|
// Update chart with signal markers and predictions
|
||||||
@@ -960,6 +1107,11 @@
|
|||||||
predictions[modelKey] = latest;
|
predictions[modelKey] = latest;
|
||||||
|
|
||||||
window.appState.chartManager.updatePredictions(predictions);
|
window.appState.chartManager.updatePredictions(predictions);
|
||||||
|
|
||||||
|
// Display live metrics on the active chart
|
||||||
|
if (data.metrics) {
|
||||||
|
window.appState.chartManager.updateLiveMetrics(data.metrics);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user