"""
AHAD QUANT Forex V4_RL — Web Command Center v4
Interface de contrôle complète — ML + RL Pipeline.
Fonctionnalités :
• Dashboard : balance, PnL, win rate, positions ouvertes
• RL Monitor : progress, courbes d'entraînement, fine-tuning
• Backtest : résultats ML seul et ML+RL
• Trades : journal paper trading + courbe equity
• MT5 Bridge : monitoring connexion, signaux, rapports
• Config : éditeur .env
• Terminal : logs temps réel
• Features : feature importance chart
Lancer:
pip install fastapi uvicorn requests
python web_ui.py
Puis ouvrir: http://localhost:8080
"""
import os, json, time, subprocess, sys, threading, queue, pickle
from datetime import datetime
from pathlib import Path
from typing import Optional, Generator
try:
import requests as _ext_requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
from fastapi import FastAPI, Request, HTTPException, Depends
from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse, FileResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import uvicorn
import config
import csv as _csv_mod
# ═══════════════════════════════════════════════════════════════════════════════
# MT5 Bridge — lecteurs CSV directs
# ═══════════════════════════════════════════════════════════════════════════════
def _mt5_path() -> Optional[Path]:
raw = getattr(config, "MT5_FILES_PATH", os.getenv("MT5_FILES_PATH", ""))
if not raw: return None
p = Path(raw)
return p if p.exists() else None
def read_mt5_status() -> dict:
p = _mt5_path()
if not p: return {}
try:
f = p / "status.csv"
if not f.exists(): return {}
with open(f, encoding="utf-8", newline="") as fh:
rows = list(_csv_mod.DictReader(fh))
if not rows: return {}
r = rows[-1]
return {
"timestamp": r.get("timestamp", ""),
"balance": float(r.get("balance", 0) or 0),
"equity": float(r.get("equity", 0) or 0),
"margin": float(r.get("margin", 0) or 0),
"free_margin": float(r.get("free_margin", 0) or 0),
"open_positions": int(r.get("open_positions", 0) or 0),
"daily_pnl": float(r.get("daily_pnl", 0) or 0),
}
except Exception:
return {}
def _normalize_mt5_status(raw: str) -> tuple:
raw = (raw or "").strip().upper()
if raw in ("OPENED", "OPEN"):
return "OPEN", raw, ""
if raw in ("SL_HIT", "TP_HIT", "TIMEOUT", "MANUAL") or raw.startswith("CLOSED"):
return "CLOSED", raw, raw
if raw in ("SYMBOL_NOT_FOUND", "NO_PRICE", "INSUFFICIENT_MARGIN") or raw.startswith("FAILED"):
return "REJECTED", raw, raw
return raw, raw, ""
def read_mt5_reports(limit: int = 200) -> list:
p = _mt5_path()
if not p: return []
try:
f = p / "reports.csv"
if not f.exists(): return []
with open(f, encoding="utf-8-sig", newline="") as fh:
rows = list(_csv_mod.DictReader(fh))
normalized = []
for row in rows:
sn, sr, cr = _normalize_mt5_status(row.get("status", ""))
row["status"] = sn; row["status_raw"] = sr; row["close_reason"] = cr
normalized.append(row)
return list(reversed(normalized[-limit:]))
except Exception:
return []
def read_bridge_health() -> dict:
p = _mt5_path()
out = {"signals_rows": 0, "reports_rows": 0,
"last_signal_id": None, "last_signal_ts": None,
"last_report_id": None, "last_report_ts": None,
"pending_signals": 0, "gap_seconds": None}
if not p: return out
try:
sf = p / "signals.csv"
if sf.exists():
with open(sf, encoding="utf-8-sig", newline="") as fh:
rows = list(_csv_mod.DictReader(fh))
out["signals_rows"] = len(rows)
if rows:
out["last_signal_id"] = rows[-1].get("signal_id")
out["last_signal_ts"] = rows[-1].get("timestamp")
rf = p / "reports.csv"
ack_ids = set()
if rf.exists():
with open(rf, encoding="utf-8-sig", newline="") as fh:
rrows = list(_csv_mod.DictReader(fh))
out["reports_rows"] = len(rrows)
ack_ids = {r.get("signal_id") for r in rrows}
if rrows:
out["last_report_id"] = rrows[-1].get("signal_id")
out["last_report_ts"] = rrows[-1].get("open_time") or rrows[-1].get("close_time")
if sf.exists():
out["pending_signals"] = sum(1 for r in rows if r.get("signal_id") not in ack_ids)
if out["last_signal_ts"] and out["last_report_ts"]:
try:
t1 = datetime.fromisoformat(out["last_signal_ts"].replace("Z", "+00:00")).replace(tzinfo=None)
t2 = datetime.fromisoformat(out["last_report_ts"].replace("Z", "+00:00")).replace(tzinfo=None)
out["gap_seconds"] = round((t2 - t1).total_seconds(), 1)
except Exception:
pass
except Exception:
pass
return out
def _mt5_connected(status: dict) -> bool:
ts = status.get("timestamp", "")
if not ts: return False
try:
dt = datetime.fromisoformat(ts.replace("Z", "+00:00")).replace(tzinfo=None)
return (datetime.utcnow() - dt).total_seconds() < 90
except Exception:
return False
# ═══════════════════════════════════════════════════════════════════════════════
# App + Middleware
# ═══════════════════════════════════════════════════════════════════════════════
app = FastAPI(title="AHAD QUANT V4_RL", docs_url=None, redoc_url=None)
_ALLOWED_ORIGINS = os.getenv(
"WEB_UI_CORS_ORIGINS",
"http://localhost:8080,http://127.0.0.1:8080"
).split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=_ALLOWED_ORIGINS,
allow_methods=["*"],
allow_headers=["*"],
)
_WEB_UI_TOKEN: str = os.getenv("WEB_UI_TOKEN", "")
_http_bearer = HTTPBearer(auto_error=False)
if not _WEB_UI_TOKEN:
import warnings
warnings.warn(
"[SÉCURITÉ] WEB_UI_TOKEN non configuré — API accessible sans auth. "
"Définir WEB_UI_TOKEN dans .env avant déploiement VPS.",
stacklevel=1,
)
def require_auth(credentials: HTTPAuthorizationCredentials = Depends(_http_bearer)):
if not _WEB_UI_TOKEN:
return
if credentials is None or credentials.credentials != _WEB_UI_TOKEN:
raise HTTPException(status_code=401, detail="Token invalide. Configurer WEB_UI_TOKEN dans .env")
# ═══════════════════════════════════════════════════════════════════════════════
# Log infrastructure
# ═══════════════════════════════════════════════════════════════════════════════
_log_queue: queue.Queue = queue.Queue(maxsize=2000)
_log_history: list = []
_log_lock = threading.Lock()
_reset_version: int = 0 # incrémenté à chaque reset global — le client peut détecter un nouveau reset
def _emit(msg: str, proc: str = "UI"):
ts = datetime.now().strftime("%H:%M:%S")
entry = {"t": ts, "p": proc, "m": msg.rstrip()}
with _log_lock:
_log_history.append(entry)
if len(_log_history) > 1000:
_log_history.pop(0)
try: _log_queue.put_nowait(entry)
except queue.Full: pass
# ═══════════════════════════════════════════════════════════════════════════════
# Process registry
# ═══════════════════════════════════════════════════════════════════════════════
_procs: dict[str, Optional[subprocess.Popen]] = {
"bot": None, "train": None, "download": None, "backtest": None, "finetune": None,
"daily_local": None, # [NEW] cycle quotidien local (warm-start ML + RL réel) déclenché manuellement
"setup": None, # [NEW] pipeline setup initial (download → train → rl_train → export)
}
_pending: set = set()
_proc_lock = threading.Lock()
BASE_DIR = Path(__file__).parent.resolve()
def _is_running(name: str) -> bool:
with _proc_lock:
if name in _pending: return True
p = _procs.get(name)
return p is not None and p.poll() is None
def _kill(name: str):
with _proc_lock:
p = _procs.get(name)
if p and p.poll() is None:
p.terminate()
try: p.wait(timeout=5)
except subprocess.TimeoutExpired: p.kill()
_procs[name] = None
_emit(f"⛔ Processus '{name}' arrêté", "UI")
def _launch(name: str, script: str, label: str, extra_args: list = None):
with _proc_lock:
if name in _pending or (_procs.get(name) and _procs[name].poll() is None):
_emit(f"[!] {name} déjà en cours — lancement ignoré", "UI")
return
_pending.add(name)
_emit(f"▶ {label} démarré ({script})", name.upper())
try:
cmd = [sys.executable, "-u", str(BASE_DIR / script)] + (extra_args or [])
p = subprocess.Popen(
cmd,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, bufsize=1, cwd=str(BASE_DIR),
encoding="utf-8", errors="replace"
)
with _proc_lock:
_procs[name] = p
_pending.discard(name)
for line in iter(p.stdout.readline, ''):
_emit(line.rstrip(), name.upper())
p.wait()
_emit(f"[{'OK' if p.returncode == 0 else 'FAIL'}] {label} terminé (code {p.returncode})", name.upper())
except Exception as e:
_emit(f"[ERREUR] Lancement {script}: {e}", name.upper())
finally:
with _proc_lock:
_procs[name] = None
_pending.discard(name)
# ═══════════════════════════════════════════════════════════════════════════════
# Config (.env) reader/writer
# ═══════════════════════════════════════════════════════════════════════════════
ENV_PATH = BASE_DIR / ".env"
def read_env() -> dict:
result = {}
if not ENV_PATH.exists(): return result
for line in ENV_PATH.read_text(encoding="utf-8").splitlines():
s = line.strip()
if s and not s.startswith('#') and '=' in s:
k, _, v = s.partition('=')
result[k.strip()] = v.strip()
return result
def write_env(updates: dict):
lines = ENV_PATH.read_text(encoding="utf-8").splitlines() if ENV_PATH.exists() else []
written = set()
new_lines = []
for line in lines:
s = line.strip()
if s and not s.startswith('#') and '=' in s:
k = s.split('=')[0].strip()
if k in updates:
new_lines.append(f"{k}={updates[k]}")
written.add(k)
continue
new_lines.append(line)
for k, v in updates.items():
if k not in written:
new_lines.append(f"{k}={v}")
ENV_PATH.write_text('\n'.join(new_lines) + '\n', encoding="utf-8")
SAFE_CONFIG_KEYS = {
# Trading core
"PAPER_MODE", "PAPER_INITIAL_BALANCE",
"LEVERAGE", "MAX_POSITIONS", "RISK_PER_TRADE",
"MAX_DAILY_LOSS_PCT", "STOP_LOSS_PCT", "TAKE_PROFIT_PCT",
"MIN_CONFIDENCE", "MAIN_LOOP_SECONDS",
"PAIRS", "CANDLE_INTERVAL",
"MIN_LOT_SIZE", "MAX_LOT_SIZE",
# Session filter
"SESSION_FILTER_ENABLED",
"SESSION_LONDON_START", "SESSION_LONDON_END",
"SESSION_NY_START", "SESSION_NY_END",
# Models
"USE_ENSEMBLE",
# RL
"USE_RL_AGENT", "RL_MODE", "RL_CONFIDENCE_BOOST",
"RL_OVERRIDE_THRESHOLD", "RL_FINETUNE_STEPS",
# Apprentissage continu (quotidien, local) — ML + RL
"DAILY_LOCAL_RETRAIN_ENABLED", "DAILY_LOCAL_RETRAIN_INTERVAL_HOURS",
"CONTINUOUS_LEARNING_ENABLED", "WARMSTART_ENABLED", "WARMSTART_MIN_TRADES",
"RL_REPLAY_MIN_TRADES", "RL_RETRAIN_INTERVAL_HOURS",
"AUTO_RETRAIN_INTERVAL_HOURS", "AUTO_RETRAIN_MIN_ACCURACY",
"EXPERIENCE_BUFFER_MAX_SIZE",
"RL_AUTO_RETRAIN_ENABLED", "RL_REAL_REPLAY_ENABLED",
# MT5
"MT5_BRIDGE_ENABLED", "MT5_FILES_PATH",
"MT5_SIGNAL_TIMEOUT", "MT5_POLL_INTERVAL",
"MT5_SYMBOL_SUFFIX", "MT5_SERVER",
# Auth
"WEB_UI_TOKEN",
}
CREDENTIAL_KEYS = {"MT5_LOGIN", "MT5_PASSWORD", "WEB_UI_TOKEN"}
# ═══════════════════════════════════════════════════════════════════════════════
# State readers
# ═══════════════════════════════════════════════════════════════════════════════
def read_paper_state() -> dict:
try:
with open(BASE_DIR / "paper_state.json") as f:
return json.load(f)
except Exception:
return {"balance": 0, "positions": {}, "trades": [], "daily_pnl": 0, "total_pnl": 0, "peak_equity": 0}
def read_backtest_results() -> dict:
try:
with open(BASE_DIR / "backtest_results.json") as f:
return json.load(f)
except Exception:
return {}
def read_model_info() -> dict:
info = {}
try:
with open(BASE_DIR / "last_retrain.json") as f:
info = json.load(f)
except Exception:
pass
ensemble_path = BASE_DIR / getattr(config, "ENSEMBLE_MODEL_PATH", "model_ensemble.pkl")
model_path = BASE_DIR / getattr(config, "MODEL_PATH", "model.pkl")
if ensemble_path.exists():
info["model_file"] = ensemble_path.name
info["model_size_mb"] = round(ensemble_path.stat().st_size / 1024 / 1024, 1)
info["model_mtime"] = datetime.fromtimestamp(ensemble_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
info["has_ensemble"] = True
elif model_path.exists():
info["model_file"] = model_path.name
info["model_size_mb"] = round(model_path.stat().st_size / 1024 / 1024, 1)
info["model_mtime"] = datetime.fromtimestamp(model_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
info["has_ensemble"] = False
else:
info["model_file"] = None
return info
def _detect_device() -> str:
"""Détecte CUDA réellement, sans planter si torch n'est pas installé."""
try:
import torch
return "CUDA (GPU)" if torch.cuda.is_available() else "CPU"
except Exception:
return "CPU (torch non installé)"
def read_rl_status() -> dict:
"""Lit l'état complet de la couche RL."""
agent_path = BASE_DIR / "rl_agent.zip"
scaler_path = BASE_DIR / "rl_scaler.pkl"
ckpt_path = BASE_DIR / "rl_checkpoints" / "best_model.zip"
curves_path = BASE_DIR / "rl_training_curves.png"
out = {
"agent_exists": agent_path.exists(),
"scaler_exists": scaler_path.exists(),
"checkpoint_exists": ckpt_path.exists(),
"curves_exists": curves_path.exists(),
"agent_size_mb": round(agent_path.stat().st_size / 1024 / 1024, 2) if agent_path.exists() else 0,
"agent_mtime": datetime.fromtimestamp(agent_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M") if agent_path.exists() else None,
"progress": {},
# ── [FIX] Avant : "Device CUDA (GPU)" et "Fine-tune Steps 200 000"
# étaient écrits en dur dans le HTML, peu importe la machine réelle
# ou la config courante. Désormais lus dynamiquement. ──
"device": _detect_device(),
"finetune_steps": getattr(config, "RL_FINETUNE_STEPS", 200_000),
}
try:
with open(BASE_DIR / "rl_progress.json") as f:
out["progress"] = json.load(f)
except Exception:
pass
# ── Dernier résultat de fine-tune (reward avant/après, accepté ou non,
# ET la source des données — "real_trades_only" vs "simulated") —
# persisté par rl_train.py::fine_tune()/fine_tune_real_only() dans
# last_rl_retrain.json. AVANT : ce résultat n'existait que dans le
# stdout du process, perdu une fois celui-ci terminé ; et même une fois
# lu ici, "data_source"/"n_real_trades" n'étaient jamais affichés côté UI. ──
out["last_finetune"] = None
try:
with open(BASE_DIR / "last_rl_retrain.json") as f:
out["last_finetune"] = json.load(f)
except Exception:
pass
# ── [FIX] Statut du cycle quotidien LOCAL (warm-start ML + RL réel) —
# remplace le bloc "Planning Fine-tuning" statique ("Semaine 3/4",
# "hebdomadaire") qui ne correspondait plus au cycle quotidien actuel. ──
out["daily_local"] = {
"last_run": None,
"interval_hours": getattr(config, "DAILY_LOCAL_RETRAIN_INTERVAL_HOURS", 24),
}
try:
with open(BASE_DIR / "last_daily_local.json") as f:
out["daily_local"]["last_run"] = json.load(f).get("datetime")
except Exception:
pass
return out
def read_data_status() -> dict:
d = BASE_DIR / "data"
files = list(d.glob("*.json")) if d.exists() else []
return {
# Champs utilisés par la page Setup Initial (loadSetupStatus JS)
"ensemble_model": (BASE_DIR / "model_ensemble.pkl").exists(),
"rl_model": (BASE_DIR / "rl_agent.zip").exists(),
"unified_model": (BASE_DIR / "ahad_quant_unified.zip").exists(),
"data_files_count": len(files),
# Champs originaux conservés (utilisés ailleurs)
"count": len(files),
"files": sorted(f.stem.replace("_1h", "") for f in files),
"total_mb": round(sum(f.stat().st_size for f in files) / 1024 / 1024, 1) if files else 0,
}
def read_bot_state() -> dict:
try:
with open(BASE_DIR / "bot_state.json") as f:
return json.load(f)
except Exception:
return {}
def read_feature_importance() -> dict:
try:
with open(BASE_DIR / "feature_importance.json") as f:
return json.load(f)
except Exception:
return {}
def _calc_win_rate(trades: list) -> float:
if not trades: return 0.0
return round(sum(1 for t in trades if float(t.get("pnl", 0)) > 0) / len(trades) * 100, 1)
def _equity_curve(trades: list) -> list:
bal, curve = 100.0, []
for t in trades:
bal += float(t.get("pnl", 0))
curve.append({"t": t.get("closed_at", ""), "v": round(bal, 2)})
return curve[-300:]
def fetch_prices(symbols: list) -> dict:
"""Prix Forex via Frankfurter (ECB rates, gratuit, ~55 paires)."""
if not HAS_REQUESTS or not symbols: return {}
out = {s: 0.0 for s in symbols}
try:
r = _ext_requests.get("https://api.frankfurter.app/latest", params={"from": "EUR"}, timeout=5)
if r.ok:
rates = r.json().get("rates", {})
rates["EUR"] = 1.0
for sym in symbols:
if len(sym) != 6: continue
bc, qc = sym[:3], sym[3:]
b, q = rates.get(bc), rates.get(qc)
if b and q:
out[sym] = round(q / b, 6)
except Exception:
pass
return out
# ═══════════════════════════════════════════════════════════════════════════════
# API
# ═══════════════════════════════════════════════════════════════════════════════
@app.get("/api/status", dependencies=[Depends(require_auth)])
def api_status():
model = read_model_info()
data = read_data_status()
rl = read_rl_status()
# ── Badge de synchronisation ML/RL — compare les horodatages des
# DERNIÈRES mises à jour effectives (pas juste la dernière tentative) :
# - ML : last_retrain.json["datetime"] (écrit uniquement si accepté)
# - RL : last_rl_retrain.json["datetime"], MAIS uniquement si accepted=True
# AVANT : aucune comparaison n'existait — impossible de savoir si l'un
# avait dérivé de l'autre. ──
sync = {"status": "unknown", "ml_updated_at": None, "rl_updated_at": None, "drift_days": None}
try:
ml_dt_str = model.get("datetime")
rl_finetune = rl.get("last_finetune") or {}
rl_dt_str = rl_finetune.get("datetime") if rl_finetune.get("accepted") else None
sync["ml_updated_at"] = ml_dt_str
sync["rl_updated_at"] = rl_dt_str
if ml_dt_str and rl_dt_str:
ml_dt = datetime.fromisoformat(ml_dt_str)
rl_dt = datetime.fromisoformat(rl_dt_str)
drift_days = abs((ml_dt - rl_dt).total_seconds()) / 86400
sync["drift_days"] = round(drift_days, 1)
sync["status"] = "synced" if drift_days <= 1 else "drifted"
elif ml_dt_str or rl_dt_str:
sync["status"] = "partial" # un seul des deux a déjà été (re)validé
except Exception:
pass
paper_mode = getattr(config, "PAPER_MODE", True)
if paper_mode:
paper = read_paper_state()
pos = paper.get("positions", {})
prices = fetch_prices(list(pos.keys()))
for coin, p in pos.items():
price = prices.get(coin)
if price and p.get("entry"):
mult = 1 if p.get("side") == "long" else -1
p["upnl"] = round(mult * (price - p["entry"]) / p["entry"] * p.get("qty", 0) * p["entry"] * getattr(config, "LEVERAGE", 30), 3)
p["current_price"] = round(price, 6)
balance = round(paper.get("balance", 0), 2)
daily_pnl = round(paper.get("daily_pnl", 0), 2)
total_pnl = round(paper.get("total_pnl", 0), 2)
peak_eq = round(paper.get("peak_equity", 0), 2)
open_pos = pos
total_tr = len(paper.get("trades", []))
win_rate = _calc_win_rate(paper.get("trades", []))
else:
mt5 = read_mt5_status()
reports = read_mt5_reports(limit=500)
balance = round(mt5.get("balance", 0), 2)
daily_pnl = round(mt5.get("daily_pnl", 0), 2)
total_pnl = round(sum(float(r.get("pnl", 0)) for r in reports), 2)
peak_eq = round(mt5.get("equity", balance), 2)
open_pos = {}
total_tr = len(reports)
win_rate = round(
sum(1 for r in reports if float(r.get("profit", 0)) > 0) / len(reports) * 100
if reports else 0, 1)
pairs = getattr(config, "PAIRS", [])
return {
"paper_mode": paper_mode,
"balance": balance,
"daily_pnl": daily_pnl,
"total_pnl": total_pnl,
"peak_equity": peak_eq,
"open_positions": open_pos,
"total_trades": total_tr,
"win_rate": win_rate,
"model": model,
"sync": sync,
"data": data,
"rl": rl,
"leverage": getattr(config, "LEVERAGE", 30),
"max_positions": getattr(config, "MAX_POSITIONS", 5),
"risk_per_trade": getattr(config, "RISK_PER_TRADE", 0.02),
"min_confidence": getattr(config, "MIN_CONFIDENCE", 0.72),
"pairs_count": len(pairs),
"use_ensemble": getattr(config, "USE_ENSEMBLE", True),
"use_rl_agent": getattr(config, "USE_RL_AGENT", False),
"rl_mode": getattr(config, "RL_MODE", "filter"),
"rl_override_threshold": getattr(config, "RL_OVERRIDE_THRESHOLD", 0.82),
"rl_confidence_boost": getattr(config, "RL_CONFIDENCE_BOOST", 0.05),
"session_filter": getattr(config, "SESSION_FILTER_ENABLED", False),
"mt5_bridge": getattr(config, "MT5_BRIDGE_ENABLED", False),
"candle_interval": getattr(config, "CANDLE_INTERVAL", "1h"),
"bot_running": _is_running("bot"),
"training": _is_running("train"),
"downloading": _is_running("download"),
"backtesting": _is_running("backtest"),
"finetuning": _is_running("train"), # UNIFIÉ : même slot que "training" (un seul système ML+RL)
"daily_local_running": _is_running("daily_local"), # [NEW]
"setup_running": _is_running("setup"), # [NEW] pipeline setup initial
"paused": (BASE_DIR / ".paused").exists(),
"stopped": (BASE_DIR / ".stopped").exists(),
}
@app.get("/api/trades", dependencies=[Depends(require_auth)])
def api_trades(limit: int = 200):
paper_mode = getattr(config, "PAPER_MODE", True)
if paper_mode:
state = read_paper_state()
trades = list(reversed(state.get("trades", [])))[:limit]
return {"trades": trades, "equity_curve": _equity_curve(state.get("trades", []))}
else:
reports = read_mt5_reports(limit=limit)
return {"trades": reports, "equity_curve": _equity_curve(reports)}
@app.get("/api/backtest/results", dependencies=[Depends(require_auth)])
def api_backtest_results():
return read_backtest_results()
@app.get("/api/data/status", dependencies=[Depends(require_auth)])
def api_data_status():
return read_data_status()
@app.get("/api/rl/status", dependencies=[Depends(require_auth)])
def api_rl_status():
return read_rl_status()
@app.get("/api/rl/progress", dependencies=[Depends(require_auth)])
def api_rl_progress():
try:
with open(BASE_DIR / "rl_progress.json") as f:
return json.load(f)
except Exception:
return {}
@app.get("/api/rl/curves", dependencies=[Depends(require_auth)])
def api_rl_curves():
path = BASE_DIR / "rl_training_curves.png"
if not path.exists():
raise HTTPException(status_code=404, detail="Courbes non disponibles — lancer l'entraînement RL d'abord")
return FileResponse(path, media_type="image/png", headers={"Cache-Control": "no-cache"})
def _start_unified_training() -> dict:
"""
Lance train_unified.py (ML complet + RL) en arrière-plan.
[FIX] Fonction extraite — AVANT, /api/train et /api/rl/finetune
dupliquaient exactement la même logique en copier-coller (risque de
divergence future). De plus, le bouton frontend ("🧠 Entraîner ML+RL")
n'appelait QUE /api/rl/finetune : /api/train n'était appelé par AUCUN
élément de l'UI — un endpoint mort, accessible seulement en curl direct.
Les deux routes appellent désormais ce même helper.
"""
if _is_running("train"):
return {"ok": False, "message": "Entraînement déjà en cours"}
steps = str(getattr(config, "RL_FINETUNE_STEPS", 200_000))
threading.Thread(
target=_launch,
args=("train", "train_unified.py", "Entraînement Unifié (ML+RL)"),
kwargs={"extra_args": ["--rl-steps", steps]},
daemon=True
).start()
return {"ok": True, "message": "Entraînement Unifié ML+RL démarré ▶ (un seul système, ne remplace l'ancien que s'il est meilleur)"}
@app.post("/api/rl/finetune", dependencies=[Depends(require_auth)])
def api_rl_finetune():
return _start_unified_training()
@app.get("/api/bot-state", dependencies=[Depends(require_auth)])
def api_bot_state():
return read_bot_state()
@app.get("/api/feature-importance", dependencies=[Depends(require_auth)])
def api_feature_importance():
return read_feature_importance()
# ── Bot controls ──────────────────────────────────────────────────────────────
@app.post("/api/bot/start", dependencies=[Depends(require_auth)])
def api_bot_start():
if _is_running("bot"):
return {"ok": False, "message": "Bot déjà en cours"}
(BASE_DIR / ".stopped").unlink(missing_ok=True)
(BASE_DIR / ".paused").unlink(missing_ok=True)
threading.Thread(target=_launch, args=("bot", "ahad_quant.py", "Bot principal"), daemon=True).start()
return {"ok": True, "message": "Bot démarré ✅"}
@app.post("/api/bot/stop", dependencies=[Depends(require_auth)])
def api_bot_stop():
(BASE_DIR / ".stopped").touch()
_kill("bot")
return {"ok": True, "message": "Bot arrêté"}
@app.post("/api/bot/pause", dependencies=[Depends(require_auth)])
def api_bot_pause():
(BASE_DIR / ".paused").touch()
_emit("⏸ Bot mis en pause", "UI")
return {"ok": True, "message": "Bot en pause ⏸"}
@app.post("/api/bot/resume", dependencies=[Depends(require_auth)])
def api_bot_resume():
(BASE_DIR / ".paused").unlink(missing_ok=True)
_emit("▶ Bot repris", "UI")
return {"ok": True, "message": "Bot repris ▶"}
@app.post("/api/bot/emergency", dependencies=[Depends(require_auth)])
def api_bot_emergency():
(BASE_DIR / ".stopped").touch()
_kill("bot")
_emit("⚠️ ARRÊT D'URGENCE — processus tué + flag .stopped posé", "UI")
return {"ok": True, "message": "Arrêt d'urgence déclenché ⚠️"}
@app.post("/api/bot/reset", dependencies=[Depends(require_auth)])
def api_bot_reset():
(BASE_DIR / ".stopped").unlink(missing_ok=True)
(BASE_DIR / ".paused").unlink(missing_ok=True)
_emit("↺ Flags réinitialisés", "UI")
return {"ok": True}
# ── Process controls ──────────────────────────────────────────────────────────
@app.post("/api/download", dependencies=[Depends(require_auth)])
def api_download():
if _is_running("download"):
return {"ok": False, "message": "Téléchargement déjà en cours"}
threading.Thread(target=_launch, args=("download", "download_data.py", "Téléchargement données"), daemon=True).start()
return {"ok": True, "message": "Téléchargement démarré ⬇"}
@app.post("/api/train", dependencies=[Depends(require_auth)])
def api_train():
# Alias historique — conservé pour compatibilité avec d'éventuels
# scripts externes, même si le bouton UI utilise /api/rl/finetune.
# Délègue désormais au même helper (voir _start_unified_training).
return _start_unified_training()
@app.post("/api/retrain/daily-local", dependencies=[Depends(require_auth)])
def api_retrain_daily_local():
"""
[NEW] Déclenche manuellement le cycle quotidien LOCAL (warm-start
LightGBM + RL fine-tune 100% réel) — exactement le même cycle que le
thread de fond AutoRetrainer exécute tout seul toutes les
DAILY_LOCAL_RETRAIN_INTERVAL_HOURS, mais à la demande, sans attendre
l'intervalle. Avant ce fix, ce mécanisme léger n'était accessible que
via le bot en tournant en continu — aucun moyen de le forcer depuis l'UI.
"""
if _is_running("train"):
return {"ok": False, "message": "Un entraînement complet (ML+RL) est déjà en cours — attends qu'il termine"}
if _is_running("daily_local"):
return {"ok": False, "message": "Cycle quotidien local déjà en cours"}
threading.Thread(
target=_launch,
args=("daily_local", "daily_local_retrain.py", "Cycle quotidien local (ML+RL léger)"),
daemon=True
).start()
return {"ok": True, "message": "Cycle quotidien local démarré ⚡ (warm-start ML + RL réel)"}
@app.post("/api/setup/run", dependencies=[Depends(require_auth)])
def api_setup_run():
"""
[NEW] Pipeline setup initial complet :
1. download_data.py — téléchargement données historiques (réseau)
2. train.py — entraînement ML complet (LGB + XGBoost + RF)
3. rl_train.py — entraînement PPO RL from scratch (1 000 000 steps)
4. export_unified.py — bundle ahad_quant_unified.zip
Chaque étape tourne dans le même slot "setup" — le terminal affiche
la progression en temps réel. À n'exécuter qu'une seule fois.
"""
if _is_running("setup"):
return {"ok": False, "message": "Setup initial déjà en cours"}
if _is_running("train"):
return {"ok": False, "message": "Un entraînement est déjà en cours — attends qu'il termine"}
if _is_running("bot"):
return {"ok": False, "message": "Arrête le bot avant de lancer le setup initial"}
def _run_setup():
steps = [
("download_data.py", "Téléchargement données historiques"),
("train.py", "Entraînement ML (LGB + XGBoost + RF)"),
("rl_train.py", "Entraînement RL PPO (1 000 000 steps)"),
("export_unified.py","Export bundle ahad_quant_unified.zip"),
]
_emit("═" * 60, "SETUP")
_emit(" AHAD QUANT — Setup Initial (4 étapes)", "SETUP")
_emit("═" * 60, "SETUP")
for i, (script, label) in enumerate(steps, 1):
_emit(f"\n[{i}/4] {label}...", "SETUP")
try:
cmd = [sys.executable, "-u", str(BASE_DIR / script)]
p = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, bufsize=1, cwd=str(BASE_DIR),
encoding="utf-8", errors="replace"
)
with _proc_lock:
_procs["setup"] = p
for line in iter(p.stdout.readline, ''):
_emit(line.rstrip(), "SETUP")
p.wait()
if p.returncode != 0:
_emit(f"❌ Étape {i} échouée (code {p.returncode}) — setup interrompu", "SETUP")
return
_emit(f"✅ Étape {i}/4 terminée : {label}", "SETUP")
except Exception as e:
_emit(f"❌ Erreur étape {i} : {e}", "SETUP")
return
_emit("\n" + "═" * 60, "SETUP")
_emit(" ✅ Setup initial terminé — AHAD QUANT prêt à trader !", "SETUP")
_emit("═" * 60, "SETUP")
with _proc_lock:
_procs["setup"] = None
with _proc_lock:
_pending.add("setup")
threading.Thread(target=_run_setup, daemon=True).start()
with _proc_lock:
_pending.discard("setup")
return {"ok": True, "message": "Setup initial démarré ▶ — 4 étapes en cours (voir Terminal)"}
@app.post("/api/setup/stop", dependencies=[Depends(require_auth)])
def api_setup_stop():
"""Interrompt le setup initial en cours."""
if not _is_running("setup"):
return {"ok": False, "message": "Aucun setup en cours"}
_kill("setup")
return {"ok": True, "message": "Setup interrompu"}
@app.post("/api/backtest/run", dependencies=[Depends(require_auth)])
def api_backtest_run():
if _is_running("backtest"):
return {"ok": False, "message": "Backtest déjà en cours"}
threading.Thread(target=_launch, args=("backtest", "backtest.py", "Backtest"), daemon=True).start()
return {"ok": True, "message": "Backtest démarré — voir Terminal"}
@app.post("/api/stop/{proc}", dependencies=[Depends(require_auth)])
def api_stop_proc(proc: str):
if proc not in _procs:
return {"ok": False, "message": f"Processus inconnu: {proc}"}
_kill(proc)
return {"ok": True, "message": f"{proc} arrêté"}
# ── Logs stream ───────────────────────────────────────────────────────────────
@app.get("/api/logs/stream", dependencies=[Depends(require_auth)])
def api_logs_stream():
def gen() -> Generator:
with _log_lock:
hist = list(_log_history)
for entry in hist:
yield f"data: {json.dumps(entry)}\n\n"
while True:
try:
entry = _log_queue.get(timeout=30)
yield f"data: {json.dumps(entry)}\n\n"
except queue.Empty:
yield f"data: {json.dumps({'ping': True})}\n\n"
return StreamingResponse(gen(), media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
# ── MT5 Bridge ────────────────────────────────────────────────────────────────
@app.get("/api/mt5/status", dependencies=[Depends(require_auth)])
def api_mt5_status():
status = read_mt5_status()
connected = _mt5_connected(status)
return {
"connected": connected,
"path_configured": bool(getattr(config, "MT5_FILES_PATH", "")),
"mt5_path": getattr(config, "MT5_FILES_PATH", ""),
"bridge": read_bridge_health(),
**status,
}
@app.get("/api/mt5/reports", dependencies=[Depends(require_auth)])
def api_mt5_reports(limit: int = 200):
reports = read_mt5_reports(limit)
open_trades = [r for r in reports if r.get("status") == "OPEN"]
closed_trades = [r for r in reports if r.get("status") == "CLOSED"]
errors = [r for r in reports if r.get("status") in ("ERROR", "REJECTED")]
total_profit = sum(float(r.get("profit") or 0) for r in closed_trades)
wins = sum(1 for r in closed_trades if float(r.get("profit") or 0) > 0)
return {
"reports": reports,
"open_count": len(open_trades),
"closed_count": len(closed_trades),
"error_count": len(errors),
"total_profit": round(total_profit, 2),
"win_count": wins,
"loss_count": len(closed_trades) - wins,
"win_rate": round(wins / len(closed_trades) * 100, 1) if closed_trades else 0.0,
}
@app.get("/api/mt5/stream", dependencies=[Depends(require_auth)])
def api_mt5_stream():
def gen() -> Generator:
last_hash = None
while True:
try:
status = read_mt5_status()
reports = read_mt5_reports(200)
connected = _mt5_connected(status)
cur_hash = f"{status.get('timestamp','')}{len(reports)}"
if cur_hash != last_hash:
last_hash = cur_hash
closed = [r for r in reports if r.get("status") == "CLOSED"]
wins = sum(1 for r in closed if float(r.get("profit") or 0) > 0)
payload = {
"type": "mt5_update", "connected": connected, "status": status,
"open_count": len([r for r in reports if r.get("status") == "OPEN"]),
"closed_count": len(closed),
"total_profit": round(sum(float(r.get("profit") or 0) for r in closed), 2),
"win_rate": round(wins / len(closed) * 100, 1) if closed else 0.0,
"latest_reports": reports[:10],
}
yield f"data: {json.dumps(payload)}\n\n"
else:
yield f"data: {json.dumps({'ping': True})}\n\n"
except GeneratorExit:
break
except Exception:
yield f"data: {json.dumps({'ping': True})}\n\n"
time.sleep(5)
return StreamingResponse(gen(), media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
# ── Config / Paper / Export ───────────────────────────────────────────────────
@app.get("/api/config", dependencies=[Depends(require_auth)])
def api_get_config():
env = read_env()
secret_patterns = ["secret","key","password","token","passphrase","private"]
result = {k: v for k, v in env.items() if not any(p in k.lower() for p in secret_patterns)}
_defaults = {
"PAIRS": ",".join(getattr(config, "PAIRS", [])),
"LEVERAGE": str(getattr(config, "LEVERAGE", 30)),
"PAPER_MODE": str(getattr(config, "PAPER_MODE", True)).lower(),
"PAPER_INITIAL_BALANCE": str(getattr(config, "PAPER_INITIAL_BALANCE", 100)),
"MIN_CONFIDENCE": str(getattr(config, "MIN_CONFIDENCE", 0.72)),
"MAX_POSITIONS": str(getattr(config, "MAX_POSITIONS", 5)),
"RISK_PER_TRADE": str(getattr(config, "RISK_PER_TRADE", 0.02)),
"MAX_DAILY_LOSS_PCT": str(getattr(config, "MAX_DAILY_LOSS_PCT", 0.05)),
"STOP_LOSS_PCT": str(getattr(config, "STOP_LOSS_PCT", 0.01)),
"TAKE_PROFIT_PCT": str(getattr(config, "TAKE_PROFIT_PCT", 0.02)),
"MAIN_LOOP_SECONDS": str(getattr(config, "MAIN_LOOP_SECONDS", 60)),
"CANDLE_INTERVAL": getattr(config, "CANDLE_INTERVAL", "1h"),
"MIN_LOT_SIZE": str(getattr(config, "MIN_LOT_SIZE", 0.01)),
"MAX_LOT_SIZE": str(getattr(config, "MAX_LOT_SIZE", 1.0)),
"USE_ENSEMBLE": str(getattr(config, "USE_ENSEMBLE", True)).lower(),
"USE_RL_AGENT": str(getattr(config, "USE_RL_AGENT", True)).lower(),
"RL_MODE": getattr(config, "RL_MODE", "filter"),
"RL_OVERRIDE_THRESHOLD": str(getattr(config, "RL_OVERRIDE_THRESHOLD", 0.82)),
"RL_CONFIDENCE_BOOST": str(getattr(config, "RL_CONFIDENCE_BOOST", 0.05)),
"RL_FINETUNE_STEPS": str(getattr(config, "RL_FINETUNE_STEPS", 200000)),
"DAILY_LOCAL_RETRAIN_ENABLED": str(getattr(config, "DAILY_LOCAL_RETRAIN_ENABLED", True)).lower(),
"DAILY_LOCAL_RETRAIN_INTERVAL_HOURS": str(getattr(config, "DAILY_LOCAL_RETRAIN_INTERVAL_HOURS", 24)),
"CONTINUOUS_LEARNING_ENABLED": str(getattr(config, "CONTINUOUS_LEARNING_ENABLED", True)).lower(),
"WARMSTART_ENABLED": str(getattr(config, "WARMSTART_ENABLED", True)).lower(),
"WARMSTART_MIN_TRADES": str(getattr(config, "WARMSTART_MIN_TRADES", 5)),
"RL_REPLAY_MIN_TRADES": str(getattr(config, "RL_REPLAY_MIN_TRADES", 10)),
"RL_RETRAIN_INTERVAL_HOURS": str(getattr(config, "RL_RETRAIN_INTERVAL_HOURS", 24)),
"AUTO_RETRAIN_INTERVAL_HOURS": str(getattr(config, "AUTO_RETRAIN_INTERVAL_HOURS", 24)),
"AUTO_RETRAIN_MIN_ACCURACY": str(getattr(config, "AUTO_RETRAIN_MIN_ACCURACY", 0.70)),
"EXPERIENCE_BUFFER_MAX_SIZE": str(getattr(config, "EXPERIENCE_BUFFER_MAX_SIZE", 10000)),
"RL_AUTO_RETRAIN_ENABLED": str(getattr(config, "RL_AUTO_RETRAIN_ENABLED", True)).lower(),
"RL_REAL_REPLAY_ENABLED": str(getattr(config, "RL_REAL_REPLAY_ENABLED", True)).lower(),
"MT5_BRIDGE_ENABLED": str(getattr(config, "MT5_BRIDGE_ENABLED", False)).lower(),
"MT5_FILES_PATH": getattr(config, "MT5_FILES_PATH", ""),
"MT5_SYMBOL_SUFFIX": getattr(config, "MT5_SYMBOL_SUFFIX", ""),
"MT5_SERVER": getattr(config, "MT5_SERVER", ""),
"MT5_SIGNAL_TIMEOUT": str(getattr(config, "MT5_SIGNAL_TIMEOUT", 30)),
"MT5_POLL_INTERVAL": str(getattr(config, "MT5_POLL_INTERVAL", 5)),
"SESSION_FILTER_ENABLED": str(getattr(config, "SESSION_FILTER_ENABLED", False)).lower(),
}
for k, v in _defaults.items():
result.setdefault(k, v)
return result
@app.post("/api/config", dependencies=[Depends(require_auth)])
async def api_save_config(request: Request):
body = await request.json()
filtered = {k: str(v) for k, v in body.items() if k in SAFE_CONFIG_KEYS}
write_env(filtered)
_emit(f"✅ Config sauvegardée : {list(filtered.keys())}", "UI")
return {"ok": True, "saved": list(filtered.keys()), "restart_needed": _is_running("bot")}
@app.post("/api/paper/reset", dependencies=[Depends(require_auth)])
def api_paper_reset():
init_bal = getattr(config, "PAPER_INITIAL_BALANCE", 100)
fresh = {
"balance": init_bal,
"positions": {},
"trades": [],
"daily_pnl": 0,
"total_pnl": 0,
"peak_equity": init_bal,
"daily_losses": 0,
"circuit_breaker_until": 0,
"reset_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
with open(BASE_DIR / "paper_state.json", "w") as f:
json.dump(fresh, f, indent=2)
_emit(f"↺ Paper trading réinitialisé — balance: ${init_bal:,.0f}", "UI")
return {"ok": True, "balance": init_bal}
# ── RESET GLOBAL ──────────────────────────────────────────────────────────────
@app.post("/api/reset/all", dependencies=[Depends(require_auth)])
def api_reset_all():
"""Réinitialise TOUT : logs mémoire, paper state, backtest, rl_progress,
last_retrain, bot_state. Laisse les modèles et checkpoints intacts."""
global _log_history
errors = []
# 1. Vider l'historique de logs en mémoire
with _log_lock:
_log_history.clear()
# 2. Paper state → balance initiale
init_bal = getattr(config, "PAPER_INITIAL_BALANCE", 100)
fresh_paper = {
"balance": init_bal,
"positions": {},
"trades": [],
"daily_pnl": 0,
"total_pnl": 0,
"peak_equity": init_bal,
"daily_losses": 0,
"circuit_breaker_until": 0,
"reset_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
try:
with open(BASE_DIR / "paper_state.json", "w") as f:
json.dump(fresh_paper, f, indent=2)
except Exception as e:
errors.append(f"paper_state: {e}")
# 3. Backtest results → vide
try:
(BASE_DIR / "backtest_results.json").write_text("{}")
except Exception as e:
errors.append(f"backtest_results: {e}")
# 4. RL progress → vide
try:
(BASE_DIR / "rl_progress.json").write_text("{}")
except Exception as e:
errors.append(f"rl_progress: {e}")
# 5. Last retrain → vide
try:
lr = BASE_DIR / "last_retrain.json"
if lr.exists():
lr.write_text("{}")
except Exception as e:
errors.append(f"last_retrain: {e}")
# 6. Bot state → vide
try:
(BASE_DIR / "bot_state.json").write_text("{}")
except Exception as e:
errors.append(f"bot_state: {e}")
# 7. Flags .stopped / .paused
(BASE_DIR / ".stopped").unlink(missing_ok=True)
(BASE_DIR / ".paused").unlink(missing_ok=True)
if errors:
_emit(f"⚠️ Réinitialisation partielle — erreurs : {', '.join(errors)}", "UI")
return {"ok": False, "errors": errors}
global _reset_version
_reset_version += 1
# Émettre un signal spécial que les clients SSE peuvent détecter
special = {"t": time.strftime("%H:%M:%S"), "p": "UI", "m": "🗑 Réinitialisation complète effectuée (logs, trades, backtest, RL, bot state)", "reset": _reset_version}
try: _log_queue.put_nowait(special)
except queue.Full: pass
return {"ok": True, "balance": init_bal, "reset_version": _reset_version}
@app.get("/api/export/{filename}", dependencies=[Depends(require_auth)])
def api_export_json(filename: str):
ALLOWED = {"paper_state.json", "backtest_results.json", "last_retrain.json", "rl_progress.json"}
if filename not in ALLOWED:
raise HTTPException(status_code=403, detail="Fichier non autorisé")
path = BASE_DIR / filename
if not path.exists():
raise HTTPException(status_code=404, detail="Fichier introuvable")
return FileResponse(path, media_type="application/json", filename=filename)
# ═══════════════════════════════════════════════════════════════════════════════
# HTML
# ═══════════════════════════════════════════════════════════════════════════════
HTML = r"""
AHAD QUANT V4_RL — Command Center
α
DEEPALPHA
V4 · ML + RL · Forex
PAPER
OFFLINE
RL ACTIF
--:--:--
🗑 Reset tout
Dashboard Live
▶ Démarrer
⏸ Pause
▶ Reprendre
⏹ Stop
⚠️ Urgence
↺
PnL Journalier
—
vs. ouverture
Données yfinance
—
—
⬇ Sync
Confiance Min
72%
ML threshold
Seuil RL Override
82%
RL mode: filter
Paires actives
25
Forex 1H
RL Monitor PPO
🧠 Full Retrain (ML+RL)
⚡ Cycle local (léger)
↺
rl_scaler.pkl
—
Normalisation obs
best_model.zip
—
Checkpoint RL
Algorithme PPO (Stable-Baselines3)
Steps entraîné —
Device —
N_ENVS 4 (parallèle)
TRAIN_PAIRS 8 paires majeures
EVAL_PAIRS GBPJPY, NZDUSD
Mode filter
Override Threshold 82%
Confidence Boost +5%
Steps Full Retrain (RL) —
Pas encore de backtest — onglet Backtest
Courbes d'Entraînement RL
✅ Disponible
Courbes non générées — lancer l'entraînement RL complet
Cycle quotidien local ML+RL réel
Dernier cycle local
—
Warm-start ML + RL réel
Intervalle
—
Automatique si bot lancé
Dernier fine-tune RL
—
Réel vs simulé
Lancer un backtest pour voir l'état du système
Pas de résultats — lancer un backtest
Journal des Trades
Tous
Long
Short
Tous résultats
Gagnants
Perdants
Paire Dir. Entrée Sortie PnL
Confiance ML RL Filtre Ouvert Fermé
Aucun trade enregistré
🚀 Setup Initial
⚠️
À exécuter une seule fois avant le premier démarrage du bot.
Le pipeline télécharge les données, entraîne ML + RL, puis génère le bundle final.
Durée estimée : 2–6h selon ta machine.
Pipeline — 4 étapes séquentielles
1
Téléchargement données
download_data.py — 25 paires × 1000 jours (internet requis)
EN ATTENTE
2
Entraînement ML
train.py — LightGBM + XGBoost + RandomForest ensemble
EN ATTENTE
3
Entraînement RL
rl_train.py — PPO 1 000 000 steps from scratch (~2–4h GPU)
EN ATTENTE
4
Export bundle
export_unified.py — génère ahad_quant_unified.zip
EN ATTENTE
🚀 Lancer le Setup Initial
⛔ Interrompre
↺
État des fichiers
model_ensemble.pkl —
rl_agent.zip —
ahad_quant_unified.zip —
Fichiers données /data —
Positions Ouvertes
0
PnL jour: —
Chemin configuré —
Signaux envoyés 0
Rapports reçus 0
Signaux en attente 0
Dernier signal ID —
Dernier rapport ID —
Gap sig→rep —
MT5 Path —
Signal ID Paire Dir. Entrée
Profit Statut Raison Temps
Aucun rapport MT5 — bridge non connecté
Feature Importance LightGBM
↺
🔬
62 features techniques (RSI, MACD, Bollinger, ATR, etc.) — top 15 ci-dessous. Le PPO utilise ces mêmes features dans _get_obs() via le cache ML.
Feature importance non disponible — lancer un Full Retrain (bouton "🧠 Full Retrain (ML+RL)") d'abord
ℹ️
Les logs affichent tous les processus en temps réel. Couleurs : BOT · TRAIN · DOWNLOAD · FINETUNE · BACKTEST
⛔ Stop Train
⛔ Stop DL
⛔ Stop BT
⛔ Stop RL
Configuration .env
💾 Sauvegarder
↺ Recharger
⚠️ Redémarrage du bot nécessaire pour appliquer les changements.
"""
# ═══════════════════════════════════════════════════════════════════════════════
@app.get("/", response_class=HTMLResponse)
def root():
return HTML
if __name__ == "__main__":
_emit("🚀 AHAD QUANT V4_RL — Web Command Center v4 démarré", "UI")
print("\n" + "=" * 58)
print(" AHAD QUANT Forex V4_RL — Command Center")
print("=" * 58)
print(" → http://localhost:8080")
print(" ML + RL (PPO) · 25 paires · 1H · Paper Mode")
print(" Ctrl+C pour arrêter")
print("=" * 58 + "\n")
uvicorn.run(app, host="0.0.0.0", port=8080, log_level="warning")