From 4fe952dbee136b2b403e855f9c3d594b0cde5534 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Mon, 8 Sep 2025 11:44:15 +0300 Subject: [PATCH] wip --- core/orchestrator.py | 285 +++++++++++++++------------- core/training_integration.py | 2 +- data/predictions.db | Bin 20480 -> 20480 bytes web/clean_dashboard.py | 184 +++++++++++++++++- web/layout_manager.py | 61 +++++- web/prediction_chart.py | 352 +++++++++++++++++++++++++++++++++++ 6 files changed, 743 insertions(+), 141 deletions(-) create mode 100644 web/prediction_chart.py diff --git a/core/orchestrator.py b/core/orchestrator.py index 3864e7a..1433c55 100644 --- a/core/orchestrator.py +++ b/core/orchestrator.py @@ -385,7 +385,7 @@ class TradingOrchestrator: logger.info(f"Transformer checkpoint loaded: {metadata.checkpoint_id}") except Exception as e: logger.debug(f"No transformer checkpoint found: {e}") - + if not checkpoint_loaded: self.model_states['transformer']['checkpoint_loaded'] = False self.model_states['transformer']['checkpoint_filename'] = 'none (fresh start)' @@ -1182,137 +1182,156 @@ class TradingOrchestrator: logger.debug(f"Error getting current price for {symbol}: {e}") return 0.0 - # Get standard feature matrix for this timeframe - feature_matrix = self.data_provider.get_feature_matrix( - symbol=symbol, - timeframes=[timeframe], - window_size=getattr(model, 'window_size', 20) - ) - - # Enhance with COB feature matrix if available - enhanced_features = feature_matrix - if feature_matrix is not None and self.cob_integration: - try: - # Get COB feature matrix (5-minute history) - cob_feature_matrix = self.get_cob_feature_matrix(symbol, sequence_length=60) - - if cob_feature_matrix is not None: - # Take the latest COB features to augment the standard features - latest_cob_features = cob_feature_matrix[-1:, :] # Shape: (1, 400) - - # Resize to match the feature matrix timeframe dimension - timeframe_count = feature_matrix.shape[0] - cob_features_expanded = np.repeat(latest_cob_features, timeframe_count, axis=0) - - # Concatenate COB features with standard features - # Standard features shape: (timeframes, window_size, features) - # COB features shape: (timeframes, 400) - # We'll add COB as additional features to each timeframe - window_size = feature_matrix.shape[1] - cob_features_reshaped = cob_features_expanded.reshape(timeframe_count, 1, 400) - cob_features_tiled = np.tile(cob_features_reshaped, (1, window_size, 1)) - - # Concatenate along feature dimension - enhanced_features = np.concatenate([feature_matrix, cob_features_tiled], axis=2) - - logger.debug(f"Enhanced CNN features with COB data for {symbol}: " - f"{feature_matrix.shape} + COB -> {enhanced_features.shape}") - - except Exception as cob_error: - logger.debug(f"Could not enhance CNN features with COB data: {cob_error}") - enhanced_features = feature_matrix - - # Add extrema features if available - if self.extrema_trainer: - try: - extrema_features = self.extrema_trainer.get_context_features_for_model(symbol) - if extrema_features is not None: - # Reshape and tile to match the enhanced_features shape - extrema_features = extrema_features.flatten() - tiled_extrema = np.tile(extrema_features, (enhanced_features.shape[0], enhanced_features.shape[1], 1)) - enhanced_features = np.concatenate([enhanced_features, tiled_extrema], axis=2) - logger.debug(f"Enhanced CNN features with Extrema data for {symbol}") - except Exception as extrema_error: - logger.debug(f"Could not enhance CNN features with Extrema data: {extrema_error}") - - if enhanced_features is not None: - # Get CNN prediction - use the actual underlying model - try: - # Ensure features are properly shaped and limited - if isinstance(enhanced_features, np.ndarray): - # Flatten and limit features to prevent shape mismatches - enhanced_features = enhanced_features.flatten() - if len(enhanced_features) > 100: # Limit to 100 features - enhanced_features = enhanced_features[:100] - elif len(enhanced_features) < 100: # Pad with zeros - padded = np.zeros(100) - padded[:len(enhanced_features)] = enhanced_features - enhanced_features = padded - - if hasattr(model.model, 'act'): - # Use the CNN's act method - action_result = model.model.act(enhanced_features, explore=False) - if isinstance(action_result, tuple): - action_idx, confidence = action_result - else: - action_idx = action_result - confidence = 0.7 # Default confidence - - # Convert to action probabilities - action_probs = [0.1, 0.1, 0.8] # Default distribution - action_probs[action_idx] = confidence - else: - # Fallback to generic predict method - action_probs, confidence = model.predict(enhanced_features) - except Exception as e: - logger.warning(f"CNN prediction failed: {e}") - action_probs, confidence = None, None - - if action_probs is not None: - # Convert to prediction object - action_names = ['SELL', 'HOLD', 'BUY'] - best_action_idx = np.argmax(action_probs) - best_action = action_names[best_action_idx] - - prediction = Prediction( - action=best_action, - confidence=float(confidence) if confidence is not None else float(action_probs[best_action_idx]), - probabilities={name: float(prob) for name, prob in zip(action_names, action_probs)}, - timeframe=timeframe, - timestamp=datetime.now(), - model_name=model.name, - metadata={ - 'timeframe_specific': True, - 'cob_enhanced': enhanced_features is not feature_matrix, - 'feature_shape': str(enhanced_features.shape) - } - ) - - predictions.append(prediction) - - # Capture CNN prediction for dashboard visualization - current_price = self._get_current_price(symbol) - if current_price: - direction = best_action_idx # 0=SELL, 1=HOLD, 2=BUY - pred_confidence = float(confidence) if confidence is not None else float(action_probs[best_action_idx]) - predicted_price = current_price * (1 + (pred_confidence * 0.01 if best_action == 'BUY' else -pred_confidence * 0.01 if best_action == 'SELL' else 0)) - self.capture_cnn_prediction(symbol, int(direction), pred_confidence, current_price, predicted_price) - - except Exception as e: - logger.error(f"Error getting CNN predictions: {e}") - - return predictions - - async def _get_rl_prediction(self, model: RLAgentInterface, symbol: str) -> Optional[Prediction]: - """Get prediction from RL agent""" + async def _get_cob_rl_prediction(self, model: COBRLModelInterface, symbol: str) -> Optional[Prediction]: + """Get prediction from COB RL model""" try: - # Get current state for RL agent - state = self._get_rl_state(symbol) - if state is None: + # Get COB state from current market data + cob_state = self._get_cob_state(symbol) + if cob_state is None: return None - # Get RL agent's action, confidence, and q_values from the underlying model + # Get prediction from COB RL model if hasattr(model.model, 'act_with_confidence'): + result = model.model.act_with_confidence(cob_state) + if len(result) == 2: + action_idx, confidence = result + else: + action_idx = result[0] if isinstance(result, (list, tuple)) else result + confidence = 0.6 + else: + action_idx = model.model.act(cob_state) + confidence = 0.6 + + # Convert to action name + action_names = ['BUY', 'SELL', 'HOLD'] + if 0 <= action_idx < len(action_names): + action = action_names[action_idx] + else: + return None + + # Store prediction in database for tracking + if (hasattr(self, 'enhanced_training_system') and + self.enhanced_training_system and + hasattr(self.enhanced_training_system, 'store_model_prediction')): + + current_price = self._get_current_price_safe(symbol) + if current_price > 0: + prediction_id = self.enhanced_training_system.store_model_prediction( + model_name=f"COB_RL_{model.model_name}" if hasattr(model, 'model_name') else "COB_RL", + symbol=symbol, + prediction_type=action, + confidence=confidence, + current_price=current_price + ) + logger.debug(f"Stored COB RL prediction {prediction_id} for {symbol}") + + # Create prediction object + prediction = Prediction( + model_name=f"COB_RL_{model.model_name}" if hasattr(model, 'model_name') else "COB_RL", + symbol=symbol, + signal=action, + confidence=confidence, + reasoning=f"COB RL model prediction based on order book imbalance", + features=cob_state.tolist() if isinstance(cob_state, np.ndarray) else [], + metadata={ + 'action_idx': action_idx, + 'cob_state_size': len(cob_state) if cob_state is not None else 0 + } + ) + + return prediction + + except Exception as e: + logger.error(f"Error getting COB RL prediction for {symbol}: {e}") + return None + + async def _get_generic_prediction(self, model, symbol: str) -> Optional[Prediction]: + """Get prediction from generic model interface""" + try: + # Placeholder for generic model prediction + logger.debug(f"Getting generic prediction from {model} for {symbol}") + return None + except Exception as e: + logger.error(f"Error getting generic prediction for {symbol}: {e}") + return None + + def _get_rl_state(self, symbol: str) -> Optional[np.ndarray]: + """Build RL state vector for DQN agent""" + try: + # Use data provider to get comprehensive RL state + if hasattr(self.data_provider, 'get_dqn_state_for_inference'): + symbols_timeframes = [(symbol, '1m'), (symbol, '5m'), (symbol, '1h')] + state = self.data_provider.get_dqn_state_for_inference(symbols_timeframes, target_size=100) + if state is not None: + return state + + # Fallback: build basic state from market data + market_features = [] + + # Get latest price data + latest_data = self.data_provider.get_latest_data(symbol) + if latest_data and 'close' in latest_data: + current_price = float(latest_data['close']) + market_features.extend([ + current_price, + latest_data.get('volume', 0.0), + latest_data.get('high', current_price) - latest_data.get('low', current_price), # Range + latest_data.get('open', current_price) + ]) + else: + market_features.extend([4300.0, 100.0, 10.0, 4295.0]) # Default values + + # Pad to standard size + while len(market_features) < 100: + market_features.append(0.0) + + return np.array(market_features[:100], dtype=np.float32) + + except Exception as e: + logger.debug(f"Error building RL state for {symbol}: {e}") + return None + + def _get_cob_state(self, symbol: str) -> Optional[np.ndarray]: + """Build COB state vector for COB RL agent""" + try: + # Get COB data from integration + if hasattr(self, 'cob_integration') and self.cob_integration: + cob_snapshot = self.cob_integration.get_cob_snapshot(symbol) + if cob_snapshot: + # Extract features from COB snapshot + features = [] + + # Add bid/ask imbalance + bid_volume = sum([level['volume'] for level in cob_snapshot.get('bids', [])]) + ask_volume = sum([level['volume'] for level in cob_snapshot.get('asks', [])]) + if bid_volume + ask_volume > 0: + imbalance = (bid_volume - ask_volume) / (bid_volume + ask_volume) + else: + imbalance = 0.0 + features.append(imbalance) + + # Add spread + if cob_snapshot.get('bids') and cob_snapshot.get('asks'): + spread = cob_snapshot['asks'][0]['price'] - cob_snapshot['bids'][0]['price'] + features.append(spread) + else: + features.append(0.0) + + # Pad to standard size + while len(features) < 50: + features.append(0.0) + + return np.array(features[:50], dtype=np.float32) + + # Fallback state + return np.zeros(50, dtype=np.float32) + + except Exception as e: + logger.debug(f"Error building COB state for {symbol}: {e}") + return None + + def _combine_predictions(self, symbol: str, price: float, predictions: List[Prediction], + timestamp: datetime) -> TradingDecision: # Call act_with_confidence and handle different return formats result = model.model.act_with_confidence(state) @@ -1728,7 +1747,7 @@ class TradingOrchestrator: ) if needs_refresh: - result = load_best_checkpoint(model_name) + result = load_best_checkpoint(model_name) self._checkpoint_cache[model_name] = result self._checkpoint_cache_time[model_name] = current_time @@ -1869,14 +1888,14 @@ class TradingOrchestrator: logger.warning("EnhancedRealtimeTrainingSystem not available - training disabled") self.training_enabled = False return - + # Initialize enhanced training system directly (no external training_integration module needed) try: from NN.training.enhanced_realtime_training import EnhancedRealtimeTrainingSystem - self.enhanced_training_system = EnhancedRealtimeTrainingSystem( - orchestrator=self, - data_provider=self.data_provider, + self.enhanced_training_system = EnhancedRealtimeTrainingSystem( + orchestrator=self, + data_provider=self.data_provider, dashboard=None ) diff --git a/core/training_integration.py b/core/training_integration.py index ea1419a..55f7dee 100644 --- a/core/training_integration.py +++ b/core/training_integration.py @@ -13,7 +13,7 @@ import logging from datetime import datetime from typing import Dict, List, Any, Optional import numpy as np -from utils.reward_calculator import RewardCalculator +from core.reward_calculator import RewardCalculator import threading import time diff --git a/data/predictions.db b/data/predictions.db index 4c6332405b41455f7d2dc154e79894820d7d2e8b..d4b67b0863850e52590fcdf6ba48133be1a87775 100644 GIT binary patch literal 20480 zcmeI3Uuaup6u|FI(j+(kZl;d1VcmsM*0MFde{z%Dg2c3mTUna6=?$z;AxmzzVAFKT zt*%v=lkH(IJ`6!7sBeM@f-f>(EP^k>d>QDoGLg9rMc9k@2fzE>+}tnsrmLbNjPudb zn|sdpopXNYoX{jEE6c^Eu5oMaPOGMK5*4RtntG1oD2h6aen-%+wLIv;XFWiFY2tdX z%fnRS#cR=MfbxZRsAw(9hIb;L@1+d)fdCKy0zd!=00AHX1c1QfOW?JW-oWVeG=1e+ zy>@n8YqcBNdUaFl;D5z8>h2r9xpE<=7C1FGTP$!ceQqMeIhLujQEO?OS~#O}OXc~6 zTzQo{U06NgQr6pg4LQ)!8qK=iY;SbA`I1^VStxV)!t=RGQRR5|rh2>6(dzo&-DcYA zXfM?|4Gu+Bv~47YSXr-i_3GAUqo!*O9La$5lR2M%v@%Uo&5ee3v3p@1y`ZVqw)8fB zTy=q}3a(qm%bcK5YD*sf=<(z9Do#o7sIGQlOG629JoFBv!y%k7pr1fHC$BzD2S$$` zrLSGWXA*pgOV7YrgkVTgn~i>Ahf4*W`r2E)-riViHVlepy7$)9n=P%Y*IJwVd(f<-Jd?Gz-`N9YWAw_@ z6t}pg*W0K;)|%RSqwCUWo8s!ATW)H#y1s?m$8f5CzP54BjMS}3RJ~i+>{fd%WhL3I zJKvuC@Tr%7 zvsE}DWp2M1G)zBfnhrWl&sJ8mcTF_*R?;#uosQpg$qdV)lFG=t@7!FzfAyK0zuPLd z!Ic-BHgL-hwSYO&+5kU!QS>_J}T{4`=q$o()rLzmSe|~>h9=Ah8 z386e9nsUo3p;L>+eD?k$a0)3w6ytvkw=60mzx%3bnNaED+-6*+?L^}0{o#hO-f|s} zt5-^=g!n_33z;%=n zMLw&Wz2mi2`qsDoMBrux+stv`-#sGRd|FN`@vrT3Nz-X0-MwO(_ShNe)Kf@1v3Th*k zXr)U&!>fKi2(LuTE1Z%Cn2V!*dv1woImJ6u(sK*-M_RU0;%IHW{j%R{doajFusk42 z@`jDppx^UAP$bk1j?DBtNTw9IpPB3@dxF`aqtux7yxPkTYD&uof00bTn0xa!4 zO53v=-AFX{CKZh3DfTP&Ap0)68EeJzvAeNPACGV$3LpRkfB+Bx0zd!=00AHX1b_e# z_%{h03;7SxOfY8-k`N;Z#FT<$OomJ_h9Zp>g_egI{6rm%DD?+9*wfUpzu@9Tq<0{m zg7W+_{6J%_>3G`c<*A7_{V`m&8ZDd8S$u%-a45eVe(_`W_* z`Owipnmk%0K9?uX#PbVDugep`JxM_9!ZQbxw1132!D9bWRvZS%*?_herdz<}i-#|bC2mk>f00e*l5C8%|00;m9AOHk_01)^G1VY|JwE3;BNYs0T o?!RFX#sd6Zose}Yw3xVeoF?9<5Pgmtg4c0__k_J;jP=&QpL1A4oB#j- delta 115 zcmZozz}T>Wae_1>%S0JxMwX2U3;8)27#JA&TNwCTHVZ0L@lS4%Z({kt!2bcryT?B< zfJ2Ipky)9upeQvZGr1%)Kd+b dcc.Graph: + """Create a timeline chart showing predictions and their outcomes""" + try: + if not predictions_data: + # Empty chart + fig = go.Figure() + fig.add_annotation( + text="No prediction data available", + xref="paper", yref="paper", + x=0.5, y=0.5, xanchor='center', yanchor='middle', + showarrow=False, font=dict(size=16, color="gray") + ) + fig.update_layout( + title="Model Predictions Timeline", + xaxis_title="Time", + yaxis_title="Confidence", + height=300 + ) + return dcc.Graph(figure=fig, id="prediction-timeline") + + # Convert to DataFrame + df = pd.DataFrame(predictions_data) + df['timestamp'] = pd.to_datetime(df['timestamp']) + + # Create the plot + fig = go.Figure() + + # Add prediction points + for prediction_type in ['BUY', 'SELL', 'HOLD']: + type_data = df[df['prediction_type'] == prediction_type] + if not type_data.empty: + # Different markers for resolved vs pending + resolved_data = type_data[type_data['is_resolved'] == True] + pending_data = type_data[type_data['is_resolved'] == False] + + if not resolved_data.empty: + # Resolved predictions + colors = [self.colors['reward'] if r > 0 else self.colors['penalty'] + for r in resolved_data['reward']] + fig.add_trace(go.Scatter( + x=resolved_data['timestamp'], + y=resolved_data['confidence'], + mode='markers', + marker=dict( + size=10, + color=colors, + symbol='circle', + line=dict(width=2, color=self.colors[prediction_type]) + ), + name=f'{prediction_type} (Resolved)', + text=[f"Model: {m}
Confidence: {c:.3f}
Reward: {r:.2f}" + for m, c, r in zip(resolved_data['model_name'], + resolved_data['confidence'], + resolved_data['reward'])], + hovertemplate='%{text}' + )) + + if not pending_data.empty: + # Pending predictions + fig.add_trace(go.Scatter( + x=pending_data['timestamp'], + y=pending_data['confidence'], + mode='markers', + marker=dict( + size=8, + color=self.colors[prediction_type], + symbol='circle-open', + line=dict(width=2) + ), + name=f'{prediction_type} (Pending)', + text=[f"Model: {m}
Confidence: {c:.3f}
Status: Pending" + for m, c in zip(pending_data['model_name'], + pending_data['confidence'])], + hovertemplate='%{text}' + )) + + # Update layout + fig.update_layout( + title="Model Predictions Timeline", + xaxis_title="Time", + yaxis_title="Confidence", + yaxis=dict(range=[0, 1]), + height=400, + showlegend=True, + legend=dict(x=0.02, y=0.98), + hovermode='closest' + ) + + return dcc.Graph(figure=fig, id="prediction-timeline") + + except Exception as e: + logger.error(f"Error creating prediction timeline chart: {e}") + # Return empty chart on error + fig = go.Figure() + fig.add_annotation(text=f"Error: {str(e)}", x=0.5, y=0.5) + return dcc.Graph(figure=fig, id="prediction-timeline") + + def create_model_performance_chart(self, model_stats: List[Dict[str, Any]]) -> dcc.Graph: + """Create a bar chart showing model performance metrics""" + try: + if not model_stats: + fig = go.Figure() + fig.add_annotation( + text="No model performance data available", + xref="paper", yref="paper", + x=0.5, y=0.5, xanchor='center', yanchor='middle', + showarrow=False, font=dict(size=16, color="gray") + ) + fig.update_layout( + title="Model Performance Comparison", + height=300 + ) + return dcc.Graph(figure=fig, id="model-performance") + + # Extract data + model_names = [stats['model_name'] for stats in model_stats] + accuracies = [stats['accuracy'] * 100 for stats in model_stats] # Convert to percentage + total_rewards = [stats['total_reward'] for stats in model_stats] + total_predictions = [stats['total_predictions'] for stats in model_stats] + + # Create subplots + fig = go.Figure() + + # Add accuracy bars + fig.add_trace(go.Bar( + x=model_names, + y=accuracies, + name='Accuracy (%)', + marker_color='lightblue', + yaxis='y', + text=[f"{a:.1f}%" for a in accuracies], + textposition='auto' + )) + + # Add total reward on secondary y-axis + fig.add_trace(go.Scatter( + x=model_names, + y=total_rewards, + mode='markers+text', + name='Total Reward', + marker=dict( + size=12, + color='orange', + symbol='diamond' + ), + yaxis='y2', + text=[f"{r:.1f}" for r in total_rewards], + textposition='top center' + )) + + # Update layout + fig.update_layout( + title="Model Performance Comparison", + xaxis_title="Model", + yaxis=dict( + title="Accuracy (%)", + side="left", + range=[0, 100] + ), + yaxis2=dict( + title="Total Reward", + side="right", + overlaying="y" + ), + height=400, + showlegend=True, + legend=dict(x=0.02, y=0.98) + ) + + return dcc.Graph(figure=fig, id="model-performance") + + except Exception as e: + logger.error(f"Error creating model performance chart: {e}") + fig = go.Figure() + fig.add_annotation(text=f"Error: {str(e)}", x=0.5, y=0.5) + return dcc.Graph(figure=fig, id="model-performance") + + def create_prediction_table(self, recent_predictions: List[Dict[str, Any]]) -> dash_table.DataTable: + """Create a table showing recent predictions""" + try: + if not recent_predictions: + return dash_table.DataTable( + id="prediction-table", + columns=[ + {"name": "Model", "id": "model_name"}, + {"name": "Symbol", "id": "symbol"}, + {"name": "Prediction", "id": "prediction_type"}, + {"name": "Confidence", "id": "confidence"}, + {"name": "Status", "id": "status"}, + {"name": "Reward", "id": "reward"} + ], + data=[], + style_cell={'textAlign': 'center'}, + style_header={'backgroundColor': 'rgb(230, 230, 230)', 'fontWeight': 'bold'}, + page_size=10 + ) + + # Format data for table + table_data = [] + for pred in recent_predictions[-20:]: # Show last 20 predictions + table_data.append({ + 'model_name': pred.get('model_name', 'Unknown'), + 'symbol': pred.get('symbol', 'N/A'), + 'prediction_type': pred.get('prediction_type', 'N/A'), + 'confidence': f"{pred.get('confidence', 0):.3f}", + 'status': 'Resolved' if pred.get('is_resolved', False) else 'Pending', + 'reward': f"{pred.get('reward', 0):.2f}" if pred.get('is_resolved', False) else 'Pending' + }) + + return dash_table.DataTable( + id="prediction-table", + columns=[ + {"name": "Model", "id": "model_name"}, + {"name": "Symbol", "id": "symbol"}, + {"name": "Prediction", "id": "prediction_type"}, + {"name": "Confidence", "id": "confidence"}, + {"name": "Status", "id": "status"}, + {"name": "Reward", "id": "reward"} + ], + data=table_data, + style_cell={'textAlign': 'center', 'fontSize': '12px'}, + style_header={'backgroundColor': 'rgb(230, 230, 230)', 'fontWeight': 'bold'}, + style_data_conditional=[ + { + 'if': {'filter_query': '{status} = Resolved and {reward} > 0'}, + 'backgroundColor': 'rgba(40, 167, 69, 0.1)', + 'color': 'black', + }, + { + 'if': {'filter_query': '{status} = Resolved and {reward} < 0'}, + 'backgroundColor': 'rgba(220, 53, 69, 0.1)', + 'color': 'black', + }, + { + 'if': {'filter_query': '{status} = Pending'}, + 'backgroundColor': 'rgba(108, 117, 125, 0.1)', + 'color': 'black', + } + ], + page_size=10, + sort_action="native" + ) + + except Exception as e: + logger.error(f"Error creating prediction table: {e}") + return dash_table.DataTable( + id="prediction-table", + columns=[{"name": "Error", "id": "error"}], + data=[{"error": str(e)}] + ) + + def create_prediction_panel(self, prediction_stats: Dict[str, Any]) -> html.Div: + """Create a complete prediction tracking panel""" + try: + predictions_data = prediction_stats.get('predictions', []) + model_stats = prediction_stats.get('models', []) + + return html.Div([ + html.H4("📊 Prediction Tracking & Performance", className="mb-3"), + + # Summary cards + html.Div([ + html.Div([ + html.H6(f"{prediction_stats.get('total_predictions', 0)}", className="mb-0"), + html.Small("Total Predictions", className="text-muted") + ], className="card-body text-center"), + ], className="card col-md-3 mx-1"), + + html.Div([ + html.Div([ + html.H6(f"{prediction_stats.get('active_predictions', 0)}", className="mb-0"), + html.Small("Pending Resolution", className="text-muted") + ], className="card-body text-center"), + ], className="card col-md-3 mx-1"), + + html.Div([ + html.Div([ + html.H6(f"{len(model_stats)}", className="mb-0"), + html.Small("Active Models", className="text-muted") + ], className="card-body text-center"), + ], className="card col-md-3 mx-1"), + + html.Div([ + html.Div([ + html.H6(f"{sum(s.get('total_reward', 0) for s in model_stats):.1f}", className="mb-0"), + html.Small("Total Rewards", className="text-muted") + ], className="card-body text-center"), + ], className="card col-md-3 mx-1") + + ], className="row mb-4"), + + # Charts + html.Div([ + html.Div([ + self.create_prediction_timeline_chart(predictions_data) + ], className="col-md-6"), + + html.Div([ + self.create_model_performance_chart(model_stats) + ], className="col-md-6") + ], className="row mb-4"), + + # Recent predictions table + html.Div([ + html.H5("Recent Predictions", className="mb-2"), + self.create_prediction_table(predictions_data) + ], className="mb-3") + + except Exception as e: + logger.error(f"Error creating prediction panel: {e}") + return html.Div([ + html.H4("📊 Prediction Tracking & Performance"), + html.P(f"Error loading prediction data: {str(e)}", className="text-danger") + ]) + +# Global instance +_prediction_chart = None + +def get_prediction_chart() -> PredictionChartComponent: + """Get global prediction chart component""" + global _prediction_chart + if _prediction_chart is None: + _prediction_chart = PredictionChartComponent() + return _prediction_chart