# ============================================================
# 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
Décision IA
ACTION IA
HOLD
Sentiment & Macro
BEARISHNEUTREBULLISH
0.00
Positions Ouvertes
| Ticket | Type | Lots |
Entrée | SL | TP | P&L |
| Aucune position ouverte |
"""
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.get("/settings")
async def get_settings():
"""Retourne les paramètres actuels."""
try:
import config as _cfg
return {
"capital": getattr(_cfg, "INITIAL_CAPITAL", 10000),
"rr": getattr(_cfg, "TAKE_PROFIT_ATR_MULT", 3.0) / max(getattr(_cfg, "STOP_LOSS_ATR_MULT", 1.0), 0.001),
"lot": getattr(_cfg, "MANUAL_LOT_SIZE", 0.05),
"protection": getattr(_cfg, "DAILY_MAX_LOSS", 0.05) * 100,
}
except Exception as e:
logger.error(f"get_settings error: {e}")
return {"capital": 10000, "rr": 3.0, "lot": 0.05, "protection": 5.0}
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
self._clients.add(websocket)
logger.info(f"✅ Dashboard: nouveau client WebSocket 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"🔹 COMMAND REÇUE: {cmd} | data={data} | 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", "")
logger.info(f"MT5 login tentative: {login} @ {server}")
try:
import config as _cfg
import MetaTrader5 as _mt5
except ImportError as e:
logger.error(f"MetaTrader5 non installé: {e}")
await websocket.send_text(json.dumps({
"login_error": "MetaTrader5 non installé"
}))
return
try:
# 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
logger.info(f"Initialisation MT5...")
if not _mt5.initialize():
error = _mt5.last_error()
msg = f"Init MT5 échouée: {error}" if error else "Impossible d'initialiser MT5"
logger.error(msg)
await websocket.send_text(json.dumps({
"login_error": msg
}))
return
logger.info(f"Tentative login: {login}@{server}")
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()
logger.error(f"❌ Login échoué: {error_msg}")
await websocket.send_text(json.dumps({"login_error": error_msg}))
except Exception as ex:
logger.exception(f"Exception during MT5 login: {ex}")
try:
_mt5.shutdown()
except:
pass
await websocket.send_text(json.dumps({
"login_error": f"Erreur: {str(ex)[:80]}"
}))
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
# Supprimer les logs asyncio bruyants sur Windows
import logging as _logging
_logging.getLogger("asyncio").setLevel(_logging.WARNING)
app = self._build_app()
if app is None:
logger.error("Impossible de construire l'app FastAPI")
return
try:
# Créer une boucle d'événements dédiée pour ce thread
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
logger.info(f"🚀 Démarrage serveur FastAPI sur 0.0.0.0:{self.port}...")
config = uvicorn.Config(
app,
host="0.0.0.0", # Écouter sur toutes les interfaces
port=self.port,
log_level="warning",
access_log=False,
loop="asyncio",
)
server = uvicorn.Server(config)
logger.info(f"✅ Serveur FastAPI lancé")
self._loop.run_until_complete(server.serve())
except Exception as e:
logger.error(f"❌ Erreur démarrage serveur: {e}")
import traceback
traceback.print_exc()
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.")