Add daily model training scripts and terminal UI for live trading
- 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.
This commit is contained in:
399
live_trading/ui/panels.py
Normal file
399
live_trading/ui/panels.py
Normal file
@@ -0,0 +1,399 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user