This commit is contained in:
Dobromir Popov
2025-12-09 00:34:51 +02:00
parent 8c3dc5423e
commit d6ada4b416
5 changed files with 534 additions and 429 deletions

View File

@@ -570,62 +570,6 @@ class RealTrainingAdapter:
except Exception as e: except Exception as e:
logger.debug(f" {timeframe}: Replay failed: {e}") logger.debug(f" {timeframe}: Replay failed: {e}")
# CRITICAL FIX: For 1s timeframe, if we still don't have enough data, generate from 1m candles
if timeframe == '1s' and (df is None or df.empty or len(df) < min_required_candles):
try:
df_1m = None
# First, try to get 1m data from already fetched timeframes
if '1m' in fetched_timeframes and '1m' in market_state.get('timeframes', {}):
# Convert dict format to DataFrame
tf_1m = market_state['timeframes']['1m']
try:
df_1m = pd.DataFrame({
'open': tf_1m['open'],
'high': tf_1m['high'],
'low': tf_1m['low'],
'close': tf_1m['close'],
'volume': tf_1m['volume']
}, index=pd.to_datetime(tf_1m['timestamps'], utc=True))
except Exception as e:
logger.debug(f" {timeframe}: Could not convert 1m dict to DataFrame: {e}")
# If we don't have 1m data yet, fetch it
if df_1m is None or df_1m.empty:
logger.debug(f" {timeframe}: Fetching 1m data to generate 1s candles...")
if duckdb_storage:
try:
df_1m = duckdb_storage.get_ohlcv_data(
symbol=symbol,
timeframe='1m',
start_time=tf_start_time,
end_time=tf_end_time,
limit=limit,
direction='before'
)
except Exception as e:
logger.debug(f" {timeframe}: Could not get 1m from DuckDB: {e}")
if df_1m is None or df_1m.empty:
# Try API for 1m
df_1m = self._fetch_historical_from_api(symbol, '1m', tf_start_time, tf_end_time, limit)
# Generate 1s candles from 1m if we have 1m data
if df_1m is not None and not df_1m.empty:
# Generate 1s candles from 1m
generated_1s = self._generate_1s_from_1m(df_1m, min_required_candles)
if generated_1s is not None and len(generated_1s) >= min_required_candles:
df = generated_1s
logger.info(f" {timeframe}: Generated {len(df)} candles from {len(df_1m)} 1m candles")
else:
logger.warning(f" {timeframe}: Generated only {len(generated_1s) if generated_1s is not None else 0} candles from 1m (need {min_required_candles})")
else:
logger.debug(f" {timeframe}: No 1m data available to generate 1s candles")
except Exception as e:
logger.warning(f" {timeframe}: Failed to generate from 1m: {e}")
import traceback
logger.debug(traceback.format_exc())
# Validate data quality before storing # Validate data quality before storing
if df is not None and not df.empty: if df is not None and not df.empty:
# Check minimum candle count # Check minimum candle count
@@ -1446,122 +1390,6 @@ class RealTrainingAdapter:
state_size = agent.state_size if hasattr(agent, 'state_size') else 100 state_size = agent.state_size if hasattr(agent, 'state_size') else 100
return [0.0] * state_size return [0.0] * state_size
def _generate_1s_from_1m(self, df_1m: pd.DataFrame, min_candles: int) -> Optional[pd.DataFrame]:
"""
Generate 1s candles from 1m candles by splitting each 1m candle into 60 1s candles.
Args:
df_1m: DataFrame with 1m OHLCV data
min_candles: Minimum number of 1s candles to generate
Returns:
DataFrame with 1s OHLCV data or None if generation fails
"""
import pandas as pd
from datetime import timedelta
try:
if df_1m is None or df_1m.empty:
return None
# Ensure we have required columns
required_cols = ['open', 'high', 'low', 'close', 'volume']
if not all(col in df_1m.columns for col in required_cols):
logger.warning("1m DataFrame missing required columns for 1s generation")
return None
# Generate 1s candles from each 1m candle
# Each 1m candle becomes 60 1s candles
candles_1s = []
for idx, row in df_1m.iterrows():
# Get timestamp (handle both index and column)
if isinstance(df_1m.index, pd.DatetimeIndex):
timestamp = idx
elif 'timestamp' in df_1m.columns:
timestamp = pd.to_datetime(row['timestamp'])
else:
logger.warning("Cannot determine timestamp for 1m candle")
continue
# Extract OHLCV values
open_price = float(row['open'])
high_price = float(row['high'])
low_price = float(row['low'])
close_price = float(row['close'])
volume = float(row['volume'])
# Calculate price change per second (linear interpolation)
price_change = close_price - open_price
price_per_second = price_change / 60.0
# Volume per second (distributed evenly)
volume_per_second = volume / 60.0
# Generate 60 1s candles from this 1m candle
for second in range(60):
# Calculate timestamp for this second
candle_time = timestamp + timedelta(seconds=second)
# Interpolate price (linear from open to close)
if second == 0:
candle_open = open_price
candle_close = open_price + price_per_second
elif second == 59:
candle_open = open_price + (price_per_second * 59)
candle_close = close_price
else:
candle_open = open_price + (price_per_second * second)
candle_close = open_price + (price_per_second * (second + 1))
# High and low: use the interpolated prices, but ensure they stay within the 1m candle's range
# Add small random variation to make it more realistic (but keep within bounds)
candle_high = max(candle_open, candle_close)
candle_low = min(candle_open, candle_close)
# Ensure high/low don't exceed the 1m candle's range
candle_high = min(candle_high, high_price)
candle_low = max(candle_low, low_price)
# If high == low, add small spread
if candle_high == candle_low:
spread = (high_price - low_price) / 60.0
candle_high = candle_high + (spread * 0.5)
candle_low = candle_low - (spread * 0.5)
candles_1s.append({
'timestamp': candle_time,
'open': candle_open,
'high': candle_high,
'low': candle_low,
'close': candle_close,
'volume': volume_per_second
})
if not candles_1s:
return None
# Convert to DataFrame
df_1s = pd.DataFrame(candles_1s)
df_1s['timestamp'] = pd.to_datetime(df_1s['timestamp'])
df_1s = df_1s.set_index('timestamp')
# Sort by timestamp
df_1s = df_1s.sort_index()
# Take the most recent candles up to the limit
if len(df_1s) > min_candles:
df_1s = df_1s.tail(min_candles)
logger.debug(f"Generated {len(df_1s)} 1s candles from {len(df_1m)} 1m candles")
return df_1s
except Exception as e:
logger.warning(f"Error generating 1s candles from 1m: {e}")
import traceback
logger.debug(traceback.format_exc())
return None
def _fetch_historical_from_api(self, symbol: str, timeframe: str, start_time: datetime, end_time: datetime, limit: int) -> Optional[pd.DataFrame]: def _fetch_historical_from_api(self, symbol: str, timeframe: str, start_time: datetime, end_time: datetime, limit: int) -> Optional[pd.DataFrame]:
""" """
Fetch historical OHLCV data from exchange APIs for a specific time range Fetch historical OHLCV data from exchange APIs for a specific time range
@@ -3288,34 +3116,13 @@ class RealTrainingAdapter:
logger.info(f"Stopped real-time inference: {inference_id}") logger.info(f"Stopped real-time inference: {inference_id}")
def get_latest_signals(self, limit: int = 50) -> List[Dict]: def get_latest_signals(self, limit: int = 50) -> List[Dict]:
"""Get latest inference signals from orchestrator predictions and active sessions""" """Get latest inference signals from all active sessions"""
if not hasattr(self, 'inference_sessions'):
return []
all_signals = [] all_signals = []
for session in self.inference_sessions.values():
# CRITICAL FIX: Get signals from orchestrator's stored predictions (primary source) all_signals.extend(session.get('signals', []))
if self.orchestrator and hasattr(self.orchestrator, 'recent_transformer_predictions'):
# Get predictions for all symbols
for symbol, predictions in self.orchestrator.recent_transformer_predictions.items():
if predictions:
# Convert predictions to signal format
for pred in list(predictions)[-limit:]:
signal = {
'timestamp': pred.get('timestamp', datetime.now(timezone.utc).isoformat()),
'action': pred.get('action', 'HOLD'),
'confidence': pred.get('confidence', 0.0),
'current_price': pred.get('current_price', 0.0),
'price': pred.get('current_price', 0.0), # Alias for compatibility
'predicted_price': pred.get('predicted_price', pred.get('current_price', 0.0)),
'price_change': pred.get('price_change', 0.0),
'model': 'transformer',
'predicted_candle': pred.get('predicted_candle', {}),
'source': pred.get('source', 'live_inference')
}
all_signals.append(signal)
# Also get signals from active inference sessions (secondary source)
if hasattr(self, 'inference_sessions'):
for session in self.inference_sessions.values():
all_signals.extend(session.get('signals', []))
# Sort by timestamp and return latest # Sort by timestamp and return latest
all_signals.sort(key=lambda x: x.get('timestamp', ''), reverse=True) all_signals.sort(key=lambda x: x.get('timestamp', ''), reverse=True)

View File

@@ -1189,7 +1189,7 @@ class AnnotationDashboard:
# Add ALL pivot points to the map # Add ALL pivot points to the map
for pivot in trend_level.pivot_points: for pivot in trend_level.pivot_points:
ts_str = pivot.timestamp.strftime('%Y-%m-%d %H:%M:%S') ts_str = self._format_timestamp_utc(pivot.timestamp)
if ts_str not in pivot_map: if ts_str not in pivot_map:
pivot_map[ts_str] = {'highs': [], 'lows': []} pivot_map[ts_str] = {'highs': [], 'lows': []}
@@ -1226,6 +1226,51 @@ class AnnotationDashboard:
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return {} return {}
def _format_timestamp_utc(self, ts):
"""
Format timestamp in ISO format with UTC indicator ('Z' suffix)
This ensures frontend JavaScript parses it as UTC, not local time
Args:
ts: pandas Timestamp or datetime object
Returns:
str: ISO format timestamp with 'Z' suffix (e.g., '2025-12-08T21:00:00Z')
"""
try:
# Ensure timestamp is UTC
if hasattr(ts, 'tz'):
if ts.tz is not None:
ts_utc = ts.tz_convert('UTC') if hasattr(ts, 'tz_convert') else ts
else:
try:
ts_utc = ts.tz_localize('UTC') if hasattr(ts, 'tz_localize') else ts
except:
ts_utc = ts
else:
ts_utc = ts
# Format as ISO with 'Z' for UTC
if hasattr(ts_utc, 'strftime'):
return ts_utc.strftime('%Y-%m-%dT%H:%M:%SZ')
else:
return str(ts_utc)
except Exception as e:
logger.debug(f"Error formatting timestamp: {e}")
return str(ts)
def _format_timestamps_utc(self, timestamp_series):
"""
Format a series of timestamps in ISO format with UTC indicator
Args:
timestamp_series: pandas Index or Series with timestamps
Returns:
list: List of ISO format timestamps with 'Z' suffix
"""
return [self._format_timestamp_utc(ts) for ts in timestamp_series]
def _setup_routes(self): def _setup_routes(self):
"""Setup Flask routes""" """Setup Flask routes"""
@@ -1418,7 +1463,7 @@ class AnnotationDashboard:
chart_data = { chart_data = {
timeframe: { timeframe: {
'timestamps': df.index.strftime('%Y-%m-%d %H:%M:%S').tolist(), 'timestamps': self._format_timestamps_utc(df.index),
'open': df['open'].tolist(), 'open': df['open'].tolist(),
'high': df['high'].tolist(), 'high': df['high'].tolist(),
'low': df['low'].tolist(), 'low': df['low'].tolist(),
@@ -1496,7 +1541,7 @@ class AnnotationDashboard:
# Convert to format suitable for Plotly # Convert to format suitable for Plotly
chart_data[timeframe] = { chart_data[timeframe] = {
'timestamps': df.index.strftime('%Y-%m-%d %H:%M:%S').tolist(), 'timestamps': self._format_timestamps_utc(df.index),
'open': df['open'].tolist(), 'open': df['open'].tolist(),
'high': df['high'].tolist(), 'high': df['high'].tolist(),
'low': df['low'].tolist(), 'low': df['low'].tolist(),
@@ -1766,8 +1811,31 @@ class AnnotationDashboard:
# Get pivot markers for this timeframe # Get pivot markers for this timeframe
pivot_markers = self._get_pivot_markers_for_timeframe(symbol, timeframe, df) pivot_markers = self._get_pivot_markers_for_timeframe(symbol, timeframe, df)
# CRITICAL FIX: Format timestamps in ISO format with UTC indicator
# This ensures frontend parses them as UTC, not local time
timestamps = []
for ts in df.index:
# Ensure timestamp is UTC
if hasattr(ts, 'tz'):
if ts.tz is not None:
ts_utc = ts.tz_convert('UTC') if hasattr(ts, 'tz_convert') else ts
else:
try:
ts_utc = ts.tz_localize('UTC') if hasattr(ts, 'tz_localize') else ts
except:
ts_utc = ts
else:
ts_utc = ts
# Format as ISO with 'Z' for UTC: 'YYYY-MM-DDTHH:MM:SSZ'
# Plotly handles ISO format correctly
if hasattr(ts_utc, 'strftime'):
timestamps.append(ts_utc.strftime('%Y-%m-%dT%H:%M:%SZ'))
else:
timestamps.append(str(ts_utc))
chart_data[timeframe] = { chart_data[timeframe] = {
'timestamps': df.index.strftime('%Y-%m-%d %H:%M:%S').tolist(), 'timestamps': timestamps,
'open': df['open'].tolist(), 'open': df['open'].tolist(),
'high': df['high'].tolist(), 'high': df['high'].tolist(),
'low': df['low'].tolist(), 'low': df['low'].tolist(),
@@ -2590,6 +2658,30 @@ class AnnotationDashboard:
metrics = session['metrics'].copy() metrics = session['metrics'].copy()
break break
# CRITICAL FIX: Include position state and session metrics for UI state restoration
position_state = None
session_metrics = None
# Get position state and session metrics from orchestrator if available
if self.orchestrator and hasattr(self.orchestrator, 'get_position_state'):
try:
position_state = self.orchestrator.get_position_state()
except:
pass
if self.orchestrator and hasattr(self.orchestrator, 'get_session_metrics'):
try:
session_metrics = self.orchestrator.get_session_metrics()
except:
pass
# Add position state and session metrics to metrics dict
if position_state:
metrics['position_state'] = position_state
if session_metrics:
metrics['session_pnl'] = session_metrics.get('total_pnl', 0.0)
metrics['session_metrics'] = session_metrics
return jsonify({ return jsonify({
'success': True, 'success': True,
'signals': signals, 'signals': signals,
@@ -3106,7 +3198,7 @@ class AnnotationDashboard:
if not df_before.empty: if not df_before.empty:
recent = df_before.tail(200) recent = df_before.tail(200)
market_state['timeframes'][tf] = { market_state['timeframes'][tf] = {
'timestamps': recent.index.strftime('%Y-%m-%d %H:%M:%S').tolist(), 'timestamps': self._format_timestamps_utc(recent.index),
'open': recent['open'].tolist(), 'open': recent['open'].tolist(),
'high': recent['high'].tolist(), 'high': recent['high'].tolist(),
'low': recent['low'].tolist(), 'low': recent['low'].tolist(),

View File

@@ -18,6 +18,8 @@ class ChartManager {
this.ghostCandleHistory = {}; // Store ghost candles per timeframe (max 50 each) this.ghostCandleHistory = {}; // Store ghost candles per timeframe (max 50 each)
this.maxGhostCandles = 150; // Maximum number of ghost candles to keep this.maxGhostCandles = 150; // Maximum number of ghost candles to keep
this.modelAccuracyMetrics = {}; // Track overall model accuracy per timeframe this.modelAccuracyMetrics = {}; // Track overall model accuracy per timeframe
this.predictionHistory = []; // Store last 20 predictions with fading
this.maxPredictions = 20; // Maximum number of predictions to display
// Helper to ensure all timestamps are in UTC // Helper to ensure all timestamps are in UTC
this.normalizeTimestamp = (timestamp) => { this.normalizeTimestamp = (timestamp) => {
@@ -310,15 +312,34 @@ class ChartManager {
}; };
} }
// Parse timestamp - format to match chart data format // CRITICAL FIX: Parse timestamp ensuring UTC handling
const candleTimestamp = new Date(candle.timestamp); // Backend now sends ISO format with 'Z' (e.g., '2025-12-08T21:00:00Z')
// JavaScript Date will parse this correctly as UTC
let candleTimestamp;
if (typeof candle.timestamp === 'string') {
// If it's already ISO format with 'Z', parse directly
if (candle.timestamp.includes('T') && (candle.timestamp.endsWith('Z') || candle.timestamp.includes('+'))) {
candleTimestamp = new Date(candle.timestamp);
} else if (candle.timestamp.includes('T')) {
// ISO format without timezone - assume UTC
candleTimestamp = new Date(candle.timestamp + 'Z');
} else {
// Old format: 'YYYY-MM-DD HH:MM:SS' - convert to ISO and treat as UTC
candleTimestamp = new Date(candle.timestamp.replace(' ', 'T') + 'Z');
}
} else {
candleTimestamp = new Date(candle.timestamp);
}
// Format using UTC methods and ISO format with 'Z' for consistency
const year = candleTimestamp.getUTCFullYear(); const year = candleTimestamp.getUTCFullYear();
const month = String(candleTimestamp.getUTCMonth() + 1).padStart(2, '0'); const month = String(candleTimestamp.getUTCMonth() + 1).padStart(2, '0');
const day = String(candleTimestamp.getUTCDate()).padStart(2, '0'); const day = String(candleTimestamp.getUTCDate()).padStart(2, '0');
const hours = String(candleTimestamp.getUTCHours()).padStart(2, '0'); const hours = String(candleTimestamp.getUTCHours()).padStart(2, '0');
const minutes = String(candleTimestamp.getUTCMinutes()).padStart(2, '0'); const minutes = String(candleTimestamp.getUTCMinutes()).padStart(2, '0');
const seconds = String(candleTimestamp.getUTCSeconds()).padStart(2, '0'); const seconds = String(candleTimestamp.getUTCSeconds()).padStart(2, '0');
const formattedTimestamp = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // Format as ISO with 'Z' so it's consistently treated as UTC
const formattedTimestamp = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}Z`;
// Get current chart data from Plotly // Get current chart data from Plotly
const chartData = Plotly.Plots.data(plotId); const chartData = Plotly.Plots.data(plotId);
@@ -364,7 +385,13 @@ class ChartManager {
chart.data.close.push(candle.close); chart.data.close.push(candle.close);
chart.data.volume.push(candle.volume); chart.data.volume.push(candle.volume);
console.log(`[${timeframe}] Added new candle: ${formattedTimestamp}`); console.log(`[${timeframe}] Added new candle: ${formattedTimestamp}`, {
open: candle.open,
high: candle.high,
low: candle.low,
close: candle.close,
volume: candle.volume
});
} else { } else {
// Update last candle - update both Plotly and internal data structure // Update last candle - update both Plotly and internal data structure
const x = [...candlestickTrace.x]; const x = [...candlestickTrace.x];
@@ -632,27 +659,31 @@ class ChartManager {
const ghostTime = new Date(furthestGhost.targetTime); const ghostTime = new Date(furthestGhost.targetTime);
const currentMax = new Date(xMax); const currentMax = new Date(xMax);
if (ghostTime > currentMax) { if (ghostTime > currentMax) {
const year = ghostTime.getUTCFullYear(); // CRITICAL FIX: Format as ISO with 'Z' to match chart timestamp format
const month = String(ghostTime.getUTCMonth() + 1).padStart(2, '0'); xMax = ghostTime.toISOString();
const day = String(ghostTime.getUTCDate()).padStart(2, '0');
const hours = String(ghostTime.getUTCHours()).padStart(2, '0');
const minutes = String(ghostTime.getUTCMinutes()).padStart(2, '0');
const seconds = String(ghostTime.getUTCSeconds()).padStart(2, '0');
xMax = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
console.log(`[${timeframe}] Pivot lines extended to include ${ghosts.length} ghost candles (to ${xMax})`); console.log(`[${timeframe}] Pivot lines extended to include ${ghosts.length} ghost candles (to ${xMax})`);
} }
} }
} }
// Process each timestamp that has pivot markers // Process each timestamp that has pivot markers
Object.entries(data.pivot_markers).forEach(([timestamp, pivots]) => { // CRITICAL FIX: Ensure pivot marker timestamps are in ISO format
Object.entries(data.pivot_markers).forEach(([timestampKey, pivots]) => {
// Convert pivot marker timestamp to ISO format if needed
let pivotTimestamp = timestampKey;
if (typeof timestampKey === 'string' && !timestampKey.includes('T')) {
pivotTimestamp = new Date(timestampKey.replace(' ', 'T') + 'Z').toISOString();
} else if (typeof timestampKey === 'string' && !timestampKey.endsWith('Z') && !timestampKey.includes('+')) {
pivotTimestamp = new Date(timestampKey + 'Z').toISOString();
}
// Process high pivots // Process high pivots
if (pivots.highs && pivots.highs.length > 0) { if (pivots.highs && pivots.highs.length > 0) {
pivots.highs.forEach(pivot => { pivots.highs.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'high'); const color = this._getPivotColor(pivot.level, 'high');
// Draw dot on the pivot candle (above the high) // Draw dot on the pivot candle (above the high) - use converted timestamp
pivotDots.x.push(timestamp); pivotDots.x.push(pivotTimestamp);
pivotDots.y.push(pivot.price); pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} High Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`); pivotDots.text.push(`L${pivot.level} High Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color); pivotDots.marker.color.push(color);
@@ -698,8 +729,8 @@ class ChartManager {
pivots.lows.forEach(pivot => { pivots.lows.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'low'); const color = this._getPivotColor(pivot.level, 'low');
// Draw dot on the pivot candle (below the low) // Draw dot on the pivot candle (below the low) - use converted timestamp
pivotDots.x.push(timestamp); pivotDots.x.push(pivotTimestamp);
pivotDots.y.push(pivot.price); pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} Low Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`); pivotDots.text.push(`L${pivot.level} Low Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color); pivotDots.marker.color.push(color);
@@ -1061,38 +1092,57 @@ class ChartManager {
const annotations = []; const annotations = [];
const pivotDots = { x: [], y: [], text: [], marker: { color: [], size: [], symbol: [] }, mode: 'markers', hoverinfo: 'text', showlegend: false }; const pivotDots = { x: [], y: [], text: [], marker: { color: [], size: [], symbol: [] }, mode: 'markers', hoverinfo: 'text', showlegend: false };
if (data.pivot_markers && Object.keys(data.pivot_markers).length > 0) { if (data.pivot_markers && Object.keys(data.pivot_markers).length > 0) {
const xMin = data.timestamps[0]; // CRITICAL FIX: Ensure timestamps are in ISO format for consistency
let xMax = data.timestamps[data.timestamps.length - 1]; // Parse timestamps to ensure they're treated as UTC
let xMin = data.timestamps[0];
// Extend xMax to include ghost candle predictions if they exist let xMax = data.timestamps[data.timestamps.length - 1];
if (this.ghostCandleHistory && this.ghostCandleHistory[timeframe] && this.ghostCandleHistory[timeframe].length > 0) {
const ghosts = this.ghostCandleHistory[timeframe]; // Convert to ISO format if not already
const furthestGhost = ghosts[ghosts.length - 1]; if (typeof xMin === 'string' && !xMin.includes('T')) {
if (furthestGhost && furthestGhost.targetTime) { xMin = new Date(xMin.replace(' ', 'T') + 'Z').toISOString();
const ghostTime = new Date(furthestGhost.targetTime); } else if (typeof xMin === 'string' && !xMin.endsWith('Z') && !xMin.includes('+')) {
const currentMax = new Date(xMax); xMin = new Date(xMin + 'Z').toISOString();
if (ghostTime > currentMax) { }
const year = ghostTime.getUTCFullYear();
const month = String(ghostTime.getUTCMonth() + 1).padStart(2, '0'); if (typeof xMax === 'string' && !xMax.includes('T')) {
const day = String(ghostTime.getUTCDate()).padStart(2, '0'); xMax = new Date(xMax.replace(' ', 'T') + 'Z').toISOString();
const hours = String(ghostTime.getUTCHours()).padStart(2, '0'); } else if (typeof xMax === 'string' && !xMax.endsWith('Z') && !xMax.includes('+')) {
const minutes = String(ghostTime.getUTCMinutes()).padStart(2, '0'); xMax = new Date(xMax + 'Z').toISOString();
const seconds = String(ghostTime.getUTCSeconds()).padStart(2, '0'); }
xMax = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} // Extend xMax to include ghost candle predictions if they exist
} if (this.ghostCandleHistory && this.ghostCandleHistory[timeframe] && this.ghostCandleHistory[timeframe].length > 0) {
const ghosts = this.ghostCandleHistory[timeframe];
const furthestGhost = ghosts[ghosts.length - 1];
if (furthestGhost && furthestGhost.targetTime) {
const ghostTime = new Date(furthestGhost.targetTime);
const currentMax = new Date(xMax);
if (ghostTime > currentMax) {
// Format as ISO with 'Z' to match chart timestamp format
xMax = ghostTime.toISOString();
} }
}
}
// Process each timestamp that has pivot markers // Process each timestamp that has pivot markers
Object.entries(data.pivot_markers).forEach(([timestamp, pivots]) => { // CRITICAL FIX: Ensure pivot marker timestamps are in ISO format
Object.entries(data.pivot_markers).forEach(([timestampKey, pivots]) => {
// Convert pivot marker timestamp to ISO format if needed
let pivotTimestamp = timestampKey;
if (typeof timestampKey === 'string' && !timestampKey.includes('T')) {
pivotTimestamp = new Date(timestampKey.replace(' ', 'T') + 'Z').toISOString();
} else if (typeof timestampKey === 'string' && !timestampKey.endsWith('Z') && !timestampKey.includes('+')) {
pivotTimestamp = new Date(timestampKey + 'Z').toISOString();
}
// Process high pivots // Process high pivots
if (pivots.highs && pivots.highs.length > 0) { if (pivots.highs && pivots.highs.length > 0) {
pivots.highs.forEach(pivot => { pivots.highs.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'high'); const color = this._getPivotColor(pivot.level, 'high');
// Draw dot on the pivot candle // Draw dot on the pivot candle - use converted timestamp
pivotDots.x.push(timestamp); pivotDots.x.push(pivotTimestamp);
pivotDots.y.push(pivot.price); pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} High Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`); pivotDots.text.push(`L${pivot.level} High Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color); pivotDots.marker.color.push(color);
@@ -1137,8 +1187,8 @@ class ChartManager {
pivots.lows.forEach(pivot => { pivots.lows.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'low'); const color = this._getPivotColor(pivot.level, 'low');
// Draw dot on the pivot candle // Draw dot on the pivot candle - use converted timestamp
pivotDots.x.push(timestamp); pivotDots.x.push(pivotTimestamp);
pivotDots.y.push(pivot.price); pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} Low Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`); pivotDots.text.push(`L${pivot.level} Low Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color); pivotDots.marker.color.push(color);
@@ -2121,26 +2171,31 @@ class ChartManager {
const ghostTime = new Date(furthestGhost.targetTime); const ghostTime = new Date(furthestGhost.targetTime);
const currentMax = new Date(xMax); const currentMax = new Date(xMax);
if (ghostTime > currentMax) { if (ghostTime > currentMax) {
const year = ghostTime.getUTCFullYear(); // CRITICAL FIX: Format as ISO with 'Z' to match chart timestamp format
const month = String(ghostTime.getUTCMonth() + 1).padStart(2, '0'); xMax = ghostTime.toISOString();
const day = String(ghostTime.getUTCDate()).padStart(2, '0');
const hours = String(ghostTime.getUTCHours()).padStart(2, '0');
const minutes = String(ghostTime.getUTCMinutes()).padStart(2, '0');
const seconds = String(ghostTime.getUTCSeconds()).padStart(2, '0');
xMax = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
console.log(`[${timeframe}] Pivot lines extended to include ${ghosts.length} ghost candles (to ${xMax})`); console.log(`[${timeframe}] Pivot lines extended to include ${ghosts.length} ghost candles (to ${xMax})`);
} }
} }
} }
// Process each timestamp that has pivot markers // Process each timestamp that has pivot markers
Object.entries(data.pivot_markers).forEach(([timestamp, pivots]) => { // CRITICAL FIX: Ensure pivot marker timestamps are in ISO format
Object.entries(data.pivot_markers).forEach(([timestampKey, pivots]) => {
// Convert pivot marker timestamp to ISO format if needed
let pivotTimestamp = timestampKey;
if (typeof timestampKey === 'string' && !timestampKey.includes('T')) {
pivotTimestamp = new Date(timestampKey.replace(' ', 'T') + 'Z').toISOString();
} else if (typeof timestampKey === 'string' && !timestampKey.endsWith('Z') && !timestampKey.includes('+')) {
pivotTimestamp = new Date(timestampKey + 'Z').toISOString();
}
// Process high pivots // Process high pivots
if (pivots.highs && pivots.highs.length > 0) { if (pivots.highs && pivots.highs.length > 0) {
pivots.highs.forEach(pivot => { pivots.highs.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'high'); const color = this._getPivotColor(pivot.level, 'high');
pivotDots.x.push(timestamp); // CRITICAL FIX: Use converted timestamp for consistency
pivotDots.x.push(pivotTimestamp);
pivotDots.y.push(pivot.price); pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} High Pivot<br>Price: ${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`); pivotDots.text.push(`L${pivot.level} High Pivot<br>Price: ${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color); pivotDots.marker.color.push(color);
@@ -2174,7 +2229,8 @@ class ChartManager {
pivots.lows.forEach(pivot => { pivots.lows.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'low'); const color = this._getPivotColor(pivot.level, 'low');
pivotDots.x.push(timestamp); // CRITICAL FIX: Use converted timestamp for consistency
pivotDots.x.push(pivotTimestamp);
pivotDots.y.push(pivot.price); pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} Low Pivot<br>Price: ${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`); pivotDots.text.push(`L${pivot.level} Low Pivot<br>Price: ${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color); pivotDots.marker.color.push(color);
@@ -2736,137 +2792,170 @@ class ChartManager {
this._addCNNPrediction(predictions.cnn, predictionShapes, predictionAnnotations); this._addCNNPrediction(predictions.cnn, predictionShapes, predictionAnnotations);
} }
// Add Transformer predictions (star markers with trend lines + ghost candles) // CRITICAL FIX: Manage prediction history (max 20, fade oldest)
// Add new transformer prediction to history
if (predictions.transformer) { if (predictions.transformer) {
console.log(`[updatePredictions] Processing transformer prediction:`, { // Check if this is a new prediction (different timestamp or significant change)
action: predictions.transformer.action, const newPred = predictions.transformer;
confidence: predictions.transformer.confidence, const isNew = !this.predictionHistory.length ||
has_predicted_candle: !!predictions.transformer.predicted_candle, this.predictionHistory[0].timestamp !== newPred.timestamp ||
predicted_candle_keys: predictions.transformer.predicted_candle ? Object.keys(predictions.transformer.predicted_candle) : [] Math.abs((this.predictionHistory[0].confidence || 0) - (newPred.confidence || 0)) > 0.01;
});
this._addTransformerPrediction(predictions.transformer, predictionShapes, predictionAnnotations); if (isNew) {
// Add to history (most recent first)
// Add trend vector visualization (shorter projection to avoid zoom issues) this.predictionHistory.unshift({
if (predictions.transformer.trend_vector) { ...newPred,
this._addTrendPrediction(predictions.transformer.trend_vector, predictionShapes, predictionAnnotations); addedAt: Date.now()
} });
// Handle Predicted Candles (ghost candles) // Keep only last 20 predictions
if (predictions.transformer.predicted_candle) { if (this.predictionHistory.length > this.maxPredictions) {
console.log(`[updatePredictions] predicted_candle data:`, predictions.transformer.predicted_candle); this.predictionHistory = this.predictionHistory.slice(0, this.maxPredictions);
const candleData = predictions.transformer.predicted_candle[timeframe];
console.log(`[updatePredictions] candleData for ${timeframe}:`, candleData);
if (candleData) {
// 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;
// Get the last real candle timestamp to ensure we predict the NEXT one
// CRITICAL FIX: Use Plotly data structure (chartData[0].x for timestamps)
const candlestickTrace = chartData[0]; // First trace is candlestick
const lastRealCandle = candlestickTrace && candlestickTrace.x && candlestickTrace.x.length > 0
? candlestickTrace.x[candlestickTrace.x.length - 1]
: null;
if (lastRealCandle) {
const lastCandleTime = new Date(lastRealCandle);
// Predict for the next candle period
if (timeframe === '1s') {
targetTimestamp = new Date(lastCandleTime.getTime() + 1000);
} else if (timeframe === '1m') {
targetTimestamp = new Date(lastCandleTime.getTime() + 60000);
} else if (timeframe === '1h') {
targetTimestamp = new Date(lastCandleTime.getTime() + 3600000);
} else {
targetTimestamp = new Date(lastCandleTime.getTime() + 60000);
}
} else {
// Fallback to inference time + period if no real candles yet
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);
}
}
// Round to exact candle boundary to prevent bunching
if (timeframe === '1s') {
targetTimestamp = new Date(Math.floor(targetTimestamp.getTime() / 1000) * 1000);
} else if (timeframe === '1m') {
targetTimestamp = new Date(Math.floor(targetTimestamp.getTime() / 60000) * 60000);
} else if (timeframe === '1h') {
targetTimestamp = new Date(Math.floor(targetTimestamp.getTime() / 3600000) * 3600000);
}
// 1. Initialize ghost candle history for this timeframe if needed
if (!this.ghostCandleHistory[timeframe]) {
this.ghostCandleHistory[timeframe] = [];
}
// 2. Add new ghost candle to history
const year = targetTimestamp.getUTCFullYear();
const month = String(targetTimestamp.getUTCMonth() + 1).padStart(2, '0');
const day = String(targetTimestamp.getUTCDate()).padStart(2, '0');
const hours = String(targetTimestamp.getUTCHours()).padStart(2, '0');
const minutes = String(targetTimestamp.getUTCMinutes()).padStart(2, '0');
const seconds = String(targetTimestamp.getUTCSeconds()).padStart(2, '0');
const formattedTimestamp = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
this.ghostCandleHistory[timeframe].push({
timestamp: formattedTimestamp,
candle: candleData,
targetTime: targetTimestamp
});
// 3. Keep only last 10 ghost candles
if (this.ghostCandleHistory[timeframe].length > this.maxGhostCandles) {
this.ghostCandleHistory[timeframe] = this.ghostCandleHistory[timeframe].slice(-this.maxGhostCandles);
}
// 4. Add all ghost candles from history to traces (with accuracy if validated)
for (const ghost of this.ghostCandleHistory[timeframe]) {
this._addGhostCandlePrediction(ghost.candle, timeframe, predictionTraces, ghost.targetTime, ghost.accuracy);
}
// 5. Store as "Last Prediction" for shadow rendering
if (!this.lastPredictions) this.lastPredictions = {};
this.lastPredictions[timeframe] = {
timestamp: targetTimestamp.toISOString(),
candle: candleData,
inferenceTime: predictionTimestamp
};
console.log(`[${timeframe}] Ghost candle added (${this.ghostCandleHistory[timeframe].length}/${this.maxGhostCandles}) at ${targetTimestamp.toISOString()}`, {
predicted: candleData,
timestamp: formattedTimestamp
});
} }
} }
// 3. Render "Shadow Candle" (Previous Prediction for Current Candle) console.log(`[updatePredictions] Processing ${this.predictionHistory.length} predictions (new: ${isNew}):`, {
// If we have a stored prediction that matches the CURRENT candle time, show it action: newPred.action,
if (this.lastPredictions && this.lastPredictions[timeframe]) { confidence: newPred.confidence,
const lastPred = this.lastPredictions[timeframe]; has_predicted_candle: !!newPred.predicted_candle
// CRITICAL FIX: Use Plotly data structure for timestamps });
const candlestickTrace = chartData[0]; }
const currentTimestamp = candlestickTrace && candlestickTrace.x && candlestickTrace.x.length > 0
? candlestickTrace.x[candlestickTrace.x.length - 1] // Render all predictions from history with fading (oldest = most transparent)
: null; this.predictionHistory.forEach((pred, index) => {
// Calculate opacity: newest = 1.0, oldest = 0.2
const ageRatio = index / Math.max(1, this.predictionHistory.length - 1);
const baseOpacity = 1.0 - (ageRatio * 0.8); // Fade from 1.0 to 0.2
// Create a copy of prediction with opacity applied
const fadedPred = {
...pred,
_fadeOpacity: baseOpacity
};
this._addTransformerPrediction(fadedPred, predictionShapes, predictionAnnotations);
// Add trend vector visualization (shorter projection to avoid zoom issues)
if (pred.trend_vector) {
this._addTrendPrediction(pred.trend_vector, predictionShapes, predictionAnnotations);
}
});
// Handle Predicted Candles (ghost candles) - only for the most recent prediction
if (predictions.transformer && predictions.transformer.predicted_candle) {
console.log(`[updatePredictions] predicted_candle data:`, predictions.transformer.predicted_candle);
const candleData = predictions.transformer.predicted_candle[timeframe];
console.log(`[updatePredictions] candleData for ${timeframe}:`, candleData);
if (candleData) {
// Get the prediction timestamp from the model (when inference was made)
const predictionTimestamp = predictions.transformer.timestamp || new Date().toISOString();
if (currentTimestamp) { // Calculate the target timestamp (when this prediction is for)
// Compare timestamps (allow small diff for jitter) // This should be the NEXT candle after the inference time
if (Math.abs(new Date(lastPred.timestamp).getTime() - new Date(currentTimestamp).getTime()) < 1000) { const inferenceTime = new Date(predictionTimestamp);
this._addShadowCandlePrediction(lastPred.candle, currentTimestamp, predictionTraces); let targetTimestamp;
// Get the last real candle timestamp to ensure we predict the NEXT one
// CRITICAL FIX: Use Plotly data structure (chartData[0].x for timestamps)
const candlestickTrace = chartData[0]; // First trace is candlestick
const lastRealCandle = candlestickTrace && candlestickTrace.x && candlestickTrace.x.length > 0
? candlestickTrace.x[candlestickTrace.x.length - 1]
: null;
if (lastRealCandle) {
const lastCandleTime = new Date(lastRealCandle);
// Predict for the next candle period
if (timeframe === '1s') {
targetTimestamp = new Date(lastCandleTime.getTime() + 1000);
} else if (timeframe === '1m') {
targetTimestamp = new Date(lastCandleTime.getTime() + 60000);
} else if (timeframe === '1h') {
targetTimestamp = new Date(lastCandleTime.getTime() + 3600000);
} else {
targetTimestamp = new Date(lastCandleTime.getTime() + 60000);
} }
} else {
// Fallback to inference time + period if no real candles yet
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);
}
}
// Round to exact candle boundary to prevent bunching
if (timeframe === '1s') {
targetTimestamp = new Date(Math.floor(targetTimestamp.getTime() / 1000) * 1000);
} else if (timeframe === '1m') {
targetTimestamp = new Date(Math.floor(targetTimestamp.getTime() / 60000) * 60000);
} else if (timeframe === '1h') {
targetTimestamp = new Date(Math.floor(targetTimestamp.getTime() / 3600000) * 3600000);
}
// 1. Initialize ghost candle history for this timeframe if needed
if (!this.ghostCandleHistory[timeframe]) {
this.ghostCandleHistory[timeframe] = [];
}
// 2. Add new ghost candle to history
const year = targetTimestamp.getUTCFullYear();
const month = String(targetTimestamp.getUTCMonth() + 1).padStart(2, '0');
const day = String(targetTimestamp.getUTCDate()).padStart(2, '0');
const hours = String(targetTimestamp.getUTCHours()).padStart(2, '0');
const minutes = String(targetTimestamp.getUTCMinutes()).padStart(2, '0');
const seconds = String(targetTimestamp.getUTCSeconds()).padStart(2, '0');
const formattedTimestamp = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
this.ghostCandleHistory[timeframe].push({
timestamp: formattedTimestamp,
candle: candleData,
targetTime: targetTimestamp
});
// 3. Keep only last 10 ghost candles
if (this.ghostCandleHistory[timeframe].length > this.maxGhostCandles) {
this.ghostCandleHistory[timeframe] = this.ghostCandleHistory[timeframe].slice(-this.maxGhostCandles);
}
// 4. Add all ghost candles from history to traces (with accuracy if validated)
for (const ghost of this.ghostCandleHistory[timeframe]) {
this._addGhostCandlePrediction(ghost.candle, timeframe, predictionTraces, ghost.targetTime, ghost.accuracy);
}
// 5. Store as "Last Prediction" for shadow rendering
if (!this.lastPredictions) this.lastPredictions = {};
this.lastPredictions[timeframe] = {
timestamp: targetTimestamp.toISOString(),
candle: candleData,
inferenceTime: predictionTimestamp
};
console.log(`[${timeframe}] Ghost candle added (${this.ghostCandleHistory[timeframe].length}/${this.maxGhostCandles}) at ${targetTimestamp.toISOString()}`, {
predicted: candleData,
timestamp: formattedTimestamp
});
}
}
// 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];
// CRITICAL FIX: Use Plotly data structure for timestamps
const candlestickTrace = chartData[0];
const currentTimestamp = candlestickTrace && candlestickTrace.x && candlestickTrace.x.length > 0
? candlestickTrace.x[candlestickTrace.x.length - 1]
: null;
if (currentTimestamp) {
// 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);
} }
} }
} }
@@ -3230,7 +3319,8 @@ class ChartManager {
} }
_addTransformerPrediction(prediction, shapes, annotations) { _addTransformerPrediction(prediction, shapes, annotations) {
// CRITICAL FIX: Get actual price from chart instead of using normalized prediction prices // CRITICAL FIX: Use first timeframe from currentTimeframes (ignore Primary Timeline dropdown)
// Always use the first active timeframe, not the dropdown selection
const timeframe = window.appState?.currentTimeframes?.[0] || '1m'; const timeframe = window.appState?.currentTimeframes?.[0] || '1m';
const chart = this.charts[timeframe]; const chart = this.charts[timeframe];
if (!chart) { if (!chart) {
@@ -3238,36 +3328,19 @@ class ChartManager {
return; return;
} }
// Get actual current price from the last candle on the chart // CRITICAL FIX: Use prediction's timestamp and price as starting point
let actualCurrentPrice = 0; // Parse prediction timestamp
const plotElement = document.getElementById(chart.plotId);
if (plotElement && plotElement.data && plotElement.data.length > 0) {
const candlestickTrace = plotElement.data[0]; // First trace is candlestick
if (candlestickTrace && candlestickTrace.close && candlestickTrace.close.length > 0) {
actualCurrentPrice = candlestickTrace.close[candlestickTrace.close.length - 1];
}
}
// Fallback to prediction price if chart price not available (but check if it's normalized)
if (actualCurrentPrice === 0 || actualCurrentPrice < 1) {
// Price might be normalized, try to use prediction price
actualCurrentPrice = prediction.current_price || 0;
// If still looks normalized (< 1), we can't display it properly
if (actualCurrentPrice < 1) {
console.warn('[Transformer Prediction] Price appears normalized, cannot display on chart. Chart price:', actualCurrentPrice);
return;
}
}
// CRITICAL FIX: Parse timestamp correctly (handle both ISO strings and Date objects)
let timestamp; let timestamp;
if (prediction.timestamp) { if (prediction.timestamp) {
if (typeof prediction.timestamp === 'string') { if (typeof prediction.timestamp === 'string') {
// Handle various timestamp formats if (prediction.timestamp.includes('T') && (prediction.timestamp.endsWith('Z') || prediction.timestamp.includes('+'))) {
timestamp = new Date(prediction.timestamp); timestamp = new Date(prediction.timestamp);
if (isNaN(timestamp.getTime())) { } else if (prediction.timestamp.includes('T')) {
// Try parsing as GMT format timestamp = new Date(prediction.timestamp + 'Z');
} else if (prediction.timestamp.includes('GMT')) {
timestamp = new Date(prediction.timestamp.replace('GMT', 'UTC')); timestamp = new Date(prediction.timestamp.replace('GMT', 'UTC'));
} else {
timestamp = new Date(prediction.timestamp.replace(' ', 'T') + 'Z');
} }
} else { } else {
timestamp = new Date(prediction.timestamp); timestamp = new Date(prediction.timestamp);
@@ -3276,18 +3349,50 @@ class ChartManager {
timestamp = new Date(); timestamp = new Date();
} }
// Use current time if timestamp parsing failed // Ensure timestamp is valid
if (isNaN(timestamp.getTime())) { if (isNaN(timestamp.getTime())) {
timestamp = new Date(); timestamp = new Date();
} }
// Get prediction price - use current_price from prediction
let predictionPrice = prediction.current_price || 0;
// If price looks normalized (< 1), try to get actual price from chart
if (predictionPrice < 1) {
const plotElement = document.getElementById(chart.plotId);
if (plotElement && plotElement.data && plotElement.data.length > 0) {
const candlestickTrace = plotElement.data[0];
if (candlestickTrace && candlestickTrace.close && candlestickTrace.close.length > 0) {
// Find the candle closest to prediction timestamp
const predTimeMs = timestamp.getTime();
let closestPrice = candlestickTrace.close[candlestickTrace.close.length - 1];
let minDiff = Infinity;
for (let i = 0; i < candlestickTrace.x.length; i++) {
const candleTime = new Date(candlestickTrace.x[i]).getTime();
const diff = Math.abs(candleTime - predTimeMs);
if (diff < minDiff) {
minDiff = diff;
closestPrice = candlestickTrace.close[i];
}
}
predictionPrice = closestPrice;
}
}
}
if (predictionPrice === 0 || predictionPrice < 1) {
console.warn('[Transformer Prediction] Cannot determine prediction price');
return;
}
const confidence = prediction.confidence || 0; const confidence = prediction.confidence || 0;
const priceChange = prediction.price_change || 0; const priceChange = prediction.price_change || 0;
const horizonMinutes = prediction.horizon_minutes || 10; const horizonMinutes = prediction.horizon_minutes || 10;
if (confidence < 0.3 || actualCurrentPrice === 0) return; if (confidence < 0.3) return;
// CRITICAL: Calculate predicted price from actual current price and price change // Calculate predicted price from prediction price and price change
// priceChange is typically a percentage or ratio
let actualPredictedPrice; let actualPredictedPrice;
if (prediction.predicted_price && prediction.predicted_price > 1) { if (prediction.predicted_price && prediction.predicted_price > 1) {
// Use predicted_price if it looks like actual price (not normalized) // Use predicted_price if it looks like actual price (not normalized)
@@ -3296,19 +3401,19 @@ class ChartManager {
// Calculate from price change (could be percentage or ratio) // Calculate from price change (could be percentage or ratio)
if (Math.abs(priceChange) > 10) { if (Math.abs(priceChange) > 10) {
// Looks like percentage (e.g., 1.0 = 1%) // Looks like percentage (e.g., 1.0 = 1%)
actualPredictedPrice = actualCurrentPrice * (1 + priceChange / 100); actualPredictedPrice = predictionPrice * (1 + priceChange / 100);
} else { } else {
// Looks like ratio (e.g., 0.01 = 1%) // Looks like ratio (e.g., 0.01 = 1%)
actualPredictedPrice = actualCurrentPrice * (1 + priceChange); actualPredictedPrice = predictionPrice * (1 + priceChange);
} }
} else { } else {
// Fallback: use action to determine direction // Fallback: use action to determine direction
if (prediction.action === 'BUY') { if (prediction.action === 'BUY') {
actualPredictedPrice = actualCurrentPrice * 1.01; // +1% actualPredictedPrice = predictionPrice * 1.01; // +1%
} else if (prediction.action === 'SELL') { } else if (prediction.action === 'SELL') {
actualPredictedPrice = actualCurrentPrice * 0.99; // -1% actualPredictedPrice = predictionPrice * 0.99; // -1%
} else { } else {
actualPredictedPrice = actualCurrentPrice; // HOLD actualPredictedPrice = predictionPrice; // HOLD
} }
} }
@@ -3325,16 +3430,37 @@ class ChartManager {
color = 'rgba(150, 150, 255, 0.5)'; // Light blue for STABLE/HOLD color = 'rgba(150, 150, 255, 0.5)'; // Light blue for STABLE/HOLD
} }
// Add trend line from current price to predicted price // CRITICAL FIX: Format timestamps as ISO strings to match chart data format
const timestampISO = timestamp.toISOString();
const endTimeISO = endTime.toISOString();
// Apply fade opacity if provided (for prediction history)
const fadeOpacity = prediction._fadeOpacity !== undefined ? prediction._fadeOpacity : 1.0;
// Extract RGB from color and apply fade opacity
let fadedColor = color;
if (typeof color === 'string' && color.startsWith('rgba')) {
// Parse rgba and apply fade
const rgbaMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
if (rgbaMatch) {
const r = parseInt(rgbaMatch[1]);
const g = parseInt(rgbaMatch[2]);
const b = parseInt(rgbaMatch[3]);
const originalAlpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 0.6;
fadedColor = `rgba(${r}, ${g}, ${b}, ${originalAlpha * fadeOpacity})`;
}
}
// Add trend line from prediction timestamp/price to predicted price
shapes.push({ shapes.push({
type: 'line', type: 'line',
x0: timestamp, x0: timestampISO, // Start at prediction timestamp
y0: actualCurrentPrice, y0: predictionPrice, // Start at prediction price
x1: endTime, x1: endTimeISO, // End at predicted time
y1: actualPredictedPrice, y1: actualPredictedPrice, // End at predicted price
line: { line: {
color: color, color: fadedColor,
width: 2 + confidence * 2, width: (2 + confidence * 2) * fadeOpacity, // Also fade width slightly
dash: 'dashdot' dash: 'dashdot'
}, },
layer: 'above' layer: 'above'
@@ -3343,20 +3469,20 @@ class ChartManager {
// Add star marker at target with action label // Add star marker at target with action label
const actionText = prediction.action === 'BUY' ? '▲' : prediction.action === 'SELL' ? '▼' : '★'; const actionText = prediction.action === 'BUY' ? '▲' : prediction.action === 'SELL' ? '▼' : '★';
annotations.push({ annotations.push({
x: endTime, x: endTimeISO, // Use ISO string format to match chart timestamps
y: actualPredictedPrice, y: actualPredictedPrice,
text: `${actionText} ${(confidence * 100).toFixed(0)}%`, text: `${actionText} ${(confidence * 100).toFixed(0)}%`,
showarrow: false, showarrow: false,
font: { font: {
size: 12 + confidence * 4, size: (12 + confidence * 4) * fadeOpacity, // Fade font size
color: color color: fadedColor
}, },
bgcolor: 'rgba(31, 41, 55, 0.8)', bgcolor: `rgba(31, 41, 55, ${0.8 * fadeOpacity})`, // Fade background
borderpad: 3, borderpad: 3,
opacity: 0.8 + confidence * 0.2 opacity: (0.8 + confidence * 0.2) * fadeOpacity // Apply fade to overall opacity
}); });
console.log(`[Transformer Prediction] Added prediction marker: ${prediction.action} @ ${actualCurrentPrice.toFixed(2)} -> ${actualPredictedPrice.toFixed(2)} (${(confidence * 100).toFixed(1)}% confidence)`); console.log(`[Transformer Prediction] Added prediction marker: ${prediction.action} @ ${predictionPrice.toFixed(2)} -> ${actualPredictedPrice.toFixed(2)} (${(confidence * 100).toFixed(1)}% confidence)`);
} }
/** /**

View File

@@ -102,6 +102,11 @@
checkActiveTraining(); checkActiveTraining();
} }
// Check for active inference session (resume PnL and position state after page reload)
if (typeof checkActiveInference === 'function') {
checkActiveInference();
}
// Keyboard shortcuts for chart maximization // Keyboard shortcuts for chart maximization
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
// ESC key to exit maximized mode // ESC key to exit maximized mode

View File

@@ -234,6 +234,24 @@
activeTrainingId = data.session.training_id; activeTrainingId = data.session.training_id;
showTrainingStatus(); showTrainingStatus();
// CRITICAL FIX: Immediately restore training progress state
// Don't wait for first poll - restore current state now
if (data.session.current_epoch !== undefined) {
document.getElementById('training-epoch').textContent = data.session.current_epoch || 0;
}
if (data.session.total_epochs !== undefined) {
document.getElementById('training-total-epochs').textContent = data.session.total_epochs || 0;
}
if (data.session.current_loss !== undefined && data.session.current_loss !== null) {
document.getElementById('training-loss').textContent = data.session.current_loss.toFixed(4);
}
// Update progress bar immediately
if (data.session.current_epoch && data.session.total_epochs) {
const percentage = (data.session.current_epoch / data.session.total_epochs) * 100;
document.getElementById('training-progress-bar').style.width = percentage + '%';
}
// Populate annotation count and timeframe if available // Populate annotation count and timeframe if available
if (data.session.annotation_count) { if (data.session.annotation_count) {
document.getElementById('training-annotation-count').textContent = data.session.annotation_count; document.getElementById('training-annotation-count').textContent = data.session.annotation_count;
@@ -242,6 +260,7 @@
document.getElementById('training-timeframe').textContent = data.session.timeframe.toUpperCase(); document.getElementById('training-timeframe').textContent = data.session.timeframe.toUpperCase();
} }
// Start polling for continued updates (will update GPU/CPU and future progress)
pollTrainingProgress(activeTrainingId); pollTrainingProgress(activeTrainingId);
} else { } else {
console.log('No active training session'); console.log('No active training session');
@@ -252,6 +271,62 @@
}); });
} }
function checkActiveInference() {
/**
* Check if there's an active real-time inference session on page load
* This allows resuming PnL tracking and position state after page reload
*/
fetch('/api/realtime-inference/signals')
.then(response => response.json())
.then(data => {
if (data.success) {
// Check if inference is active (signals endpoint returns data if active)
if (data.signals && data.signals.length > 0) {
console.log('Active inference session found, restoring state');
// Restore PnL and position state from metrics
if (data.metrics) {
// Update session PnL if available
if (data.metrics.session_pnl !== undefined) {
const sessionPnlEl = document.getElementById('session-pnl');
if (sessionPnlEl) {
const totalPnl = data.metrics.session_pnl || 0;
const pnlColor = totalPnl >= 0 ? 'text-success' : 'text-danger';
const pnlSign = totalPnl >= 0 ? '+' : '';
sessionPnlEl.textContent = `${pnlSign}$${totalPnl.toFixed(2)}`;
sessionPnlEl.className = `fw-bold ${pnlColor}`;
}
}
// Update position state if available
if (data.metrics.position_state) {
updatePositionStateDisplay(data.metrics.position_state, data.metrics.session_metrics || data.metrics);
}
}
// Restore live metrics (accuracy, loss) if available
if (data.metrics) {
if (data.metrics.accuracy !== undefined) {
const liveAccuracyEl = document.getElementById('live-accuracy');
if (liveAccuracyEl) {
liveAccuracyEl.textContent = (data.metrics.accuracy * 100).toFixed(1) + '%';
}
}
if (data.metrics.loss !== undefined) {
const liveLossEl = document.getElementById('live-loss');
if (liveLossEl) {
liveLossEl.textContent = data.metrics.loss.toFixed(4);
}
}
}
}
}
})
.catch(error => {
console.error('Error checking active inference:', error);
});
}
function loadAvailableModels() { function loadAvailableModels() {
fetch('/api/available-models') fetch('/api/available-models')
.then(response => response.json()) .then(response => response.json())