xauusd_bot/dashboard.py

207 lines
8.7 KiB
Python

# ============================================================
# dashboard.py — Dashboard Console (Rich)
# ============================================================
import logging
import threading
import time
from datetime import datetime
from typing import List, Optional, Dict
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.layout import Layout
from rich.text import Text
from rich.live import Live
from rich import box
import config
logger = logging.getLogger(__name__)
console = Console()
class TradingDashboard:
"""
Dashboard console temps-réel avec Rich.
Affiche : prix, sentiment, compte, positions, logs récents.
"""
def __init__(self):
self._state: Dict = {
"price": 0.0,
"bid": 0.0,
"ask": 0.0,
"spread": 0.0,
"sentiment": 0.0,
"sent_label": "⚪ NEUTRE",
"balance": 0.0,
"equity": 0.0,
"daily_pnl": 0.0,
"daily_pnl_pct": 0.0,
"drawdown": 0.0,
"open_trades": 0,
"positions": [],
"ai_action": "HOLD",
"ai_probs": [0.25, 0.25, 0.25, 0.25],
"last_news": [],
"status": "🟢 EN COURS",
"total_steps": 0,
"last_update": datetime.now(),
}
self._log_buffer: List[str] = []
self._lock = threading.Lock()
self._running = False
def update(self, **kwargs):
"""Met à jour l'état du dashboard (thread-safe)."""
with self._lock:
self._state.update(kwargs)
self._state["last_update"] = datetime.now()
def add_log(self, message: str):
"""Ajoute une ligne au buffer de logs."""
with self._lock:
timestamp = datetime.now().strftime("%H:%M:%S")
self._log_buffer.append(f"[dim]{timestamp}[/dim] {message}")
if len(self._log_buffer) > 20:
self._log_buffer.pop(0)
def _build_layout(self) -> Layout:
"""Construit le layout Rich du dashboard."""
with self._lock:
s = dict(self._state)
logs = list(self._log_buffer)
layout = Layout()
layout.split_column(
Layout(name="header", size=3),
Layout(name="main", ratio=1),
Layout(name="footer", size=3),
)
layout["main"].split_row(
Layout(name="left", ratio=2),
Layout(name="right", ratio=1),
)
layout["left"].split_column(
Layout(name="market", size=9),
Layout(name="positions"),
)
# ── Header ─────────────────────────────────────────────
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
header_text = Text(
f" 🤖 XAUUSD AI TRADING BOT | {now} | {s['status']}",
style="bold white on dark_blue",
justify="center"
)
layout["header"].update(Panel(header_text, box=box.HEAVY))
# ── Marché & Compte ────────────────────────────────────
market_table = Table(box=box.SIMPLE_HEAVY, expand=True, show_header=False)
market_table.add_column("Clé", style="bold cyan", width=20)
market_table.add_column("Valeur", style="bold white")
market_table.add_column("Clé2", style="bold cyan", width=20)
market_table.add_column("Valeur2",style="bold white")
pnl_color = "green" if s["daily_pnl"] >= 0 else "red"
pnl_str = f"[{pnl_color}]{s['daily_pnl']:+.2f} ({s['daily_pnl_pct']:+.2f}%)[/{pnl_color}]"
dd_color = "green" if s["drawdown"] < 1.0 else ("yellow" if s["drawdown"] < 2.0 else "red")
ai_action_colors = {"HOLD": "white", "BUY": "green", "SELL": "red", "CLOSE": "yellow"}
ai_color = ai_action_colors.get(s["ai_action"], "white")
market_table.add_row("💰 XAUUSD", f"{s['price']:.2f}",
"📊 Balance", f"{s['balance']:.2f} USD")
market_table.add_row("📈 Bid", f"{s['bid']:.2f}",
"💳 Equity", f"{s['equity']:.2f} USD")
market_table.add_row("📉 Ask", f"{s['ask']:.2f}",
"📅 PnL jour", pnl_str)
market_table.add_row("↔️ Spread", f"{s['spread']:.1f} pts",
"📉 Drawdown", f"[{dd_color}]{s['drawdown']:.2f}%[/{dd_color}]")
market_table.add_row("📰 Sentiment", s["sent_label"],
"🤖 Action IA", f"[bold {ai_color}]{s['ai_action']}[/bold {ai_color}]")
layout["market"].update(Panel(market_table, title="[bold]MARCHÉ & COMPTE[/bold]", box=box.ROUNDED))
# ── Positions Ouvertes ─────────────────────────────────
pos_table = Table(box=box.SIMPLE, expand=True)
pos_table.add_column("Ticket", style="dim", width=10)
pos_table.add_column("Type", style="bold", width=6)
pos_table.add_column("Lots", justify="right", width=8)
pos_table.add_column("Ouvert @", justify="right", width=10)
pos_table.add_column("SL", justify="right", width=10)
pos_table.add_column("TP", justify="right", width=10)
pos_table.add_column("P&L", justify="right", width=10)
positions = s.get("positions", [])
if positions:
for p in positions:
pnl_clr = "green" if p["profit"] >= 0 else "red"
type_clr = "green" if p["type"] == "BUY" else "red"
pos_table.add_row(
str(p["ticket"]),
f"[{type_clr}]{p['type']}[/{type_clr}]",
str(p["volume"]),
f"{p['open_price']:.2f}",
f"{p['sl']:.2f}",
f"{p['tp']:.2f}",
f"[{pnl_clr}]{p['profit']:+.2f}[/{pnl_clr}]",
)
else:
pos_table.add_row("", "", "", "", "", "", "")
layout["positions"].update(
Panel(pos_table, title=f"[bold]POSITIONS OUVERTES ({len(positions)})[/bold]", box=box.ROUNDED)
)
# ── Probabilités IA & News ─────────────────────────────
right_text = Text()
probs = s.get("ai_probs", [0.25] * 4)
action_names = ["HOLD", "BUY", "SELL", "CLOSE"]
action_emojis = ["", "🟢", "🔴", "🔵"]
right_text.append("── PROBABILITÉS IA ──\n", style="bold cyan")
for i, (name, prob, emoji) in enumerate(zip(action_names, probs, action_emojis)):
bar_len = int(prob * 20)
bar = "" * bar_len + "" * (20 - bar_len)
clr = "green" if name == "BUY" else ("red" if name == "SELL" else "white")
right_text.append(f" {emoji} {name:<6} ", style=f"bold {clr}")
right_text.append(f"{bar} {prob*100:5.1f}%\n", style=clr)
right_text.append("\n── DERNIÈRES NEWS ──\n", style="bold cyan")
for news in s.get("last_news", [])[:3]:
sent = news.get("sentiment_score", 0)
clr = "green" if sent > 0.1 else ("red" if sent < -0.1 else "white")
title = news.get("title", "")[:45]
right_text.append(f" [{clr}]{sent:+.2f}[/{clr}] {title}\n")
layout["right"].update(Panel(right_text, title="[bold]IA & NEWS[/bold]", box=box.ROUNDED))
# ── Footer — Logs ──────────────────────────────────────
log_text = Text()
for line in logs[-3:]:
log_text.append(line + "\n")
layout["footer"].update(Panel(log_text, title="[bold]DERNIÈRES DÉCISIONS[/bold]", box=box.ROUNDED))
return layout
def run(self, refresh_rate: float = config.DASHBOARD_REFRESH):
"""Lance le dashboard en mode Live (bloque le thread)."""
self._running = True
try:
with Live(self._build_layout(), refresh_per_second=1/refresh_rate, screen=True) as live:
while self._running:
live.update(self._build_layout())
time.sleep(refresh_rate)
except KeyboardInterrupt:
pass
finally:
self._running = False
def start_background(self):
"""Lance le dashboard dans un thread daemon."""
t = threading.Thread(target=self.run, daemon=True)
t.start()
def stop(self):
self._running = False