- 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.
400 lines
12 KiB
Python
400 lines
12 KiB
Python
"""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")
|