# ============================================================ # web_dashboard.py — Dashboard Web Temps Réel # ============================================================ # Serveur FastAPI + WebSocket → s'ouvre automatiquement dans # le navigateur. Contrôle total du bot (start/stop/positions). # ============================================================ import asyncio import json import logging import threading import time import webbrowser from datetime import datetime from typing import Optional, Set logger = logging.getLogger(__name__) # ── HTML Frontend ──────────────────────────────────────────── DASHBOARD_HTML = r""" XAUUSD AI BOT
⚙ PARAMÈTRES TRADING
Ratio SL/TP — plus élevé = gains plus grands mais moins fréquents
0.01 = micro lot | 0.10 = mini lot | 1.00 = lot standard
Bot s'arrête si perte session > ce % du compte
--:--:--
Connexion...
ARRÊTÉ
Balance MT5
0.00 $
PnL Journalier
+0.00 $
Drawdown
0.00%
Positions Ouvertes
0
XAUUSD
0.00
0.00
0.00
0.0 pts
Décision IA
HOLD
25%
BUY
25%
SELL
25%
CLOSE
25%
ACTION IA HOLD
Sentiment & Macro
BEARISHNEUTREBULLISH
0.00
ASIE
LONDRES
NEW YORK
--
--
Positions Ouvertes
TicketTypeLots EntréeSLTPP&L
Aucune position ouverte
Courbe PnL
Min: 0 Max: 0
Journal des Décisions
""" class WebDashboardServer: """ Serveur FastAPI + WebSocket pour le dashboard web. S'ouvre automatiquement dans le navigateur. """ def __init__(self, port: int = 8765, bot_ref=None): self.port = port self.bot_ref = bot_ref # Référence vers XAUUSDBot pour start/stop self._clients: Set = set() self._app = None self._thread: Optional[threading.Thread] = None self._loop = None def set_bot(self, bot): self.bot_ref = bot def _build_app(self): """Construit l'application FastAPI.""" try: from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.responses import HTMLResponse except ImportError: logger.error("FastAPI non installé. pip install fastapi uvicorn") return None app = FastAPI(title="XAUUSD AI Bot Dashboard") @app.get("/", response_class=HTMLResponse) async def root(): return DASHBOARD_HTML @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): await websocket.accept() self._clients.add(websocket) logger.info(f"Dashboard: nouveau client connecté ({len(self._clients)} total)") # Envoyer le statut actuel au nouveau client bot = self.bot_ref is_running = bot and getattr(bot, "_run_event", None) and bot._run_event.is_set() await websocket.send_text(json.dumps({ "status": "EN COURS" if is_running else "STOPPED", "log_message": "Connecte au dashboard" if is_running else "Bot en attente — clique sur Demarrer" })) try: while True: data = await websocket.receive_text() await self._handle_command(json.loads(data), websocket) except Exception: pass finally: self._clients.discard(websocket) logger.info(f"Dashboard: client déconnecté ({len(self._clients)} restants)") return app async def _handle_command(self, data: dict, websocket): """Traite les commandes envoyées depuis le dashboard.""" cmd = data.get("command") bot = self.bot_ref logger.info(f"Commande reçue : {cmd} | bot={bot is not None}") if cmd == "start": is_running = bot and getattr(bot, "_run_event", None) and bot._run_event.is_set() logger.info(f"start cmd | is_running={is_running}") if bot and not is_running: await self.broadcast({"log_message": "Demarrage du bot...", "status": "EN COURS"}) t = threading.Thread(target=bot.start, daemon=True, name="BotStartThread") t.start() logger.info(f"Thread demarrage lance : {t.name}") else: await self.broadcast({"log_message": "Bot deja en cours"}) elif cmd == "stop": if bot and getattr(bot, "_run_event", None) and bot._run_event.is_set(): await self.broadcast({ "log_message": "Arret en cours...", "status": "STOPPING" }) def do_stop(): bot.stop() self.broadcast_sync({ "status": "STOPPED", "log_message": "Bot arrete. Cliquez Demarrer pour relancer." }) threading.Thread(target=do_stop, daemon=True).start() else: await self.broadcast({"status": "STOPPED", "log_message": "Bot non demarre"}) elif cmd == "close_all": if bot and hasattr(bot, 'mt5'): await self.broadcast({"log_message": "Fermeture des positions en cours..."}) def do_close(): n = bot.mt5.close_all_positions() self.broadcast_sync({"log_message": f"Positions fermees : {n}"}) threading.Thread(target=do_close, daemon=True).start() else: await self.broadcast({"log_message": "MT5 non connecte"}) elif cmd == "mt5_login": # Credentials envoyés depuis l'écran de login login = data.get("login", 0) password = data.get("password", "") server = data.get("server", "") try: import config as _cfg import MetaTrader5 as _mt5 # Nettoyer les données reçues login = int(login) if login else 0 password = str(password).strip() if password else "" server = str(server).strip() if server else "" if not login or not password or not server: await websocket.send_text(json.dumps({ "login_error": "Tous les champs sont requis" })) return # Test connexion rapide if not _mt5.initialize(): await websocket.send_text(json.dumps({ "login_error": "Impossible d'initialiser MT5" })) return try: ok = _mt5.login(login=login, password=password, server=server) if ok: _cfg.MT5_LOGIN = login _cfg.MT5_PASSWORD = password _cfg.MT5_SERVER = server _mt5.shutdown() await websocket.send_text(json.dumps({"login_ok": True})) logger.info(f"Login MT5 OK — compte {login} sur {server}") else: error_code = _mt5.last_error() error_msg = f"Erreur MT5: {error_code[0]} - {error_code[1]}" if error_code else "Identifiants invalides" _mt5.shutdown() await websocket.send_text(json.dumps({"login_error": error_msg})) logger.warning(f"Login MT5 échoué pour {login}: {error_msg}") except Exception as ex: _mt5.shutdown() await websocket.send_text(json.dumps({ "login_error": f"Erreur connexion: {str(ex)[:60]}" })) except Exception as e: logger.error(f"mt5_login error: {e}") await websocket.send_text(json.dumps({ "login_error": f"Erreur: {str(e)[:60]}" })) elif cmd == "set_params": # Paramètres trading (RR, Lot, Protection) rr = float(data.get("rr", 3.0)) lot = float(data.get("lot", 0.05)) protection = float(data.get("protection", 5.0)) try: import config as _cfg _cfg.TAKE_PROFIT_ATR_MULT = rr * _cfg.STOP_LOSS_ATR_MULT _cfg.DAILY_MAX_LOSS = protection / 100.0 # Lot size → stocké dans risk_manager si bot actif if bot and bot.risk_mgr: bot.risk_mgr._manual_lot = lot bot.risk_mgr._use_manual = True else: _cfg.MANUAL_LOT_SIZE = lot _cfg.USE_MANUAL_LOT = True await websocket.send_text(json.dumps({ "params_applied": True, "rr": rr, "lot": lot, "protection": protection })) await self.broadcast({"log_message": f"Params: RR 1:{rr} | Lot {lot} | Protection {protection}%"}) logger.info(f"Params mis a jour : RR=1:{rr} | Lot={lot} | Protection={protection}%") except Exception as e: logger.error(f"set_params erreur : {e}") async def broadcast(self, data: dict): """Envoie les données à tous les clients connectés.""" if not self._clients: return msg = json.dumps(data) dead = set() for client in self._clients.copy(): try: await client.send_text(msg) except Exception: dead.add(client) self._clients -= dead def broadcast_sync(self, data: dict): """Version synchrone fire-and-forget (depuis threads externes).""" if not self._loop: return try: # Fire and forget — ne bloque JAMAIS le thread appelant asyncio.run_coroutine_threadsafe(self.broadcast(data), self._loop) except Exception as e: logger.debug(f"broadcast_sync error: {e}") def start(self): """Démarre le serveur dans un thread séparé et ouvre le navigateur.""" self._thread = threading.Thread(target=self._run_server, daemon=True) self._thread.start() # Ouvrir le navigateur après 1.5 secondes (laisse le serveur démarrer) threading.Timer(1.5, self._open_browser).start() logger.info(f"Dashboard web démarré sur http://localhost:{self.port}") def _run_server(self): """Lance uvicorn dans le thread.""" try: import uvicorn except ImportError: logger.error("uvicorn non installé. pip install uvicorn") return app = self._build_app() if app is None: return self._loop = asyncio.new_event_loop() asyncio.set_event_loop(self._loop) config = uvicorn.Config( app, host="127.0.0.1", port=self.port, log_level="error", loop="asyncio", ) server = uvicorn.Server(config) self._loop.run_until_complete(server.serve()) def _open_browser(self): """Ouvre le dashboard dans le navigateur par défaut.""" webbrowser.open(f"http://localhost:{self.port}") logger.info("Dashboard ouvert dans le navigateur.")