MetaTrader 5 ne tourne que sous Windows. La bibliothèque Python officielle ne marche que sous Windows. Le langage de script MQL5 est une contrefaçon de C++ datée de 2005 qui te donne envie de t’arracher les yeux avec une fourchette rouillée. Et si tu veux faire quoi que ce soit de programmatique avec, tirer des bougies, passer des ordres, vérifier des positions, on attend de toi soit que tu écrives du MQL5, soit que tu lances Python sur une machine Windows avec le terminal ouvert.
Moi je voulais taper un endpoint HTTP depuis n’importe quelle machine, dans n’importe quel langage, et récupérer du JSON. Comme un être humain normal.
Alors j’ai construit mt5-httpapi. Une vraie VM Windows qui tourne dans Docker via QEMU/KVM, avec le terminal MT5 complet en mode portable et une API REST Flask par-dessus. Pas de Wine, pas de bidouille d’émulation, pas de contournement bancal. Un environnement Windows 11 légitime qui fait tourner le vrai binaire MetaTrader 5, accessible en HTTP/JSON tout bête depuis n’importe où.
Plusieurs brokers. Plusieurs comptes. Chaque terminal a son propre processus d’API dans la VM, et un sidecar nginx toujours actif les met tous derrière un seul port hôte à http://localhost:8888/<broker>/<account>/.... Fais tourner deux challenges FTMO en même temps, ou mélange les brokers, ou lance dix terminaux sur une machine, comme tu veux.
Comment Cette Abomination Fonctionne
Le container fait tourner dockurr/windows, une image Docker qui démarre une VM Windows complète avec la virtualisation matérielle QEMU/KVM. Au premier lancement, elle télécharge tiny11 (un Windows 11 déshabillé, ~4 Go), l’installe, puis met en place Python 3.12 tout seul, installe MetaTrader5, vire toute la merde de Windows, dégage Defender entièrement et démarre le tout.
Après le premier boot (~10 minutes), les démarrages suivants prennent environ une minute. Le container est configuré avec 2 vCPUs, 512 Mo de RAM réelle et 5 Go de swap. Ça a l’air maudit, ça marche très bien. tiny11 plus le script de débloat tourne à ~1.4 Go au repos, et MT5 plus l’API Python n’ajoutent presque rien. Windows et MT5 ne sont pas assez sensibles à la latence pour que le swap se voie.
Un dossier partagé entre l’hôte et la VM Windows (/shared → C:UsersDockerDesktopShared) contient tout: scripts, configs, installeurs de broker, le code du serveur d’API, et les logs. Le script run.sh synchronise tout dans ce dossier, génère les règles NAT iptables pour le port forwarding du container vers la VM, et lance docker-compose.
noVNC sur le port 8006 te donne une vue du bureau Windows dans le navigateur. Utile pour regarder l’installation avancer et confirmer que tout a démarré. Après ça, oublie que l’interface existe et tape juste l’API REST.
Plusieurs Terminaux Derrière Un Seul Port
Changement d’architecture (v4.0+): l’agencement d’origine exposait chaque terminal sur son propre port hôte (6542, 6543, 6544…). Terminé. Tout vit maintenant derrière un seul port hôte (par défaut 127.0.0.1:8888) devant lequel se tient un sidecar nginx toujours actif qui route par préfixe de chemin:
http://localhost:8888/<broker>/<account>/...v4.4 a étendu ça à /<broker>/<account>/<instance>/..., la forme simple /<broker>/<account>/ restant un alias vers l’instance par défaut, donc les URL existantes continuent de marcher. La forme ignore le mode: les terminaux live et backtest se routent pareil.
Un port forwardé depuis l’hôte, une seule surface TLS dont se soucier, une seule règle dans ton pare-feu. Chaque requête tape nginx, le préfixe /<broker>/<account>/ est retiré, et le reste part vers le processus d’API Python de ce terminal-là, à l’intérieur de la VM, via le bridge docker. La config nginx est générée automatiquement depuis config.yaml à chaque make up, donc ajouter ou retirer un terminal ne demande aucun tripotage manuel de reverse-proxy.
Consolidation de la config (v4.0): l’ancienne séparation accounts.json + terminals.json a disparu. Il y a maintenant un seul config/config.yaml (gitignoré) comme source de vérité:
# Bearer token for API auth. Empty = no auth.
api_token: "paste-the-output-of-openssl-rand-hex-32-here"
# VM auto-reboot every N minutes (flushes DWM/VirtIO-GPU state). 0 = disable.
reboot_interval: 30
# Default Strategy Tester timeout. POST /backtest can override per job.
backtest_timeout: "6h"
tailscale:
auth_key: "" # tskey-auth-... — empty disables the tailscale sidecar
login_server: "" # Headscale URL; empty = Tailscale cloud
# Extra pip packages installed in the VM.
requirements: []
# Broker credentials, organized by broker → account name.
accounts:
roboforex:
main:
login: 12345678
server: "RoboForex-Pro"
password: "your_password"
demo:
login: 87654321
server: "RoboForex-Demo"
password: "demo_password"
# Terminal instances — one MT5 + one API process per entry.
terminals:
- broker: roboforex
account: main
port: 6542 # container-internal only, not exposed to host
utc_offset: "3h"
- broker: roboforex
account: demo
port: 6543
utc_offset: "3h"
- broker: roboforex
account: tester
port: 6544
utc_offset: "3h"
mode: backtest # don't auto-launch terminal64.exe — reserved for /backtest jobs
symbol_suffix: ".r" # explicit suffix for tester symbol remap (e.g. EURUSD → EURUSD.r)Le champ broker correspond à la fois à la clé sous accounts: et au nom du fichier d’installeur (mt5setup-roboforex.exe, mt5setup-ftmo.exe). Le terminal de chaque broker s’installe une fois dans <broker>/base/, puis est copié dans <broker>/<account>/ au démarrage, pour que plusieurs comptes du même broker ne se marchent pas dessus.
utc_offset est par terminal parce que les brokers vivent sur des fuseaux bizarres (RoboForex/FTMO en UTC+3, TeleTrade en UTC+2). Chaque timestamp qui passe sur le fil, bougies, ticks, historique, positions, est normalisé en vrai UTC côté serveur. Ton client n’a plus jamais à penser à l’heure locale du broker. port est le port interne au container auquel nginx parle; il n’est pas exposé à l’hôte.
reboot_interval est le nouveau bouton de redémarrage auto de la VM: la pile DWM/VirtIO-GPU de MetaQuotes accumule de l’état côté noyau sur les longs uptimes et finit par devenir bizarre, donc la VM est relancée selon un planning (toutes les 30 minutes par défaut). Mets 0 pour désactiver.
mode par terminal (v4.3): live est le défaut, terminal64.exe reste lancé pour que le SDK MT5 soit initialisé pour les endpoints de trading live. backtest prépare le même répertoire de données portable mais ne lance pas terminal64.exe, laissant le répertoire libre pour un sous-processus de Strategy Tester. MT5 est mono-instance par répertoire de données portable, et c’est là que ça compte: si terminal64.exe tourne déjà, un sous-processus de tester sur le même répertoire sort en silence avec le code 0 et ne produit aucun rapport. Lance un roboforex/tester dédié à côté de ton roboforex/main live et tu as des backtests via HTTP sans flinguer le SDK live.
symbol_suffix s’occupe des brokers qui renomment tout dans le tester. Si le tien utilise EURUSDp, EURUSD.p, EURUSD-mini ou n’importe quel suffixe à la con dans son pool de symboles du Strategy Tester, tu le mets ici et mt5-httpapi l’ajoute automatiquement quand [Tester].Symbol dans l’INI ne l’a pas déjà. Chaîne vide = pas de suffixe. backtest_timeout est la borne haute par défaut de POST /backtest, même grammaire de durée que utc_offset ("6h", "30m", "3h30m", les nombres nus traités comme des heures), surchargeable par job.
Quatre terminaux qui tournent en même temps sur 2 vCPUs avec 512 Mo de RAM réelle: le CPU monte à 100% pendant le démarrage, le temps que tout s’initialise, puis retombe à ~15% au repos. Mémoire totale: 2.1 Go, entièrement absorbée par le swap. Tu pourrais en faire tourner 10+ comme ça sans transpirer, tant que tu ne scrapes pas de l’historique profond sur tous en même temps (MT5 met en cache chaque graphique chargé et ne le relâche jamais; les backfills profonds font exploser la limite de 512 Mo par le plancher).
Installation
Prérequis: hôte Linux avec KVM activé (/dev/kvm), Docker + Compose, ~20 Go de disque, 5 Go de RAM.
# Clone it
git clone https://github.com/psyb0t/mt5-httpapi
cd mt5-httpapi
# Single config file now — copy and edit
cp config/config.yaml.example config/config.yaml
# Set api_token, accounts, terminals
# Drop your broker's MT5 installer
cp ~/Downloads/mt5setup.exe mt5installers/mt5setup-roboforex.exe
# Fire it up
make upLe premier lancement télécharge l’ISO Windows, l’installe, débloate, installe MT5, redémarre deux ou trois fois, puis démarre tout. Ensuite:
make up # start
make down # stop
make logs # tail logs
make status # check VM and API status
make clean # nuke VM disk (keeps ISO)
make distclean # nuke everything including ISOAuthentification
L’API tourne ouverte par défaut. Si tu l’exposes sur un réseau (même local, avec d’autres machines dessus), mets le token dans config/config.yaml:
api_token: "$(openssl rand -hex 32)"Si api_token n’est pas vide, chaque endpoint exige Authorization: Bearer <token>. Chaîne vide = pas d’authentification, ce qui va très bien pour une installation mono-machine où rien d’autre ne peut atteindre le port.
Avec l’authentification activée, mets le token dans ton shell et inclus-le à chaque requête:
export MT5_API_TOKEN=$(grep ^api_token config/config.yaml | awk -F'"' '{print $2}')
curl -H "Authorization: Bearer $MT5_API_TOKEN"
http://localhost:8888/roboforex/main/ping
# {"status": "ok"}Tous les exemples curl ci-dessous supposent qu’il n’y a pas d’authentification. Si tu as configuré un token, ajoute -H "Authorization: Bearer $MT5_API_TOKEN" devant chacun.
L’API
Tous les terminaux sont servis derrière un seul port hôte via nginx. Point d’entrée par défaut: http://localhost:8888 (loopback uniquement). Chaque terminal vit à son propre préfixe de chemin, http://localhost:8888/<broker>/<account>/.... Les exemples ci-dessous utilisent roboforex/main; mets les tiens. GET pour lire, POST pour créer, PUT pour modifier, DELETE pour fermer. Tout en JSON.
Santé et Terminal
# Health check
curl http://localhost:8888/roboforex/main/ping
# {"status": "ok"}
# Last MT5 error
curl http://localhost:8888/roboforex/main/error
# {"code": 1, "message": "Success"}
# Terminal info (connected, trade_allowed, build, company)
curl http://localhost:8888/roboforex/main/terminal
# Force re-init, shutdown, or restart
curl -X POST http://localhost:8888/roboforex/main/terminal/init
curl -X POST http://localhost:8888/roboforex/main/terminal/shutdown
curl -X POST http://localhost:8888/roboforex/main/terminal/restartL’API s’initialise toute seule à la première requête. Si MT5 n’est pas encore connecté, un thread de fond réessaie toutes les 30 secondes. Tu n’as quasiment jamais besoin d’appeler /terminal/init à la main.
Un moniteur de santé tourne en fond, toutes les 60 secondes il vérifie que le terminal est vivant, connecté, et que l’algo trading est activé. Si le terminal est mort sur 5 vérifications d’affilée, il redémarre tout seul: il tue le processus, relance le terminal, attend que le journal confirme qu’il est debout, et reconnecte l’API. Tu peux aussi déclencher un redémarrage manuel via POST /terminal/restart.
Ce moniteur vit à l’intérieur de la VM, ce qui veut dire qu’il ne peut réparer que ce que la VM est encore assez en forme pour réparer. v4.13.0 a ajouté l’étage au-dessus: un sidecar vm-watchdog géré par Compose, qui surveille les containers de VM Windows depuis l’hôte, via la socket Docker, et en recrée un par le chemin existant recreate-vm.sh quand il est resté malade assez longtemps pour que ça veuille dire quelque chose. Pas au premier échec. WATCHDOG_MIN_FAILING_STREAK vaut 10 échecs consécutifs par défaut, à 30 secondes d’intervalle, puis il recule exponentiellement, 5 minutes, 15 minutes, une heure, et abandonne après 3 tentatives. Une VM qui se remet toute seule est laissée tranquille.
Le filtre est la partie intéressante. WATCHDOG_IMAGE_FILTER vaut dockurr/windows par défaut, et une valeur vide est refusée plutôt que traitée comme “correspond à tout”, parce que la version qui traite un filtre vide comme un joker est la version qui recrée tous les containers de ton projet à 4h du matin.
Il supervise aussi les sidecars qui partagent l’espace de noms réseau d’une VM (network_mode: service:<vm>), une fois que la VM est en bonne santé de façon continue. Un sidecar échoué dans un espace de noms obsolète est réparé tout seul, sans recréer la VM en dessous. Celui-là est sorti de deux jours d’appels TA morts: la VM allait bien, le sidecar wickworks pointait sur un espace de noms réseau qui n’existait plus, et rien ne l’a remarqué parce que les propres health checks de la VM étaient au vert tout du long. WATCHDOG_WATCH_SIDECARS=0 si tu veux revenir à une récupération VM seule. Il y a 15 variables WATCHDOG_* au total, dont un WATCHDOG_DRY_RUN pour regarder ce qu’il aurait fait avant de le laisser faire quoi que ce soit.
Compte
curl http://localhost:8888/roboforex/main/account{
"login": 12345678,
"balance": 10000.0,
"equity": 10000.0,
"margin": 0.0,
"margin_free": 10000.0,
"leverage": 500,
"currency": "USD",
"trade_allowed": true,
"margin_so_call": 70.0,
"margin_so_so": 20.0
}Données de Marché
# List all symbols (or filter)
curl http://localhost:8888/roboforex/main/symbols
curl "http://localhost:8888/roboforex/main/symbols?group=*USD*"
# Symbol details (bid, ask, spread, contract size, tick value, lot constraints)
curl http://localhost:8888/roboforex/main/symbols/EURUSD
# Latest tick
curl http://localhost:8888/roboforex/main/symbols/EURUSD/tick
# OHLCV candles
curl "http://localhost:8888/roboforex/main/symbols/EURUSD/rates?timeframe=H4&count=100"
# OHLCV + indicators in one shot (server-side TA via wickworks)
curl -X POST "http://localhost:8888/roboforex/main/symbols/EURUSD/rates/ta?timeframe=H1&count=200"
-H "Content-Type: application/json"
-d '{"indicators":{"rsi":true,"macd":true,"bbands":{"length":20,"std":2}}}'
# Tick history
curl "http://localhost:8888/roboforex/main/symbols/EURUSD/ticks?count=100"Unités de temps: M1 M2 M3 M4 M5 M6 M10 M12 M15 M20 M30 H1 H2 H3 H4 H6 H8 H12 D1 W1 MN1. Le time d’une bougie est son heure d’ouverture, en secondes unix epoch.
Passer des Ordres
# Market buy
curl -X POST http://localhost:8888/roboforex/main/orders
-H "Content-Type: application/json"
-d '{"symbol": "ADAUSD", "type": "BUY", "volume": 1000, "sl": 0.25, "tp": 0.35}'
# Pending buy limit
curl -X POST http://localhost:8888/roboforex/main/orders
-H "Content-Type: application/json"
-d '{"symbol": "ADAUSD", "type": "BUY_LIMIT", "volume": 1000, "price": 0.28, "sl": 0.25, "tp": 0.35}'Champs obligatoires: symbol, type, volume. Le prix se remplit tout seul pour les ordres au marché. Types d’ordres: BUY, SELL, BUY_LIMIT, SELL_LIMIT, BUY_STOP, SELL_STOP, BUY_STOP_LIMIT, SELL_STOP_LIMIT. Politiques de remplissage: FOK, IOC (défaut), RETURN. Expiration: GTC (défaut), DAY, SPECIFIED, SPECIFIED_DAY.
Chaque opération de trading renvoie un résultat avec un retcode, 10009 veut dire succès, tout le reste veut dire que quelque chose a foiré. Utilise GET /error pour débugger.
Gérer Positions et Ordres
# List open positions
curl http://localhost:8888/roboforex/main/positions
curl "http://localhost:8888/roboforex/main/positions?symbol=EURUSD"
# Move SL/TP
curl -X PUT http://localhost:8888/roboforex/main/positions/12345
-H "Content-Type: application/json"
-d '{"sl": 0.27, "tp": 0.36}'
# Close full position
curl -X DELETE http://localhost:8888/roboforex/main/positions/12345
# Partial close
curl -X DELETE http://localhost:8888/roboforex/main/positions/12345
-H "Content-Type: application/json"
-d '{"volume": 500}'
# Modify pending order
curl -X PUT http://localhost:8888/roboforex/main/orders/67890
-H "Content-Type: application/json"
-d '{"price": 0.29, "sl": 0.26, "tp": 0.36}'
# Cancel pending order
curl -X DELETE http://localhost:8888/roboforex/main/orders/67890Historique
# Order history (last 24h)
curl "http://localhost:8888/roboforex/main/history/orders?from=$(date -d '1 day ago' +%s)&to=$(date +%s)"
# Deal history (last 24h)
curl "http://localhost:8888/roboforex/main/history/deals?from=$(date -d '1 day ago' +%s)&to=$(date +%s)"from et to sont obligatoires, en secondes unix epoch. Les deals ont un entry (0 = ouverture, 1 = fermeture) et un profit (0 pour les entrées, P&L réalisé pour les sorties).
Analyse Technique
Deux façons de coller des indicateurs sur tes bougies, côté serveur ou à la main.
TA côté serveur: POST /symbols/:symbol/rates/ta (v4.1+)
Un seul appel HTTP, les bougies reviennent déjà enrichies d’indicateurs. Le container mt5 embarque un sidecar TA wickworks enfermé dans l’espace de noms réseau de mt5, sans ports publiés, sans déploiement séparé, sans trafic externe. Mêmes paramètres de query que GET /rates (timeframe, count, from, to), plus un corps JSON qui dit à wickworks quels indicateurs calculer:
curl -X POST "$MT5_API_URL/symbols/EURUSD/rates/ta?timeframe=H1&count=200"
-H "Content-Type: application/json"
-d '{
"indicators": {
"rsi": true,
"rsi21": {"type": "rsi", "length": 21},
"macd": true,
"bbands": {"length": 20, "std": 2}
}
}'La réponse porte les bougies brutes et la sortie de wickworks côte à côte:
{
"symbol": "EURUSD",
"timeframe": "H1",
"bars": [ { "time": 1771146000, "open": 1.0832, "high": 1.0840, "low": 1.0828, "close": 1.0835, ... } ],
"ta": { "indicators": { "rsi": [...], "macd": {...}, "bbands": {...} } }
}Chaque entrée sous indicators associe une clé de sortie soit à true (les défauts), soit à un objet de paramètres à plat. Tu n’ajoutes "type": "<name>" que si la clé de sortie diffère du nom de l’indicateur, par exemple pour lancer un second RSI sous rsi21. Le catalogue wickworks couvre les suspects habituels (RSI, MACD, bandes de Bollinger, ADX, VWAP, Ichimoku, ATR, Stochastique, MFI, des dizaines de moyennes mobiles) plus les primitives Smart Money (order blocks, fair value gaps, BOS, CHoCH, structure de swing, niveaux S/R). Des primitives uniquement, les signaux interprétatifs (divergences, événements de croisement) restent chez ton consommateur. Liste complète des types, paramètres et formes de sortie sur github.com/psyb0t/docker-wickworks.
L’URL du sidecar se configure via wickworks: dans config.yaml, par défaut http://20.20.20.1:8000/, l’IP de la passerelle dockurr telle que vue depuis l’intérieur de la VM Windows.
TA côté client: tu tires les bougies brutes et tu les mouline toi-même
Si tu veux le contrôle total ou que tu es déjà jusqu’au cou dans pandas, le dépôt livre un exemple complet dans examples/python/ qui tire les bougies via GET /rates et les passe dans pandas-ta et smartmoneyconcepts.
Indicateurs inclus: EMA 21, SMA 50/100/200, ATR, RSI, MACD, bandes de Bollinger, MFI, Stochastique, ADX, VWAP, plus les Smart Money Concepts: order blocks, fair value gaps, break of structure, change of character, et niveaux de liquidité.
# TA report with signal detection
python ta.py # EURUSD H4 200 candles (default)
python ta.py BTCUSD H1 100 # custom symbol/timeframe/count
python ta.py ADAUSD D1 200
# 1920x1080 candlestick chart with all overlays
python chart.py ADAUSD
python chart.py BTCUSD H1 100
python chart.py EURUSD D1 200 -o eurusd.pngLe rapport TA affiche les valeurs de la dernière bougie pour chaque indicateur, puis lance la détection de signaux: RSI suracheté/survendu, croisements de l’histogramme MACD, golden/death cross EMA/SMA, sorties de bandes de Bollinger, extrêmes du Stochastique, force de tendance ADX. Le graphique rend des chandeliers sur thème sombre avec moyennes mobiles, bandes de Bollinger, VWAP, overlays SMC (order blocks, FVG, lignes BOS/CHoCH, balayages de liquidité), panneau RSI et panneau MACD. Des PNG de qualité publication en 1920×1080.
Les modules d’indicateurs et de signaux sont pensés comme des briques. Tu importes add_rsi(df) ou detect_signals(df) dans tes propres scripts et tu utilises l’API comme source de données. Tu tires les bougies, tu appliques l’analyse que tu veux, tu passes des ordres, le tout depuis un script Python qui tourne sur n’importe quelle machine.
Client Go: GetRatesTA (v4.2+)
Le client Go typé dans clients/go/ enveloppe l’endpoint wickworks avec sa propre méthode:
resp, err := c.GetRatesTA(ctx, "EURUSD",
mt5.RatesQuery{Timeframe: "H1", Count: 200},
map[string]any{
"indicators": map[string]any{
"rsi": true,
"macd": true,
"bbands": map[string]any{"length": 20, "std": 2},
},
},
)Le même client couvre GetRates, GetTicks, GetAccount, CreateOrder, ListPositions, UpdatePosition, ClosePosition, les endpoints d’historique et les appels de cycle de vie du terminal. Les erreurs se mappent sur des sentinelles typées contre lesquelles tu peux faire errors.Is(), aichteeteapee.ErrUnauthorized pour un 401, aichteeteapee.ErrBadRequest pour un 400, et un mt5httpapi.ErrNotInitialized dédié pour le 503 que tu verras tant que la VM démarre encore et que le SDK MT5 n’est pas prêt.
Strategy Tester / Backtesting (v4.3+)
Backtester un EA sur MT5, normalement, ça veut dire cliquer dans la fenêtre du Strategy Tester comme un animal. v4.3 câble tout ça dans l’API HTTP: tu envoies un INI, un expert .ex5, éventuellement un fichier de paramètres .set, tu reçois un jobId, tu interroges jusqu’à la fin, tu récupères le rapport HTML et le log du terminal. Le même workflow que dans l’interface, mais sans tête et scriptable.
Pourquoi mode: backtest sur son propre terminal. MT5 est mono-instance par répertoire de données portable. Si terminal64.exe y tourne déjà pour soutenir le SDK live, un sous-processus de Strategy Tester sur le même répertoire sort en silence avec le code 0 et tu n’as rien. Donc tu dédies un terminal dans config.yaml avec mode: backtest, il reçoit la même installation portable, les mêmes identifiants de broker, mais pas de terminal64.exe lancé automatiquement. Le sous-processus de tester a l’usage exclusif du répertoire de données et produit vraiment un rapport. Lance-le à côté de tes terminaux live; ils ne se voient pas.
Workflow en deux temps. D’abord POST /backtest/build-ini transforme une petite spec JSON en un tester.ini complet (pas d’identifiants, pas de résolution de chemin d’expert, c’est un helper sans état, tu peux aussi écrire l’INI à la main). Ensuite POST /backtest prend un upload multipart de l’INI plus l’expert, met le job en file, et renvoie un jobId.
export URL=http://localhost:8888/roboforex/tester
export TOK=$MT5_API_TOKEN
# 1. Build the INI: 5-year NZDJPY M15 open-prices run with 5 ms latency.
curl -sS -X POST "$URL/backtest/build-ini"
-H "Authorization: Bearer $TOK" -H "Content-Type: application/json"
-d '{
"symbol": "NZDJPY",
"timeframe": "M15",
"expert": "EA Studio NZDJPY M15 1615044595.ex5",
"lastYears": 5,
"modelling": "open-prices",
"latencyMs": 5,
"expertParameters": "ea studio nzdjpy m15 1615044595.set"
}' > tester.ini
# 2. Submit using a host-managed expert + set file from assets/.
JOB=$(curl -sS -X POST "$URL/backtest"
-H "Authorization: Bearer $TOK"
-F "[email protected]"
-F "expert_name=EA Studio NZDJPY M15 1615044595.ex5"
-F "set_name=ea studio nzdjpy m15 1615044595.set"
| jq -r .jobId)
# 3. Poll until done.
while :; do
STATUS=$(curl -sS -H "Authorization: Bearer $TOK" "$URL/backtest/$JOB" | jq -r .status)
echo "$STATUS"
[[ "$STATUS" == completed || "$STATUS" == failed ]] && break
sleep 30
done
# 4. Fetch the report + terminal log.
curl -sS -H "Authorization: Bearer $TOK" "$URL/backtest/$JOB/report" -o report.htm
curl -sS -H "Authorization: Bearer $TOK" "$URL/backtest/$JOB/log" -o tester.logL’expert et le fichier de set peuvent être envoyés inline (préférable pour les runs ponctuels) ou référencés par nom depuis un pool géré côté hôte, assets/experts/*.ex5 et assets/sets/*.set, montés en lecture seule dans la VM à /shared/assets. Le path traversal dans expert_name / set_name est rejeté.
Ce que le serveur fait pour toi. [Common] Login / Password / Server dans l’INI envoyé sont écrasés par les identifiants de config.yaml pour ce broker/compte, donc l’INI que tu envoies n’a pas besoin de connaître tes vrais identifiants et tu ne les commites pas par accident dans un dépôt. Le chemin de l’expert est réécrit en Uploaded<basename>. Le fichier de set reçoit un espace de noms par job pour éviter les collisions. Si ton broker utilise des symboles suffixés (EURUSDp, EURUSD.p, EURUSD-mini), le symbol_suffix configuré est ajouté automatiquement à [Tester].Symbol quand il manque.
Le payload de statut. GET /backtest/<jobId> renvoie un status ∈ queued / running / completed / failed. Une fois terminé, un objet summary est extrait du rapport HTML, netProfit, profitFactor, recoveryFactor, expectedPayoff, sharpeRatio, maxDrawdown, totalTrades, profitTrades, lossTrades, pour que tu puisses classer les runs par programme sans parser du HTML toi-même.
Concurrence. Un seul tester tourne à la fois par processus d’API, sérialisé par un verrou interne. Les soumissions supplémentaires font la queue. Le timeout par défaut vient de backtest_timeout dans config.yaml (6h par défaut s’il n’est pas défini); surcharge par job via le champ multipart timeout. Si l’API redémarre avec un backtest en vol, le job orphelin est marqué failed avec API restarted before completion au démarrage suivant, pas de zombies.
Optimisation et Instances de Terminal (v4.4)
v4.3 t’a donné des backtests en une passe avec un rapport HTML. C’est la moitié facile. Les runs d’optimisation sont une autre bête: Tester.Optimization à 1 ou 2 sort un rapport XML d’optimisation, et le mode 3, “tous les symboles”, sort un .symbols.xml plus un cache binaire .opt que MT5 n’expose par aucune API documentée. v4.4 parse les deux.
POST /backtest/build-ini prend maintenant optimization (0..3) et optimizationCriterion (0..7) à côté des champs une-passe de v4.3. Et il y a un nouveau POST /backtest/build-set qui sort un fichier .set de Strategy Tester depuis une liste JSON de paramètres, valeurs fixes et plages d’optimisation (start / step / stop / optimize), dans le format natif de MT5 name=value||.... Tu peux donc générer le balayage de paramètres par programme au lieu d’éditer des fichiers de set à la main dans l’interface.
GET /backtest/<jobId> gagne trois champs sur les runs d’optimisation: optimizationType (0..3), optimizationResults (les N meilleures passes, triées) et optimizationCache, les métadonnées du parse du .opt: nom de profil, offsets d’octets, build MT5, nombre de symboles. Les trois valent None sur les runs qui ne sont pas d’optimisation. POST /backtest prend un topPasses optionnel (1..500, 50 par défaut) pour plafonner le nombre de lignes renvoyées.
Le parseur de cache est la partie marrante. mt5api/backtest/cache_parser.py fait de la rétro-ingénierie sur le format binaire .opt de MT5. Il est piloté par profils plutôt que codé en dur: des dispositions candidates d’offsets d’octets sont notées contre le vrai fichier et la meilleure gagne. Quand MT5 sort un build qui décale la disposition, tu ajoutes un candidat dans OPT_CACHE_PROFILE_CANDIDATES, pas de chemins de code par build, pas de reniflage de version.
Tail de log en direct. GET /backtest/<jobId>/tail?lines=N fusionne le log du run (stdout de terminal64.exe), le journal du terminal MT5 et le sous-log du Strategy Tester en un seul flux. Borné à 10..1000 lignes. Marche pendant que le job est en file, en cours ou terminé, donc tu peux regarder une optimisation de six heures au lieu de fixer status: running en espérant.
Instances de terminal. Une entrée de terminals[] prend maintenant un nom d’instance optionnel, qui permet à la même paire broker/compte d’apparaître plusieurs fois, avec des ports différents et des répertoires de données portables différents. C’est tout l’intérêt: un terminal live peut coexister avec un ou plusieurs terminaux de backtest sur le même login. Omets-le et il vaut default, ce qui garde l’alias de route historique. Plusieurs entrées sur une même paire broker/compte ont besoin de valeurs d’instance distinctes, sinon elles se percutent sur terminals/<broker>/<account>/<instance>/.
Dimensionnement de Position
L’endpoint de symbole te donne tout ce qu’il faut pour calculer des tailles de position correctes:
risk_amount = balance * risk_pct
sl_distance = ATR * multiplier
ticks_in_sl = sl_distance / trade_tick_size
risk_per_lot = ticks_in_sl * trade_tick_value
volume = risk_amount / risk_per_lotTu arrondis à l’inférieur sur volume_step, tu bornes à [volume_min, volume_max]. Vérification de bon sens: volume * trade_contract_size * price devrait avoir du sens par rapport à ton solde. Un lot d’EURUSD, c’est 100 000 EUR, pas 1 EUR, et trade_contract_size te le dit. Vérifie avant de balancer tout ton compte par accident sur ce que tu croyais être une micro-position.
Skill d’IA
Le dépôt embarque un répertoire .agents/skills/ contenant une définition de skill pour les agents de code IA. Ça marche dans tout ce qui lit .agents/skills/, OpenClaw compris, et ça s’installe nativement dans Claude Code et Codex depuis un seul marketplace partagé:
claude plugin marketplace add psyb0t/agents
claude plugin install mt5-httpapi@psyb0tCodex utilise le même marketplace avec un autre verbe, codex plugin marketplace add psyb0t/agents puis codex plugin add mt5-httpapi@psyb0t, parce que codex plugin install n’existe pas. Codex récupère aussi le skill tout seul dans un checkout du dépôt, vu qu’il scanne .agents/skills/ nativement et qu’il n’y a aucune installation impliquée. Tu le pointes sur ton instance qui tourne via MT5_API_URL, tu ajoutes MT5_API_TOKEN si l’auth est active, et l’agent reçoit la référence d’API complète, la checklist de sécurité avant trade, les formules de dimensionnement de position et les schémas d’usage. Il sait quels endpoints existent, quels champs vérifier avant de passer un ordre, et comment interpréter les résultats.
Ce qui veut dire que tu peux dire à ton agent IA “achète 0.1 lot d’EURUSD avec un stop loss à 2 ATR” et il a tout ce qu’il faut pour tirer les infos du symbole, calculer le prix du SL, passer l’ordre et vérifier le résultat. Aucune documentation d’API à lire à la main, le skill lui donne le manuel complet.
MCP: Un Endpoint, Tous les Terminaux
Un skill apprend à un agent à conduire l’API REST. v4.7 est allé plus loin et lui a permis de sauter complètement la couche HTTP: chaque terminal monte son propre serveur MCP à /<broker>/<account>/mcp/, à côté de l’API REST, dans le même processus. nginx retire le préfixe et fait suivre le reste tel quel, donc l’URL joignable est simplement la base normale du terminal plus /mcp/.
Ça a démarré comme un passthrough générique, un seul outil request capable d’appeler n’importe quel endpoint, et v4.8 l’a remplacé par ~24 outils typés dédiés groupés par famille: données de marché, compte, positions, ordres, historique, terminal, backtest. Le nom de chaque outil, ses paramètres typés et sa description sont ce que l’agent lit, donc plus de devinettes sur des chemins bruts. Chaque outil passe exactement par le même handler, la même auth et le même verrouillage MT5 qu’une vraie requête HTTP. Le request générique plus un catalogue endpoints sont restés en filet de sécurité. Une omission délibérée: soumettre un backtest est un upload multipart, donc ça reste en REST uniquement, get_backtest interroge statut, rapport et log, mais les nouveaux runs passent par POST /backtest.
Ce qui laissait une limitation franchement pénible. Une session MCP a un catalogue d’outils figé, donc il n’y a pas de place par appel pour nommer un compte, une session était liée en permanence au terminal auquel elle s’était connectée. Six terminaux voulaient dire six sessions. v4.9 règle ça avec un service mcpunifier: un container Linux posé à côté de la VM Windows, qui sert ces mêmes outils sur un unique /mcp avec des paramètres broker et account, plus un outil list_terminals pour qu’un appelant découvre ce qui est configuré et quels comptes sont vivants avant de faire quoi que ce soit.
L’URL que tu donnes au client décide de ton rayon d’explosion:
http://host:8888/<broker>/<account>/mcp/ one terminal, no account param to get wrong
http://host:8888/mcp/ every terminal, + broker/account + list_terminalsL’unificateur lit le même config/config.yaml qui génère le routage nginx, ce qui veut dire qu’il ne peut physiquement pas router là où nginx ne route pas, et il expédie directement vers le port de chaque terminal. La table de routage se résout une fois au démarrage et n’est jamais resondée, donc le service ne reste jamais à attendre un terminal, un terminal en carafe ne fait échouer que les appels qui le nomment au lieu d’emporter les autres, et chaque réponse réussie porte le terminal qui a réellement répondu. Demande une paire broker/compte non configurée et tu es refusé avec la liste de ce que tu aurais pu demander, plutôt que routé en silence vers quelque chose de plausible mais faux. Rien de ce qui existait n’a changé: les endpoints par terminal et toute la surface REST sont intacts.
v4.9.1 est le genre de bug qui mérite d’être écrit quelque part. La config nginx générée par v4.9.0 contenait un proxy_pass http://mcpunifier:6600/ littéral, et nginx résout un nom d’hôte d’upstream littéral pendant qu’il parse la config, pas au moment de la requête. Donc sur toute installation sans ce container, nginx s’arrêtait avec host not found in upstream "mcpunifier", et chaque route par terminal plus toute l’API REST derrière partaient en 502. Comme docker-compose.yml est gitignoré, tirer v4.9.0 te donnait le nouveau générateur sans le service qu’il référence, et le redémarrage suivant mettait la stack par terre. Maintenant un unificateur absent n’est qu’un unificateur absent. v4.9.2 a ajouté un harnais de bout en bout pour ça sous forme de script shell, que v4.10 a ensuite retiré, ses sept assertions ayant migré telles quelles dans tests/integration/test_mcpunifier.py, si bien que le projet fait tourner un seul harnais d’intégration dans un seul langage au lieu d’un script shell posé à côté. C’est make test-integration maintenant, une suite pytest adossée à des containers qui démarre un vrai nginx contre la config générée, avec une VM volontairement absente.
Et il s’avère que ce bug nginx avait un grand frère. La même résolution à l’analyse du proxy_pass littéral qui a mis la stack par terre pour un unificateur absent le faisait aussi pour les routes de terminal: un seul container de VM absent et nginx ne démarrait pas du tout, entraînant avec lui les routes de toutes les VM en bonne santé, l’API REST et /mcp/. v4.10 a fait en sorte que les routes de terminal résolvent leur upstream par requête, ce qui a aussi rendu la suite possible.
v4.10 est passé au multi-VM. Un vms.yaml déclare les ressources de chaque VM Windows et chaque terminal se lie à l’une d’elles par un nouveau champ vm:; config_helper.py génère des routes nginx visant le container de la VM propriétaire, run.sh boucle sur chaque VM pour le DNAT et les fichiers de groupe par VM, et docker-compose.yml est rendu depuis un template Jinja. Pas de vms.yaml veut dire mono-VM, et un terminal sans champ vm: route vers mt5, donc les installations existantes ne voient rien. La même version a ajouté des tests de contrat pour les handlers qui déplacent de l’argent: ils pilotent les vraies routes Flask avec le SDK MT5 simulé à la couture m() et vérifient la requête exacte qui arriverait à order_send, un BUY au marché prix ask, un SELL au bid, une fermeture partielle n’envoyant que le volume demandé, une modification sl seule préservant le tp existant. Chaque chemin d’échec vérifie aussi que order_send n’a jamais été appelé, parce qu’un handler qui plante après l’envoi a déjà tradé. La CI fait maintenant tourner tout ça sur les pushes et les PR; pipeline.yml ne se déclenchait avant que sur les tags v*, ce qui voulait dire que la suite sous tests/ n’avait jamais tourné en CI.
v4.11 a comblé le dernier écart entre les deux catalogues MCP: les outils typés par terminal et les outils unifiés exposent désormais des paramètres de plage from/to identiques pour les ticks et les rates TA, avec des tests de schéma et de parité qui couvrent les deux.
Le Débloat
La VM Windows passe par un débloat agressif au premier boot. Désactiver toutes les animations, la transparence, le fond d’écran. Tuer SysMain, l’audio, le spouleur, la recherche, la télémétrie, et une cinquantaine d’autres services inutiles. Retirer Windows Defender entièrement, pas le désactiver, le retirer. Prendre possession des répertoires de Defender et supprimer les binaires. Dégager toutes les saloperies qui violent ta vie privée: identifiant publicitaire, historique d’activité, données de diagnostic, toutes les permissions de capacités. Désactiver chaque tâche planifiée d’espionnage Microsoft. Mettre la priorité processeur sur le premier plan, réduire les délais de kill, désactiver les timestamps NTFS.
Le résultat est un Windows 11 qui démarre vite, reste bas au repos, et ne téléphone pas à Microsoft toutes les 30 secondes. Juste assez d’OS pour faire tourner MT5 et l’API Python.
Chaque Binaire Vendorisé Doit Maintenant se Déclarer
Un dépôt qui démarre une VM Windows et lui arrache Defender accumule des exécutables. Pas beaucoup, mais ceux qu’il a sont exactement ceux que tu voudrais le moins prendre sur parole, et ils traînaient dans l’arbre sans avoir été examinés.
v4.12.0 a ajouté make verify-binaries. Tout exécutable suivi doit être déclaré dans assets/binaries.lock.json avec son sha256, sa source amont et l’état de sa signature. Un binaire non déclaré fait échouer le build. Un binaire modifié fait échouer le build. Une signature dégradée fait échouer le build. Ça tourne en premier dans make test, donc la CI l’impose à chaque PR plutôt que quand quelqu’un y pense.
L’intérêt d’une barrière comme ça, ce n’est pas la règle, c’est ce que la règle trouve à l’instant où tu l’allumes. Ici, elle a immédiatement documenté scripts/defender-remover/PowerRun.exe:
"path": "scripts/defender-remover/PowerRun.exe",
"product": "PowerRun",
"vendor": "Sordum Software",
"signature": "malformed",
"note": "REPACKED, NOT PRISTINE..."Il est arrivé vendorisé à l’intérieur du kit defender-remover plutôt que directement de chez Sordum. Son répertoire de certificats n’est pas un WIN_CERTIFICATE bien formé, une longueur déclarée de 776284822, une révision 0xc496, un type 14951, face à un 0x200 et un 2 attendus, et son hash ne correspond à aucune release Sordum amont, donc la signature ne peut être vérifiée contre rien.
Pour être clair sur ce que ça veut dire et ne veut pas dire: il n’est pas connu comme malveillant. Plein d’outillage repackagé ressemble à ça. Ce qui a changé, c’est que ce n’est plus silencieux, l’état est écrit noir sur blanc, dans le dépôt, à côté du fichier, et la CI échoue s’il bouge un jour. Un binaire inexpliqué que tu connais est un risque différent d’un binaire inexpliqué que tu ne connais pas.
La même version a fait passer la suite unitaire de 244 tests à 379. Tout ce qui ne tournait auparavant que contre un terminal vivant tourne maintenant en CI, en pilotant la vraie app Flask contre un SDK scripté, si bien que mt5client, le moniteur et le client Go ont eu leur toute première couverture.
Logs
Tout se déverse dans data/metatrader5/logs/ côté hôte:
- install.log: la progression de l’installation de MT5
- start-mt5.log: le log de la séquence de boot
- pip.log: l’installation des paquets Python
- api-<broker>-<account>.log: les logs d’API par terminal
- full.log: la lance à incendie concaténée de tout ce qui précède plus les entrées du Windows Event Log tirées depuis l’intérieur de la VM. C’est celui qui attrape les kills OOM et les processus tués en silence par Defender, qui n’apparaissent nulle part ailleurs.
Un sidecar de rotation de logs séparé tourne à côté de la VM et fait tourner tout ça quotidiennement avec 7 jours de rétention. Fini les fichiers de log de 4 Go qui te bouffent le disque après une semaine de stack allumée. Quand ça pète, full.log est le premier endroit où regarder, chronologique, un seul fichier, tout dans un seul flux.
Sidecar Tailscale
Exposer publiquement une API de trading, c’est demander à se faire braquer. La plupart des gens veulent ce truc joignable depuis leur portable et de nulle part ailleurs. Il y a donc un sidecar Tailscale intégré qui rejoint ton tailnet et sert l’API sur un simple nom d’hôte MagicDNS:
http://mt5-httpapi/roboforex/main/account
http://mt5-httpapi/roboforex/main/symbols/EURUSD/rates?count=100
http://mt5-httpapi/ftmo/challenge1/positionsTu mets la clé d’authentification dans config.yaml, tu décommentes le bloc tailscale dans docker-compose.yml, make up. Marche avec Tailscale standard et avec Headscale auto-hébergé (pour ce dernier, tu règles login_server). HTTP simple par choix, la couche wireguard chiffre déjà tout à l’intérieur du tailnet, et les noms d’hôte MagicDNS nus n’ont de toute façon pas de certificats TLS correspondants.
Le sidecar tourne dans son propre netns (mode bridge, pas le réseau de l’hôte), donc il obtient sa propre identité tailnet. Tes ACL se limitent au nœud du sidecar, le Tailscale de l’hôte (s’il en a un) reste complètement hors du coup, et tout trafic à destination du tailnet depuis le sidecar passe par sa propre interface tailscale0, pas celle de l’hôte. Tailscale Serve écoute sur le port 80 à l’intérieur du netns et fait suivre vers le sidecar nginx toujours actif via le réseau interne de docker. L’état persiste dans .data/tailscale/state/, donc make down / make up réutilise la session existante, la clé d’authentification n’étant consommée qu’au premier login.
Le token d’API (s’il est défini) s’applique toujours par-dessus, Tailscale contrôle l’accessibilité réseau, le token bearer contrôle l’accès applicatif. Défense en couches.
Cloudflare Tunnel (Quand Tu as Vraiment Besoin de Public)
Si tu as réellement besoin que ce truc soit joignable depuis l’internet ouvert, disons pour le brancher sur un bot hébergé ou un frontend sur Vercel, il y a une option Cloudflare Tunnel. cloudflared appelle vers l’extérieur jusqu’au bord du réseau Cloudflare et fait suivre vers le sidecar nginx toujours actif. Un tunnel, un nom d’hôte, chaque terminal joignable derrière /<broker>/<account>/:
https://mt5-api.yourdomain.com/roboforex/main/account
https://mt5-api.yourdomain.com/ftmo/challenge1/positionsAucun port ouvert dans le pare-feu. Aucun perçage de NAT. Aucun certificat à gérer, Cloudflare termine le TLS au bord gratuitement sous leur Universal SSL. Mise en place: tu installes cloudflared sur l’hôte une fois, tu crées un tunnel, tu routes un nom d’hôte dessus, tu déposes les identifiants dans .data/cloudflared/, tu décommentes le bloc cloudflared dans compose, make up.
Traite le nom d’hôte public comme hostile et règle toujours api_token dans config.yaml quand tu utilises ça. Cloudflare contrôle l’accessibilité publique; le token bearer contrôle l’application. Si tu sautes le token ici, quiconque trouve le nom d’hôte peut vider ton compte.
En Résumé
MetaTrader 5 dans Docker avec une API REST. Vraie VM Windows via KVM, pas Wine. Plusieurs brokers et plusieurs comptes qui tournent en même temps sur des ressources minimales. Données de marché complètes, gestion d’ordres, suivi de positions, historique de trades, TA côté serveur via le sidecar wickworks, et un pipeline Strategy Tester complet en HTTP, le tout derrière du JSON tout bête. Plus un client Go typé, un skill d’agent IA et des outils MCP typés, par terminal ou unifiés sur tous les terminaux à la fois, pour laisser un LLM conduire des trades contre l’API sans lui donner la doc à la petite cuillère.
Pas de MQL5. Pas de bureau Windows. Pas de bibliothèques MT5 côté client. Juste curl et c’est parti.
Va le chercher: github.com/psyb0t/mt5-httpapi
Sous licence WTFPL, parce que le trading devrait exiger un avertissement, pas une licence logicielle.