Files
2026-06-25 14:00:20 +03:00

2824 lines
135 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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"""<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>AHAD QUANT V4_RL — Command Center</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Oxanium:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<style>
:root{
--bg:#03030F;--bg2:#06061A;--bg3:#0A0A22;
--card:rgba(255,255,255,.03);--cb:rgba(255,255,255,.07);
--blue:#2979FF;--bg-blue:rgba(41,121,255,.25);
--green:#00E676;--bg-green:rgba(0,230,118,.2);
--red:#FF1744;--bg-red:rgba(255,23,68,.2);
--amber:#FFAB00;--bg-amber:rgba(255,171,0,.2);
--cyan:#00E5FF;--purple:#7C4DFF;--teal:#1DE9B6;
--text:#E8EAF6;--text2:#90A4AE;--text3:#546E7A;
--fd:'Oxanium',sans-serif;--fm:'JetBrains Mono',monospace;
--r:12px;--rs:8px;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--fd);overflow-x:hidden}
body::before{content:'';position:fixed;inset:0;z-index:0;
background-image:linear-gradient(rgba(41,121,255,.03) 1px,transparent 1px),linear-gradient(90deg,rgba(41,121,255,.03) 1px,transparent 1px);
background-size:40px 40px;pointer-events:none}
.app{position:relative;z-index:1;min-height:100vh;display:flex;flex-direction:column}
/* ── Header ── */
header{display:flex;align-items:center;justify-content:space-between;padding:12px 24px;
background:rgba(3,3,15,.92);border-bottom:1px solid var(--cb);backdrop-filter:blur(20px);
position:sticky;top:0;z-index:100}
.logo{display:flex;align-items:center;gap:10px}
.logo-mark{width:32px;height:32px;border-radius:7px;background:linear-gradient(135deg,var(--blue),var(--purple));
display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800;color:#fff;
box-shadow:0 0 20px var(--bg-blue)}
.logo-text{font-size:16px;font-weight:700;letter-spacing:.08em}
.logo-sub{font-size:8px;color:var(--text3);letter-spacing:.25em;text-transform:uppercase}
.hrow{display:flex;align-items:center;gap:12px}
.pill{display:flex;align-items:center;gap:5px;padding:3px 9px;border-radius:100px;
font-size:9px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;border:1px solid}
.pill-live{background:rgba(0,230,118,.1);border-color:rgba(0,230,118,.3);color:var(--green)}
.pill-paper{background:rgba(255,171,0,.1);border-color:rgba(255,171,0,.3);color:var(--amber)}
.pill-running{background:rgba(41,121,255,.1);border-color:rgba(41,121,255,.3);color:var(--blue)}
.pill-stopped{background:rgba(255,23,68,.1);border-color:rgba(255,23,68,.3);color:var(--red)}
.dot{width:5px;height:5px;border-radius:50%;animation:pulse 2s infinite}
.dot-g{background:var(--green);box-shadow:0 0 5px var(--green)}
.dot-a{background:var(--amber)}.dot-r{background:var(--red)}.dot-b{background:var(--blue)}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
#clock{font-family:var(--fm);font-size:11px;color:var(--text3)}
/* ── Layout ── */
.main{display:flex;flex:1;gap:0}
.sidebar{width:210px;min-width:210px;background:rgba(3,3,15,.7);
border-right:1px solid var(--cb);padding:14px 0;display:flex;flex-direction:column;gap:1px}
.ns{padding:6px 16px 2px;font-size:9px;font-weight:700;letter-spacing:.2em;
text-transform:uppercase;color:var(--text3);margin-top:8px}
.ni{display:flex;align-items:center;gap:9px;padding:9px 16px;cursor:pointer;
font-size:12px;font-weight:500;color:var(--text2);border-left:2px solid transparent;transition:all .15s}
.ni:hover{color:var(--text);background:rgba(255,255,255,.03)}
.ni.active{color:var(--blue);border-left-color:var(--blue);background:rgba(41,121,255,.07)}
.ni-ico{font-size:13px;width:16px;text-align:center}
.badge{margin-left:auto;font-size:9px;font-family:var(--fm);padding:1px 5px;border-radius:3px;font-weight:600}
.bdg-g{background:rgba(0,230,118,.15);color:var(--green)}
.bdg-b{background:rgba(41,121,255,.15);color:var(--blue)}
.bdg-r{background:rgba(255,23,68,.15);color:var(--red)}
.bdg-a{background:rgba(255,171,0,.15);color:var(--amber)}
.bdg-p{background:rgba(124,77,255,.15);color:var(--purple)}
/* ── Content ── */
.content{flex:1;overflow-y:auto;padding:22px}
.page{display:none}.page.active{display:block}
.grid{display:grid;gap:14px}
.g2{grid-template-columns:repeat(2,1fr)}
.g3{grid-template-columns:repeat(3,1fr)}
.g4{grid-template-columns:repeat(4,1fr)}
/* ── Cards ── */
.card{background:var(--card);border:1px solid var(--cb);border-radius:var(--r);
padding:18px;position:relative;overflow:hidden;transition:border-color .2s}
.card:hover{border-color:rgba(255,255,255,.1)}
.card::before{content:'';position:absolute;inset:0;
background:linear-gradient(135deg,rgba(255,255,255,.012) 0%,transparent 60%);pointer-events:none}
.card-b{border-color:rgba(41,121,255,.2)}
.card-b::after{content:'';position:absolute;top:0;left:0;right:0;height:1px;
background:linear-gradient(90deg,transparent,var(--blue),transparent)}
.card-g{border-color:rgba(0,230,118,.2)}
.card-g::after{content:'';position:absolute;top:0;left:0;right:0;height:1px;
background:linear-gradient(90deg,transparent,var(--green),transparent)}
.card-r{border-color:rgba(255,23,68,.2)}
.card-r::after{content:'';position:absolute;top:0;left:0;right:0;height:1px;
background:linear-gradient(90deg,transparent,var(--red),transparent)}
.card-a{border-color:rgba(255,171,0,.2)}
.card-a::after{content:'';position:absolute;top:0;left:0;right:0;height:1px;
background:linear-gradient(90deg,transparent,var(--amber),transparent)}
.card-p{border-color:rgba(124,77,255,.2)}
.card-p::after{content:'';position:absolute;top:0;left:0;right:0;height:1px;
background:linear-gradient(90deg,transparent,var(--purple),transparent)}
.lbl{font-size:9px;font-weight:700;letter-spacing:.15em;text-transform:uppercase;color:var(--text3);margin-bottom:7px}
.val{font-family:var(--fm);font-size:26px;font-weight:700;line-height:1}
.val.big{font-size:30px}.val.pos{color:var(--green)}.val.neg{color:var(--red)}
.sub{font-size:10px;color:var(--text3);margin-top:5px;font-family:var(--fm)}
.sh{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
.st{font-size:13px;font-weight:700;letter-spacing:.05em}
.st span{color:var(--blue)}
/* ── Buttons ── */
.btn{display:inline-flex;align-items:center;gap:5px;padding:7px 14px;border-radius:var(--rs);
font-size:11px;font-weight:600;letter-spacing:.04em;border:1px solid;cursor:pointer;
font-family:var(--fd);transition:all .15s;white-space:nowrap}
.btn-p{background:var(--blue);border-color:var(--blue);color:#fff}
.btn-p:hover{background:#1565C0;box-shadow:0 0 18px var(--bg-blue)}
.btn-g{background:transparent;border-color:var(--cb);color:var(--text2)}
.btn-g:hover{border-color:rgba(255,255,255,.2);color:var(--text);background:rgba(255,255,255,.04)}
.btn-d{background:rgba(255,23,68,.1);border-color:rgba(255,23,68,.4);color:var(--red)}
.btn-d:hover{background:rgba(255,23,68,.2);box-shadow:0 0 12px var(--bg-red)}
.btn-s{background:rgba(0,230,118,.1);border-color:rgba(0,230,118,.4);color:var(--green)}
.btn-s:hover{background:rgba(0,230,118,.2)}
.btn-a{background:rgba(255,171,0,.1);border-color:rgba(255,171,0,.4);color:var(--amber)}
.btn-a:hover{background:rgba(255,171,0,.2)}
.btn-v{background:rgba(124,77,255,.1);border-color:rgba(124,77,255,.4);color:var(--purple)}
.btn-v:hover{background:rgba(124,77,255,.2)}
.btn-sm{padding:4px 9px;font-size:10px}
.btn:disabled{opacity:.35;cursor:not-allowed}
.btn-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
/* ── Tables ── */
table{width:100%;border-collapse:collapse;font-size:11px}
thead th{padding:8px 10px;text-align:left;font-size:8px;font-weight:700;
letter-spacing:.15em;text-transform:uppercase;color:var(--text3);border-bottom:1px solid var(--cb)}
tbody tr{border-bottom:1px solid rgba(255,255,255,.03);transition:background .1s}
tbody tr:hover{background:rgba(255,255,255,.03)}
tbody td{padding:8px 10px;color:var(--text2);font-family:var(--fm);font-size:10px}
.tc{font-weight:700;color:var(--text);font-family:var(--fd)}
.tp{padding:1px 6px;border-radius:3px;font-size:9px;font-weight:700}
.tp-l{background:rgba(0,230,118,.12);color:var(--green)}
.tp-s{background:rgba(255,23,68,.12);color:var(--red)}
/* ── Toggles ── */
.toggle{position:relative;width:38px;height:20px;cursor:pointer;flex-shrink:0}
.toggle input{opacity:0;width:0;height:0}
.toggle-t{position:absolute;inset:0;background:rgba(255,255,255,.08);
border-radius:10px;transition:.2s;border:1px solid var(--cb)}
.toggle input:checked+.toggle-t{background:var(--blue);border-color:var(--blue);box-shadow:0 0 8px var(--bg-blue)}
.toggle-th{position:absolute;top:3px;left:3px;width:12px;height:12px;background:#fff;border-radius:50%;transition:.2s}
.toggle input:checked~.toggle-th{transform:translateX(18px)}
.trow{display:flex;align-items:center;justify-content:space-between;
padding:10px 0;border-bottom:1px solid rgba(255,255,255,.04)}
.trow:last-child{border-bottom:none}
.tl{font-size:12px;font-weight:500;color:var(--text)}
.ts{font-size:10px;color:var(--text3);margin-top:1px}
/* ── Terminal ── */
.terminal{background:#020209;border:1px solid var(--cb);border-radius:var(--r);
padding:14px;height:440px;overflow-y:auto;font-family:var(--fm);font-size:10.5px;
line-height:1.75;color:#64FFDA}
.terminal::-webkit-scrollbar{width:3px}
.terminal::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1);border-radius:2px}
@keyframes fi{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
.log-line{animation:fi .2s}
.log-err{color:#FF5252}.log-ok{color:var(--green)}.log-warn{color:var(--amber)}
.log-bot{color:#64FFDA}.log-train{color:var(--cyan)}.log-download{color:var(--purple)}
.log-backtest{color:#FF6D00}.log-finetune{color:var(--teal)}.log-ui{color:var(--text3)}
/* ── Charts ── */
.cw{position:relative;height:220px;margin-top:10px}
.cw-sm{height:160px}
.cw-lg{height:300px}
/* ── Misc ── */
.sp{width:14px;height:14px;border:2px solid rgba(41,121,255,.3);
border-top-color:var(--blue);border-radius:50%;animation:spin .8s linear infinite;flex-shrink:0}
@keyframes spin{to{transform:rotate(360deg)}}
.banner{background:linear-gradient(90deg,rgba(41,121,255,.08),rgba(124,77,255,.08));
border:1px solid rgba(41,121,255,.25);border-radius:var(--rs);padding:10px 14px;
display:flex;align-items:center;gap:10px;margin-bottom:14px;font-size:11px}
.banner-s{background:rgba(0,230,118,.06);border-color:rgba(0,230,118,.2)}
.banner-a{background:rgba(255,171,0,.06);border-color:rgba(255,171,0,.2)}
.banner-r{background:rgba(255,23,68,.06);border-color:rgba(255,23,68,.2)}
.field{margin-bottom:12px}
.field label{display:block;font-size:9px;font-weight:700;letter-spacing:.1em;
text-transform:uppercase;color:var(--text3);margin-bottom:5px}
.field input,.field select{width:100%;background:rgba(255,255,255,.04);
border:1px solid var(--cb);border-radius:var(--rs);padding:8px 10px;
color:var(--text);font-family:var(--fd);font-size:11px;outline:none;transition:border .2s}
.field input:focus,.field select:focus{border-color:rgba(41,121,255,.5);background:rgba(41,121,255,.04)}
.abar{height:7px;background:rgba(255,255,255,.06);border-radius:3px;overflow:hidden;margin-top:8px}
.afill{height:100%;border-radius:3px;background:linear-gradient(90deg,var(--blue),var(--cyan));
transition:width .8s cubic-bezier(.4,0,.2,1);box-shadow:0 0 7px var(--bg-blue)}
.afill-g{background:linear-gradient(90deg,var(--green),var(--teal))}
.afill-p{background:linear-gradient(90deg,var(--purple),var(--cyan))}
.status-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px}
.sd-g{background:var(--green);box-shadow:0 0 6px var(--green)}
.sd-r{background:var(--red)}.sd-a{background:var(--amber)}.sd-b{background:var(--blue)}
.metric-row{display:flex;align-items:center;justify-content:space-between;
padding:7px 0;border-bottom:1px solid rgba(255,255,255,.04)}
.metric-row:last-child{border-bottom:none}
.metric-label{font-size:11px;color:var(--text2)}
.metric-val{font-family:var(--fm);font-size:11px;font-weight:600;color:var(--text)}
.metric-val.pos{color:var(--green)}.metric-val.neg{color:var(--red)}.metric-val.hi{color:var(--cyan)}
.section-title{font-size:14px;font-weight:700;margin-bottom:16px;letter-spacing:.05em}
.section-title span{color:var(--blue)}
.comparison-table{width:100%;border-collapse:collapse}
.comparison-table th{padding:8px 12px;text-align:left;font-size:9px;font-weight:700;
letter-spacing:.15em;text-transform:uppercase;color:var(--text3);border-bottom:1px solid var(--cb)}
.comparison-table td{padding:10px 12px;font-family:var(--fm);font-size:12px;color:var(--text2)}
.comparison-table tr:hover td{background:rgba(255,255,255,.02)}
.comparison-table .metric{font-weight:600;color:var(--text);font-family:var(--fd)}
.comparison-table .better{color:var(--green);font-weight:700}
.comparison-table .same{color:var(--text3)}
.tag{display:inline-block;padding:2px 7px;border-radius:4px;font-size:9px;font-weight:700}
.tag-g{background:rgba(0,230,118,.12);color:var(--green)}
.tag-b{background:rgba(41,121,255,.12);color:var(--blue)}
.tag-a{background:rgba(255,171,0,.12);color:var(--amber)}
.tag-p{background:rgba(124,77,255,.12);color:var(--purple)}
img.rl-curves{width:100%;border-radius:8px;border:1px solid var(--cb);margin-top:10px}
.empty{text-align:center;padding:40px;color:var(--text3);font-size:12px}
</style>
</head>
<body>
<div class="app">
<!-- ══ HEADER ══════════════════════════════════════════════════════════════ -->
<header>
<div class="logo">
<div class="logo-mark">α</div>
<div>
<div class="logo-text">DEEP<span style="color:var(--blue)">ALPHA</span></div>
<div class="logo-sub">V4 · ML + RL · Forex</div>
</div>
</div>
<div class="hrow">
<div id="h-mode-pill" class="pill pill-paper"><span class="dot dot-a"></span>PAPER</div>
<div id="h-bot-pill" class="pill pill-stopped"><span class="dot dot-r"></span>OFFLINE</div>
<div id="h-rl-pill" class="pill pill-running" style="display:none"><span class="dot dot-b"></span>RL ACTIF</div>
<div id="clock" style="margin-left:6px">--:--:--</div>
<button id="btn-global-reset" class="btn btn-d btn-sm" style="margin-left:12px;font-size:11px;padding:4px 10px" onclick="globalReset()" title="Réinitialiser tous les résultats (logs, trades, backtest, RL progress)">🗑 Reset tout</button>
</div>
</header>
<div class="main">
<!-- ══ SIDEBAR ═════════════════════════════════════════════════════════════ -->
<nav class="sidebar">
<div class="ns">Trading</div>
<div class="ni active" onclick="nav('dashboard')">
<span class="ni-ico">📊</span> Dashboard
<span id="nb-pos" class="badge bdg-b" style="display:none">0</span>
</div>
<div class="ni" onclick="nav('trades')">
<span class="ni-ico">💹</span> Paper Trades
<span id="nb-tr" class="badge bdg-g">0</span>
</div>
<div class="ni" onclick="nav('backtest')">
<span class="ni-ico">📈</span> Backtest
</div>
<div class="ns">Intelligence</div>
<div class="ni" onclick="nav('rl')">
<span class="ni-ico">🧠</span> RL Monitor
<span id="nb-rl" class="badge bdg-p">PPO</span>
</div>
<div class="ni" onclick="nav('features')">
<span class="ni-ico">🔬</span> Features
<span class="badge bdg-b">62</span>
</div>
<div class="ns">Infra</div>
<div class="ni" onclick="nav('setup')">
<span class="ni-ico">🚀</span> Setup Initial
<span id="nb-setup" class="badge bdg-g" style="display:none">EN COURS</span>
</div>
<div class="ni" onclick="nav('mt5')">
<span class="ni-ico">🔌</span> MT5 Bridge
<span id="nb-mt5" class="badge bdg-r">OFF</span>
</div>
<div class="ni" onclick="nav('terminal')">
<span class="ni-ico">💻</span> Terminal
<span id="nb-log" class="badge bdg-b">0</span>
</div>
<div class="ni" onclick="nav('config')">
<span class="ni-ico">⚙️</span> Config
</div>
<div style="margin-top:auto;padding:16px;border-top:1px solid var(--cb);margin-top:20px">
<div style="font-size:9px;color:var(--text3);text-align:center">
25 PAIRES · 1H · LEVIER 30x
</div>
</div>
</nav>
<!-- ══ CONTENT ══════════════════════════════════════════════════════════════ -->
<div class="content">
<!-- ─────────────────────────────────────────────────────── DASHBOARD ── -->
<div id="page-dashboard" class="page active">
<div class="sh">
<div class="section-title">Dashboard <span>Live</span></div>
<div class="btn-row">
<button id="btn-start" class="btn btn-s btn-sm" onclick="botAction('start')">▶ Démarrer</button>
<button id="btn-pause" class="btn btn-a btn-sm" onclick="botAction('pause')" disabled>⏸ Pause</button>
<button id="btn-resume" class="btn btn-g btn-sm" onclick="botAction('resume')" style="display:none">▶ Reprendre</button>
<button id="btn-stop" class="btn btn-d btn-sm" onclick="botAction('stop')" disabled>⏹ Stop</button>
<button class="btn btn-d btn-sm" onclick="if(confirm('Arrêt d\'urgence ?')) botAction('emergency')">⚠️ Urgence</button>
<button class="btn btn-g btn-sm" onclick="loadStatus()">↺</button>
</div>
</div>
<!-- KPI Cards -->
<div class="grid g4" style="margin-bottom:14px">
<div class="card card-b">
<div class="lbl">Balance</div>
<div id="d-balance" class="val big">—</div>
<div id="d-balance-sub" class="sub">Paper Mode</div>
</div>
<div class="card">
<div class="lbl">PnL Journalier</div>
<div id="d-dpnl" class="val">—</div>
<div id="d-dpnl-sub" class="sub">vs. ouverture</div>
</div>
<div class="card">
<div class="lbl">PnL Total</div>
<div id="d-tpnl" class="val">—</div>
<div id="d-peak" class="sub">Peak: —</div>
</div>
<div class="card card-g">
<div class="lbl">Win Rate</div>
<div id="d-wr" class="val">—</div>
<div id="d-trades" class="sub">0 trades</div>
</div>
</div>
<div class="grid g4" style="margin-bottom:14px">
<!-- Model Status -->
<div class="card card-b">
<div class="lbl">Modèles ML</div>
<div id="d-model-name" style="font-size:11px;color:var(--text);font-weight:600;margin-bottom:8px">—</div>
<div id="d-model-info" class="sub">—</div>
<div id="d-model-accuracy" class="sub" style="margin-top:4px">—</div>
<div id="d-model-bar" class="abar" style="margin-top:10px"><div id="d-model-fill" class="afill" style="width:0%"></div></div>
</div>
<!-- RL Status -->
<div class="card card-p">
<div class="lbl">RL Agent (PPO)</div>
<div id="d-rl-status" style="font-size:11px;font-weight:600;margin-bottom:8px">—</div>
<div id="d-rl-info" class="sub">—</div>
<div id="d-rl-reward" class="sub" style="margin-top:4px">—</div>
</div>
<!-- Sync badge ML+RL -->
<div class="card">
<div class="lbl">Synchro ML ↔ RL</div>
<div id="d-sync-badge" style="font-size:11px;font-weight:600;margin-bottom:8px">—</div>
<div id="d-sync-info" class="sub">—</div>
</div>
<!-- Data Status -->
<div class="card">
<div class="lbl">Données yfinance</div>
<div id="d-data-count" class="val" style="font-size:22px">—</div>
<div id="d-data-info" class="sub">—</div>
<div class="btn-row" style="margin-top:10px">
<button class="btn btn-g btn-sm" onclick="apiPost('/api/download')" id="btn-dl">⬇ Sync</button>
</div>
</div>
</div>
<!-- Config params -->
<div class="grid g4" style="margin-bottom:14px">
<div class="card">
<div class="lbl">Levier</div>
<div id="d-leverage" class="val" style="font-size:20px">30x</div>
<div class="sub">Fixe</div>
</div>
<div class="card">
<div class="lbl">Confiance Min</div>
<div id="d-confidence" class="val" style="font-size:20px">72%</div>
<div class="sub">ML threshold</div>
</div>
<div class="card">
<div class="lbl">Seuil RL Override</div>
<div id="d-rl-threshold" class="val" style="font-size:20px">82%</div>
<div class="sub">RL mode: filter</div>
</div>
<div class="card">
<div class="lbl">Paires actives</div>
<div id="d-pairs" class="val" style="font-size:20px">25</div>
<div class="sub">Forex 1H</div>
</div>
</div>
<!-- Open Positions -->
<div class="card">
<div class="sh">
<div class="st">Positions Ouvertes</div>
<button class="btn btn-g btn-sm" onclick="loadStatus()">↺</button>
</div>
<div id="d-positions">
<div class="empty">Aucune position ouverte</div>
</div>
</div>
</div>
<!-- ─────────────────────────────────────────────────────── RL MONITOR ── -->
<div id="page-rl" class="page">
<div class="sh">
<div class="section-title">RL Monitor <span>PPO</span></div>
<div class="btn-row">
<button id="btn-fullretrain" class="btn btn-v" onclick="startFullRetrain()" title="Full Retrain complet — ré-entraîne l'ensemble ML (LightGBM+XGBoost+RF) ET le PPO RL depuis zéro sur toutes les données historiques. Peut durer plusieurs heures.">🧠 Full Retrain (ML+RL)</button>
<button id="btn-daily-local" class="btn btn-p" onclick="startDailyLocal()" title="Warm-start LightGBM + RL fine-tune 100% réel — le même cycle que celui qui tourne automatiquement toutes les 24h">⚡ Cycle local (léger)</button>
<button class="btn btn-g btn-sm" onclick="loadRL()">↺</button>
</div>
</div>
<!-- RL File Status -->
<div class="grid g4" style="margin-bottom:14px">
<div class="card">
<div class="lbl">rl_agent.zip</div>
<div id="rl-agent-status" class="val" style="font-size:16px">—</div>
<div id="rl-agent-info" class="sub">—</div>
</div>
<div class="card">
<div class="lbl">rl_scaler.pkl</div>
<div id="rl-scaler-status" class="val" style="font-size:16px">—</div>
<div class="sub">Normalisation obs</div>
</div>
<div class="card">
<div class="lbl">best_model.zip</div>
<div id="rl-ckpt-status" class="val" style="font-size:16px">—</div>
<div class="sub">Checkpoint RL</div>
</div>
<div class="card card-p">
<div class="lbl">Fine-tuning RL</div>
<div id="rl-ft-status" class="val" style="font-size:16px">—</div>
<div id="rl-ft-info" class="sub">—</div>
</div>
</div>
<!-- RL Config -->
<div class="grid g2" style="margin-bottom:14px">
<div class="card card-p">
<div class="sh"><div class="st">Configuration PPO</div></div>
<div class="metric-row"><span class="metric-label">Algorithme</span><span class="metric-val hi">PPO (Stable-Baselines3)</span></div>
<div class="metric-row"><span class="metric-label">Steps entraîné</span><span id="rl-cfg-steps" class="metric-val hi">—</span></div>
<div class="metric-row"><span class="metric-label">Device</span><span id="rl-cfg-device" class="metric-val hi">—</span></div>
<div class="metric-row"><span class="metric-label">N_ENVS</span><span class="metric-val">4 (parallèle)</span></div>
<div class="metric-row"><span class="metric-label">TRAIN_PAIRS</span><span class="metric-val">8 paires majeures</span></div>
<div class="metric-row"><span class="metric-label">EVAL_PAIRS</span><span class="metric-val">GBPJPY, NZDUSD</span></div>
<div class="metric-row"><span class="metric-label">Mode</span><span id="rl-cfg-mode" class="metric-val hi">filter</span></div>
<div class="metric-row"><span class="metric-label">Override Threshold</span><span id="rl-cfg-thr" class="metric-val">82%</span></div>
<div class="metric-row"><span class="metric-label">Confidence Boost</span><span id="rl-cfg-boost" class="metric-val pos">+5%</span></div>
<div class="metric-row"><span class="metric-label">Steps Full Retrain (RL)</span><span id="rl-cfg-ft" class="metric-val">—</span></div>
</div>
<div class="card">
<div class="sh"><div class="st">Résultats Backtest ML+RL</div></div>
<div id="rl-bt-results">
<div class="empty">Pas encore de backtest — onglet Backtest</div>
</div>
</div>
</div>
<!-- Training Curves -->
<div class="card">
<div class="sh">
<div class="st">Courbes d'Entraînement RL</div>
<span id="rl-curves-tag" class="tag tag-g" style="display:none">✅ Disponible</span>
</div>
<div id="rl-curves-empty" class="empty">
Courbes non générées — lancer l'entraînement RL complet
</div>
<img id="rl-curves-img" class="rl-curves" style="display:none" src="" alt="RL Training Curves">
</div>
<!-- Cycle quotidien local — [FIX] remplace l'ancien bloc "Planning
Fine-tuning" (texte statique "Semaine 3/4", "hebdomadaire") qui ne
correspondait plus au cycle quotidien réel actuel. -->
<div class="card card-p" style="margin-top:14px">
<div class="sh"><div class="st">Cycle quotidien local <span>ML+RL réel</span></div></div>
<div class="grid g3">
<div>
<div class="lbl">Dernier cycle local</div>
<div id="rl-daily-last" style="font-size:11px;color:var(--text)">—</div>
<div class="sub">Warm-start ML + RL réel</div>
</div>
<div>
<div class="lbl">Intervalle</div>
<div id="rl-daily-interval" style="font-size:11px;color:var(--text)">—</div>
<div class="sub">Automatique si bot lancé</div>
</div>
<div>
<div class="lbl">Dernier fine-tune RL</div>
<div id="rl-daily-source" style="font-size:11px;color:var(--text)">—</div>
<div class="sub">Réel vs simulé</div>
</div>
</div>
</div>
</div>
<!-- ─────────────────────────────────────────────────────── BACKTEST ── -->
<div id="page-backtest" class="page">
<div class="sh">
<div class="section-title">Backtest <span>OOS</span></div>
<div class="btn-row">
<button class="btn btn-p btn-sm" onclick="apiPost('/api/backtest/run')">▶ Lancer Backtest</button>
<button class="btn btn-g btn-sm" onclick="loadBacktest()">↺</button>
<a href="/api/export/backtest_results.json" class="btn btn-g btn-sm">⬇ JSON</a>
</div>
</div>
<!-- Badge système unifié — reflète l'état RÉEL du dernier backtest
(champ "rl_filter_active" écrit par backtest.py). Avant, cette zone
affichait un tableau de comparaison ML vs ML+RL avec des chiffres
figés d'une ancienne expérience — trompeur, et contraire à l'idée
d'un seul système : il n'y a plus qu'UN backtest, qui teste TOUJOURS
le système complet (ML+RL si l'agent RL est disponible). -->
<div class="card" style="margin-bottom:14px">
<div class="sh"><div class="st">Système testé</div></div>
<div id="bt-system-badge" class="empty">Lancer un backtest pour voir l'état du système</div>
</div>
<!-- Dynamic results from backtest_results.json -->
<div class="grid g2">
<div class="card card-b">
<div class="sh"><div class="st">Dernier Backtest <span id="bt-ts" style="font-size:10px;color:var(--text3)"></span></div></div>
<div id="bt-results">
<div class="empty">Pas de résultats — lancer un backtest</div>
</div>
</div>
<div class="card">
<div class="sh"><div class="st">Courbe Equity</div></div>
<div class="cw"><canvas id="btChart"></canvas></div>
</div>
</div>
</div>
<!-- ─────────────────────────────────────────────────────── TRADES ── -->
<div id="page-trades" class="page">
<div class="sh">
<div class="section-title">Paper Trades</div>
<div class="btn-row">
<button class="btn btn-d btn-sm" onclick="if(confirm('Réinitialiser le paper trading ?')) apiPost('/api/paper/reset').then(()=>loadTrades())">↺ Reset</button>
<a href="/api/export/paper_state.json" class="btn btn-g btn-sm">⬇ Export</a>
<button class="btn btn-g btn-sm" onclick="loadTrades()">↺</button>
</div>
</div>
<!-- Stats cards -->
<div class="grid g4" style="margin-bottom:14px">
<div class="card">
<div class="lbl">Trades Total</div>
<div id="tr-total" class="val" style="font-size:22px">0</div>
</div>
<div class="card card-g">
<div class="lbl">Win Rate</div>
<div id="tr-wr" class="val" style="font-size:22px">0%</div>
</div>
<div class="card">
<div class="lbl">PnL Total</div>
<div id="tr-pnl" class="val" style="font-size:22px">0</div>
</div>
<div class="card">
<div class="lbl">Trades Gagnants</div>
<div id="tr-wins" class="val" style="font-size:22px">0</div>
</div>
</div>
<!-- Equity Curve -->
<div class="card" style="margin-bottom:14px">
<div class="lbl">Courbe Equity (base 100)</div>
<div class="cw-lg"><canvas id="eqChart"></canvas></div>
</div>
<!-- Trade journal -->
<div class="card">
<div class="sh">
<div class="st">Journal des Trades</div>
<div style="display:flex;gap:8px">
<select id="tr-filter-side" onchange="renderTrades()" style="background:var(--bg3);border:1px solid var(--cb);color:var(--text);border-radius:var(--rs);padding:4px 8px;font-size:10px">
<option value="">Tous</option>
<option value="long">Long</option>
<option value="short">Short</option>
</select>
<select id="tr-filter-result" onchange="renderTrades()" style="background:var(--bg3);border:1px solid var(--cb);color:var(--text);border-radius:var(--rs);padding:4px 8px;font-size:10px">
<option value="">Tous résultats</option>
<option value="win">Gagnants</option>
<option value="loss">Perdants</option>
</select>
</div>
</div>
<div style="overflow-x:auto">
<table>
<thead><tr>
<th>Paire</th><th>Dir.</th><th>Entrée</th><th>Sortie</th><th>PnL</th>
<th>Confiance ML</th><th>RL Filtre</th><th>Ouvert</th><th>Fermé</th>
</tr></thead>
<tbody id="tr-body"></tbody>
</table>
</div>
<div id="tr-empty" class="empty" style="display:none">Aucun trade enregistré</div>
</div>
</div>
<!-- ─────────────────────────────────────────────────── SETUP INITIAL ── -->
<div id="page-setup" class="page">
<div class="section-title">🚀 Setup <span>Initial</span></div>
<div class="banner banner-a" style="margin-bottom:18px">
<span>⚠️</span>
<span>À exécuter <strong>une seule fois</strong> 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 : <strong>26h</strong> selon ta machine.</span>
</div>
<div class="card" style="margin-bottom:14px">
<div style="font-size:11px;font-weight:700;margin-bottom:14px;color:var(--text3);letter-spacing:.08em;text-transform:uppercase">Pipeline — 4 étapes séquentielles</div>
<div style="display:flex;flex-direction:column;gap:10px">
<div style="display:flex;align-items:center;gap:10px;padding:10px;background:rgba(255,255,255,.03);border-radius:8px">
<div style="width:28px;height:28px;border-radius:50%;background:rgba(41,121,255,.15);border:1px solid rgba(41,121,255,.3);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--blue);flex-shrink:0">1</div>
<div style="flex:1"><div style="font-size:12px;font-weight:700">Téléchargement données</div><div style="font-size:10px;color:var(--text3)">download_data.py — 25 paires × 1000 jours (internet requis)</div></div>
<span id="step-1-status" class="tag" style="background:rgba(255,255,255,.06);color:var(--text3);font-size:9px;padding:3px 8px;border-radius:4px">EN ATTENTE</span>
</div>
<div style="display:flex;align-items:center;gap:10px;padding:10px;background:rgba(255,255,255,.03);border-radius:8px">
<div style="width:28px;height:28px;border-radius:50%;background:rgba(41,121,255,.15);border:1px solid rgba(41,121,255,.3);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--blue);flex-shrink:0">2</div>
<div style="flex:1"><div style="font-size:12px;font-weight:700">Entraînement ML</div><div style="font-size:10px;color:var(--text3)">train.py — LightGBM + XGBoost + RandomForest ensemble</div></div>
<span id="step-2-status" class="tag" style="background:rgba(255,255,255,.06);color:var(--text3);font-size:9px;padding:3px 8px;border-radius:4px">EN ATTENTE</span>
</div>
<div style="display:flex;align-items:center;gap:10px;padding:10px;background:rgba(255,255,255,.03);border-radius:8px">
<div style="width:28px;height:28px;border-radius:50%;background:rgba(41,121,255,.15);border:1px solid rgba(41,121,255,.3);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--blue);flex-shrink:0">3</div>
<div style="flex:1"><div style="font-size:12px;font-weight:700">Entraînement RL</div><div style="font-size:10px;color:var(--text3)">rl_train.py — PPO 1 000 000 steps from scratch (~24h GPU)</div></div>
<span id="step-3-status" class="tag" style="background:rgba(255,255,255,.06);color:var(--text3);font-size:9px;padding:3px 8px;border-radius:4px">EN ATTENTE</span>
</div>
<div style="display:flex;align-items:center;gap:10px;padding:10px;background:rgba(255,255,255,.03);border-radius:8px">
<div style="width:28px;height:28px;border-radius:50%;background:rgba(41,121,255,.15);border:1px solid rgba(41,121,255,.3);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--blue);flex-shrink:0">4</div>
<div style="flex:1"><div style="font-size:12px;font-weight:700">Export bundle</div><div style="font-size:10px;color:var(--text3)">export_unified.py — génère ahad_quant_unified.zip</div></div>
<span id="step-4-status" class="tag" style="background:rgba(255,255,255,.06);color:var(--text3);font-size:9px;padding:3px 8px;border-radius:4px">EN ATTENTE</span>
</div>
</div>
</div>
<div class="btn-row" style="margin-bottom:18px">
<button id="btn-setup-run" class="btn btn-s" onclick="startSetup()">🚀 Lancer le Setup Initial</button>
<button id="btn-setup-stop" class="btn btn-d" onclick="stopSetup()" disabled>⛔ Interrompre</button>
<button class="btn btn-g btn-sm" onclick="loadSetupStatus()">↺</button>
</div>
<div class="card">
<div style="font-size:11px;font-weight:700;margin-bottom:12px;color:var(--text3);letter-spacing:.08em;text-transform:uppercase">État des fichiers</div>
<div class="metric-row"><span class="metric-label">model_ensemble.pkl</span><span id="setup-model-ml" class="metric-val">—</span></div>
<div class="metric-row"><span class="metric-label">rl_agent.zip</span><span id="setup-model-rl" class="metric-val">—</span></div>
<div class="metric-row"><span class="metric-label">ahad_quant_unified.zip</span><span id="setup-model-unified" class="metric-val">—</span></div>
<div class="metric-row"><span class="metric-label">Fichiers données /data</span><span id="setup-data-status" class="metric-val">—</span></div>
</div>
</div>
<!-- ─────────────────────────────────────────────────────── MT5 BRIDGE ── -->
<div id="page-mt5" class="page">
<div class="sh">
<div class="section-title">MT5 Bridge <span>CSV</span></div>
<button class="btn btn-g btn-sm" onclick="loadMT5()">↺</button>
</div>
<div class="grid g3" style="margin-bottom:14px">
<div class="card">
<div class="lbl">Statut Connexion</div>
<div id="mt5-conn" class="val" style="font-size:18px">—</div>
<div id="mt5-ts" class="sub">—</div>
</div>
<div class="card">
<div class="lbl">Balance MT5</div>
<div id="mt5-bal" class="val" style="font-size:22px">—</div>
<div id="mt5-eq" class="sub">Equity: —</div>
</div>
<div class="card">
<div class="lbl">Positions Ouvertes</div>
<div id="mt5-pos" class="val" style="font-size:22px">0</div>
<div id="mt5-pnl" class="sub">PnL jour: —</div>
</div>
</div>
<!-- Bridge health -->
<div class="card" style="margin-bottom:14px">
<div class="sh"><div class="st">Bridge Health</div></div>
<div class="grid g2">
<div>
<div class="metric-row"><span class="metric-label">Chemin configuré</span><span id="mt5-path-cfg" class="metric-val">—</span></div>
<div class="metric-row"><span class="metric-label">Signaux envoyés</span><span id="mt5-signals" class="metric-val">0</span></div>
<div class="metric-row"><span class="metric-label">Rapports reçus</span><span id="mt5-reports-cnt" class="metric-val">0</span></div>
<div class="metric-row"><span class="metric-label">Signaux en attente</span><span id="mt5-pending" class="metric-val">0</span></div>
</div>
<div>
<div class="metric-row"><span class="metric-label">Dernier signal ID</span><span id="mt5-last-sig" class="metric-val">—</span></div>
<div class="metric-row"><span class="metric-label">Dernier rapport ID</span><span id="mt5-last-rep" class="metric-val">—</span></div>
<div class="metric-row"><span class="metric-label">Gap sig→rep</span><span id="mt5-gap" class="metric-val">—</span></div>
<div class="metric-row"><span class="metric-label">MT5 Path</span><span id="mt5-path" class="metric-val" style="font-size:9px;word-break:break-all">—</span></div>
</div>
</div>
</div>
<!-- Reports -->
<div class="card">
<div class="sh"><div class="st">Rapports MT5</div>
<div class="btn-row">
<span id="mt5-stats" class="sub">—</span>
</div>
</div>
<div style="overflow-x:auto">
<table>
<thead><tr>
<th>Signal ID</th><th>Paire</th><th>Dir.</th><th>Entrée</th>
<th>Profit</th><th>Statut</th><th>Raison</th><th>Temps</th>
</tr></thead>
<tbody id="mt5-body"></tbody>
</table>
</div>
<div id="mt5-empty" class="empty">Aucun rapport MT5 — bridge non connecté</div>
</div>
</div>
<!-- ─────────────────────────────────────────────────────── FEATURES ── -->
<div id="page-features" class="page">
<div class="sh">
<div class="section-title">Feature Importance <span>LightGBM</span></div>
<button class="btn btn-g btn-sm" onclick="loadFeatureImportance()">↺</button>
</div>
<div class="banner">
<span>🔬</span>
<span>62 features techniques (RSI, MACD, Bollinger, ATR, etc.) — top 15 ci-dessous. Le PPO utilise ces mêmes features dans <code style="color:var(--cyan)">_get_obs()</code> via le cache ML.</span>
</div>
<div class="card">
<div id="feat-imp-empty" class="empty">Feature importance non disponible — lancer un Full Retrain (bouton "🧠 Full Retrain (ML+RL)") d'abord</div>
<div id="feat-imp-updated" class="sub" style="margin-bottom:10px"></div>
<div style="height:380px"><canvas id="featImpChart"></canvas></div>
</div>
</div>
<!-- ─────────────────────────────────────────────────────── TERMINAL ── -->
<div id="page-terminal" class="page">
<div class="sh">
<div class="section-title">Terminal <span>Live</span></div>
<div class="btn-row">
<button class="btn btn-g btn-sm" onclick="clearLog()">🗑 Clear</button>
<label style="display:flex;align-items:center;gap:6px;font-size:11px;color:var(--text2);cursor:pointer">
<input type="checkbox" id="auto-scroll" checked style="cursor:pointer"> Auto-scroll
</label>
</div>
</div>
<div class="banner banner-a">
<span>️</span>
<span>Les logs affichent tous les processus en temps réel. Couleurs : <span style="color:#64FFDA">BOT</span> · <span style="color:var(--cyan)">TRAIN</span> · <span style="color:var(--purple)">DOWNLOAD</span> · <span style="color:var(--teal)">FINETUNE</span> · <span style="color:#FF6D00">BACKTEST</span></span>
</div>
<div id="terminal" class="terminal"></div>
<div class="btn-row" style="margin-top:10px">
<button class="btn btn-g btn-sm" onclick="apiPost('/api/stop/train')">⛔ Stop Train</button>
<button class="btn btn-g btn-sm" onclick="apiPost('/api/stop/download')">⛔ Stop DL</button>
<button class="btn btn-g btn-sm" onclick="apiPost('/api/stop/backtest')">⛔ Stop BT</button>
<button class="btn btn-g btn-sm" onclick="apiPost('/api/stop/train')">⛔ Stop RL</button>
</div>
</div>
<!-- ─────────────────────────────────────────────────────── CONFIG ── -->
<div id="page-config" class="page">
<div class="sh">
<div class="section-title">Configuration <span>.env</span></div>
<div class="btn-row">
<button class="btn btn-p" onclick="saveConfig()">💾 Sauvegarder</button>
<button class="btn btn-g btn-sm" onclick="loadConfig()">↺ Recharger</button>
</div>
</div>
<div id="cfg-restart-banner" class="banner banner-a" style="display:none">
⚠️ Redémarrage du bot nécessaire pour appliquer les changements.
</div>
<div class="grid g2">
<!-- Trading -->
<div class="card">
<div class="lbl" style="margin-bottom:14px;font-size:11px;font-weight:700;color:var(--text)">💱 Trading</div>
<div class="field">
<label>PAPER_MODE</label>
<select id="cfg-PAPER_MODE">
<option value="true">true — Paper Trading</option>
<option value="false">false — Live MT5</option>
</select>
</div>
<div class="field"><label>PAPER_INITIAL_BALANCE</label><input id="cfg-PAPER_INITIAL_BALANCE" type="number"></div>
<div class="field"><label>LEVERAGE</label><input id="cfg-LEVERAGE" type="number"></div>
<div class="field"><label>MAX_POSITIONS</label><input id="cfg-MAX_POSITIONS" type="number"></div>
<div class="field"><label>RISK_PER_TRADE (0.02 = 2%)</label><input id="cfg-RISK_PER_TRADE" type="number" step="0.001"></div>
<div class="field"><label>MAX_DAILY_LOSS_PCT (0.05 = 5%)</label><input id="cfg-MAX_DAILY_LOSS_PCT" type="number" step="0.01"></div>
<div class="field"><label>STOP_LOSS_PCT</label><input id="cfg-STOP_LOSS_PCT" type="number" step="0.001"></div>
<div class="field"><label>TAKE_PROFIT_PCT</label><input id="cfg-TAKE_PROFIT_PCT" type="number" step="0.001"></div>
<div class="field"><label>MAIN_LOOP_SECONDS</label><input id="cfg-MAIN_LOOP_SECONDS" type="number"></div>
<div class="field"><label>CANDLE_INTERVAL</label>
<select id="cfg-CANDLE_INTERVAL">
<option value="1h">1h</option><option value="4h">4h</option>
<option value="15m">15m</option><option value="1d">1d</option>
</select>
</div>
<div class="field"><label>MIN_LOT_SIZE</label><input id="cfg-MIN_LOT_SIZE" type="number" step="0.01"></div>
<div class="field"><label>MAX_LOT_SIZE</label><input id="cfg-MAX_LOT_SIZE" type="number" step="0.01"></div>
</div>
<!-- ML + RL -->
<div class="card">
<div class="lbl" style="margin-bottom:14px;font-size:11px;font-weight:700;color:var(--text)">🧠 ML + RL</div>
<div class="field"><label>MIN_CONFIDENCE (0.72 = 72%)</label><input id="cfg-MIN_CONFIDENCE" type="number" step="0.01"></div>
<div class="field">
<label>USE_ENSEMBLE</label>
<select id="cfg-USE_ENSEMBLE"><option value="true">true</option><option value="false">false</option></select>
</div>
<div class="field">
<label>USE_RL_AGENT</label>
<select id="cfg-USE_RL_AGENT"><option value="true">true — Activer PPO</option><option value="false">false — ML seul</option></select>
</div>
<div class="field">
<label>RL_MODE</label>
<select id="cfg-RL_MODE">
<option value="filter">filter — valide/rejette signal ML</option>
<option value="override">override — peut inverser la direction</option>
</select>
</div>
<div class="field"><label>RL_OVERRIDE_THRESHOLD (0.82 = 82%)</label><input id="cfg-RL_OVERRIDE_THRESHOLD" type="number" step="0.01"></div>
<div class="field"><label>RL_CONFIDENCE_BOOST (0.05 = +5%)</label><input id="cfg-RL_CONFIDENCE_BOOST" type="number" step="0.01"></div>
<div class="field"><label>RL_FINETUNE_STEPS</label><input id="cfg-RL_FINETUNE_STEPS" type="number"></div>
<div class="lbl" style="margin-top:18px;margin-bottom:10px;font-size:11px;font-weight:700;color:var(--text)">🔄 Apprentissage continu (quotidien, local)</div>
<div class="field">
<label>DAILY_LOCAL_RETRAIN_ENABLED</label>
<select id="cfg-DAILY_LOCAL_RETRAIN_ENABLED"><option value="true">true — cycle quotidien local actif</option><option value="false">false — désactivé</option></select>
</div>
<div class="field"><label>DAILY_LOCAL_RETRAIN_INTERVAL_HOURS</label><input id="cfg-DAILY_LOCAL_RETRAIN_INTERVAL_HOURS" type="number"></div>
<div class="field">
<label>CONTINUOUS_LEARNING_ENABLED</label>
<select id="cfg-CONTINUOUS_LEARNING_ENABLED"><option value="true">true</option><option value="false">false</option></select>
</div>
<div class="field">
<label>WARMSTART_ENABLED (ML)</label>
<select id="cfg-WARMSTART_ENABLED"><option value="true">true</option><option value="false">false</option></select>
</div>
<div class="field"><label>WARMSTART_MIN_TRADES</label><input id="cfg-WARMSTART_MIN_TRADES" type="number"></div>
<div class="field"><label>RL_REPLAY_MIN_TRADES (min trades pour RL replay)</label><input id="cfg-RL_REPLAY_MIN_TRADES" type="number"></div>
<div class="field"><label>RL_RETRAIN_INTERVAL_HOURS</label><input id="cfg-RL_RETRAIN_INTERVAL_HOURS" type="number"></div>
<div class="field"><label>AUTO_RETRAIN_INTERVAL_HOURS (full retrain réseau)</label><input id="cfg-AUTO_RETRAIN_INTERVAL_HOURS" type="number"></div>
<div class="field"><label>AUTO_RETRAIN_MIN_ACCURACY (ex: 0.70)</label><input id="cfg-AUTO_RETRAIN_MIN_ACCURACY" type="number" step="0.01"></div>
<div class="field"><label>EXPERIENCE_BUFFER_MAX_SIZE</label><input id="cfg-EXPERIENCE_BUFFER_MAX_SIZE" type="number"></div>
<div class="field">
<label>RL_AUTO_RETRAIN_ENABLED</label>
<select id="cfg-RL_AUTO_RETRAIN_ENABLED"><option value="true">true</option><option value="false">false</option></select>
</div>
<div class="field">
<label>RL_REAL_REPLAY_ENABLED</label>
<select id="cfg-RL_REAL_REPLAY_ENABLED"><option value="true">true — injecte les vrais trades dans le fine-tune RL</option><option value="false">false</option></select>
</div>
<div class="field" style="grid-column:1/-1">
<label style="opacity:.6;font-weight:400">️ Chaque jour : warm-start ML + fine-tune RL sur les données locales. Le nouveau modèle (ML ou RL) ne remplace l'ancien que s'il est meilleur — sinon l'ancien est conservé.</label>
</div>
<div class="lbl" style="margin-top:18px;margin-bottom:10px;font-size:11px;font-weight:700;color:var(--text)">🔌 MT5 Bridge</div>
<div class="field">
<label>MT5_BRIDGE_ENABLED</label>
<select id="cfg-MT5_BRIDGE_ENABLED"><option value="false">false</option><option value="true">true</option></select>
</div>
<div class="field"><label>MT5_FILES_PATH</label><input id="cfg-MT5_FILES_PATH" type="text" placeholder="C:/Users/.../Files"></div>
<div class="field"><label>MT5_SYMBOL_SUFFIX (ex: .m)</label><input id="cfg-MT5_SYMBOL_SUFFIX" type="text"></div>
<div class="field"><label>MT5_SERVER</label><input id="cfg-MT5_SERVER" type="text"></div>
<div class="field"><label>MT5_SIGNAL_TIMEOUT (s)</label><input id="cfg-MT5_SIGNAL_TIMEOUT" type="number"></div>
<div class="field"><label>MT5_POLL_INTERVAL (s)</label><input id="cfg-MT5_POLL_INTERVAL" type="number"></div>
<div class="lbl" style="margin-top:18px;margin-bottom:10px;font-size:11px;font-weight:700;color:var(--text)">🕐 Session Filter</div>
<div class="field">
<label>SESSION_FILTER_ENABLED</label>
<select id="cfg-SESSION_FILTER_ENABLED"><option value="false">false</option><option value="true">true</option></select>
</div>
<div class="field"><label>SESSION_LONDON_START (ex: 08:00)</label><input id="cfg-SESSION_LONDON_START" type="text"></div>
<div class="field"><label>SESSION_LONDON_END</label><input id="cfg-SESSION_LONDON_END" type="text"></div>
<div class="field"><label>SESSION_NY_START</label><input id="cfg-SESSION_NY_START" type="text"></div>
<div class="field"><label>SESSION_NY_END</label><input id="cfg-SESSION_NY_END" type="text"></div>
</div>
</div>
</div>
</div><!-- /content -->
</div><!-- /main -->
</div><!-- /app -->
<script>
// ═══ GLOBALS ════════════════════════════════════════════════════════════════
let _statusData = null;
let _tradeData = [];
let eqChart = null;
let btChart = null;
let featImpChart= null;
let _logCount = 0;
const TOKEN = ''; // Laisser vide si WEB_UI_TOKEN non configuré
// ═══ NAVIGATION ═════════════════════════════════════════════════════════════
function nav(page) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.ni').forEach(n => n.classList.remove('active'));
const el = document.getElementById('page-' + page);
if (el) el.classList.add('active');
document.querySelectorAll('.ni').forEach(n => {
if (n.getAttribute('onclick') && n.getAttribute('onclick').includes("'" + page + "'"))
n.classList.add('active');
});
if (page === 'rl') loadRL();
if (page === 'backtest') loadBacktest();
if (page === 'trades') loadTrades();
if (page === 'mt5') loadMT5();
if (page === 'features') loadFeatureImportance();
if (page === 'config') loadConfig();
if (page === 'setup') loadSetupStatus();
}
// ═══ API HELPERS ════════════════════════════════════════════════════════════
async function api(url) {
try {
const h = TOKEN ? {Authorization: 'Bearer ' + TOKEN} : {};
const r = await fetch(url, {headers: h});
if (!r.ok) return null;
return await r.json();
} catch { return null; }
}
async function apiPost(url, body = {}) {
try {
const h = {'Content-Type': 'application/json'};
if (TOKEN) h['Authorization'] = 'Bearer ' + TOKEN;
const r = await fetch(url, {method: 'POST', headers: h, body: JSON.stringify(body)});
const d = await r.json();
if (d.message) showToast(d.message, d.ok !== false ? 'ok' : 'err');
return d;
} catch (e) {
showToast('Erreur réseau: ' + e, 'err');
return null;
}
}
function setEl(id, html) {
const el = document.getElementById(id);
if (el) el.innerHTML = html;
}
function show(id, v) {
const el = document.getElementById(id);
if (el) el.style.display = v ? '' : 'none';
}
function showToast(msg, type = 'ok') {
const t = document.createElement('div');
t.style.cssText = `position:fixed;bottom:20px;right:20px;z-index:9999;
padding:10px 16px;border-radius:8px;font-size:11px;font-family:var(--fd);font-weight:600;
background:${type==='ok'?'rgba(0,230,118,.15)':'rgba(255,23,68,.15)'};
border:1px solid ${type==='ok'?'rgba(0,230,118,.4)':'rgba(255,23,68,.4)'};
color:${type==='ok'?'var(--green)':'var(--red)'};
animation:fi .2s;`;
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 3000);
}
// ═══ CLOCK ══════════════════════════════════════════════════════════════════
(function clock() {
const el = document.getElementById('clock');
function tick() {
const now = new Date();
el.textContent = now.toUTCString().slice(17,25) + ' UTC';
}
tick();
setInterval(tick, 1000);
})();
// ═══ STATUS ═════════════════════════════════════════════════════════════════
async function loadStatus() {
const s = await api('/api/status');
if (!s) return;
_statusData = s;
// Header pills
const modePill = document.getElementById('h-mode-pill');
if (modePill) {
modePill.className = 'pill ' + (s.paper_mode ? 'pill-paper' : 'pill-live');
modePill.innerHTML = `<span class="dot ${s.paper_mode?'dot-a':'dot-g'}"></span>${s.paper_mode?'PAPER':'LIVE'}`;
}
const botPill = document.getElementById('h-bot-pill');
if (botPill) {
if (s.bot_running && !s.paused) { botPill.className='pill pill-running'; botPill.innerHTML='<span class="dot dot-b"></span>RUNNING'; }
else if (s.paused) { botPill.className='pill pill-paper'; botPill.innerHTML='<span class="dot dot-a"></span>PAUSED'; }
else { botPill.className='pill pill-stopped'; botPill.innerHTML='<span class="dot dot-r"></span>OFFLINE'; }
}
const rlPill = document.getElementById('h-rl-pill');
if (rlPill) rlPill.style.display = s.use_rl_agent ? '' : 'none';
// KPI
const bal = s.balance || 0;
const initBal = s.paper_mode ? (parseFloat(s.paper_initial_balance) || 100) : 0;
setEl('d-balance', '$' + bal.toLocaleString('fr-FR', {minimumFractionDigits:2}));
setEl('d-balance-sub', s.paper_mode ? 'Paper Mode' : 'Live MT5');
const dpnl = s.daily_pnl || 0;
setEl('d-dpnl', (dpnl>=0?'+':'') + dpnl.toFixed(2));
document.getElementById('d-dpnl').className = 'val ' + (dpnl>=0?'pos':'neg');
const tpnl = s.total_pnl || 0;
setEl('d-tpnl', (tpnl>=0?'+':'') + tpnl.toFixed(2));
document.getElementById('d-tpnl').className = 'val ' + (tpnl>=0?'pos':'neg');
setEl('d-peak', 'Peak: $' + (s.peak_equity||0).toFixed(2));
setEl('d-wr', (s.win_rate||0).toFixed(1) + '%');
setEl('d-trades', (s.total_trades||0) + ' trades');
// Model
const m = s.model || {};
if (m.model_file) {
setEl('d-model-name', m.model_file);
setEl('d-model-info', (m.model_size_mb||0) + ' MB · ' + (m.model_mtime||'—'));
const pct = Math.min(100, (m.model_size_mb||0) / 100 * 100);
const fill = document.getElementById('d-model-fill');
if (fill) fill.style.width = pct + '%';
} else {
setEl('d-model-name', '❌ Aucun modèle');
setEl('d-model-info', 'Lancer l\'entraînement (bouton ML+RL) d\'abord');
}
setEl('d-model-accuracy', (m.accuracy !== undefined && m.accuracy !== null)
? `Accuracy : ${(m.accuracy*100).toFixed(2)}% · validée le ${(m.datetime||'').replace('T',' ').slice(0,16)}`
: 'Pas encore de run validé');
// RL
const rl = s.rl || {};
const rlOk = rl.agent_exists && rl.scaler_exists;
setEl('d-rl-status', rlOk
? `<span class="status-dot sd-g"></span>Chargé · ${rl.agent_size_mb||0} MB`
: `<span class="status-dot sd-r"></span>Non trouvé`);
setEl('d-rl-info', s.use_rl_agent
? `Actif · Mode: ${s.rl_mode||'filter'} · Seuil: ${((s.rl_override_threshold||0.82)*100).toFixed(0)}%`
: 'USE_RL_AGENT=false dans .env');
const lastFt = rl.last_finetune;
if (lastFt) {
const tag = lastFt.accepted ? '✅ Accepté' : '❌ Rejeté';
// [FIX] data_source ("real_trades_only" vs "simulated") était déjà
// persisté par rl_train.py mais jamais affiché — impossible de savoir
// depuis l'UI si le dernier fine-tune avait appris sur de vrais trades.
const src = lastFt.data_source === 'real_trades_only'
? `🟢 réel (${lastFt.n_real_trades||0} trades)`
: (lastFt.data_source === 'simulated' ? '🟡 simulé' : '');
setEl('d-rl-reward', `${tag} · reward ${lastFt.old_mean_reward.toFixed(3)} → ${lastFt.new_mean_reward.toFixed(3)}`
+ (src ? ` · ${src}` : '')
+ ` · ${(lastFt.datetime||'').replace('T',' ').slice(0,16)}`);
} else {
setEl('d-rl-reward', 'Pas encore de fine-tune effectué');
}
// Synchro ML ↔ RL
const sync = s.sync || {};
const syncBadgeEl = document.getElementById('d-sync-badge');
if (syncBadgeEl) {
if (sync.status === 'synced') {
syncBadgeEl.innerHTML = '<span class="status-dot sd-g"></span>🟢 Synchronisés';
} else if (sync.status === 'drifted') {
syncBadgeEl.innerHTML = '<span class="status-dot sd-r"></span>🟡 Désynchronisés';
} else if (sync.status === 'partial') {
syncBadgeEl.innerHTML = '<span class="status-dot sd-r"></span>⚪ Un seul des deux validé';
} else {
syncBadgeEl.innerHTML = '<span class="status-dot sd-r"></span>❔ Inconnu';
}
}
setEl('d-sync-info', sync.drift_days !== null && sync.drift_days !== undefined
? `Écart : ${sync.drift_days} jour(s) entre dernière maj ML et RL`
: 'Lancer un entraînement pour établir la référence');
// Data
const d = s.data || {};
setEl('d-data-count', (d.count||0) + ' paires');
setEl('d-data-info', (d.total_mb||0) + ' MB · 1H yfinance');
// Config params
setEl('d-leverage', (s.leverage||30) + 'x');
setEl('d-confidence', ((s.min_confidence||0.72)*100).toFixed(0) + '%');
setEl('d-rl-threshold', ((s.rl_override_threshold||0.82)*100).toFixed(0) + '%');
setEl('d-pairs', s.pairs_count||25);
// Positions
const pos = s.open_positions || {};
const posKeys = Object.keys(pos);
const nbPos = document.getElementById('nb-pos');
if (nbPos) { nbPos.textContent = posKeys.length; nbPos.style.display = posKeys.length > 0 ? '' : 'none'; }
const posEl = document.getElementById('d-positions');
if (posEl) {
if (!posKeys.length) {
posEl.innerHTML = '<div class="empty">Aucune position ouverte</div>';
} else {
let html = '<table><thead><tr><th>Paire</th><th>Dir.</th><th>Entrée</th><th>Taille</th><th>uPnL</th><th>Prix actuel</th></tr></thead><tbody>';
posKeys.forEach(sym => {
const p = pos[sym];
const upnl = p.upnl || 0;
html += `<tr>
<td class="tc">${sym}</td>
<td><span class="tp ${p.side==='long'?'tp-l':'tp-s'}">${(p.side||'').toUpperCase()}</span></td>
<td>${(p.entry||0).toFixed(5)}</td>
<td>${p.qty||0}</td>
<td class="${upnl>=0?'pos':'neg'}">${upnl>=0?'+':''}${upnl.toFixed(2)}</td>
<td>${(p.current_price||0).toFixed(5)}</td>
</tr>`;
});
html += '</tbody></table>';
posEl.innerHTML = html;
}
}
// Bot buttons
const running = s.bot_running;
const paused = s.paused;
const btnStart = document.getElementById('btn-start');
const btnPause = document.getElementById('btn-pause');
const btnResume = document.getElementById('btn-resume');
const btnStop = document.getElementById('btn-stop');
if (btnStart) { btnStart.disabled = running; }
if (btnPause) { btnPause.disabled = !running || paused; btnPause.style.display = paused ? 'none' : ''; }
if (btnResume) { btnResume.style.display = paused ? '' : 'none'; }
if (btnStop) { btnStop.disabled = !running; }
// nav badge
const nbTr = document.getElementById('nb-tr');
if (nbTr) nbTr.textContent = s.total_trades||0;
}
async function botAction(action) {
await apiPost('/api/bot/' + action);
setTimeout(loadStatus, 800);
}
// ═══ RL MONITOR ═════════════════════════════════════════════════════════════
async function loadRL() {
const rl = await api('/api/rl/status');
if (!rl) return;
// File status cards
const agentOk = rl.agent_exists;
setEl('rl-agent-status', agentOk
? `<span class="status-dot sd-g"></span>✅ OK`
: `<span class="status-dot sd-r"></span>❌ Manquant`);
setEl('rl-agent-info', agentOk ? `${rl.agent_size_mb} MB · ${rl.agent_mtime||''}` : 'Lancer l\'entraînement (bouton ML+RL)');
setEl('rl-scaler-status', rl.scaler_exists
? `<span class="status-dot sd-g"></span>✅ OK`
: `<span class="status-dot sd-r"></span>❌ Manquant`);
setEl('rl-ckpt-status', rl.checkpoint_exists
? `<span class="status-dot sd-g"></span>✅ OK`
: `<span class="status-dot sd-a"></span>⚠ Absent`);
// [FIX] "Hebdomadaire" était figé en dur, alors que le cycle réel est
// désormais quotidien. Le slot "train" (full retrain) ET le slot
// "daily_local" (cycle léger) déclenchent tous les deux un fine-tune RL.
const fullRunning = _statusData && _statusData.finetuning;
const localRunning = _statusData && _statusData.daily_local_running;
const ftRunning = fullRunning || localRunning;
setEl('rl-ft-status', ftRunning
? `<span class="status-dot sd-b" style="animation:pulse 1s infinite"></span>EN COURS…`
: `<span class="status-dot sd-a"></span>En attente`);
setEl('rl-ft-info', fullRunning ? 'Entraînement complet ML+RL actif'
: localRunning ? 'Cycle local léger actif'
: `Quotidien (réel) · intervalle ${(rl.daily_local && rl.daily_local.interval_hours) || 24}h`);
const ftBtn = document.getElementById('btn-fullretrain');
if (ftBtn) ftBtn.disabled = ftRunning;
const dlBtn = document.getElementById('btn-daily-local');
if (dlBtn) dlBtn.disabled = ftRunning;
// Config from status
if (_statusData) {
setEl('rl-cfg-mode', _statusData.rl_mode || 'filter');
setEl('rl-cfg-thr', ((_statusData.rl_override_threshold||0.82)*100).toFixed(0) + '%');
setEl('rl-cfg-boost', '+' + ((_statusData.rl_confidence_boost||0.05)*100).toFixed(0) + '%');
}
// [FIX] Steps Full Retrain (RL) / Device — étaient figés en dur
// ("1 000 000", "CUDA (GPU)", "200 000") peu importe l'état réel ou la
// machine. Désormais lus depuis rl_progress.json et la config courante.
const prog = rl.progress || {};
setEl('rl-cfg-steps', prog.steps_done !== undefined
? `${(prog.steps_done||0).toLocaleString()} / ${(prog.total_steps||0).toLocaleString()}`
: '—');
setEl('rl-cfg-device', rl.device || '—');
setEl('rl-cfg-ft', (rl.finetune_steps || 200000).toLocaleString());
// [FIX] Panel "Résultats Backtest ML+RL" — était 100% statique, déconnecté
// de tout vrai run. Réutilise le même backtest que la page Backtest.
try {
const bt = await api('/api/backtest/results');
const rlBtEl = document.getElementById('rl-bt-results');
if (rlBtEl) {
if (bt && Object.keys(bt).length) {
rlBtEl.innerHTML = _renderBacktestMetricsHTML(bt);
} else {
rlBtEl.innerHTML = '<div class="empty">Pas encore de backtest — onglet Backtest</div>';
}
}
} catch (e) { /* non-bloquant */ }
// [FIX] Remplace le bloc "Planning Fine-tuning" statique — statut réel du
// cycle quotidien local + source du dernier fine-tune RL (réel/simulé,
// déjà persistée par rl_train.py mais jamais affichée auparavant).
const dl = rl.daily_local || {};
setEl('rl-daily-last', dl.last_run ? (dl.last_run||'').replace('T',' ').slice(0,16) : 'Jamais');
setEl('rl-daily-interval', `${dl.interval_hours||24}h ${_statusData && _statusData.bot_running ? '(auto, bot actif)' : '(auto seulement si bot lancé)'}`);
const lastFt2 = rl.last_finetune;
if (lastFt2) {
const srcTxt = lastFt2.data_source === 'real_trades_only'
? `🟢 réel (${lastFt2.n_real_trades||0} trades)`
: (lastFt2.data_source === 'simulated' ? '🟡 simulé' : '—');
setEl('rl-daily-source', `${lastFt2.accepted ? '✅' : '❌'} ${srcTxt}`);
} else {
setEl('rl-daily-source', 'Aucun fine-tune encore');
}
// Training curves image
const curvesImg = document.getElementById('rl-curves-img');
const curvesEmpty = document.getElementById('rl-curves-empty');
const curvesTag = document.getElementById('rl-curves-tag');
if (rl.curves_exists) {
curvesImg.src = '/api/rl/curves?' + Date.now();
curvesImg.style.display = '';
curvesEmpty.style.display = 'none';
curvesTag.style.display = '';
} else {
curvesImg.style.display = 'none';
curvesEmpty.style.display = '';
curvesTag.style.display = 'none';
}
}
async function startFullRetrain() {
const r = await apiPost('/api/rl/finetune'); // route backend inchangée
if (r && r.ok) setTimeout(loadRL, 1000);
}
async function startDailyLocal() {
// [NEW] Déclenche le cycle quotidien local (warm-start ML + RL réel) à
// la demande, sans attendre l'intervalle automatique de 24h.
const r = await apiPost('/api/retrain/daily-local');
if (r && r.ok) setTimeout(loadRL, 1000);
}
// ═══ RESET GLOBAL ═══════════════════════════════════════════════════════════
async function globalReset() {
var msg = "REINITIALISATION COMPLETE\n\nCette action va effacer :\n- Tous les logs\n- Tous les trades paper\n- Resultats de backtest\n- Progression RL\n- Etat du bot\n\nModeles et checkpoints conserves.\n\nContinuer ?";
if (!confirm(msg)) return;
const r = await apiPost('/api/reset/all');
if (r && r.ok) {
// Rafraîchir toutes les vues
if (typeof loadTrades === 'function') loadTrades();
if (typeof loadBacktest === 'function') loadBacktest();
if (typeof loadRL === 'function') loadRL();
if (typeof refreshStatus === 'function') refreshStatus();
// Notif visuelle
const btn = document.getElementById('btn-global-reset');
if (btn) {
const orig = btn.textContent;
btn.textContent = '✅ Réinitialisé';
btn.disabled = true;
setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 2500);
}
} else {
alert('Erreur lors de la réinitialisation : ' + JSON.stringify(r));
}
}
// ═══ SETUP INITIAL ══════════════════════════════════════════════════════════
const SETUP_STEPS_LABELS = [
null,
'Téléchargement données',
'Entraînement ML',
'Entraînement RL',
'Export bundle',
];
function _setStepStatus(n, status) {
const el = document.getElementById('step-' + n + '-status');
if (!el) return;
const styles = {
'EN ATTENTE': 'background:rgba(255,255,255,.06);color:var(--text3)',
'EN COURS': 'background:rgba(255,171,0,.15);color:var(--amber)',
'OK': 'background:rgba(0,230,118,.15);color:var(--green)',
'ERREUR': 'background:rgba(255,23,68,.15);color:var(--red)',
};
el.textContent = status;
el.style.cssText = (styles[status] || styles['EN ATTENTE']) + ';font-size:9px;padding:3px 8px;border-radius:4px;font-weight:700';
}
async function loadSetupStatus() {
const s = await api('/api/status');
if (!s) return;
const running = s.setup_running;
const btnRun = document.getElementById('btn-setup-run');
const btnStop = document.getElementById('btn-setup-stop');
const badge = document.getElementById('nb-setup');
if (btnRun) btnRun.disabled = running;
if (btnStop) btnStop.disabled = !running;
if (badge) { badge.style.display = running ? '' : 'none'; }
// Vérifier l'état des fichiers via /api/data/status
const ds = await api('/api/data/status');
if (ds) {
const mlEl = document.getElementById('setup-model-ml');
const rlEl = document.getElementById('setup-model-rl');
const uniEl = document.getElementById('setup-model-unified');
const dataEl = document.getElementById('setup-data-status');
const ok = v => `<span style="color:var(--green);font-weight:700">✅ Présent</span>`;
const nok = v => `<span style="color:var(--red)">❌ Manquant</span>`;
if (mlEl) mlEl.innerHTML = ds.ensemble_model ? ok() : nok();
if (rlEl) rlEl.innerHTML = ds.rl_model ? ok() : nok();
if (uniEl) uniEl.innerHTML = ds.unified_model ? ok() : nok();
if (dataEl) dataEl.innerHTML = ds.data_files_count !== undefined
? `<span style="color:var(--cyan)">${ds.data_files_count} fichiers JSON</span>` : nok();
}
}
async function startSetup() {
if (!confirm('Lancer le setup initial (26h) ?\nLe terminal affichera la progression en temps réel.')) return;
[1,2,3,4].forEach(n => _setStepStatus(n, 'EN ATTENTE'));
const r = await apiPost('/api/setup/run');
if (r && r.ok) {
_setStepStatus(1, 'EN COURS');
nav('terminal');
// Suivre la progression dans les logs via polling
let stepDone = 0;
const poll = setInterval(async () => {
const s = await api('/api/status');
if (!s || !s.setup_running) {
clearInterval(poll);
[1,2,3,4].forEach(n => {
const el = document.getElementById('step-' + n + '-status');
if (el && el.textContent === 'EN COURS') _setStepStatus(n, 'EN ATTENTE');
});
loadSetupStatus();
return;
}
}, 3000);
}
}
async function stopSetup() {
if (!confirm('Interrompre le setup en cours ?')) return;
await apiPost('/api/setup/stop');
setTimeout(loadSetupStatus, 1000);
}
// ═══ BACKTEST ═══════════════════════════════════════════════════════════════
// [FIX] Rendu des métriques factorisé — réutilisé par la page Backtest
// (#bt-results) ET le panel "Résultats Backtest ML+RL" de RL Monitor
// (#rl-bt-results), qui affichait auparavant des chiffres figés en dur
// (+208.4%, Win Rate 82.7%...) sans aucun lien avec un vrai backtest.
function _renderBacktestMetricsHTML(d) {
const metrics = [
['Win Rate', d.win_rate, v => v.toFixed(1)+'%', v=>v>=50],
['Sharpe Ratio', d.sharpe, v => v.toFixed(2), v=>v>=1.5],
['Max Drawdown', d.max_drawdown, v => v.toFixed(2)+'%', v=>v<15],
['Profit Factor', d.profit_factor, v => v.toFixed(2), v=>v>1.5],
['Total Trades', d.total_trades, v => v, ()=>true],
['Total PnL %', d.pnl_pct, v => '+'+v.toFixed(1)+'%', v=>v>0],
['Balance Finale', d.balance_final, v => '$'+Math.round(v).toLocaleString(), v=>v>=(d.balance_initial||0)],
['Capital initial',d.balance_initial, v => '$'+Math.round(v).toLocaleString(), ()=>true],
];
let html = '';
metrics.forEach(([label, val, fmt, isGood]) => {
if (val === undefined || val === null) return;
const ok = isGood(val);
html += `<div class="metric-row">
<span class="metric-label">${label}</span>
<span class="metric-val ${ok?'pos':'neg'}">${fmt(val)}</span>
</div>`;
});
return html;
}
let _btData = null;
async function loadBacktest() {
const d = await api('/api/backtest/results');
if (!d) return;
_btData = d;
const btResults = document.getElementById('bt-results');
const btTs = document.getElementById('bt-ts');
if (!d || !Object.keys(d).length) {
if (btResults) btResults.innerHTML = '<div class="empty">Pas de résultats — lancer un backtest</div>';
return;
}
// [FIX] Les clés ci-dessous ne correspondaient pas à celles écrites par
// backtest.py::save_results() — sharpe_ratio/total_pnl_pct/timestamp/
// equity_curve n'existent pas dans backtest_results.json (les vraies
// clés sont sharpe/pnl_pct/run_timestamp/equity_curve_sampled), donc ces
// métriques restaient silencieusement vides ("undefined" → ligne sautée).
if (btTs && d.run_timestamp) btTs.textContent = '· ' + (d.run_timestamp||'').replace('T',' ').slice(0,16);
const sysBadge = document.getElementById('bt-system-badge');
if (sysBadge) {
if (d.rl_filter_active) {
sysBadge.innerHTML = '<span class="tag tag-g">✅</span> ML + RL unifié — ce backtest a appliqué le filtre RL sur chaque signal ML, exactement comme en live.';
} else {
sysBadge.innerHTML = '<span class="tag tag-a">⚠️</span> ML seul — RL non appliqué pendant ce backtest (vérifier USE_RL_AGENT et rl_agent.zip, ou stable-baselines3/gymnasium installés).';
}
}
const html = _renderBacktestMetricsHTML(d);
if (btResults) btResults.innerHTML = html || '<div class="empty">Données incomplètes</div>';
// Equity curve chart
const curve = d.equity_curve_sampled || [];
if (curve.length && document.getElementById('btChart')) {
if (btChart) btChart.destroy();
btChart = new Chart(document.getElementById('btChart'), {
type: 'line',
data: {
labels: curve.map((_, i) => i),
datasets: [{
data: curve.map(p => typeof p === 'object' ? p.v||p : p),
borderColor: '#2979FF', borderWidth: 1.5, fill: true,
backgroundColor: 'rgba(41,121,255,.06)', tension: 0.4, pointRadius: 0,
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: {legend: {display: false}},
scales: {
x: {display: false},
y: {grid: {color: 'rgba(255,255,255,.04)'}, ticks: {color: '#888', font:{size:9}}}
}
}
});
}
}
// ═══ TRADES ══════════════════════════════════════════════════════════════════
let _allTrades = [];
async function loadTrades() {
const d = await api('/api/trades');
if (!d) return;
_allTrades = d.trades || [];
const wins = _allTrades.filter(t => parseFloat(t.pnl||0) > 0).length;
const tpnl = _allTrades.reduce((a,t) => a + parseFloat(t.pnl||0), 0);
setEl('tr-total', _allTrades.length);
setEl('tr-wr', _allTrades.length ? (wins/_allTrades.length*100).toFixed(1)+'%' : '0%');
setEl('tr-pnl', (tpnl>=0?'+':'') + tpnl.toFixed(2));
document.getElementById('tr-pnl').className = 'val ' + (tpnl>=0?'pos':'neg') + ' ' + 'big'.replace('big','') ;
document.getElementById('tr-pnl').style.fontSize = '22px';
setEl('tr-wins', wins);
// Equity chart
const curve = d.equity_curve || [];
if (curve.length && document.getElementById('eqChart')) {
if (eqChart) eqChart.destroy();
eqChart = new Chart(document.getElementById('eqChart'), {
type: 'line',
data: {
labels: curve.map(p => p.t ? p.t.slice(5,16) : ''),
datasets: [{
data: curve.map(p => p.v),
borderColor: '#00E676', borderWidth: 1.5, fill: true,
backgroundColor: 'rgba(0,230,118,.06)', tension: 0.4, pointRadius: 0,
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: {legend: {display: false}},
scales: {
x: {ticks: {color:'#888', font:{size:8}, maxTicksLimit:8}, grid:{color:'rgba(255,255,255,.03)'}},
y: {ticks: {color:'#888', font:{size:9}}, grid:{color:'rgba(255,255,255,.04)'}}
}
}
});
}
renderTrades();
}
function renderTrades() {
const sideF = (document.getElementById('tr-filter-side') || {}).value || '';
const resultF = (document.getElementById('tr-filter-result') || {}).value || '';
let trades = _allTrades;
if (sideF) trades = trades.filter(t => (t.side||'').toLowerCase() === sideF);
if (resultF === 'win') trades = trades.filter(t => parseFloat(t.pnl||0) > 0);
if (resultF === 'loss') trades = trades.filter(t => parseFloat(t.pnl||0) <= 0);
const tbody = document.getElementById('tr-body');
const empty = document.getElementById('tr-empty');
if (!trades.length) {
if (tbody) tbody.innerHTML = '';
if (empty) empty.style.display = '';
return;
}
if (empty) empty.style.display = 'none';
let html = '';
trades.forEach(t => {
const pnl = parseFloat(t.pnl||0);
const conf = parseFloat(t.ml_confidence||t.confidence||0);
const rlFiltre = t.rl_filtered !== undefined ? (t.rl_filtered ? '✅' : '❌') : '—';
html += `<tr>
<td class="tc">${t.symbol||t.pair||'—'}</td>
<td><span class="tp ${t.side==='long'?'tp-l':'tp-s'}">${(t.side||'').toUpperCase()}</span></td>
<td>${parseFloat(t.entry||0).toFixed(5)}</td>
<td>${parseFloat(t.exit||t.close||0).toFixed(5)}</td>
<td class="${pnl>=0?'pos':'neg'}">${pnl>=0?'+':''}${pnl.toFixed(2)}</td>
<td>${conf ? (conf*100).toFixed(1)+'%' : '—'}</td>
<td style="text-align:center">${rlFiltre}</td>
<td>${(t.opened_at||t.open_time||'—').slice(0,16)}</td>
<td>${(t.closed_at||t.close_time||'—').slice(0,16)}</td>
</tr>`;
});
if (tbody) tbody.innerHTML = html;
}
// ═══ MT5 ════════════════════════════════════════════════════════════════════
async function loadMT5() {
const [status, reports] = await Promise.all([
api('/api/mt5/status'),
api('/api/mt5/reports?limit=50')
]);
if (status) {
const conn = status.connected;
setEl('mt5-conn', conn
? `<span class="status-dot sd-g"></span>Connecté`
: `<span class="status-dot sd-r"></span>Déconnecté`);
setEl('mt5-ts', conn ? 'Dernier ping: ' + (status.timestamp||'').slice(0,19) : 'Aucun signal récent');
setEl('mt5-bal', status.balance ? '$'+parseFloat(status.balance).toFixed(2) : '—');
setEl('mt5-eq', 'Equity: ' + (status.equity ? '$'+parseFloat(status.equity).toFixed(2) : '—'));
setEl('mt5-pos', status.open_positions||0);
const dpnl = parseFloat(status.daily_pnl||0);
setEl('mt5-pnl', 'PnL jour: ' + (dpnl>=0?'+':'') + dpnl.toFixed(2));
const b = status.bridge || {};
setEl('mt5-path-cfg', status.path_configured ? '✅ Oui' : '❌ Non — configurer MT5_FILES_PATH');
setEl('mt5-signals', b.signals_rows||0);
setEl('mt5-reports-cnt', b.reports_rows||0);
setEl('mt5-pending', b.pending_signals||0);
setEl('mt5-last-sig', b.last_signal_id||'—');
setEl('mt5-last-rep', b.last_report_id||'—');
setEl('mt5-gap', b.gap_seconds !== null && b.gap_seconds !== undefined ? b.gap_seconds+'s' : '—');
setEl('mt5-path', status.mt5_path||'Non configuré');
const nbMt5 = document.getElementById('nb-mt5');
if (nbMt5) { nbMt5.textContent = conn?'ON':'OFF'; nbMt5.className = 'badge ' + (conn?'bdg-g':'bdg-r'); }
}
if (reports) {
setEl('mt5-stats',
`${reports.open_count} ouverts · ${reports.closed_count} fermés · WR ${reports.win_rate}% · Profit ${reports.total_profit>=0?'+':''}${reports.total_profit}`);
const tbody = document.getElementById('mt5-body');
const empty = document.getElementById('mt5-empty');
if (!reports.reports || !reports.reports.length) {
if (empty) empty.style.display = '';
if (tbody) tbody.innerHTML = '';
return;
}
if (empty) empty.style.display = 'none';
let html = '';
reports.reports.slice(0,50).forEach(r => {
const profit = parseFloat(r.profit||0);
const statusCls = r.status === 'OPEN' ? 'tag-b' : r.status === 'CLOSED' ? 'tag-g' : 'tag-a';
html += `<tr>
<td style="font-size:9px">${r.signal_id||'—'}</td>
<td class="tc">${r.symbol||'—'}</td>
<td><span class="tp ${(r.direction||r.side||'').toLowerCase()==='long'?'tp-l':'tp-s'}">${(r.direction||r.side||'—').toUpperCase()}</span></td>
<td>${parseFloat(r.open_price||0).toFixed(5)}</td>
<td class="${profit>=0?'pos':'neg'}">${profit>=0?'+':''}${profit.toFixed(2)}</td>
<td><span class="tag ${statusCls}">${r.status||'—'}</span></td>
<td style="font-size:9px">${r.close_reason||'—'}</td>
<td style="font-size:9px">${(r.open_time||r.close_time||'—').slice(0,16)}</td>
</tr>`;
});
if (tbody) tbody.innerHTML = html;
}
}
// ═══ FEATURES ═══════════════════════════════════════════════════════════════
async function loadFeatureImportance() {
const d = await api('/api/feature-importance');
const empty = document.getElementById('feat-imp-empty');
const updated = document.getElementById('feat-imp-updated');
if (!d || !d.features || !d.features.length) {
if (empty) empty.style.display = '';
return;
}
if (empty) empty.style.display = 'none';
if (updated && d.updated_at) updated.textContent = 'Mis à jour : ' + d.updated_at.slice(0,16).replace('T',' ') + ' UTC';
const top = d.features.slice(0,15).reverse();
const maxV = Math.max(...top.map(f => f.importance));
const colors = top.map(f => {
const r = f.importance / maxV;
return r > 0.7 ? '#00E676' : r > 0.4 ? '#2979FF' : '#546E7A';
});
if (featImpChart) { featImpChart.destroy(); featImpChart = null; }
const ctx = document.getElementById('featImpChart');
if (!ctx) return;
featImpChart = new Chart(ctx, {
type: 'bar',
data: {
labels: top.map(f => f.feature),
datasets: [{
data: top.map(f => f.importance),
backgroundColor: colors, borderRadius: 3, borderSkipped: false,
}]
},
options: {
indexAxis: 'y', responsive: true, maintainAspectRatio: false,
plugins: {legend: {display: false}, tooltip: {callbacks: {label: c => ' gain: '+Math.round(c.parsed.x).toLocaleString()}}},
scales: {
x: {grid:{color:'rgba(255,255,255,.04)'}, ticks:{color:'#888',font:{size:9}}},
y: {grid:{display:false}, ticks:{color:'#ccc',font:{size:10},autoSkip:false}}
}
}
});
}
// ═══ CONFIG ══════════════════════════════════════════════════════════════════
async function loadConfig() {
const d = await api('/api/config');
if (!d) return;
Object.keys(d).forEach(k => {
const el = document.getElementById('cfg-' + k);
if (el) el.value = d[k];
});
}
async function saveConfig() {
const keys = [
'PAPER_MODE','PAPER_INITIAL_BALANCE','LEVERAGE','MAX_POSITIONS',
'RISK_PER_TRADE','MAX_DAILY_LOSS_PCT','STOP_LOSS_PCT','TAKE_PROFIT_PCT',
'MAIN_LOOP_SECONDS','CANDLE_INTERVAL','MIN_LOT_SIZE','MAX_LOT_SIZE',
'MIN_CONFIDENCE','USE_ENSEMBLE','USE_RL_AGENT','RL_MODE',
'RL_OVERRIDE_THRESHOLD','RL_CONFIDENCE_BOOST','RL_FINETUNE_STEPS',
'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_BRIDGE_ENABLED','MT5_FILES_PATH','MT5_SYMBOL_SUFFIX','MT5_SERVER',
'MT5_SIGNAL_TIMEOUT','MT5_POLL_INTERVAL',
'SESSION_FILTER_ENABLED','SESSION_LONDON_START','SESSION_LONDON_END',
'SESSION_NY_START','SESSION_NY_END',
];
const body = {};
keys.forEach(k => {
const el = document.getElementById('cfg-' + k);
if (el && el.value !== '') body[k] = el.value;
});
const r = await apiPost('/api/config', body);
const banner = document.getElementById('cfg-restart-banner');
if (banner) banner.style.display = (r && r.restart_needed) ? '' : 'none';
}
// ═══ TERMINAL ════════════════════════════════════════════════════════════════
function clearLog() {
const el = document.getElementById('terminal');
if (el) el.innerHTML = '';
_logCount = 0;
setEl('nb-log', '0');
}
function appendLog(entry) {
if (entry.ping) return;
const el = document.getElementById('terminal');
if (!el) return;
const p = entry.p || 'UI';
const cls = {
BOT: 'log-bot', TRAIN: 'log-train', DOWNLOAD: 'log-download',
BACKTEST: 'log-backtest', FINETUNE: 'log-finetune', UI: 'log-ui',
}[p.toUpperCase()] || '';
const errCls = /error|erreur|fail|exception/i.test(entry.m) ? ' log-err' : /ok|success|terminé|✅/i.test(entry.m) ? ' log-ok' : /warn|warning/i.test(entry.m) ? ' log-warn' : '';
const line = document.createElement('div');
line.className = 'log-line ' + cls + errCls;
line.textContent = `[${entry.t}][${p}] ${entry.m}`;
el.appendChild(line);
_logCount++;
setEl('nb-log', _logCount > 999 ? '999+' : _logCount);
if ((document.getElementById('auto-scroll') || {}).checked) {
el.scrollTop = el.scrollHeight;
}
if (el.children.length > 500) el.removeChild(el.firstChild);
}
// ═══ SSE LOG STREAM ══════════════════════════════════════════════════════════
let _sseResetVersion = 0;
(function startSSE() {
const evtSrc = new EventSource('/api/logs/stream');
evtSrc.onmessage = e => {
try {
const entry = JSON.parse(e.data);
// Détecter un signal reset du serveur — vider le terminal avant d'afficher le msg
if (entry.reset !== undefined && entry.reset !== _sseResetVersion) {
_sseResetVersion = entry.reset;
clearLogs();
// Rafraîchir toutes les sections
if (typeof loadTrades === 'function') loadTrades();
if (typeof loadBacktest === 'function') loadBacktest();
if (typeof loadRL === 'function') loadRL();
if (typeof refreshStatus === 'function') refreshStatus();
}
appendLog(entry);
} catch {}
};
evtSrc.onerror = () => setTimeout(startSSE, 5000);
})();
// ═══ POLLING ═════════════════════════════════════════════════════════════════
loadStatus();
setInterval(loadStatus, 10000);
</script>
</body>
</html>"""
# ═══════════════════════════════════════════════════════════════════════════════
@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")