MetaTrader 5 draait alleen op Windows. De officiële Python-bibliotheek werkt alleen op Windows. De scripttaal MQL5 is een C++-imitatie uit 2005 waar je je ogen van uit je kop wil krabben met een roestige vork. En wil je er iets programmatisch mee doen, candles ophalen, orders plaatsen, posities checken, dan wordt van je verwacht dat je of MQL5 schrijft of Python draait op een Windows-bak met de terminal open.
Ik wilde een HTTP-endpoint aantikken vanaf welke machine dan ook, in welke taal dan ook, en JSON terugkrijgen. Als een normaal mens.
Dus bouwde ik mt5-httpapi. Een echte Windows-VM die binnen Docker draait via QEMU/KVM, met de volledige MT5-terminal in portable modus en een Flask REST-API erbovenop. Geen Wine, geen emulatietrucs, geen wankele omwegen. Een legitieme Windows 11-omgeving die de echte MetaTrader 5-binary draait, van overal bereikbaar over kale HTTP/JSON.
Meerdere brokers. Meerdere accounts. Elke terminal krijgt zijn eigen API-proces in de VM, en een altijd draaiende nginx-sidecar zet ze allemaal achter één hostpoort op http://localhost:8888/<broker>/<account>/.... Draai twee FTMO-challenges tegelijk, of meng brokers, of zet tien terminals op één bak, wat je maar nodig hebt.
Hoe Dit Gedrocht Werkt
De container draait dockurr/windows, een Docker-image dat een volledige Windows-VM opstart met QEMU/KVM-hardwarevirtualisatie. Bij de eerste start haalt het tiny11 binnen (een uitgeklede Windows 11, ~4 GB), installeert het, zet daarna zelf Python 3.12 klaar, installeert MetaTrader5, schraapt alle stront uit Windows, gooit Defender er helemaal uit en start alles op.
Na de eerste boot (~10 minuten) duren de volgende starts ongeveer een minuut. De container is ingesteld op 2 vCPUs, 512 MB echt RAM en 5 GB swap. Klinkt vervloekt, werkt prima. tiny11 plus het opschoonscript blijft in rust op ~1.4 GB, en MT5 plus de Python-API doen er nauwelijks iets bij. Windows en MT5 zijn niet gevoelig genoeg voor latency om swap te merken.
Een gedeelde map tussen de host en de Windows-VM (/shared → C:UsersDockerDesktopShared) bewaart alles: scripts, configs, brokerinstallers, de code van de API-server en de logs. Het script run.sh synchroniseert alles die map in, genereert iptables NAT-regels voor port forwarding van de container naar de VM, en trekt docker-compose op.
noVNC op poort 8006 geeft je een blik op het Windows-bureaublad vanuit de browser. Handig om de installatie te zien vorderen en te bevestigen dat alles is opgestart. Daarna vergeet je dat de interface bestaat en tik je alleen de REST-API aan.
Meerdere Terminals Achter Eén Poort
Architectuurwijziging (v4.0+): de oorspronkelijke opzet zette elke terminal op zijn eigen hostpoort (6542, 6543, 6544…). Weg. Alles zit nu achter één hostpoort (standaard 127.0.0.1:8888), met ervoor een altijd draaiende nginx-sidecar die routeert op padprefix:
http://localhost:8888/<broker>/<account>/...v4.4 breidde dat uit naar /<broker>/<account>/<instance>/..., waarbij de kale vorm /<broker>/<account>/ als alias voor de standaardinstantie blijft, dus bestaande URL’s blijven werken. De vorm trekt zich niets aan van de modus: live- en backtestterminals routeren identiek.
Eén poort doorgestuurd vanaf de host, één TLS-oppervlak om je zorgen over te maken, één regel in je firewall. Elk verzoek raakt nginx, het prefix /<broker>/<account>/ wordt eraf geknipt, en de rest gaat door naar het Python-API-proces van precies die terminal, binnen de VM, over de docker-bridge. De nginx-config wordt bij elke make up uit config.yaml gegenereerd, dus een terminal toevoegen of weghalen vraagt geen handwerk aan de reverse proxy.
Config-consolidatie (v4.0): de oude splitsing accounts.json + terminals.json is weg. Er is nu één config/config.yaml (gitignored) als bron van waarheid:
# 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)Het veld broker matcht zowel de sleutel onder accounts: als de bestandsnaam van de installer (mt5setup-roboforex.exe, mt5setup-ftmo.exe). De terminal van elke broker installeert zich één keer naar <broker>/base/ en wordt bij het starten gekopieerd naar <broker>/<account>/, zodat meerdere accounts van dezelfde broker elkaar niet in de weg lopen.
utc_offset staat per terminal omdat brokers in rare tijdzones leven (RoboForex/FTMO op UTC+3, TeleTrade op UTC+2). Elke timestamp die over de lijn gaat, candles, ticks, historie, posities, wordt serverkant genormaliseerd naar echte UTC. Je client hoeft nooit meer aan de lokale tijd van de broker te denken. port is de containerinterne poort waarmee nginx praat; die is niet naar de host opengezet.
reboot_interval is de nieuwe knop voor het automatisch herstarten van de VM: de DWM/VirtIO-GPU-stack van MetaQuotes stapelt bij lange uptimes kernelstate op en wordt uiteindelijk raar, dus de VM wordt volgens schema opnieuw opgestart (standaard elke 30 minuten). Op 0 zetten schakelt het uit.
mode per terminal (v4.3): live is de standaard, terminal64.exe blijft draaien zodat de MT5-SDK geïnitialiseerd is voor de live trading-endpoints. backtest maakt dezelfde portable datamap klaar maar start niet terminal64.exe, en laat de map vrij voor een Strategy Tester-subproces. MT5 is single-instance per portable datamap, en daar zit het hem in: draait terminal64.exe al, dan stopt een testersubproces op dezelfde map stilletjes met code 0 en levert het geen rapport. Draai een aparte roboforex/tester naast je live roboforex/main en je hebt backtests over HTTP zonder de live SDK te slopen.
symbol_suffix vangt de brokers op die alles hernoemen in de tester. Gebruikt die van jou EURUSDp, EURUSD.p, EURUSD-mini of wat voor kloteachtervoegsel dan ook in hun Strategy Tester-symboolpool, dan zet je het hier en plakt mt5-httpapi het er automatisch achter wanneer [Tester].Symbol in de INI het nog niet heeft. Lege string = geen achtervoegsel. backtest_timeout is de standaard bovengrens voor POST /backtest, dezelfde duurgrammatica als bij utc_offset ("6h", "30m", "3h30m", kale getallen gelden als uren), per job te overschrijven.
Vier terminals die tegelijk draaien op 2 vCPUs met 512 MB echt RAM: de CPU schiet naar 100% tijdens het opstarten, terwijl alles initialiseert, en zakt daarna naar ~15% in rust. Totaal geheugen: 2.1 GB, helemaal door swap gedragen. Zo zou je er 10+ kunnen draaien zonder te zweten, zolang je niet op allemaal tegelijk diepe historie zit te schrapen (MT5 cachet elke geladen grafiek en geeft hem nooit meer vrij; diepe backfills knallen de grens van 512 MB door de vloer).
Opzetten
Vereisten: Linux-host met KVM aan (/dev/kvm), Docker + Compose, ~20 GB schijf, 5 GB 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 upDe eerste start haalt de Windows-ISO binnen, installeert hem, schoont op, installeert MT5, herstart een paar keer en start dan alles. Daarna:
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 ISOAuthenticatie
De API draait standaard open. Zet je hem op een netwerk (ook een lokaal netwerk met andere machines erop), zet dan het token in config/config.yaml:
api_token: "$(openssl rand -hex 32)"Is api_token niet leeg, dan eist elk endpoint Authorization: Bearer <token>. Lege string = geen authenticatie, wat prima werkt voor een opstelling op één machine waar niets anders bij de poort kan.
Met authenticatie aan zet je het token in je shell en hang je het aan elk verzoek:
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"}Alle curl-voorbeelden hieronder gaan ervan uit dat er geen authenticatie is. Heb je een token ingesteld, zet dan -H "Authorization: Bearer $MT5_API_TOKEN" voor elk ervan.
De API
Alle terminals worden via nginx achter één hostpoort geserveerd. Standaard ingang: http://localhost:8888 (alleen loopback). Elke terminal leeft op zijn eigen padprefix, http://localhost:8888/<broker>/<account>/.... De voorbeelden hieronder gebruiken roboforex/main; vul je eigen in. GET om te lezen, POST om aan te maken, PUT om te wijzigen, DELETE om te sluiten. Allemaal JSON.
Gezondheid en 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/restartDe API initialiseert zichzelf bij het eerste verzoek. Is MT5 nog niet verbonden, dan probeert een achtergrondthread het elke 30 seconden opnieuw. Je hoeft /terminal/init vrijwel nooit met de hand aan te roepen.
Een health monitor draait op de achtergrond, elke 60 seconden kijkt hij of de terminal leeft, ingelogd is en algo trading aan heeft staan. Is de terminal 5 controles op rij dood, dan herstart hij zichzelf: proces killen, terminal opnieuw starten, wachten tot het journaal bevestigt dat hij op is, en de API opnieuw verbinden. Je kunt ook handmatig een herstart afvuren met POST /terminal/restart.
Die monitor leeft binnen de VM, wat betekent dat hij alleen kan repareren waar de VM nog gezond genoeg voor is. v4.13.0 voegde de verdieping erboven toe: een door Compose beheerde vm-watchdog-sidecar die de Windows-VM-containers vanaf de host in de gaten houdt, via de Docker-socket, en er eentje opnieuw aanmaakt langs het bestaande pad recreate-vm.sh zodra hij lang genoeg ongezond is geweest om iets te betekenen. Niet bij de eerste mislukte controle. WATCHDOG_MIN_FAILING_STREAK staat standaard op 10 mislukkingen op rij met 30 seconden ertussen, daarna loopt hij exponentieel terug, 5 minuten, 15 minuten, een uur, en geeft het op na 3 pogingen. Een VM die uit zichzelf herstelt wordt met rust gelaten.
Het filter is het interessante deel. WATCHDOG_IMAGE_FILTER staat standaard op dockurr/windows, en een lege waarde wordt geweigerd in plaats van gelezen als “matcht alles”, want de versie die een leeg filter als wildcard behandelt is de versie die om 4 uur ‘s nachts elke container in je project opnieuw aanmaakt.
Hij houdt ook toezicht op de sidecars die de netwerknamespace van een VM delen (network_mode: service:<vm>), zodra de VM onafgebroken gezond is geweest. Een sidecar die vastzit in een verouderde namespace wordt op zichzelf gerepareerd, zonder de VM eronder opnieuw aan te maken. Die kwam voort uit twee dagen dode TA-aanroepen: de VM was prima, de wickworks-sidecar wees naar een netwerknamespace die niet meer bestond, en niemand merkte het omdat de eigen health checks van de VM de hele tijd groen stonden. WATCHDOG_WATCH_SIDECARS=0 als je terug wil naar herstel van alleen de VM. Er zijn in totaal 15 WATCHDOG_*-variabelen, waaronder een WATCHDOG_DRY_RUN zodat je kunt zien wat hij gedaan zou hebben voordat je hem iets laat doen.
Account
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
}Marktdata
# 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"Tijdframes: M1 M2 M3 M4 M5 M6 M10 M12 M15 M20 M30 H1 H2 H3 H4 H6 H8 H12 D1 W1 MN1. De time van een candle is zijn openingstijd, in unix epoch-seconden.
Orders Plaatsen
# 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}'Verplichte velden: symbol, type, volume. De prijs vult zichzelf in bij marktorders. Ordertypes: BUY, SELL, BUY_LIMIT, SELL_LIMIT, BUY_STOP, SELL_STOP, BUY_STOP_LIMIT, SELL_STOP_LIMIT. Fill-policies: FOK, IOC (standaard), RETURN. Vervaldatum: GTC (standaard), DAY, SPECIFIED, SPECIFIED_DAY.
Elke handelsoperatie geeft een resultaat terug met een retcode, 10009 betekent geslaagd, al het andere betekent dat er iets misging. Gebruik GET /error om te debuggen.
Posities en Orders Beheren
# 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/67890Historie
# 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 en to zijn verplicht, in unix epoch-seconden. Deals hebben een entry (0 = opening, 1 = sluiting) en een profit (0 bij openingen, gerealiseerde winst of verlies bij sluitingen).
Technische Analyse
Twee manieren om indicatoren op je candles te krijgen, serverkant of zelf doen.
TA op de server: POST /symbols/:symbol/rates/ta (v4.1+)
Eén HTTP-aanroep en de candles komen al verrijkt met indicatoren terug. De mt5-container brengt een wickworks-TA-sidecar mee, opgesloten in de netwerknamespace van mt5, zonder gepubliceerde poorten, zonder aparte uitrol, zonder extern verkeer. Dezelfde queryparameters als GET /rates (timeframe, count, from, to), plus een JSON-body die wickworks vertelt welke indicatoren hij moet rekenen:
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}
}
}'Het antwoord draagt de ruwe bars en de uitvoer van wickworks naast elkaar:
{
"symbol": "EURUSD",
"timeframe": "H1",
"bars": [ { "time": 1771146000, "open": 1.0832, "high": 1.0840, "low": 1.0828, "close": 1.0835, ... } ],
"ta": { "indicators": { "rsi": [...], "macd": {...}, "bbands": {...} } }
}Elke ingang onder indicators koppelt een uitvoersleutel aan ofwel true (de standaardwaarden) ofwel een plat parameterobject. Je voegt "type": "<name>" alleen toe als de uitvoersleutel afwijkt van de naam van de indicator, bijvoorbeeld om een tweede RSI onder rsi21 te draaien. De catalogus van wickworks dekt de gebruikelijke verdachten (RSI, MACD, bollingerbanden, ADX, VWAP, Ichimoku, ATR, Stochastic, MFI, tientallen voortschrijdende gemiddelden) plus Smart Money-primitieven (order blocks, fair value gaps, BOS, CHoCH, swingstructuur, S/R-niveaus). Alleen primitieven, de interpreterende signalen (divergenties, kruisingen) horen bij jouw consument. Volledige lijst met types, parameters en uitvoervormen op github.com/psyb0t/docker-wickworks.
De URL van de sidecar stel je in via wickworks: in config.yaml, standaard http://20.20.20.1:8000/, het gateway-IP van dockurr zoals het vanuit de Windows-VM gezien wordt.
TA aan de clientkant: ruwe candles ophalen en zelf vermalen
Wil je volledige controle of zit je toch al tot je nek in pandas, dan levert de repo een compleet voorbeeld in examples/python/ dat candles ophaalt via GET /rates en ze door pandas-ta en smartmoneyconcepts haalt.
Meegeleverde indicatoren: EMA 21, SMA 50/100/200, ATR, RSI, MACD, bollingerbanden, MFI, Stochastic, ADX, VWAP, plus Smart Money Concepts: order blocks, fair value gaps, break of structure, change of character en liquiditeitsniveaus.
# 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.pngHet TA-rapport drukt voor elke indicator de waarden van de laatste candle af en draait daarna de signaaldetectie: RSI overbought/oversold, kruisingen van het MACD-histogram, EMA/SMA golden en death cross, uitbraken uit de bollingerbanden, extremen van de Stochastic, trendsterkte via ADX. De grafiek rendert candlesticks op een donker thema met voortschrijdende gemiddelden, bollingerbanden, VWAP, SMC-overlays (order blocks, FVG’s, BOS/CHoCH-lijnen, liquidity sweeps), een RSI-paneel en een MACD-paneel. PNG’s van publicatiekwaliteit op 1920×1080.
De indicator- en signaalmodules zijn bedoeld als bouwstenen. Je importeert add_rsi(df) of detect_signals(df) in je eigen scripts en gebruikt de API als databron. Candles ophalen, de analyse toepassen die je wil, trades plaatsen, allemaal vanuit een Python-script op welke machine dan ook.
Go-client: GetRatesTA (v4.2+)
De getypeerde Go-client in clients/go/ verpakt het wickworks-endpoint in zijn eigen methode:
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},
},
},
)Dezelfde client dekt GetRates, GetTicks, GetAccount, CreateOrder, ListPositions, UpdatePosition, ClosePosition, de historie-endpoints en de levenscyclusaanroepen van de terminal. Fouten mappen naar getypeerde sentinels waar je errors.Is() tegenaan kunt gooien, aichteeteapee.ErrUnauthorized voor een 401, aichteeteapee.ErrBadRequest voor een 400, en een eigen mt5httpapi.ErrNotInitialized voor de 503 die je ziet zolang de VM nog opstart en de MT5-SDK niet klaar is.
Strategy Tester / Backtesting (v4.3+)
Een EA backtesten op MT5 betekent normaal gesproken als een beest door het Strategy Tester-venster klikken. v4.3 knoopt het hele ding aan de HTTP-API: je uploadt een INI, een .ex5-expert, eventueel een .set-parameterbestand, krijgt een jobId terug, vraagt net zo lang tot het klaar is, en haalt het HTML-rapport en het terminallog op. Dezelfde workflow als in de GUI, maar zonder kop en scriptbaar.
Waarom mode: backtest op zijn eigen terminal. MT5 is single-instance per portable datamap. Draait terminal64.exe daar al om de live SDK te dragen, dan stopt een Strategy Tester-subproces op dezelfde map stilletjes met code 0 en krijg je niks. Dus wijd je in config.yaml een terminal met mode: backtest, die krijgt dezelfde portable installatie, dezelfde brokergegevens, maar geen automatisch gestarte terminal64.exe. Het testersubproces heeft de datamap exclusief en levert daadwerkelijk een rapport. Draai hem naast je live terminals; ze zien elkaar niet.
Workflow in twee stappen. Eerst maakt POST /backtest/build-ini van een kleine JSON-spec een volledig uitgewerkte tester.ini (geen inloggegevens, geen expertpad-resolutie, het is een hulpje zonder state, je kunt de INI ook met de hand schrijven). Daarna neemt POST /backtest een multipart-upload van de INI plus de expert aan, zet de job in de wachtrij en geeft een jobId terug.
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.logDe expert en het setbestand kun je inline meesturen (aan te raden voor losse runs) of op naam laten refereren vanuit een pool die op de host beheerd wordt, assets/experts/*.ex5 en assets/sets/*.set, read-only in de VM gemount op /shared/assets. Path traversal in expert_name / set_name wordt geweigerd.
Wat de server voor je doet. [Common] Login / Password / Server in de geüploade INI worden overschreven met de gegevens uit config.yaml voor die broker en dat account, dus de INI die je uploadt hoeft je echte gegevens niet te kennen en je commit ze niet per ongeluk naar een repo. Het expertpad wordt herschreven naar Uploaded<basename>. Het setbestand krijgt per job een eigen namespace om botsingen te vermijden. Gebruikt je broker symbolen met een achtervoegsel (EURUSDp, EURUSD.p, EURUSD-mini), dan wordt het ingestelde symbol_suffix automatisch aan [Tester].Symbol geplakt wanneer het ontbreekt.
De statuspayload. GET /backtest/<jobId> geeft status ∈ queued / running / completed / failed terug. Bij voltooiing wordt een summary-object uit het HTML-rapport geparsed, netProfit, profitFactor, recoveryFactor, expectedPayoff, sharpeRatio, maxDrawdown, totalTrades, profitTrades, lossTrades, zodat je runs programmatisch kunt rangschikken zonder zelf HTML te parsen.
Gelijktijdigheid. Er draait per API-proces maar één tester tegelijk, geserialiseerd door een interne lock. Extra inzendingen komen in de wachtrij. De standaardtimeout komt uit backtest_timeout in config.yaml (standaard 6h als hij niet gezet is); per job te overschrijven met het multipart-veld timeout. Herstart de API terwijl er een backtest onderweg is, dan wordt de verweesde job bij de volgende start als failed gemarkeerd met API restarted before completion, geen zombies.
Optimalisatie en Terminalinstanties (v4.4)
v4.3 gaf je backtests in één doorloop met een HTML-rapport. Dat is de makkelijke helft. Optimalisatieruns zijn een ander beest: Tester.Optimization op 1 of 2 spuugt een XML-optimalisatierapport uit, en modus 3, “alle symbolen”, spuugt een .symbols.xml uit plus een binaire .opt-cache die MT5 via geen enkele gedocumenteerde API blootgeeft. v4.4 parseert allebei.
POST /backtest/build-ini neemt nu optimization (0..3) en optimizationCriterion (0..7) naast de velden voor één doorloop uit v4.3. En er is een nieuwe POST /backtest/build-set die uit een JSON-parameterlijst een Strategy Tester-.set-bestand uitspuugt, zowel vaste waarden als optimalisatiebereiken (start / step / stop / optimize), in het eigen formaat van MT5 name=value||.... Je kunt de parametersweep dus programmatisch genereren in plaats van setbestanden met de hand in de GUI te bewerken.
GET /backtest/<jobId> krijgt er bij optimalisatieruns drie velden bij: optimizationType (0..3), optimizationResults (de beste N doorlopen, gesorteerd) en optimizationCache, metadata uit het parsen van de .opt: profielnaam, byte-offsets, MT5-build, aantal symbolen. Alle drie zijn None bij runs die geen optimalisatie zijn. POST /backtest neemt een optionele topPasses (1..500, standaard 50) om af te toppen hoeveel rijen er terugkomen.
De cacheparser is het leuke deel. mt5api/backtest/cache_parser.py reverse-engineert het binaire .opt-formaat van MT5. Hij wordt gestuurd door profielen in plaats van hardgecodeerd te zijn: kandidaat-indelingen van byte-offsets krijgen een score tegen het echte bestand en de beste wint. Brengt MT5 een nieuwe build uit die de indeling verschuift, dan hang je een kandidaat aan OPT_CACHE_PROFILE_CANDIDATES, geen codepaden per build, geen versiesnuffelen.
Live log-tail. GET /backtest/<jobId>/tail?lines=N voegt het runlog (stdout van terminal64.exe), het MT5-terminaljournaal en het sublog van de Strategy Tester samen tot één stroom. Begrensd op 10..1000 regels. Werkt terwijl de job in de wachtrij staat, draait of klaar is, dus je kunt naar een optimalisatie van zes uur kijken in plaats van naar status: running te staren en te hopen.
Terminalinstanties. Een ingang in terminals[] neemt nu een optionele instance-naam, waardoor hetzelfde broker/account-paar meer dan eens mag voorkomen, met verschillende poorten en verschillende portable datamappen. Dat is het hele punt: een live terminal kan naast een of meer backtestterminals bestaan op dezelfde login. Laat je hem weg, dan valt hij terug op default en blijft de oude route-alias overeind. Meerdere ingangen op één broker/account-paar hebben verschillende instantiewaarden nodig, anders botsen ze op terminals/<broker>/<account>/<instance>/.
Positiegrootte
Het symbool-endpoint geeft je alles wat je nodig hebt om fatsoenlijke positiegroottes te rekenen:
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_lotNaar beneden afronden op volume_step, begrenzen op [volume_min, volume_max]. Gezondverstandcheck: volume * trade_contract_size * price hoort te kloppen met je saldo. Eén lot EURUSD is 100.000 EUR, niet 1 EUR, en trade_contract_size vertelt je dat. Check het voordat je per ongeluk je hele account erin gooit op wat je voor een microstand aanzag.
AI-skill
De repo brengt een map .agents/skills/ mee met een skilldefinitie voor AI-codeagents. Het werkt in alles dat .agents/skills/ leest, OpenClaw inbegrepen, en installeert native in Claude Code en Codex vanuit één gedeelde marketplace:
claude plugin marketplace add psyb0t/agents
claude plugin install mt5-httpapi@psyb0tCodex gebruikt dezelfde marketplace met een ander werkwoord, codex plugin marketplace add psyb0t/agents en dan codex plugin add mt5-httpapi@psyb0t, omdat codex plugin install niet bestaat. Codex pikt de skill bovendien zelf op in een checkout van de repo, aangezien hij .agents/skills/ native scant en er helemaal geen installatie aan te pas komt. Je richt hem via MT5_API_URL op je draaiende instantie, voegt MT5_API_TOKEN toe als de auth aanstaat, en de agent krijgt de volledige API-referentie, de veiligheidschecklist van voor de trade, de formules voor positiegrootte en de gebruikspatronen. Hij weet welke endpoints er zijn, welke velden hij moet checken voordat hij een trade plaatst, en hoe hij de resultaten moet lezen.
Wat betekent dat je tegen je AI-agent kunt zeggen “koop 0.1 lot EURUSD met een stop loss van 2 ATR” en hij heeft alles wat hij nodig heeft om de symboolinfo op te halen, de SL-prijs te rekenen, de order te plaatsen en het resultaat te verifiëren. Geen API-documentatie met de hand doorspitten, de skill geeft hem het complete draaiboek.
MCP: Eén Endpoint, Alle Terminals
Een skill leert een agent de REST-API besturen. v4.7 ging verder en liet hem de HTTP-laag helemaal overslaan: elke terminal hangt zijn eigen MCP-server op /<broker>/<account>/mcp/, naast de REST-API, in hetzelfde proces. nginx knipt het prefix eraf en stuurt de rest ongewijzigd door, dus de bereikbare URL is simpelweg de normale basis van de terminal plus /mcp/.
Het begon als een generieke passthrough, één request-tool die elk endpoint kon aanroepen, en v4.8 verving dat door ~24 toegewijde getypeerde tools, gegroepeerd per familie: marktdata, account, posities, orders, historie, terminal, backtest. De naam van elke tool, zijn getypeerde parameters en zijn beschrijving zijn wat de agent leest, dus er wordt niet meer naar ruwe paden gegokt. Elke tool loopt door precies dezelfde handler, dezelfde auth en dezelfde MT5-locking als een echt HTTP-verzoek. De generieke request plus een endpoints-catalogus bleven als vangnet. Eén bewuste weglating: een backtest indienen is een multipart-upload, dus dat blijft alleen REST, get_backtest vraagt status, rapport en log op, maar nieuwe runs gaan via POST /backtest.
Wat één eerlijk gezegd irritante beperking overliet. Een MCP-sessie heeft een vaste toolcatalogus, dus er is geen plek per aanroep om een account te noemen, een sessie zat permanent vast aan de terminal waarmee hij verbond. Zes terminals betekende zes sessies. v4.9 lost dat op met een mcpunifier-service: een Linux-container naast de Windows-VM, die diezelfde tools op één enkele /mcp serveert met parameters broker en account, plus een list_terminals-tool zodat een aanroeper kan ontdekken wat er is ingesteld en welke accounts leven voordat hij iets anders doet.
De URL die je de client geeft bepaalt je explosiestraal:
http://host:8888/<broker>/<account>/mcp/ one terminal, no account param to get wrong
http://host:8888/mcp/ every terminal, + broker/account + list_terminalsDe unifier leest dezelfde config/config.yaml die ook de nginx-routering genereert, wat betekent dat hij fysiek nergens heen kan routeren waar nginx niet heen routeert, en hij stuurt rechtstreeks naar de poort van elke terminal. De routeringstabel wordt één keer bij het starten opgelost en nooit opnieuw bevraagd, dus de service staat nooit op een terminal te wachten, een uitgevallen terminal laat alleen de aanroepen mislukken die hem noemen in plaats van de rest mee te sleuren, en elk geslaagd antwoord draagt de terminal die daadwerkelijk antwoordde. Vraag je om een broker/account-paar dat niet is ingesteld, dan word je geweigerd met de lijst van wat je had kunnen vragen, in plaats van stilletjes doorgestuurd naar iets aannemelijks maar verkeerds. Er is niets aan het bestaande veranderd: de endpoints per terminal en het hele REST-oppervlak blijven ongemoeid.
v4.9.1 is het soort bug dat het opschrijven waard is. De door v4.9.0 gegenereerde nginx-config had een letterlijke proxy_pass http://mcpunifier:6600/, en nginx lost een letterlijke upstream-hostnaam op terwijl het de config parseert, niet op het moment van het verzoek. Dus op elke installatie zonder die container brak nginx af met host not found in upstream "mcpunifier", en gingen elke route per terminal plus de hele REST-API erachter naar 502. Omdat docker-compose.yml gitignored is, kreeg je bij het binnenhalen van v4.9.0 de nieuwe generator zonder de service waarnaar hij verwijst, en legde de eerstvolgende herstart je stack plat. Een ontbrekende unifier is nu gewoon een ontbrekende unifier. v4.9.2 voegde daar een end-to-end harnas voor toe als shellscript, dat v4.10 vervolgens weer afschafte, zijn zeven asserties verhuisden ongewijzigd naar tests/integration/test_mcpunifier.py, zodat het project één integratieharnas in één taal draait in plaats van een shellscript ernaast. Het heet nu make test-integration, een door containers gedragen pytest-suite die echte nginx opstart tegen de gegenereerde config, met één VM opzettelijk afwezig.
En die nginx-bug bleek een grote broer te hebben. Dezelfde parse-tijd-resolutie van de letterlijke proxy_pass die de stack platlegde om een ontbrekende unifier deed dat ook voor terminal-routes: één ontbrekende VM-container en nginx startte helemaal niet meer, en sleurde de routes van elke gezonde VM, de REST-API en /mcp/ mee. v4.10 zorgde ervoor dat terminalroutes hun upstream per verzoek oplossen, wat ook het volgende mogelijk maakte.
v4.10 ging multi-VM. Een vms.yaml declareert de resources van elke Windows-VM en elke terminal bindt zich via een nieuw veld vm: aan er eentje; config_helper.py genereert nginx-routes gericht op de container van de bezittende VM, run.sh loopt elke VM langs voor DNAT en groepsbestanden per VM, en docker-compose.yml wordt uit een Jinja-template gerenderd. Geen vms.yaml betekent één VM, en een terminal zonder vm:-veld routeert naar mt5, dus bestaande installaties merken er niets van. Dezelfde versie voegde contracttests toe voor de handlers die geld verplaatsen: ze sturen de echte Flask-routes aan met de MT5-SDK nagebootst op de m()-naad en controleren het exacte verzoek dat bij order_send zou aankomen, een market-BUY tegen de ask, een SELL tegen de bid, een gedeeltelijke sluiting die alleen het gevraagde volume stuurt, een wijziging van alleen sl die de bestaande tp behoudt. Elk faalpad controleert bovendien dat order_send nooit is aangeroepen, want een handler die na het versturen klapt heeft al gehandeld. De CI draait dat nu ook echt bij pushes en PR’s; pipeline.yml ging eerder alleen af op v*-tags, wat betekende dat de suite onder tests/ nooit in de CI had gedraaid.
v4.11 dichtte het laatste gat tussen de twee MCP-catalogi: de getypeerde tools per terminal en de verenigde tools bieden nu overeenkomende from/to-bereikparameters voor ticks en TA-rates, met schema- en pariteitstests die beide dekken.
Het Uitkleden
De Windows-VM gaat bij de eerste boot door een agressieve opschoonbeurt. Alle animaties, transparantie en de achtergrond uitzetten. SysMain, audio, de printerspooler, zoeken, telemetrie en een stuk of vijftig andere nutteloze diensten killen. Windows Defender helemaal verwijderen, niet uitschakelen, verwijderen. De Defender-mappen in bezit nemen en de binaries wissen. Alle privacyschendende troep eruit gooien: advertentie-ID, activiteitengeschiedenis, diagnosegegevens, alle capability-permissies. Elke geplande spionagetaak van Microsoft uitzetten. Processorprioriteit op de voorgrond, kill-timeouts verkorten, NTFS-timestamps uitzetten.
Het resultaat is een Windows 11 dat snel opstart, laag blijft in rust en niet elke 30 seconden naar Microsoft belt. Precies genoeg besturingssysteem om MT5 en de Python-API te draaien.
Elke Meegeleverde Binary Moet Zich Nu Verantwoorden
Een repo die een Windows-VM opstart en Defender eruit rukt verzamelt uitvoerbare bestanden. Niet veel, maar die hij heeft zijn precies degene die je het minst op goed vertrouwen zou willen aannemen, en ze lagen ongecontroleerd in de boom.
v4.12.0 voegde make verify-binaries toe. Elk meegeleverd uitvoerbaar bestand moet in assets/binaries.lock.json aangegeven staan met zijn sha256, zijn upstream-bron en de staat van zijn handtekening. Een niet-aangegeven binary laat de build falen. Een gewijzigde laat de build falen. Een aangetaste handtekening laat de build falen. Het draait als eerste binnen make test, dus de CI dwingt het af bij elke PR in plaats van wanneer iemand eraan denkt.
Het punt van zo’n poort is niet de regel, maar wat de regel vindt op het moment dat je hem aanzet. Hier documenteerde hij meteen scripts/defender-remover/PowerRun.exe:
"path": "scripts/defender-remover/PowerRun.exe",
"product": "PowerRun",
"vendor": "Sordum Software",
"signature": "malformed",
"note": "REPACKED, NOT PRISTINE..."Hij kwam meegeleverd binnen de defender-remover-toolkit in plaats van rechtstreeks van Sordum. Zijn certificaatmap is geen goedgevormde WIN_CERTIFICATE, een opgegeven lengte van 776284822, revisie 0xc496, type 14951, tegenover een vereiste 0x200 en een 2, en zijn hash komt met geen enkele upstream Sordum-release overeen, dus de handtekening valt nergens tegen te controleren.
Om duidelijk te zijn over wat dat wel en niet betekent: het staat niet bekend als kwaadaardig. Een hoop herverpakt gereedschap ziet er zo uit. Wat er veranderde is dat het niet langer stil is, de staat staat zwart op wit, in de repo, naast het bestand, en de CI faalt als hij ooit verschuift. Een onverklaarde binary waar je van weet is een ander risico dan een onverklaarde binary waar je niets van weet.
Dezelfde versie bracht de unit-suite van 244 tests naar 379. Alles wat eerder alleen tegen een levende terminal draaide draait nu in de CI, waarbij de echte Flask-app tegen een gescripte SDK wordt aangestuurd, zodat mt5client, de monitor en de Go-client hun allereerste dekking kregen.
Logs
Alles komt op de host samen in data/metatrader5/logs/:
- install.log: de voortgang van de MT5-installatie
- start-mt5.log: het log van de bootvolgorde
- pip.log: de installatie van de Python-pakketten
- api-<broker>-<account>.log: de API-logs per terminal
- full.log: de aaneengeschakelde brandslang van alles hierboven plus de Windows Event Log-regels die vanuit de VM worden afgetapt. Dit is degene die de OOM-kills vangt en de processen die Defender stilletjes omlegt, die nergens anders opduiken.
Een aparte sidecar voor logrotatie draait naast de VM en roteert alles dagelijks met 7 dagen bewaartermijn. Geen logbestanden van 4 GB meer die je schijf opvreten na een week met de stack aan. Als er iets breekt is full.log de eerste plek om te kijken, chronologisch, één bestand, alles in één stroom.
Tailscale-sidecar
Een trading-API publiek blootstellen is vragen om beroofd te worden. De meeste mensen willen dit ding bereikbaar hebben vanaf hun laptop en nergens anders vandaan. Dus is er een ingebouwde Tailscale-sidecar die zich bij je tailnet aansluit en de API op een kale MagicDNS-hostnaam serveert:
http://mt5-httpapi/roboforex/main/account
http://mt5-httpapi/roboforex/main/symbols/EURUSD/rates?count=100
http://mt5-httpapi/ftmo/challenge1/positionsJe zet de auth key in config.yaml, haalt het tailscale-blok in docker-compose.yml uit commentaar, make up. Werkt met gewoon Tailscale en met zelf gehost Headscale (voor dat laatste zet je login_server). Kaal HTTP met opzet, de wireguard-laag versleutelt binnen het tailnet toch al elke byte, en kale MagicDNS-hostnamen hebben sowieso geen bijpassende TLS-certificaten.
De sidecar draait in zijn eigen netns (bridge-modus, niet het hostnetwerk), dus hij krijgt zijn eigen tailnet-identiteit. Je ACL’s beperken zich tot de node van de sidecar, de Tailscale van de host (als die er een heeft) blijft er volledig buiten, en al het tailnet-gerichte verkeer vanuit de sidecar gaat via zijn eigen tailscale0-interface, niet die van de host. Tailscale Serve luistert binnen de netns op poort 80 en stuurt door naar de altijd draaiende nginx-sidecar over het interne netwerk van docker. De staat blijft bewaard in .data/tailscale/state/, dus make down / make up hergebruikt de bestaande login, de auth key wordt alleen bij de eerste login verbruikt.
Het API-token (als je het gezet hebt) geldt er nog steeds bovenop, Tailscale regelt de bereikbaarheid op netwerkniveau, het bearer token regelt de toegang tot de applicatie. Verdediging in lagen.
Cloudflare Tunnel (Als Je Echt Publiek Nodig Hebt)
Heb je dit ding echt bereikbaar nodig vanaf het open internet, zeg om het aan een gehoste bot of een frontend op Vercel te hangen, dan is er een Cloudflare Tunnel-optie. cloudflared belt naar buiten naar de rand van Cloudflare en stuurt door naar de altijd draaiende nginx-sidecar. Eén tunnel, één hostnaam, elke terminal bereikbaar achter /<broker>/<account>/:
https://mt5-api.yourdomain.com/roboforex/main/account
https://mt5-api.yourdomain.com/ftmo/challenge1/positionsGeen poorten open in de firewall. Geen NAT doorboren. Geen certificaten te beheren, Cloudflare beëindigt TLS gratis aan de rand onder hun Universal SSL. Opzetten: je installeert cloudflared één keer op de host, maakt een tunnel aan, routeert er een hostnaam heen, zet de gegevens in .data/cloudflared/, haalt het cloudflared-blok in compose uit commentaar, make up.
Behandel de publieke hostnaam als vijandig en zet altijd api_token in config.yaml wanneer je dit gebruikt. Cloudflare regelt de publieke bereikbaarheid; het bearer token regelt de applicatie. Sla je het token hier over, dan kan iedereen die de hostnaam vindt je account leegtrekken.
De Kern van de Zaak
MetaTrader 5 in Docker met een REST-API. Een echte Windows-VM via KVM, geen Wine. Meerdere brokers en meerdere accounts die tegelijk draaien op minimale resources. Volledige marktdata, orderbeheer, positiebewaking, handelshistorie, TA op de server via de wickworks-sidecar, en een volledige Strategy Tester-pijplijn over HTTP, allemaal achter kale JSON. Plus een getypeerde Go-client, een AI-agentskill en getypeerde MCP-tools, per terminal of verenigd over alle terminals tegelijk, zodat je een LLM trades tegen de API kunt laten doen zonder hem de documentatie voor te kauwen.
Geen MQL5. Geen Windows-bureaublad. Geen MT5-bibliotheken aan de clientkant. Gewoon curl en gaan.
Haal hem hier: github.com/psyb0t/mt5-httpapi
Onder WTFPL-licentie, want traden zou een disclaimer moeten vereisen, geen softwarelicentie.