- Introduced `train_daily.sh` for automating daily model retraining, including data download and model training steps. - Added `install_cron.sh` for setting up a cron job to run the daily training script. - Created `setup_schedule.sh` for configuring Systemd timers for daily training tasks. - Implemented a terminal UI using Rich for real-time monitoring of trading performance, including metrics display and log handling. - Updated `pyproject.toml` to include the `rich` dependency for UI functionality. - Enhanced `.gitignore` to exclude model and log files. - Added database support for trade persistence and metrics calculation. - Updated README with installation and usage instructions for the new features.
241 lines
7.4 KiB
Python
241 lines
7.4 KiB
Python
"""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
|