""" Trading Dashboard - Clean Web Interface This module provides a modern, responsive web dashboard for the trading system: - Real-time price charts with multiple timeframes - Model performance monitoring - Trading decisions visualization - System health monitoring - Memory usage tracking """ import asyncio import json import logging import time from datetime import datetime, timedelta from threading import Thread from typing import Dict, List, Optional, Any import dash from dash import dcc, html, Input, Output, State, callback_context import plotly.graph_objects as go import plotly.express as px from plotly.subplots import make_subplots import pandas as pd import numpy as np from core.config import get_config from core.data_provider import DataProvider from core.orchestrator import TradingOrchestrator, TradingDecision from models import get_model_registry logger = logging.getLogger(__name__) class TradingDashboard: """Modern trading dashboard with real-time updates""" def __init__(self, data_provider: DataProvider = None, orchestrator: TradingOrchestrator = None): """Initialize the dashboard""" self.config = get_config() self.data_provider = data_provider or DataProvider() self.orchestrator = orchestrator or TradingOrchestrator(self.data_provider) self.model_registry = get_model_registry() # Dashboard state self.recent_decisions = [] self.performance_data = {} self.current_prices = {} self.last_update = datetime.now() # Create Dash app self.app = dash.Dash(__name__, external_stylesheets=[ 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css' ]) # Setup layout and callbacks self._setup_layout() self._setup_callbacks() logger.info("Trading Dashboard initialized") def _setup_layout(self): """Setup the dashboard layout""" self.app.layout = html.Div([ # Header html.Div([ html.H1([ html.I(className="fas fa-chart-line me-3"), "Trading System Dashboard" ], className="text-white mb-0"), html.P(f"Multi-Modal AI Trading • Memory: {self.model_registry.total_memory_limit_mb/1024:.1f}GB Limit", className="text-light mb-0 opacity-75") ], className="bg-dark p-4 mb-4"), # Auto-refresh component dcc.Interval( id='interval-component', interval=2000, # Update every 2 seconds n_intervals=0 ), # Main content html.Div([ # Top row - Key metrics html.Div([ html.Div([ html.Div([ html.H4(id="current-price", className="text-success mb-1"), html.P("Current Price", className="text-muted mb-0 small") ], className="card-body text-center") ], className="card bg-light"), html.Div([ html.Div([ html.H4(id="total-pnl", className="mb-1"), html.P("Total P&L", className="text-muted mb-0 small") ], className="card-body text-center") ], className="card bg-light"), html.Div([ html.Div([ html.H4(id="win-rate", className="text-info mb-1"), html.P("Win Rate", className="text-muted mb-0 small") ], className="card-body text-center") ], className="card bg-light"), html.Div([ html.Div([ html.H4(id="memory-usage", className="text-warning mb-1"), html.P("Memory Usage", className="text-muted mb-0 small") ], className="card-body text-center") ], className="card bg-light"), ], className="row g-3 mb-4"), # Charts row html.Div([ # Price chart html.Div([ html.Div([ html.H5([ html.I(className="fas fa-chart-candlestick me-2"), "Price Chart" ], className="card-title"), dcc.Graph(id="price-chart", style={"height": "400px"}) ], className="card-body") ], className="card"), # Model performance chart html.Div([ html.Div([ html.H5([ html.I(className="fas fa-brain me-2"), "Model Performance" ], className="card-title"), dcc.Graph(id="model-performance-chart", style={"height": "400px"}) ], className="card-body") ], className="card") ], className="row g-3 mb-4"), # Bottom row - Recent decisions and system status html.Div([ # Recent decisions html.Div([ html.Div([ html.H5([ html.I(className="fas fa-robot me-2"), "Recent Trading Decisions" ], className="card-title"), html.Div(id="recent-decisions", style={"maxHeight": "300px", "overflowY": "auto"}) ], className="card-body") ], className="card"), # System status html.Div([ html.Div([ html.H5([ html.I(className="fas fa-server me-2"), "System Status" ], className="card-title"), html.Div(id="system-status") ], className="card-body") ], className="card") ], className="row g-3") ], className="container-fluid") ]) def _setup_callbacks(self): """Setup dashboard callbacks for real-time updates""" @self.app.callback( [ Output('current-price', 'children'), Output('total-pnl', 'children'), Output('total-pnl', 'className'), Output('win-rate', 'children'), Output('memory-usage', 'children'), Output('price-chart', 'figure'), Output('model-performance-chart', 'figure'), Output('recent-decisions', 'children'), Output('system-status', 'children') ], [Input('interval-component', 'n_intervals')] ) def update_dashboard(n_intervals): """Update all dashboard components""" try: # Get current prices symbol = self.config.symbols[0] if self.config.symbols else "ETH/USDT" current_price = self.data_provider.get_current_price(symbol) # Get model performance metrics performance_metrics = self.orchestrator.get_performance_metrics() # Get memory stats memory_stats = self.model_registry.get_memory_stats() # Calculate P&L from recent decisions total_pnl = 0.0 wins = 0 total_trades = len(self.recent_decisions) for decision in self.recent_decisions[-20:]: # Last 20 decisions if hasattr(decision, 'pnl') and decision.pnl: total_pnl += decision.pnl if decision.pnl > 0: wins += 1 # Format outputs price_text = f"${current_price:.2f}" if current_price else "Loading..." pnl_text = f"${total_pnl:.2f}" pnl_class = "text-success mb-1" if total_pnl >= 0 else "text-danger mb-1" win_rate_text = f"{(wins/total_trades*100):.1f}%" if total_trades > 0 else "0.0%" memory_text = f"{memory_stats['utilization_percent']:.1f}%" # Create charts price_chart = self._create_price_chart(symbol) performance_chart = self._create_performance_chart(performance_metrics) # Create recent decisions list decisions_list = self._create_decisions_list() # Create system status system_status = self._create_system_status(memory_stats) return ( price_text, pnl_text, pnl_class, win_rate_text, memory_text, price_chart, performance_chart, decisions_list, system_status ) except Exception as e: logger.error(f"Error updating dashboard: {e}") # Return safe defaults empty_fig = go.Figure() empty_fig.add_annotation(text="Loading...", xref="paper", yref="paper", x=0.5, y=0.5) return ( "Loading...", "$0.00", "text-muted mb-1", "0.0%", "0.0%", empty_fig, empty_fig, [], html.P("Loading system status...") ) def _create_price_chart(self, symbol: str) -> go.Figure: """Create enhanced price chart optimized for 1s scalping""" try: # Create subplots for scalping view fig = make_subplots( rows=4, cols=1, shared_xaxes=True, vertical_spacing=0.05, subplot_titles=( f"{symbol} Price Chart (1s Scalping)", "RSI & Momentum", "MACD", "Volume & Tick Activity" ), row_heights=[0.5, 0.2, 0.15, 0.15] ) # Use 1s timeframe for scalping (fall back to 1m if 1s not available) timeframes_to_try = ['1s', '1m', '5m'] df = None actual_timeframe = None for tf in timeframes_to_try: df = self.data_provider.get_latest_candles(symbol, tf, limit=200) # More data for 1s if not df.empty: actual_timeframe = tf break if df is None or df.empty: fig.add_annotation(text="No scalping data available", xref="paper", yref="paper", x=0.5, y=0.5) return fig # Main candlestick chart (or line chart for 1s data) if actual_timeframe == '1s': # Use line chart for 1s data as candlesticks might be too dense fig.add_trace(go.Scatter( x=df['timestamp'], y=df['close'], mode='lines', name=f"{symbol} {actual_timeframe.upper()}", line=dict(color='#00ff88', width=2), hovertemplate='%{y:.2f}
%{x}' ), row=1, col=1) # Add high/low bands for reference fig.add_trace(go.Scatter( x=df['timestamp'], y=df['high'], mode='lines', name='High', line=dict(color='rgba(0,255,136,0.3)', width=1), showlegend=False ), row=1, col=1) fig.add_trace(go.Scatter( x=df['timestamp'], y=df['low'], mode='lines', name='Low', line=dict(color='rgba(255,107,107,0.3)', width=1), fill='tonexty', fillcolor='rgba(128,128,128,0.1)', showlegend=False ), row=1, col=1) else: # Use candlestick for longer timeframes fig.add_trace(go.Candlestick( x=df['timestamp'], open=df['open'], high=df['high'], low=df['low'], close=df['close'], name=f"{symbol} {actual_timeframe.upper()}", increasing_line_color='#00ff88', decreasing_line_color='#ff6b6b' ), row=1, col=1) # Add short-term moving averages for scalping if len(df) > 20: # Very short-term EMAs for scalping if 'ema_12' in df.columns: fig.add_trace(go.Scatter( x=df['timestamp'], y=df['ema_12'], name='EMA 12', line=dict(color='#ffa500', width=1), opacity=0.8 ), row=1, col=1) if 'sma_20' in df.columns: fig.add_trace(go.Scatter( x=df['timestamp'], y=df['sma_20'], name='SMA 20', line=dict(color='#ff1493', width=1), opacity=0.8 ), row=1, col=1) # RSI for scalping (look for quick oversold/overbought) if 'rsi_14' in df.columns: fig.add_trace(go.Scatter( x=df['timestamp'], y=df['rsi_14'], name='RSI 14', line=dict(color='#ffeb3b', width=2), opacity=0.8 ), row=2, col=1) # RSI levels for scalping fig.add_hline(y=80, line_dash="dash", line_color="red", opacity=0.6, row=2, col=1) fig.add_hline(y=20, line_dash="dash", line_color="green", opacity=0.6, row=2, col=1) fig.add_hline(y=70, line_dash="dot", line_color="orange", opacity=0.4, row=2, col=1) fig.add_hline(y=30, line_dash="dot", line_color="orange", opacity=0.4, row=2, col=1) # Add momentum composite for quick signals if 'momentum_composite' in df.columns: fig.add_trace(go.Scatter( x=df['timestamp'], y=df['momentum_composite'] * 100, name='Momentum', line=dict(color='#9c27b0', width=2), opacity=0.7 ), row=2, col=1) # MACD for trend confirmation if all(col in df.columns for col in ['macd', 'macd_signal']): fig.add_trace(go.Scatter( x=df['timestamp'], y=df['macd'], name='MACD', line=dict(color='#2196f3', width=2) ), row=3, col=1) fig.add_trace(go.Scatter( x=df['timestamp'], y=df['macd_signal'], name='Signal', line=dict(color='#ff9800', width=2) ), row=3, col=1) if 'macd_histogram' in df.columns: colors = ['red' if val < 0 else 'green' for val in df['macd_histogram']] fig.add_trace(go.Bar( x=df['timestamp'], y=df['macd_histogram'], name='Histogram', marker_color=colors, opacity=0.6 ), row=3, col=1) # Volume activity (crucial for scalping) fig.add_trace(go.Bar( x=df['timestamp'], y=df['volume'], name='Volume', marker_color='rgba(70,130,180,0.6)', yaxis='y4' ), row=4, col=1) # Mark recent trading decisions with proper positioning for decision in self.recent_decisions[-10:]: # Show more decisions for scalping if hasattr(decision, 'timestamp') and hasattr(decision, 'price'): # Find the closest timestamp in our data for proper positioning if not df.empty: closest_idx = df.index[df['timestamp'].searchsorted(decision.timestamp)] if 0 <= closest_idx < len(df): closest_time = df.iloc[closest_idx]['timestamp'] # Use the actual price from decision, not from chart data marker_price = decision.price color = '#00ff88' if decision.action == 'BUY' else '#ff6b6b' if decision.action == 'SELL' else '#ffa500' symbol_shape = 'triangle-up' if decision.action == 'BUY' else 'triangle-down' if decision.action == 'SELL' else 'circle' fig.add_trace(go.Scatter( x=[closest_time], y=[marker_price], mode='markers', marker=dict( color=color, size=12, symbol=symbol_shape, line=dict(color='white', width=2) ), name=f"{decision.action}", showlegend=False, hovertemplate=f"{decision.action}
Price: ${decision.price:.2f}
Time: %{{x}}
Confidence: {decision.confidence:.1%}" ), row=1, col=1) # Update layout for scalping view fig.update_layout( title=f"{symbol} Scalping View ({actual_timeframe.upper()})", template="plotly_dark", height=800, xaxis_rangeslider_visible=False, margin=dict(l=0, r=0, t=50, b=0), legend=dict( orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1 ) ) # Update y-axis labels fig.update_yaxes(title_text="Price ($)", row=1, col=1) fig.update_yaxes(title_text="RSI/Momentum", row=2, col=1, range=[0, 100]) fig.update_yaxes(title_text="MACD", row=3, col=1) fig.update_yaxes(title_text="Volume", row=4, col=1) # Update x-axis for better time resolution fig.update_xaxes( tickformat='%H:%M:%S' if actual_timeframe in ['1s', '1m'] else '%H:%M', row=4, col=1 ) return fig except Exception as e: logger.error(f"Error creating scalping chart: {e}") fig = go.Figure() fig.add_annotation(text=f"Chart Error: {str(e)}", xref="paper", yref="paper", x=0.5, y=0.5) return fig def _create_performance_chart(self, performance_metrics: Dict) -> go.Figure: """Create enhanced model performance chart with feature matrix information""" try: # Create subplots for different performance metrics fig = make_subplots( rows=2, cols=2, subplot_titles=( "Model Accuracy by Timeframe", "Feature Matrix Dimensions", "Model Memory Usage", "Prediction Confidence" ), specs=[[{"type": "bar"}, {"type": "bar"}], [{"type": "pie"}, {"type": "scatter"}]] ) # Get feature matrix info for visualization try: symbol = self.config.symbols[0] if self.config.symbols else "ETH/USDT" feature_matrix = self.data_provider.get_feature_matrix( symbol, timeframes=['1m', '1h', '4h', '1d'], window_size=20 ) if feature_matrix is not None: n_timeframes, window_size, n_features = feature_matrix.shape # Feature matrix dimensions chart fig.add_trace(go.Bar( x=['Timeframes', 'Window Size', 'Features'], y=[n_timeframes, window_size, n_features], name='Dimensions', marker_color=['#1f77b4', '#ff7f0e', '#2ca02c'], text=[f'{n_timeframes}', f'{window_size}', f'{n_features}'], textposition='auto' ), row=1, col=2) # Model accuracy by timeframe (simulated data for demo) timeframe_names = ['1m', '1h', '4h', '1d'][:n_timeframes] simulated_accuracies = [0.65 + i*0.05 + np.random.uniform(-0.03, 0.03) for i in range(n_timeframes)] fig.add_trace(go.Bar( x=timeframe_names, y=[acc * 100 for acc in simulated_accuracies], name='Accuracy %', marker_color=['#ff9999', '#66b3ff', '#99ff99', '#ffcc99'][:n_timeframes], text=[f'{acc:.1%}' for acc in simulated_accuracies], textposition='auto' ), row=1, col=1) else: # No feature matrix available fig.add_annotation( text="Feature matrix not available", xref="paper", yref="paper", x=0.75, y=0.75, showarrow=False ) except Exception as e: logger.warning(f"Could not get feature matrix info: {e}") fig.add_annotation( text="Feature analysis unavailable", xref="paper", yref="paper", x=0.75, y=0.75, showarrow=False ) # Model memory usage memory_stats = self.model_registry.get_memory_stats() if memory_stats.get('models'): model_names = list(memory_stats['models'].keys()) model_usage = [memory_stats['models'][model]['memory_mb'] for model in model_names] fig.add_trace(go.Pie( labels=model_names, values=model_usage, name="Memory Usage", hole=0.4, marker_colors=['#ff9999', '#66b3ff', '#99ff99', '#ffcc99'] ), row=2, col=1) else: fig.add_annotation( text="No models loaded", xref="paper", yref="paper", x=0.25, y=0.25, showarrow=False ) # Prediction confidence over time (from recent decisions) if self.recent_decisions: recent_times = [d.timestamp for d in self.recent_decisions[-20:] if hasattr(d, 'timestamp')] recent_confidences = [d.confidence * 100 for d in self.recent_decisions[-20:] if hasattr(d, 'confidence')] if recent_times and recent_confidences: fig.add_trace(go.Scatter( x=recent_times, y=recent_confidences, mode='lines+markers', name='Confidence %', line=dict(color='#9c27b0', width=2), marker=dict(size=6) ), row=2, col=2) # Add confidence threshold line if recent_times: fig.add_hline( y=50, line_dash="dash", line_color="red", opacity=0.6, row=2, col=2 ) # Alternative: show model performance comparison if available if not self.recent_decisions and performance_metrics.get('model_performance'): models = list(performance_metrics['model_performance'].keys()) accuracies = [performance_metrics['model_performance'][model]['accuracy'] * 100 for model in models] fig.add_trace(go.Bar( x=models, y=accuracies, name='Model Accuracy', marker_color=['#1f77b4', '#ff7f0e', '#2ca02c'][:len(models)] ), row=1, col=1) # Update layout fig.update_layout( title="AI Model Performance & Feature Analysis", template="plotly_dark", height=500, margin=dict(l=0, r=0, t=50, b=0), showlegend=False ) # Update y-axis labels fig.update_yaxes(title_text="Accuracy (%)", row=1, col=1, range=[0, 100]) fig.update_yaxes(title_text="Count", row=1, col=2) fig.update_yaxes(title_text="Confidence (%)", row=2, col=2, range=[0, 100]) return fig except Exception as e: logger.error(f"Error creating enhanced performance chart: {e}") fig = go.Figure() fig.add_annotation(text=f"Error: {str(e)}", xref="paper", yref="paper", x=0.5, y=0.5) return fig def _create_decisions_list(self) -> List: """Create list of recent trading decisions""" try: if not self.recent_decisions: return [html.P("No recent decisions", className="text-muted")] decisions_html = [] for decision in self.recent_decisions[-10:][::-1]: # Last 10, newest first # Determine action color and icon if decision.action == 'BUY': action_class = "text-success" icon_class = "fas fa-arrow-up" elif decision.action == 'SELL': action_class = "text-danger" icon_class = "fas fa-arrow-down" else: action_class = "text-secondary" icon_class = "fas fa-minus" time_str = decision.timestamp.strftime("%H:%M:%S") if hasattr(decision, 'timestamp') else "N/A" confidence_pct = f"{decision.confidence*100:.1f}%" if hasattr(decision, 'confidence') else "N/A" decisions_html.append( html.Div([ html.Div([ html.I(className=f"{icon_class} me-2"), html.Strong(decision.action, className=action_class), html.Span(f" {decision.symbol} ", className="text-muted"), html.Small(f"@${decision.price:.2f}", className="text-muted") ], className="d-flex align-items-center"), html.Small([ html.Span(f"Confidence: {confidence_pct} • ", className="text-info"), html.Span(time_str, className="text-muted") ]) ], className="border-bottom pb-2 mb-2") ) return decisions_html except Exception as e: logger.error(f"Error creating decisions list: {e}") return [html.P(f"Error: {str(e)}", className="text-danger")] def _create_system_status(self, memory_stats: Dict) -> List: """Create system status display""" try: status_items = [] # Memory usage memory_pct = memory_stats.get('utilization_percent', 0) memory_class = "text-success" if memory_pct < 70 else "text-warning" if memory_pct < 90 else "text-danger" status_items.append( html.Div([ html.I(className="fas fa-memory me-2"), html.Span("Memory: "), html.Strong(f"{memory_pct:.1f}%", className=memory_class), html.Small(f" ({memory_stats.get('total_used_mb', 0):.0f}MB / {memory_stats.get('total_limit_mb', 0):.0f}MB)", className="text-muted") ], className="mb-2") ) # Model status models_count = len(memory_stats.get('models', {})) status_items.append( html.Div([ html.I(className="fas fa-brain me-2"), html.Span("Models: "), html.Strong(f"{models_count} active", className="text-info") ], className="mb-2") ) # Data provider status data_health = self.data_provider.health_check() streaming_status = "✓ Streaming" if data_health.get('streaming') else "✗ Offline" streaming_class = "text-success" if data_health.get('streaming') else "text-danger" status_items.append( html.Div([ html.I(className="fas fa-wifi me-2"), html.Span("Data: "), html.Strong(streaming_status, className=streaming_class) ], className="mb-2") ) # System uptime uptime = datetime.now() - self.last_update status_items.append( html.Div([ html.I(className="fas fa-clock me-2"), html.Span("Uptime: "), html.Strong(f"{uptime.seconds//3600:02d}:{(uptime.seconds//60)%60:02d}:{uptime.seconds%60:02d}", className="text-info") ], className="mb-2") ) return status_items except Exception as e: logger.error(f"Error creating system status: {e}") return [html.P(f"Error: {str(e)}", className="text-danger")] def add_trading_decision(self, decision: TradingDecision): """Add a trading decision to the dashboard""" self.recent_decisions.append(decision) # Keep only last 100 decisions if len(self.recent_decisions) > 100: self.recent_decisions = self.recent_decisions[-100:] def run(self, host: str = '127.0.0.1', port: int = 8050, debug: bool = False): """Run the dashboard server""" try: logger.info("="*60) logger.info("STARTING TRADING DASHBOARD") logger.info(f"ACCESS WEB UI AT: http://{host}:{port}/") logger.info("Real-time trading data and charts") logger.info("AI model performance monitoring") logger.info("Memory usage tracking") logger.info("="*60) # Run the app (updated API for newer Dash versions) self.app.run( host=host, port=port, debug=debug, use_reloader=False, # Disable reloader to avoid conflicts threaded=True # Enable threading for better performance ) except Exception as e: logger.error(f"Error running dashboard: {e}") raise # Convenience function for integration def create_dashboard(data_provider: DataProvider = None, orchestrator: TradingOrchestrator = None) -> TradingDashboard: """Create and return a trading dashboard instance""" return TradingDashboard(data_provider, orchestrator)