diff --git a/main_clean.py b/main_clean.py index 2ec6645..105f8de 100644 --- a/main_clean.py +++ b/main_clean.py @@ -18,6 +18,8 @@ import argparse import logging import sys from pathlib import Path +from threading import Thread +import time # Add project root to path project_root = Path(__file__).parent @@ -179,6 +181,95 @@ def run_orchestrator_test(): logger.error(traceback.format_exc()) raise +def run_web_dashboard(port: int = 8050, demo_mode: bool = True): + """Run the web dashboard""" + try: + from web.dashboard import TradingDashboard + + logger.info("Starting Web Dashboard...") + + # Initialize components + data_provider = DataProvider(symbols=['ETH/USDT'], timeframes=['1h', '4h']) + orchestrator = TradingOrchestrator(data_provider) + + # Create dashboard + dashboard = TradingDashboard(data_provider, orchestrator) + + # Add orchestrator callback to send decisions to dashboard + async def decision_callback(decision): + dashboard.add_trading_decision(decision) + + orchestrator.add_decision_callback(decision_callback) + + if demo_mode: + # Start demo mode with mock decisions + logger.info("Starting demo mode with simulated trading decisions...") + + def demo_thread(): + """Generate demo trading decisions""" + import random + import time + from datetime import datetime + from core.orchestrator import TradingDecision + + actions = ['BUY', 'SELL', 'HOLD'] + base_price = 3000.0 + + while True: + try: + # Simulate price movement + price_change = random.uniform(-50, 50) + current_price = max(base_price + price_change, 1000) + + # Create mock decision + action = random.choice(actions) + confidence = random.uniform(0.6, 0.95) + + decision = TradingDecision( + action=action, + confidence=confidence, + symbol='ETH/USDT', + price=current_price, + timestamp=datetime.now(), + reasoning={'demo_mode': True, 'random_decision': True}, + memory_usage={'demo': 0} + ) + + dashboard.add_trading_decision(decision) + logger.info(f"Demo decision: {action} ETH/USDT @${current_price:.2f} (confidence: {confidence:.2f})") + + # Update base price occasionally + if random.random() < 0.1: + base_price = current_price + + time.sleep(5) # New decision every 5 seconds + + except Exception as e: + logger.error(f"Error in demo thread: {e}") + time.sleep(10) + + # Start demo thread + demo_thread_instance = Thread(target=demo_thread, daemon=True) + demo_thread_instance.start() + + # Start data streaming if available + try: + logger.info("Starting real-time data streaming...") + # Don't use asyncio.run here as we're already in an event loop context + # Just log that streaming would be started in a real deployment + logger.info("Real-time streaming would be started in production deployment") + except Exception as e: + logger.warning(f"Could not start real-time streaming: {e}") + + # Run dashboard + dashboard.run(port=port, debug=False) + + except Exception as e: + logger.error(f"Error running web dashboard: {e}") + import traceback + logger.error(traceback.format_exc()) + raise + async def main(): """Main entry point""" parser = argparse.ArgumentParser(description='Clean Trading System') @@ -187,6 +278,10 @@ async def main(): parser.add_argument('--symbol', type=str, help='Override default symbol') parser.add_argument('--config', type=str, default='config.yaml', help='Configuration file path') + parser.add_argument('--port', type=int, default=8050, + help='Port for web dashboard') + parser.add_argument('--demo', action='store_true', + help='Run web dashboard in demo mode with simulated data') args = parser.parse_args() @@ -203,6 +298,8 @@ async def main(): run_data_test() elif args.mode == 'orchestrator': run_orchestrator_test() + elif args.mode == 'web': + run_web_dashboard(port=args.port, demo_mode=args.demo) else: logger.info(f"Mode '{args.mode}' not yet implemented in clean architecture") diff --git a/web/__init__.py b/web/__init__.py index e69de29..0a6a59c 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -0,0 +1 @@ +# Web module for trading system dashboard diff --git a/web/dashboard.py b/web/dashboard.py new file mode 100644 index 0000000..fc79f65 --- /dev/null +++ b/web/dashboard.py @@ -0,0 +1,468 @@ +""" +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 price chart with multiple timeframes""" + try: + # Get recent data + df = self.data_provider.get_latest_candles(symbol, '1h', limit=24) + + if df.empty: + fig = go.Figure() + fig.add_annotation(text="No data available", xref="paper", yref="paper", x=0.5, y=0.5) + return fig + + # Create candlestick chart + fig = go.Figure(data=[go.Candlestick( + x=df['timestamp'], + open=df['open'], + high=df['high'], + low=df['low'], + close=df['close'], + name=symbol + )]) + + # Add moving averages if available + if 'sma_20' in df.columns: + fig.add_trace(go.Scatter( + x=df['timestamp'], + y=df['sma_20'], + name='SMA 20', + line=dict(color='orange', width=1) + )) + + # Mark recent trading decisions + for decision in self.recent_decisions[-10:]: + if hasattr(decision, 'timestamp') and hasattr(decision, 'price'): + color = 'green' if decision.action == 'BUY' else 'red' if decision.action == 'SELL' else 'gray' + fig.add_trace(go.Scatter( + x=[decision.timestamp], + y=[decision.price], + mode='markers', + marker=dict(color=color, size=10, symbol='triangle-up' if decision.action == 'BUY' else 'triangle-down'), + name=f"{decision.action}", + showlegend=False + )) + + fig.update_layout( + title=f"{symbol} Price Chart (1H)", + template="plotly_dark", + height=400, + xaxis_rangeslider_visible=False, + margin=dict(l=0, r=0, t=30, b=0) + ) + + return fig + + except Exception as e: + logger.error(f"Error creating price 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_performance_chart(self, performance_metrics: Dict) -> go.Figure: + """Create model performance comparison chart""" + try: + if not performance_metrics.get('model_performance'): + fig = go.Figure() + fig.add_annotation(text="No model performance data", xref="paper", yref="paper", x=0.5, y=0.5) + return fig + + models = list(performance_metrics['model_performance'].keys()) + accuracies = [performance_metrics['model_performance'][model]['accuracy'] * 100 + for model in models] + + fig = go.Figure(data=[ + go.Bar(x=models, y=accuracies, marker_color=['#1f77b4', '#ff7f0e', '#2ca02c']) + ]) + + fig.update_layout( + title="Model Accuracy Comparison", + yaxis_title="Accuracy (%)", + template="plotly_dark", + height=400, + margin=dict(l=0, r=0, t=30, b=0) + ) + + return fig + + except Exception as e: + logger.error(f"Error creating 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) \ No newline at end of file