Align branding, prompts, and deployment tooling

This commit is contained in:
2026-03-28 22:16:56 +08:00
parent 4aa69650e8
commit 4295293a21
90 changed files with 1320 additions and 2044 deletions

View File

@@ -1,352 +0,0 @@
# -*- coding: utf-8 -*-
"""
Terminal Dashboard - Persistent unified panel using Rich Live
"""
# pylint: disable=R0915,R0912
import logging
import threading
import time
from typing import Any, Dict, List, Optional
from rich.console import Console
from rich.live import Live
from rich.panel import Panel
from rich.table import Table
logger = logging.getLogger(__name__)
class TerminalDashboard:
"""Unified persistent terminal dashboard"""
def __init__(self, console: Console = None):
self.console = console or Console()
self.live: Optional[Live] = None
# Config state
self.mode = "live"
self.config_name = ""
self.host = "0.0.0.0"
self.port = 8765
self.poll_interval = 10
self.trigger_time = "now"
self.enable_memory = False
self.local_time = ""
self.nyse_time = ""
self.start_date = ""
self.end_date = ""
self.tickers: List[str] = []
self.initial_cash = 100000.0
self.data_sources: Dict[str, Any] = {}
# Trading state
self.current_date = "-"
self.status = "Initializing"
self.total_value = 0.0
self.cash = 0.0
self.pnl_pct = 0.0
self.holdings: List[Dict] = []
self.trades: List[Dict] = []
self.days_completed = 0
self.days_total = 0
# Progress message (last line)
self.progress = ""
self._dots_index = 0
self._animator_running = False
self._animator_thread: Optional[threading.Thread] = None
def set_config(
self,
mode: str,
config_name: str,
host: str,
port: int,
poll_interval: int,
trigger_time: str = "now",
enable_memory: bool = False,
local_time: str = "",
nyse_time: str = "",
start_date: str = "",
end_date: str = "",
tickers: List[str] = None,
initial_cash: float = 100000.0,
data_sources: Dict[str, Any] = None,
):
"""Set configuration state"""
self.mode = mode
self.config_name = config_name
self.host = host
self.port = port
self.poll_interval = poll_interval
self.trigger_time = trigger_time
self.enable_memory = enable_memory
self.local_time = local_time
self.nyse_time = nyse_time
self.start_date = start_date
self.end_date = end_date
self.tickers = tickers or []
self.initial_cash = initial_cash
self.data_sources = data_sources or {}
self.total_value = initial_cash
self.cash = initial_cash
def _build_panel(self) -> Panel:
"""Build the unified dashboard panel"""
# Main grid
main_table = Table.grid(padding=(0, 2))
main_table.add_column(width=28)
main_table.add_column(width=22)
main_table.add_column(width=22)
# Left: Config + Status
left = Table.grid(padding=(0, 0))
left.add_column()
# Mode line
if self.mode == "backtest":
mode_str = "[cyan]Backtest[/cyan]"
else:
mode_str = "[green]LIVE[/green]"
left.add_row(f"[bold]Mode:[/bold] {mode_str}")
left.add_row(f"[dim]Config:[/dim] {self.config_name}")
left.add_row(f"[dim]Server:[/dim] {self.host}:{self.port}")
preferred_sources = self.data_sources.get("preferred", [])
if preferred_sources:
left.add_row(
f"[dim]Data:[/dim] {' -> '.join(preferred_sources)}",
)
if self.mode == "live" and self.nyse_time:
left.add_row(f"[dim]NYSE:[/dim] {self.nyse_time[:19]}")
trigger_display = (
"[green]NOW[/green]"
if self.trigger_time == "now"
else self.trigger_time
)
left.add_row(f"[dim]Trigger:[/dim] {trigger_display}")
# Status
left.add_row("")
status_style = "green" if self.status == "Running" else "yellow"
left.add_row(
"[bold]Status:[/bold] "
f"[{status_style}]{self.status}[/{status_style}]",
)
if self.mode == "backtest":
left.add_row(
f"[dim]Backtesting Period:[/dim] {self.days_total} days\n"
f" {self.start_date} -> {self.end_date}",
)
left.add_row(f"[dim]Current Date:[/dim] {self.current_date}")
# Middle: Portfolio
mid = Table.grid(padding=(0, 0))
mid.add_column()
pnl_style = "green" if self.pnl_pct >= 0 else "red"
mid.add_row("[bold]Portfolio[/bold]")
mid.add_row(f"NAV: [bold]${self.total_value:,.0f}[/bold]")
mid.add_row(f"Cash: ${self.cash:,.0f}")
mid.add_row(f"P&L: [{pnl_style}]{self.pnl_pct:+.2f}%[/{pnl_style}]")
# Positions
mid.add_row("")
mid.add_row("[bold]Positions[/bold]")
stock_holdings = [
h for h in self.holdings if h.get("ticker") != "CASH"
]
if stock_holdings:
for h in stock_holdings[:7]:
qty = h.get("quantity", 0)
ticker = h.get("ticker", "")[:5]
val = h.get("marketValue", 0)
qty_str = f"{qty:+d}" if qty != 0 else "0"
mid.add_row(
f"[cyan]{ticker:<5}[/cyan] {qty_str:>5} ${val:>7,.0f}",
)
if len(stock_holdings) > 7:
mid.add_row(f"[dim]+{len(stock_holdings) - 7} more[/dim]")
else:
mid.add_row("[dim]No positions[/dim]")
# Right: Recent Trades
right = Table.grid(padding=(0, 0))
right.add_column()
right.add_row("[bold]Recent Trades[/bold]")
if self.trades:
for t in self.trades[:10]:
side = t.get("side", "")
ticker = t.get("ticker", "")[:5]
qty = t.get("qty", 0)
if side == "LONG":
side_str = "[green]L[/green]"
elif side == "SHORT":
side_str = "[red]S[/red]"
else:
side_str = "[dim]H[/dim]"
right.add_row(f"{side_str} [cyan]{ticker:<5}[/cyan] {qty:>4}")
if len(self.trades) > 10:
right.add_row(f"[dim]+{len(self.trades) - 10} more[/dim]")
else:
right.add_row("[dim]No trades[/dim]")
main_table.add_row(left, mid, right)
# Outer table to add progress line at bottom
outer = Table.grid(padding=(0, 0))
outer.add_column()
outer.add_row(main_table)
# Progress line (last row) with animated dots
if self.progress:
DOTS_FRAMES = [" ", ". ", ".. ", "..."]
dots = DOTS_FRAMES[self._dots_index % len(DOTS_FRAMES)]
outer.add_row("")
outer.add_row(f"[dim]> {self.progress}{dots}[/dim]")
# Build panel
title = "[bold cyan]EvoTraders[/bold cyan]"
if self.mode == "backtest":
title += " [dim]Backtest[/dim]"
else:
title += " [dim]Live[/dim]"
return Panel(
outer,
title=title,
border_style="cyan",
padding=(0, 1),
)
def _run_animator(self):
"""Background thread to animate the dots"""
while self._animator_running:
time.sleep(0.3)
if self.progress and self.live:
self._dots_index += 1
self.live.update(self._build_panel())
def start(self):
"""Start the live dashboard display"""
self.live = Live(
self._build_panel(),
console=self.console,
refresh_per_second=4,
vertical_overflow="visible",
)
self.live.start()
# Start animator thread
self._animator_running = True
self._animator_thread = threading.Thread(
target=self._run_animator,
daemon=True,
)
self._animator_thread.start()
def stop(self):
"""Stop the live dashboard"""
self._animator_running = False
if self._animator_thread:
self._animator_thread.join(timeout=0.5)
self._animator_thread = None
if self.live:
self.live.stop()
self.live = None
def update(
self,
date: str = None,
status: str = None,
portfolio: Dict[str, Any] = None,
holdings: List[Dict] = None,
trades: List[Dict] = None,
days_completed: int = None,
days_total: int = None,
data_sources: Dict[str, Any] = None,
):
"""Update dashboard state and refresh display"""
if date:
self.current_date = date
if status:
self.status = status
if days_completed is not None:
self.days_completed = days_completed
if days_total is not None:
self.days_total = days_total
if portfolio:
self.total_value = portfolio.get(
"totalAssetValue",
0,
) or portfolio.get(
"total_value",
self.initial_cash,
)
self.cash = portfolio.get("cashPosition", 0) or portfolio.get(
"cash",
self.initial_cash,
)
if self.total_value > 0 and self.initial_cash > 0:
self.pnl_pct = (
(self.total_value - self.initial_cash) / self.initial_cash
) * 100
if holdings is not None:
self.holdings = holdings
if trades is not None:
self.trades = trades
if data_sources is not None:
self.data_sources = data_sources
if self.live:
self.live.update(self._build_panel())
def log(self, msg: str, also_log: bool = True):
"""
Update progress message and refresh panel
Args:
msg: Progress message to display
also_log: Whether to also write to logger (default True)
"""
self.progress = msg
if also_log:
logger.info(msg)
if self.live:
self.live.update(self._build_panel())
def print_final_summary(self):
"""Print final summary when dashboard stops"""
pnl_style = "green" if self.pnl_pct >= 0 else "red"
if self.mode == "backtest":
msg = (
f"[bold]Backtest Complete[/bold] | "
f"Days: {self.days_completed} | "
f"NAV: ${self.total_value:,.0f} | "
f"Return: [{pnl_style}]{self.pnl_pct:+.2f}%[/{pnl_style}]"
)
else:
msg = (
f"[bold]Session End[/bold] | "
f"NAV: ${self.total_value:,.0f} | "
f"P&L: [{pnl_style}]{self.pnl_pct:+.2f}%[/{pnl_style}]"
)
self.console.print(Panel(msg, border_style="green"))
# Global instance
_dashboard: Optional[TerminalDashboard] = None
def get_dashboard() -> TerminalDashboard:
"""Get or create global dashboard instance"""
global _dashboard
if _dashboard is None:
_dashboard = TerminalDashboard()
return _dashboard