2824 lines
135 KiB
Python
2824 lines
135 KiB
Python
"""
|
||
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>2–6h</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 (~2–4h 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 (2–6h) ?\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")
|