"""Main trading dashboard UI orchestration.""" import logging import queue import threading import time from typing import Optional, Callable from rich.console import Console from rich.layout import Layout from rich.live import Live from rich.panel import Panel from rich.text import Text from .state import SharedState from .log_handler import LogBuffer, UILogHandler from .keyboard import KeyboardHandler from .panels import ( HeaderPanel, TabBar, LogPanel, HelpBar, build_summary_panel, ) from ..db.database import TradingDatabase from ..db.metrics import MetricsCalculator, PeriodMetrics logger = logging.getLogger(__name__) class TradingDashboard: """ Main trading dashboard orchestrator. Runs in a separate thread and provides real-time UI updates while the trading loop runs in the main thread. """ def __init__( self, state: SharedState, db: TradingDatabase, log_queue: queue.Queue, on_quit: Optional[Callable] = None, ): self.state = state self.db = db self.log_queue = log_queue self.on_quit = on_quit self.console = Console() self.log_buffer = LogBuffer(max_entries=1000) self.keyboard = KeyboardHandler() self.metrics_calculator = MetricsCalculator(db) self._running = False self._thread: Optional[threading.Thread] = None self._active_tab = 0 self._cached_metrics: dict[int, PeriodMetrics] = {} self._last_metrics_refresh = 0.0 def start(self) -> None: """Start the dashboard in a separate thread.""" if self._running: return self._running = True self._thread = threading.Thread(target=self._run, daemon=True) self._thread.start() logger.debug("Dashboard thread started") def stop(self) -> None: """Stop the dashboard.""" self._running = False if self._thread: self._thread.join(timeout=2.0) logger.debug("Dashboard thread stopped") def _run(self) -> None: """Main dashboard loop.""" try: with self.keyboard: with Live( self._build_layout(), console=self.console, refresh_per_second=1, screen=True, ) as live: while self._running and self.state.is_running(): # Process keyboard input action = self.keyboard.get_action(timeout=0.1) if action: self._handle_action(action) # Drain log queue self.log_buffer.drain_queue(self.log_queue) # Refresh metrics periodically (every 5 seconds) now = time.time() if now - self._last_metrics_refresh > 5.0: self._refresh_metrics() self._last_metrics_refresh = now # Update display live.update(self._build_layout()) # Small sleep to prevent CPU spinning time.sleep(0.1) except Exception as e: logger.error(f"Dashboard error: {e}", exc_info=True) finally: self._running = False def _handle_action(self, action: str) -> None: """Handle keyboard action.""" if action == "quit": logger.info("Quit requested from UI") self.state.stop() if self.on_quit: self.on_quit() elif action == "refresh": self._refresh_metrics() logger.debug("Manual refresh triggered") elif action == "filter": new_filter = self.log_buffer.cycle_filter() logger.debug(f"Log filter changed to: {new_filter}") elif action == "filter_trades": self.log_buffer.set_filter(LogBuffer.FILTER_TRADES) logger.debug("Log filter set to: trades") elif action == "filter_all": self.log_buffer.set_filter(LogBuffer.FILTER_ALL) logger.debug("Log filter set to: all") elif action == "filter_errors": self.log_buffer.set_filter(LogBuffer.FILTER_ERRORS) logger.debug("Log filter set to: errors") elif action == "tab_general": self._active_tab = 0 elif action == "tab_monthly": if self._has_monthly_data(): self._active_tab = 1 elif action == "tab_weekly": if self._has_weekly_data(): self._active_tab = 2 elif action == "tab_daily": self._active_tab = 3 def _refresh_metrics(self) -> None: """Refresh metrics from database.""" try: self._cached_metrics[0] = self.metrics_calculator.get_all_time_metrics() self._cached_metrics[1] = self.metrics_calculator.get_monthly_metrics() self._cached_metrics[2] = self.metrics_calculator.get_weekly_metrics() self._cached_metrics[3] = self.metrics_calculator.get_daily_metrics() except Exception as e: logger.warning(f"Failed to refresh metrics: {e}") def _has_monthly_data(self) -> bool: """Check if monthly tab should be shown.""" try: return self.metrics_calculator.has_monthly_data() except Exception: return False def _has_weekly_data(self) -> bool: """Check if weekly tab should be shown.""" try: return self.metrics_calculator.has_weekly_data() except Exception: return False def _build_layout(self) -> Layout: """Build the complete dashboard layout.""" layout = Layout() # Calculate available height term_height = self.console.height or 40 # Header takes 3 lines # Help bar takes 1 line # Summary panel takes about 12-14 lines # Rest goes to logs log_height = max(8, term_height - 20) layout.split_column( Layout(name="header", size=3), Layout(name="summary", size=14), Layout(name="logs", size=log_height), Layout(name="help", size=1), ) # Header layout["header"].update(HeaderPanel(self.state).render()) # Summary panel with tabs current_metrics = self._cached_metrics.get(self._active_tab) tab_bar = TabBar(active_tab=self._active_tab) layout["summary"].update( build_summary_panel( state=self.state, metrics=current_metrics, tab_bar=tab_bar, has_monthly=self._has_monthly_data(), has_weekly=self._has_weekly_data(), ) ) # Log panel layout["logs"].update(LogPanel(self.log_buffer).render(height=log_height)) # Help bar layout["help"].update(HelpBar().render()) return layout def setup_ui_logging(log_queue: queue.Queue) -> UILogHandler: """ Set up logging to capture messages for UI. Args: log_queue: Queue to send log messages to Returns: UILogHandler instance """ handler = UILogHandler(log_queue) handler.setLevel(logging.INFO) # Add handler to root logger root_logger = logging.getLogger() root_logger.addHandler(handler) return handler