"""UI panel components using Rich.""" from typing import Optional from rich.console import Console, Group from rich.panel import Panel from rich.table import Table from rich.text import Text from rich.layout import Layout from .state import SharedState, PositionState, StrategyState, AccountState from .log_handler import LogBuffer, LogEntry from ..db.metrics import PeriodMetrics def format_pnl(value: float, include_sign: bool = True) -> Text: """Format PnL value with color.""" if value > 0: sign = "+" if include_sign else "" return Text(f"{sign}${value:.2f}", style="green") elif value < 0: return Text(f"${value:.2f}", style="red") else: return Text(f"${value:.2f}", style="white") def format_pct(value: float, include_sign: bool = True) -> Text: """Format percentage value with color.""" if value > 0: sign = "+" if include_sign else "" return Text(f"{sign}{value:.2f}%", style="green") elif value < 0: return Text(f"{value:.2f}%", style="red") else: return Text(f"{value:.2f}%", style="white") def format_side(side: str) -> Text: """Format position side with color.""" if side.lower() == "long": return Text("LONG", style="bold green") else: return Text("SHORT", style="bold red") class HeaderPanel: """Header panel with title and mode indicator.""" def __init__(self, state: SharedState): self.state = state def render(self) -> Panel: """Render the header panel.""" mode = self.state.get_mode() eth_symbol, _ = self.state.get_symbols() mode_style = "yellow" if mode == "DEMO" else "bold red" mode_text = Text(f"[{mode}]", style=mode_style) title = Text() title.append("REGIME REVERSION STRATEGY - LIVE TRADING", style="bold white") title.append(" ") title.append(mode_text) title.append(" ") title.append(eth_symbol, style="cyan") return Panel(title, style="blue", height=3) class TabBar: """Tab bar for period selection.""" TABS = ["1:General", "2:Monthly", "3:Weekly", "4:Daily"] def __init__(self, active_tab: int = 0): self.active_tab = active_tab def render( self, has_monthly: bool = True, has_weekly: bool = True, ) -> Text: """Render the tab bar.""" text = Text() text.append(" ") for i, tab in enumerate(self.TABS): # Check if tab should be shown if i == 1 and not has_monthly: continue if i == 2 and not has_weekly: continue if i == self.active_tab: text.append(f"[{tab}]", style="bold white on blue") else: text.append(f"[{tab}]", style="dim") text.append(" ") return text class MetricsPanel: """Panel showing trading metrics.""" def __init__(self, metrics: Optional[PeriodMetrics] = None): self.metrics = metrics def render(self) -> Table: """Render metrics as a table.""" table = Table( show_header=False, show_edge=False, box=None, padding=(0, 1), ) table.add_column("Label", style="dim") table.add_column("Value") if self.metrics is None or self.metrics.total_trades == 0: table.add_row("Status", Text("No trade data", style="dim")) return table m = self.metrics table.add_row("Total PnL:", format_pnl(m.total_pnl)) table.add_row("Win Rate:", Text(f"{m.win_rate:.1f}%", style="white")) table.add_row("Total Trades:", Text(str(m.total_trades), style="white")) table.add_row( "Win/Loss:", Text(f"{m.winning_trades}/{m.losing_trades}", style="white"), ) table.add_row( "Avg Duration:", Text(f"{m.avg_trade_duration_hours:.1f}h", style="white"), ) table.add_row("Max Drawdown:", format_pnl(-m.max_drawdown)) table.add_row("Best Trade:", format_pnl(m.best_trade)) table.add_row("Worst Trade:", format_pnl(m.worst_trade)) return table class PositionPanel: """Panel showing current position.""" def __init__(self, position: Optional[PositionState] = None): self.position = position def render(self) -> Table: """Render position as a table.""" table = Table( show_header=False, show_edge=False, box=None, padding=(0, 1), ) table.add_column("Label", style="dim") table.add_column("Value") if self.position is None: table.add_row("Status", Text("No open position", style="dim")) return table p = self.position table.add_row("Side:", format_side(p.side)) table.add_row("Entry:", Text(f"${p.entry_price:.2f}", style="white")) table.add_row("Current:", Text(f"${p.current_price:.2f}", style="white")) # Unrealized PnL pnl_text = Text() pnl_text.append_text(format_pnl(p.unrealized_pnl)) pnl_text.append(" (") pnl_text.append_text(format_pct(p.unrealized_pnl_pct)) pnl_text.append(")") table.add_row("Unrealized:", pnl_text) table.add_row("Size:", Text(f"${p.size_usdt:.2f}", style="white")) # SL/TP if p.side == "long": sl_dist = (p.stop_loss_price / p.entry_price - 1) * 100 tp_dist = (p.take_profit_price / p.entry_price - 1) * 100 else: sl_dist = (1 - p.stop_loss_price / p.entry_price) * 100 tp_dist = (1 - p.take_profit_price / p.entry_price) * 100 sl_text = Text(f"${p.stop_loss_price:.2f} ({sl_dist:+.1f}%)", style="red") tp_text = Text(f"${p.take_profit_price:.2f} ({tp_dist:+.1f}%)", style="green") table.add_row("Stop Loss:", sl_text) table.add_row("Take Profit:", tp_text) return table class AccountPanel: """Panel showing account information.""" def __init__(self, account: Optional[AccountState] = None): self.account = account def render(self) -> Table: """Render account info as a table.""" table = Table( show_header=False, show_edge=False, box=None, padding=(0, 1), ) table.add_column("Label", style="dim") table.add_column("Value") if self.account is None: table.add_row("Status", Text("Loading...", style="dim")) return table a = self.account table.add_row("Balance:", Text(f"${a.balance:.2f}", style="white")) table.add_row("Available:", Text(f"${a.available:.2f}", style="white")) table.add_row("Leverage:", Text(f"{a.leverage}x", style="cyan")) return table class StrategyPanel: """Panel showing strategy state.""" def __init__(self, strategy: Optional[StrategyState] = None): self.strategy = strategy def render(self) -> Table: """Render strategy state as a table.""" table = Table( show_header=False, show_edge=False, box=None, padding=(0, 1), ) table.add_column("Label", style="dim") table.add_column("Value") if self.strategy is None: table.add_row("Status", Text("Waiting...", style="dim")) return table s = self.strategy # Z-score with color based on threshold z_style = "white" if abs(s.z_score) > 1.0: z_style = "yellow" if abs(s.z_score) > 1.5: z_style = "bold yellow" table.add_row("Z-Score:", Text(f"{s.z_score:.2f}", style=z_style)) # Probability with color prob_style = "white" if s.probability > 0.5: prob_style = "green" if s.probability > 0.7: prob_style = "bold green" table.add_row("Probability:", Text(f"{s.probability:.2f}", style=prob_style)) # Funding rate funding_style = "green" if s.funding_rate >= 0 else "red" table.add_row( "Funding:", Text(f"{s.funding_rate:.4f}", style=funding_style), ) # Last action action_style = "white" if s.last_action == "entry": action_style = "bold cyan" elif s.last_action == "check_exit": action_style = "yellow" table.add_row("Last Action:", Text(s.last_action, style=action_style)) return table class LogPanel: """Panel showing log entries.""" def __init__(self, log_buffer: LogBuffer): self.log_buffer = log_buffer def render(self, height: int = 10) -> Panel: """Render log panel.""" filter_name = self.log_buffer.get_current_filter().title() entries = self.log_buffer.get_filtered(limit=height - 2) lines = [] for entry in entries: line = Text() line.append(f"{entry.timestamp} ", style="dim") line.append(f"[{entry.level}] ", style=entry.level_color) line.append(entry.message) lines.append(line) if not lines: lines.append(Text("No logs to display", style="dim")) content = Group(*lines) # Build "tabbed" title tabs = [] # All Logs tab if filter_name == "All": tabs.append("[bold white on blue] [L]ogs [/]") else: tabs.append("[dim] [L]ogs [/]") # Trades tab if filter_name == "Trades": tabs.append("[bold white on blue] [T]rades [/]") else: tabs.append("[dim] [T]rades [/]") # Errors tab if filter_name == "Errors": tabs.append("[bold white on blue] [E]rrors [/]") else: tabs.append("[dim] [E]rrors [/]") title = " ".join(tabs) subtitle = "Press 'l', 't', 'e' to switch tabs" return Panel( content, title=title, subtitle=subtitle, title_align="left", subtitle_align="right", border_style="blue", ) class HelpBar: """Bottom help bar with keyboard shortcuts.""" def render(self) -> Text: """Render help bar.""" text = Text() text.append(" [q]", style="bold") text.append("Quit ", style="dim") text.append("[r]", style="bold") text.append("Refresh ", style="dim") text.append("[1-4]", style="bold") text.append("Tabs ", style="dim") text.append("[l/t/e]", style="bold") text.append("LogView", style="dim") return text def build_summary_panel( state: SharedState, metrics: Optional[PeriodMetrics], tab_bar: TabBar, has_monthly: bool, has_weekly: bool, ) -> Panel: """Build the complete summary panel with all sections.""" # Create layout for summary content layout = Layout() # Tab bar at top tabs = tab_bar.render(has_monthly, has_weekly) # Create tables for each section metrics_table = MetricsPanel(metrics).render() position_table = PositionPanel(state.get_position()).render() account_table = AccountPanel(state.get_account()).render() strategy_table = StrategyPanel(state.get_strategy()).render() # Build two-column layout left_col = Table(show_header=True, show_edge=False, box=None, padding=(0, 2)) left_col.add_column("PERFORMANCE", style="bold cyan") left_col.add_column("ACCOUNT", style="bold cyan") left_col.add_row(metrics_table, account_table) right_col = Table(show_header=True, show_edge=False, box=None, padding=(0, 2)) right_col.add_column("CURRENT POSITION", style="bold cyan") right_col.add_column("STRATEGY STATE", style="bold cyan") right_col.add_row(position_table, strategy_table) # Combine into main table main_table = Table(show_header=False, show_edge=False, box=None, expand=True) main_table.add_column(ratio=1) main_table.add_column(ratio=1) main_table.add_row(left_col, right_col) content = Group(tabs, Text(""), main_table) return Panel(content, border_style="blue")