You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
577 lines
21 KiB
Python
577 lines
21 KiB
Python
"""Rich console output for paper trading status, positions, trades, and equity."""
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
from typing import Any
|
|
|
|
from rich import box
|
|
from rich.console import Console
|
|
from rich.panel import Panel
|
|
from rich.table import Table
|
|
from rich.text import Text
|
|
|
|
from apps.paper_trader.alpaca_broker import AlpacaBroker, Position
|
|
from apps.paper_trader.state import SessionRow, StateManager, StrategyStateRow
|
|
|
|
_console = Console(width=140)
|
|
|
|
|
|
def _pnl_color(val: float) -> str:
|
|
return "green" if val >= 0 else "red"
|
|
|
|
|
|
def _fmt_pct(val: float) -> str:
|
|
sign = "+" if val >= 0 else ""
|
|
return f"{sign}{val:.2f}%"
|
|
|
|
|
|
def _fmt_pnl(val: float) -> str:
|
|
sign = "+" if val >= 0 else ""
|
|
return f"{sign}${val:,.2f}"
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Session status
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def print_status(
|
|
session: SessionRow,
|
|
broker: AlpacaBroker,
|
|
state: StateManager,
|
|
) -> None:
|
|
account = broker.get_account()
|
|
session_st = state.get_session_state(session.session_id)
|
|
snapshots = state.list_snapshots(session.session_id)
|
|
|
|
initial_equity = session.initial_equity
|
|
# Use latest snapshot equity if available, otherwise fall back to initial
|
|
if snapshots:
|
|
current_equity = snapshots[-1]["equity"]
|
|
else:
|
|
current_equity = initial_equity
|
|
total_pnl = current_equity - initial_equity
|
|
total_pnl_pct = total_pnl / initial_equity * 100 if initial_equity else 0.0
|
|
peak_equity = state.get_peak_equity(session.session_id, initial_equity)
|
|
drawdown_pct = max(0.0, (peak_equity - current_equity) / peak_equity * 100) if peak_equity > 0 else 0.0
|
|
|
|
positions = broker.list_positions()
|
|
kill_status = "[bold red]ON[/]" if session_st.kill_switch_triggered else "[green]OFF[/]"
|
|
cooldown_str = f"{session_st.cooldown_remaining}d" if session_st.cooldown_remaining > 0 else "none"
|
|
|
|
pnl_style = _pnl_color(total_pnl)
|
|
|
|
_console.print()
|
|
_console.print(Panel(
|
|
f"[bold cyan]Session:[/] {session.session_name} "
|
|
f"[dim]|[/] [bold]Config:[/] {session.config_path} "
|
|
f"[dim]|[/] [bold]Status:[/] {session.status}",
|
|
border_style="cyan",
|
|
padding=(0, 1),
|
|
))
|
|
|
|
tbl = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
|
|
tbl.add_column("Key", style="bold yellow", no_wrap=True)
|
|
tbl.add_column("Value", no_wrap=True)
|
|
|
|
tbl.add_row("Equity", f"[bold]${current_equity:,.2f}[/] [{pnl_style}]{_fmt_pnl(total_pnl)} ({_fmt_pct(total_pnl_pct)})[/]")
|
|
tbl.add_row("Cash", f"${account.cash:,.2f}")
|
|
tbl.add_row("Market Value", f"${account.long_market_value:,.2f}")
|
|
tbl.add_row("Open Positions", str(len(positions)))
|
|
tbl.add_row("Drawdown", f"[{'red' if drawdown_pct > 5 else 'green'}]{drawdown_pct:.2f}%[/]")
|
|
tbl.add_row("Consecutive Losses", str(session_st.consecutive_losses))
|
|
tbl.add_row("Cooldown", cooldown_str)
|
|
tbl.add_row("Kill Switch", kill_status)
|
|
tbl.add_row("Total Trades", str(len(state.list_trades(session.session_id))))
|
|
if snapshots:
|
|
tbl.add_row("Last Processed", snapshots[-1]["date"])
|
|
|
|
_console.print(tbl)
|
|
_console.print()
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Positions
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def print_positions(
|
|
session: SessionRow,
|
|
broker: AlpacaBroker,
|
|
state: StateManager,
|
|
) -> None:
|
|
positions = broker.list_positions()
|
|
strategy_states = {
|
|
ss.symbol: ss
|
|
for ss in state.get_open_strategy_states(session.session_id)
|
|
}
|
|
|
|
if not positions:
|
|
_console.print("[dim]No open positions.[/]")
|
|
return
|
|
|
|
tbl = Table(
|
|
box=box.SIMPLE_HEAD,
|
|
show_header=True,
|
|
header_style="bold yellow",
|
|
padding=(0, 1),
|
|
title=f"[bold cyan]Open Positions[/] — {session.session_name}",
|
|
title_justify="left",
|
|
)
|
|
tbl.add_column("Symbol", style="bold", no_wrap=True)
|
|
tbl.add_column("Qty", justify="right")
|
|
tbl.add_column("Entry", justify="right")
|
|
tbl.add_column("Current", justify="right")
|
|
tbl.add_column("P&L $", justify="right")
|
|
tbl.add_column("P&L %", justify="right")
|
|
tbl.add_column("Days", justify="right")
|
|
tbl.add_column("Stop", justify="right")
|
|
tbl.add_column("Target", justify="right")
|
|
|
|
for pos in sorted(positions, key=lambda p: p.symbol):
|
|
ss = strategy_states.get(pos.symbol)
|
|
stop_str = f"${ss.current_stop:.2f}" if ss else "-"
|
|
target_str = f"${ss.target_price:.2f}" if ss else "-"
|
|
days_str = str(ss.days_held) if ss else "-"
|
|
|
|
pnl = pos.unrealized_pl
|
|
pnl_pct = pnl / (pos.avg_entry_price * pos.qty) * 100 if pos.avg_entry_price and pos.qty else 0.0
|
|
pnl_color = _pnl_color(pnl)
|
|
|
|
tbl.add_row(
|
|
pos.symbol,
|
|
str(pos.qty),
|
|
f"${pos.avg_entry_price:.2f}",
|
|
f"${pos.current_price:.2f}",
|
|
f"[{pnl_color}]{_fmt_pnl(pnl)}[/]",
|
|
f"[{pnl_color}]{_fmt_pct(pnl_pct)}[/]",
|
|
days_str,
|
|
stop_str,
|
|
target_str,
|
|
)
|
|
|
|
_console.print()
|
|
_console.print(tbl)
|
|
_console.print()
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Trades
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def print_trades(
|
|
session: SessionRow,
|
|
state: StateManager,
|
|
last: int | None = None,
|
|
) -> None:
|
|
trades = state.list_trades(session.session_id, limit=last)
|
|
|
|
if not trades:
|
|
_console.print("[dim]No trades recorded yet.[/]")
|
|
return
|
|
|
|
tbl = Table(
|
|
box=box.SIMPLE_HEAD,
|
|
show_header=True,
|
|
header_style="bold yellow",
|
|
padding=(0, 1),
|
|
title=f"[bold cyan]Trades[/] — {session.session_name}",
|
|
title_justify="left",
|
|
)
|
|
tbl.add_column("Symbol", style="bold", no_wrap=True)
|
|
tbl.add_column("Entry Date", no_wrap=True)
|
|
tbl.add_column("Exit Date", no_wrap=True)
|
|
tbl.add_column("Entry $", justify="right")
|
|
tbl.add_column("Exit $", justify="right")
|
|
tbl.add_column("Shares", justify="right")
|
|
tbl.add_column("Net P&L", justify="right")
|
|
tbl.add_column("R", justify="right")
|
|
tbl.add_column("Days", justify="right")
|
|
tbl.add_column("Reason", style="dim")
|
|
|
|
for t in trades:
|
|
pnl = t.get("net_pnl") or 0.0
|
|
r = t.get("r_multiple") or 0.0
|
|
pnl_color = _pnl_color(pnl)
|
|
r_color = _pnl_color(r)
|
|
|
|
tbl.add_row(
|
|
str(t.get("symbol", "")),
|
|
str(t.get("entry_date", "-")),
|
|
str(t.get("exit_date", "-")),
|
|
f"${t.get('entry_price') or 0:.2f}",
|
|
f"${t.get('exit_price') or 0:.2f}",
|
|
str(t.get("shares", "-")),
|
|
f"[{pnl_color}]{_fmt_pnl(pnl)}[/]",
|
|
f"[{r_color}]{r:+.2f}R[/]",
|
|
str(t.get("holding_days", "-")),
|
|
str(t.get("exit_reason", "-")),
|
|
)
|
|
|
|
_console.print()
|
|
_console.print(tbl)
|
|
_console.print()
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Equity curve (ASCII sparkline)
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def print_equity(
|
|
session: SessionRow,
|
|
state: StateManager,
|
|
) -> None:
|
|
snapshots = state.list_snapshots(session.session_id)
|
|
|
|
if not snapshots:
|
|
_console.print("[dim]No equity history yet.[/]")
|
|
return
|
|
|
|
tbl = Table(
|
|
box=box.SIMPLE_HEAD,
|
|
show_header=True,
|
|
header_style="bold yellow",
|
|
padding=(0, 1),
|
|
title=f"[bold cyan]Equity Curve[/] — {session.session_name}",
|
|
title_justify="left",
|
|
)
|
|
tbl.add_column("Date", no_wrap=True)
|
|
tbl.add_column("Equity", justify="right")
|
|
tbl.add_column("Daily P&L", justify="right")
|
|
tbl.add_column("Total P&L", justify="right")
|
|
tbl.add_column("Drawdown", justify="right")
|
|
tbl.add_column("Positions", justify="right")
|
|
|
|
for snap in snapshots:
|
|
equity = snap.get("equity", 0.0)
|
|
daily_pnl = snap.get("daily_pnl") or 0.0
|
|
total_pnl = snap.get("total_pnl") or 0.0
|
|
dd = snap.get("drawdown_pct") or 0.0
|
|
n_pos = snap.get("open_position_count")
|
|
|
|
daily_color = _pnl_color(daily_pnl)
|
|
total_color = _pnl_color(total_pnl)
|
|
dd_color = "red" if dd > 5 else "green"
|
|
|
|
tbl.add_row(
|
|
str(snap.get("date", "-")),
|
|
f"${equity:,.2f}",
|
|
f"[{daily_color}]{_fmt_pnl(daily_pnl)}[/]",
|
|
f"[{total_color}]{_fmt_pnl(total_pnl)}[/]",
|
|
f"[{dd_color}]{dd:.2f}%[/]",
|
|
str(n_pos) if n_pos is not None else "-",
|
|
)
|
|
|
|
_console.print()
|
|
_console.print(tbl)
|
|
_console.print()
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Sessions list
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def print_sessions(sessions: list[SessionRow]) -> None:
|
|
if not sessions:
|
|
_console.print("[dim]No sessions found.[/]")
|
|
return
|
|
|
|
tbl = Table(
|
|
box=box.SIMPLE_HEAD,
|
|
show_header=True,
|
|
header_style="bold yellow",
|
|
padding=(0, 1),
|
|
title="[bold cyan]Paper Trading Sessions[/]",
|
|
title_justify="left",
|
|
)
|
|
tbl.add_column("ID", style="dim", no_wrap=True)
|
|
tbl.add_column("Name", style="bold", no_wrap=True)
|
|
tbl.add_column("Config", no_wrap=True)
|
|
tbl.add_column("Capital", justify="right")
|
|
tbl.add_column("Status", no_wrap=True)
|
|
tbl.add_column("Created", no_wrap=True)
|
|
|
|
for s in sessions:
|
|
status_color = "green" if s.status == "active" else "dim"
|
|
tbl.add_row(
|
|
s.session_id,
|
|
s.session_name,
|
|
s.config_path,
|
|
f"${s.initial_equity:,.0f}",
|
|
f"[{status_color}]{s.status}[/]",
|
|
s.created_at[:10],
|
|
)
|
|
|
|
_console.print()
|
|
_console.print(tbl)
|
|
_console.print()
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Run summary
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def _print_overlay_detail(r: dict) -> None:
|
|
"""Print overlay regime allocation + per-book summary."""
|
|
name = r["session_name"]
|
|
|
|
# Regime day counts
|
|
regime_counts = r.get("regime_day_counts", {})
|
|
if regime_counts:
|
|
regime_tbl = Table(
|
|
box=box.SIMPLE_HEAD, show_header=True, header_style="bold yellow",
|
|
padding=(0, 1), title=f"[bold magenta]Regime Days — {name}[/]", title_justify="left",
|
|
)
|
|
regime_tbl.add_column("Regime", style="bold")
|
|
regime_tbl.add_column("Days", justify="right")
|
|
regime_tbl.add_column("Allocation", no_wrap=True)
|
|
allocations = r.get("allocations", {})
|
|
for regime, count in sorted(regime_counts.items()):
|
|
alloc = allocations.get(regime, {})
|
|
alloc_str = " ".join(f"{k}={v:.0%}" for k, v in alloc.items())
|
|
regime_tbl.add_row(regime, str(count), alloc_str)
|
|
_console.print(regime_tbl)
|
|
|
|
# Per-book summary
|
|
book_results = r.get("book_results", [])
|
|
if book_results:
|
|
book_tbl = Table(
|
|
box=box.SIMPLE_HEAD, show_header=True, header_style="bold yellow",
|
|
padding=(0, 1), title=f"[bold magenta]Books — {name}[/]", title_justify="left",
|
|
)
|
|
book_tbl.add_column("Book", style="bold")
|
|
book_tbl.add_column("Return", justify="right")
|
|
book_tbl.add_column("MaxDD", justify="right")
|
|
book_tbl.add_column("Trades", justify="right")
|
|
book_tbl.add_column("WinRate", justify="right")
|
|
book_tbl.add_column("Sharpe", justify="right")
|
|
for br in book_results:
|
|
bs = br["result"]["summary"]
|
|
ret_color = "green" if bs["return_pct"] >= 0 else "red"
|
|
book_tbl.add_row(
|
|
br["label"],
|
|
f"[{ret_color}]{bs['return_pct']:+.2f}%[/{ret_color}]",
|
|
f"[red]-{bs['max_dd_pct']:.2f}%[/]",
|
|
str(bs["trade_count"]),
|
|
f"{bs['win_rate']:.0f}%",
|
|
f"{bs['sharpe']:+.2f}",
|
|
)
|
|
_console.print(book_tbl)
|
|
|
|
|
|
def print_backtest_results(results: list[dict], output_dir: str | None = None, show_trades: bool = True) -> None:
|
|
"""Print equity curve comparison, summary table, and per-strategy trade logs."""
|
|
import csv
|
|
import os
|
|
|
|
if not results:
|
|
_console.print("[dim]No backtest results.[/]")
|
|
return
|
|
|
|
# ── Summary table ──────────────────────────────────────────────────────
|
|
sum_tbl = Table(
|
|
box=box.SIMPLE_HEAD,
|
|
show_header=True,
|
|
header_style="bold yellow",
|
|
padding=(0, 1),
|
|
title="[bold cyan]Summary[/]",
|
|
title_justify="left",
|
|
)
|
|
sum_tbl.add_column("Strategy", style="bold", no_wrap=True)
|
|
sum_tbl.add_column("Return", justify="right")
|
|
sum_tbl.add_column("MaxDD", justify="right")
|
|
sum_tbl.add_column("Trades", justify="right")
|
|
sum_tbl.add_column("WinRate", justify="right")
|
|
sum_tbl.add_column("Sharpe", justify="right")
|
|
|
|
for r in results:
|
|
s = r["summary"]
|
|
ret_color = "green" if s["return_pct"] >= 0 else "red"
|
|
name = r["session_name"]
|
|
if r.get("is_overlay"):
|
|
name = f"{name} [overlay]"
|
|
sum_tbl.add_row(
|
|
name,
|
|
f"[{ret_color}]{s['return_pct']:+.2f}%[/{ret_color}]",
|
|
f"[red]-{s['max_dd_pct']:.2f}%[/]",
|
|
str(s["trade_count"]),
|
|
f"{s['win_rate']:.0f}%" if s.get("win_rate") else "-",
|
|
f"{s['sharpe']:+.2f}",
|
|
)
|
|
|
|
_console.print(sum_tbl)
|
|
|
|
# ── Overlay detail sections ───────────────────────────────────────────
|
|
for r in results:
|
|
if not r.get("is_overlay"):
|
|
continue
|
|
_print_overlay_detail(r)
|
|
|
|
# ── Per-strategy trade logs ────────────────────────────────────────────
|
|
if not show_trades:
|
|
if output_dir:
|
|
import csv as _csv
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
for r in results:
|
|
name = r["session_name"]
|
|
eq_path = os.path.join(output_dir, f"{name}_equity.csv")
|
|
with open(eq_path, "w", newline="") as f:
|
|
w = _csv.DictWriter(f, fieldnames=["date", "equity"])
|
|
w.writeheader()
|
|
for row in r["equity_curve"]:
|
|
w.writerow({"date": row["date"].isoformat(), "equity": row["equity"]})
|
|
trades_path = os.path.join(output_dir, f"{name}_trades.csv")
|
|
if r.get("trades"):
|
|
with open(trades_path, "w", newline="") as f:
|
|
fieldnames = ["symbol", "event_type", "score", "entry_date", "exit_date", "shares", "entry_price", "exit_price", "pnl", "reason"]
|
|
w = _csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
|
|
w.writeheader()
|
|
w.writerows(r["trades"])
|
|
_console.print(f"\n[dim]Results saved to {output_dir}[/]")
|
|
return
|
|
|
|
for r in results:
|
|
trades = r.get("trades", [])
|
|
if not trades:
|
|
_console.print(f" [dim]{r['session_name']}: no completed trades[/]")
|
|
continue
|
|
|
|
tbl = Table(
|
|
box=box.SIMPLE_HEAD,
|
|
show_header=True,
|
|
header_style="bold yellow",
|
|
padding=(0, 1),
|
|
title=f"[bold cyan]Trades — {r['session_name']}[/]",
|
|
title_justify="left",
|
|
)
|
|
tbl.add_column("Symbol", style="bold", no_wrap=True)
|
|
tbl.add_column("EventType", no_wrap=True)
|
|
tbl.add_column("Score", justify="right")
|
|
tbl.add_column("Entry", no_wrap=True)
|
|
tbl.add_column("Exit", no_wrap=True)
|
|
tbl.add_column("Days", justify="right")
|
|
tbl.add_column("Shares", justify="right")
|
|
tbl.add_column("EntryPx", justify="right")
|
|
tbl.add_column("ExitPx", justify="right")
|
|
tbl.add_column("P&L", justify="right")
|
|
tbl.add_column("Reason", style="dim")
|
|
|
|
for t in sorted(trades, key=lambda x: x.get("entry_date", "")):
|
|
pnl = t.get("pnl", 0.0)
|
|
color = _pnl_color(pnl)
|
|
|
|
entry_d = t.get("entry_date", "")
|
|
exit_d = t.get("exit_date", "")
|
|
try:
|
|
import datetime as _dt
|
|
holding_days = (_dt.date.fromisoformat(str(exit_d)) - _dt.date.fromisoformat(str(entry_d))).days if entry_d and exit_d else "-"
|
|
except Exception:
|
|
holding_days = "-"
|
|
|
|
score = t.get("score")
|
|
score_str = f"{score:.2f}" if score is not None else "-"
|
|
entry_px = t.get("entry_price")
|
|
exit_px = t.get("exit_price")
|
|
entry_px_str = f"${entry_px:.2f}" if entry_px is not None else "-"
|
|
exit_px_str = f"${exit_px:.2f}" if exit_px is not None else "-"
|
|
|
|
tbl.add_row(
|
|
t.get("symbol", ""),
|
|
t.get("event_type", "-"),
|
|
score_str,
|
|
str(entry_d) if entry_d else "-",
|
|
str(exit_d) if exit_d else "-",
|
|
str(holding_days),
|
|
str(t.get("shares", "-")),
|
|
entry_px_str,
|
|
exit_px_str,
|
|
f"[{color}]{_fmt_pnl(pnl)}[/{color}]",
|
|
t.get("reason", "-"),
|
|
)
|
|
_console.print(tbl)
|
|
|
|
# ── Optional CSV save ──────────────────────────────────────────────────
|
|
if output_dir:
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
for r in results:
|
|
name = r["session_name"]
|
|
|
|
eq_path = os.path.join(output_dir, f"{name}_equity.csv")
|
|
with open(eq_path, "w", newline="") as f:
|
|
w = csv.DictWriter(f, fieldnames=["date", "equity"])
|
|
w.writeheader()
|
|
for row in r["equity_curve"]:
|
|
w.writerow({"date": row["date"].isoformat(), "equity": row["equity"]})
|
|
|
|
trades_path = os.path.join(output_dir, f"{name}_trades.csv")
|
|
if r.get("trades"):
|
|
with open(trades_path, "w", newline="") as f:
|
|
fieldnames = ["symbol", "event_type", "score", "entry_date", "exit_date", "shares", "entry_price", "exit_price", "pnl", "reason"]
|
|
w = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
|
|
w.writeheader()
|
|
w.writerows(r["trades"])
|
|
|
|
_console.print(f"\n[dim]Results saved to {output_dir}[/]")
|
|
|
|
|
|
def print_run_summary(summary: dict) -> None:
|
|
date = summary.get("date", "-")
|
|
status = summary.get("status", "-")
|
|
|
|
if status in ("already_processed", "non_trading_day"):
|
|
_console.print(f"[dim]{date}: {status}[/]")
|
|
return
|
|
|
|
_console.print(f"\nProcessing [bold]{date}[/]...")
|
|
|
|
exits = summary.get("exits", [])
|
|
entries = summary.get("entries", [])
|
|
rejected = summary.get("rejected", [])
|
|
candidates_detected = summary.get("candidates_detected", None)
|
|
account = summary.get("account", {})
|
|
|
|
# Exits
|
|
if exits:
|
|
_console.print(" [bold]EXITS:[/]")
|
|
for e in exits:
|
|
pnl = e.get("pnl", 0.0)
|
|
r = e.get("r_multiple", 0.0)
|
|
color = _pnl_color(pnl)
|
|
_console.print(
|
|
f" [{color}]{e['symbol']}[/] — {e['reason']} "
|
|
f"P&L: [{color}]{_fmt_pnl(pnl)}[/] R: [{color}]{r:+.2f}R[/]"
|
|
)
|
|
else:
|
|
_console.print(" [dim]EXITS: none[/]")
|
|
|
|
# Candidates detected
|
|
if candidates_detected is not None:
|
|
if candidates_detected == 0:
|
|
_console.print(" [dim]CANDIDATES: 0 events in DB for this date[/]")
|
|
else:
|
|
_console.print(f" [dim]CANDIDATES: {candidates_detected} events detected from DB[/]")
|
|
|
|
# Entries
|
|
if entries:
|
|
_console.print(" [bold]ENTRIES:[/]")
|
|
for e in entries:
|
|
_console.print(
|
|
f" [green]{e['symbol']}[/] ({e['event_type']}, score={e['score']:.2f}) "
|
|
f"-> BUY {e['shares']} shares stop=${e['stop']:.2f} target=${e['target']:.2f}"
|
|
)
|
|
else:
|
|
_console.print(" [dim]ENTRIES: none[/]")
|
|
|
|
# Rejected (only if non-zero)
|
|
if rejected:
|
|
_console.print(f" [dim]REJECTED: {len(rejected)} candidates[/]")
|
|
|
|
# Account summary
|
|
if account:
|
|
pnl = account.get("total_pnl", 0.0)
|
|
color = _pnl_color(pnl)
|
|
_console.print(
|
|
f"\n Equity: [bold]${account.get('equity', 0):,.2f}[/] "
|
|
f"Total P&L: [{color}]{_fmt_pnl(pnl)}[/] "
|
|
f"Drawdown: {account.get('drawdown_pct', 0):.2f}%"
|
|
)
|
|
_console.print()
|