mt5-httpapi: MetaTrader 5 în Docker cu REST API, Pentru Că MQL5 Poate Să Se Ducă Dracului

MetaTrader 5 merge doar pe Windows. Biblioteca oficială de Python merge doar pe Windows. Limbajul de scripting MQL5 e o imitație de C++ din 2005 care îți vine să îți scoți ochii cu o furculiță ruginită. Iar dacă vrei să faci ceva programatic cu el, să tragi lumânări, să pui ordine, să verifici pozițiile, se presupune că ori scrii MQL5, ori rulezi Python pe o mașină de Windows cu terminalul deschis.
Eu voiam să lovesc un endpoint HTTP de pe orice mașină, în orice limbaj, și să primesc JSON înapoi. Ca un om normal.
Așa că am construit mt5-httpapi. O mașină virtuală Windows adevărată care rulează în Docker prin QEMU/KVM, cu terminalul MT5 complet în mod portabil și un REST API pe Flask deasupra. Fără Wine, fără șmecherii de emulare, fără cârpeli. Un mediu Windows 11 legitim care rulează binarul MetaTrader 5 adevărat, accesibil prin HTTP/JSON simplu de oriunde.
Mai mulți brokeri. Mai multe conturi. Fiecare terminal primește propriul proces de API înăuntrul mașinii virtuale, iar un sidecar de nginx mereu pornit le pune pe toate în spatele unui singur port de host la http://localhost:8888/<broker>/<account>/.... Rulează două challenge-uri FTMO simultan, sau amestecă brokeri, sau ține zece terminale pe o singură mașină, ce ai nevoie.

Cum Funcționează Urâciunea Asta

Containerul rulează dockurr/windows, o imagine de Docker care pornește o mașină virtuală Windows completă folosind virtualizare hardware QEMU/KVM. La prima rulare descarcă tiny11 (un Windows 11 dezbrăcat, ~4 GB), îl instalează, apoi pune automat Python 3.12, instalează MetaTrader5, îi scoate lui Windows tot căcatul din el, scoate Defender cu totul și pornește tot.
După prima pornire (~10 minute), următoarele durează cam un minut. Containerul e configurat cu 2 vCPUs, 512 MB de RAM real și 5 GB de swap. Sună blestemat, merge perfect. tiny11 plus scriptul de debloat stă la ~1.4 GB în idle, iar MT5 plus API-ul de Python nu adaugă mai nimic. Windows și MT5 nu sunt suficient de sensibile la latență cât să conteze swap-ul.
Un folder partajat între host și mașina virtuală Windows (/sharedC:UsersDockerDesktopShared) ține tot: scripturi, configurări, installere de broker, codul serverului de API și logurile. Scriptul run.sh sincronizează tot în folderul ăsta, generează reguli de NAT iptables pentru port forwarding din container în VM și ridică docker-compose.
noVNC pe portul 8006 îți dă o vedere din browser a desktopului de Windows. Util ca să te uiți la progresul instalării și să confirmi că a pornit tot. După aia uită că există interfața și lovește doar REST API-ul.

Mai Multe Terminale în Spatele Unui Singur Port

Actualizare de arhitectură (v4.0+): aranjamentul inițial expunea fiecare terminal pe propriul port de host (6542, 6543, 6544…). S-a dus. Acum totul stă în spatele unui singur port de host (implicit 127.0.0.1:8888), în fața căruia stă un sidecar de nginx mereu pornit care rutează după prefixul de cale:

http://localhost:8888/<broker>/<account>/...

v4.4 a extins asta la /<broker>/<account>/<instance>/..., cu forma simplă /<broker>/<account>/ păstrată ca alias pentru instanța implicită, deci URL-urile existente merg în continuare. Forma nu ține cont de mod: terminalele live și cele de backtest se rutează identic.

Un singur port forwardat de pe host, o singură suprafață TLS de care să îți pese, o singură regulă în firewall. Fiecare cerere lovește nginx, prefixul /<broker>/<account>/ se taie, iar restul se dă mai departe către procesul de API Python al terminalului ăluia, dinăuntrul VM-ului, peste bridge-ul de docker. Configurarea de nginx se generează automat din config.yaml la fiecare make up, așa că adăugarea sau scoaterea unui terminal nu cere umblat manual prin reverse-proxy.

Consolidarea configurării (v4.0): vechea împărțire accounts.json + terminals.json s-a dus. Acum există un singur config/config.yaml (ținut în gitignore) ca sursă a adevărului:

# 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)

Câmpul broker se potrivește și cu cheia din accounts: și cu numele fișierului de installer (mt5setup-roboforex.exe, mt5setup-ftmo.exe). Terminalul fiecărui broker se instalează o dată în <broker>/base/, apoi se copiază în <broker>/<account>/ la pornire, ca mai multe conturi ale aceluiași broker să nu se calce în picioare.

utc_offset e per terminal pentru că brokerii merg pe fusuri orare ciudate (RoboForex/FTMO pe UTC+3, TeleTrade pe UTC+2). Fiecare timestamp de pe fir, lumânări, tick-uri, istoric, poziții, se normalizează la UTC adevărat pe server. Clientul tău nu mai trebuie să se gândească niciodată la ora locală a brokerului. port e portul intern din container cu care vorbește nginx; nu e expus pe host.

reboot_interval e noua manetă de auto-reboot a VM-ului: stiva DWM/VirtIO-GPU de la MetaQuotes adună stare în kernel peste uptime-uri lungi și până la urmă o ia razna, așa că VM-ul e repornit după un program (implicit la fiecare 30 de minute). Pui 0 ca să îl oprești.

mode per terminal (v4.3): live e implicit, terminal64.exe rămâne pornit ca SDK-ul MT5 să fie inițializat pentru endpointurile de tranzacționare live. backtest pregătește același director portabil de date, dar nu pornește terminal64.exe, lăsând directorul liber pentru un subproces de Strategy Tester. MT5 e single-instance per director portabil de date, deci asta contează: dacă terminal64.exe rulează deja, un subproces de tester pe același director iese în tăcere cu codul 0 și nu produce niciun raport. Rulează un roboforex/tester dedicat lângă roboforex/main-ul tău live și ai backtesturi prin HTTP fără să distrugi SDK-ul live.

symbol_suffix se ocupă de brokerii care redenumesc tot în tester. Dacă brokerul tău folosește EURUSDp, EURUSD.p, EURUSD-mini sau ce alt sufix de căcat au ei în pool-ul de simboluri din Strategy Tester, îl pui aici și mt5-httpapi îl adaugă automat când [Tester].Symbol din INI nu îl are deja. String gol = fără sufix. backtest_timeout e limita implicită de sus pentru POST /backtest, aceeași gramatică de durată ca la utc_offset ("6h", "30m", "3h30m", numerele goale tratate ca ore), care se poate suprascrie per job.

Patru terminale care merg simultan pe 2 vCPUs cu 512 MB de RAM real: CPU-ul sare la 100% în timpul pornirii, cât se inițializează totul, apoi cade la ~15% în idle. Memorie totală: 2.1 GB, toată dusă de swap. Ai putea ține 10+ terminale așa fără să transpiri, atâta timp cât nu tragi istoric adânc pe toate deodată (MT5 cachează fiecare grafic încărcat și nu îl mai eliberează niciodată; backfill-urile adânci trec limita de 512 MB prin podea).

Instalare

Cerințe: host de Linux cu KVM activat (/dev/kvm), Docker + Compose, ~20 GB de disc, 5 GB 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 up

Prima rulare descarcă ISO-ul de Windows, îl instalează, debloatează, instalează MT5, repornește de câteva ori, apoi pornește tot. După aia:

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 ISO

Autentificare

API-ul rulează deschis implicit. Dacă îl expui pe o rețea (chiar și una locală cu alte mașini pe ea), pui tokenul în config/config.yaml:

api_token: "$(openssl rand -hex 32)"

Dacă api_token nu e gol, fiecare endpoint cere Authorization: Bearer <token>. String gol = fără autentificare, merge bine pentru o configurație pe o singură mașină unde nimic altceva nu ajunge la port.
Cu autentificarea pornită, pui tokenul în shell și îl incluzi la fiecare cerere:

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"}

Toate exemplele de curl de mai jos presupun că nu e autentificare. Dacă ai configurat un token, pui -H "Authorization: Bearer $MT5_API_TOKEN" în fața fiecăruia.

API-ul

Toate terminalele sunt servite în spatele unui singur port de host prin nginx. Punct de intrare implicit: http://localhost:8888 (doar pe loopback). Fiecare terminal stă la propriul prefix de cale, http://localhost:8888/<broker>/<account>/.... Exemplele de mai jos folosesc roboforex/main; pune-l pe al tău. GET pentru citit, POST pentru creat, PUT pentru modificat, DELETE pentru închis. Tot JSON.

Sănătate și 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/restart

API-ul se inițializează singur la prima cerere. Dacă MT5 nu e încă conectat, un thread de fundal reîncearcă la fiecare 30 de secunde. Aproape niciodată nu trebuie să chemi /terminal/init manual.

Un monitor de sănătate rulează în fundal, la fiecare 60 de secunde verifică dacă terminalul e viu, logat și are algo trading activat. Dacă terminalul e mort la 5 verificări consecutive, se repornește singur: omoară procesul, relansează terminalul, așteaptă jurnalul să confirme că a urcat și reconectează API-ul. Poți declanșa și o repornire manuală prin POST /terminal/restart.

Monitorul ăla stă înăuntrul VM-ului, ceea ce înseamnă că poate repara doar lucrurile pe care VM-ul e încă destul de sănătos cât să le repare. v4.13.0 a adăugat nivelul de deasupra: un sidecar vm-watchdog gestionat de Compose, care se uită la containerele de VM Windows de pe host, prin socketul de Docker, și recreează unul pe calea existentă recreate-vm.sh atunci când a fost nesănătos suficient cât să însemne ceva. Nu de la prima verificare picată. WATCHDOG_MIN_FAILING_STREAK e implicit 10 eșecuri consecutive la un interval de 30 de secunde, apoi dă înapoi exponențial, 5 minute, 15 minute, o oră, și renunță după 3 încercări. Un VM care își revine singur e lăsat în pace.

Filtrul e partea interesantă. WATCHDOG_IMAGE_FILTER e implicit dockurr/windows, iar o valoare goală e refuzată în loc să fie tratată ca “se potrivește cu tot”, pentru că versiunea asta care tratează un filtru gol ca pe un wildcard e versiunea care îți recreează toate containerele din proiect la 4 dimineața.

Supraveghează și sidecarurile care împart spațiul de nume de rețea al unui VM (network_mode: service:<vm>), odată ce VM-ul a fost sănătos continuu. Un sidecar rămas blocat într-un spațiu de nume vechi e reparat singur, fără să fie recreat VM-ul de sub el. Ăla a ieșit din două zile de apeluri TA moarte: VM-ul era în regulă, sidecarul de wickworks era îndreptat spre un spațiu de nume de rețea care nu mai exista, și n-a observat nimic pentru că verificările de sănătate ale VM-ului au fost verzi tot timpul. WATCHDOG_WATCH_SIDECARS=0 dacă vrei înapoi recuperarea doar pe VM. Sunt 15 variabile WATCHDOG_* în total, inclusiv un WATCHDOG_DRY_RUN ca să te uiți ce ar fi făcut înainte să îl lași să facă ceva.

Cont

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
}

Date de Piață

# 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"

Timeframe-uri: M1 M2 M3 M4 M5 M6 M10 M12 M15 M20 M30 H1 H2 H3 H4 H6 H8 H12 D1 W1 MN1. time-ul lumânării e ora de deschidere, secunde unix epoch.

Pui Ordine

# 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}'

Câmpuri obligatorii: symbol, type, volume. Prețul se completează singur pentru ordinele la piață. Tipuri de ordine: BUY, SELL, BUY_LIMIT, SELL_LIMIT, BUY_STOP, SELL_STOP, BUY_STOP_LIMIT, SELL_STOP_LIMIT. Politici de umplere: FOK, IOC (implicit), RETURN. Expirare: GTC (implicit), DAY, SPECIFIED, SPECIFIED_DAY.
Fiecare operație de tranzacționare întoarce un rezultat cu retcode, 10009 înseamnă succes, orice altceva înseamnă că ceva a mers prost. Folosește GET /error ca să depanezi.

Administrarea Pozițiilor și a Ordinelor

# 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/67890

Istoric

# 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 și to sunt obligatorii, secunde unix epoch. Tranzacțiile au entry (0 = deschidere, 1 = închidere) și profit (0 la intrări, P&L realizat la ieșiri).

Analiză Tehnică

Două feluri de a pune indicatori pe lumânările tale, pe server sau de mână.

TA pe server: POST /symbols/:symbol/rates/ta (v4.1+)

Un singur apel HTTP, lumânările vin înapoi deja îmbogățite cu indicatori. Containerul de mt5 vine cu un sidecar de TA wickworks blocat în spațiul de nume de rețea al mt5, fără porturi publicate, fără deploy separat, fără trafic extern. Aceiași parametri de query ca la GET /rates (timeframe, count, from, to) și un body JSON care îi spune lui wickworks ce indicatori să calculeze:

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}
        }
      }'

Răspunsul cară barele brute și ieșirea de la wickworks ca frați:

{
  "symbol": "EURUSD",
  "timeframe": "H1",
  "bars": [ { "time": 1771146000, "open": 1.0832, "high": 1.0840, "low": 1.0828, "close": 1.0835, ... } ],
  "ta": { "indicators": { "rsi": [...], "macd": {...}, "bbands": {...} } }
}

Fiecare intrare de sub indicators mapează o cheie de ieșire fie la true (valorile implicite), fie la un obiect plat de parametri. Adaugi "type": "<name>" doar când cheia de ieșire diferă de numele indicatorului, de exemplu ca să rulezi un al doilea RSI sub rsi21. Catalogul wickworks acoperă suspecții obișnuiți (RSI, MACD, Bollinger Bands, ADX, VWAP, Ichimoku, ATR, Stochastic, MFI, zeci de medii mobile) plus primitive de Smart Money (order blocks, fair value gaps, BOS, CHoCH, structură de swing, niveluri S/R). Doar primitive, semnalele interpretative (divergențe, evenimente de încrucișare) stau la tine în consumator. Lista completă de tipuri, parametri și forme de ieșire la github.com/psyb0t/docker-wickworks.

Configurezi URL-ul sidecarului prin wickworks: în config.yaml, implicit http://20.20.20.1:8000/, IP-ul gateway-ului dockurr așa cum se vede dinăuntrul VM-ului de Windows.

TA la client: tragi lumânări brute și le macini singur

Dacă vrei control complet sau ești deja băgat până la gât în pandas, repo-ul vine cu un exemplu complet în examples/python/ care trage lumânări prin GET /rates și le dă prin pandas-ta și smartmoneyconcepts.

Indicatori incluși: EMA 21, SMA 50/100/200, ATR, RSI, MACD, Bollinger Bands, MFI, Stochastic, ADX, VWAP, plus Smart Money Concepts: order blocks, fair value gaps, break of structure, change of character și niveluri de lichiditate.

# 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.png

Raportul de TA scoate valorile ultimei lumânări pentru fiecare indicator și apoi rulează detecția de semnale: RSI supracumpărat/supravândut, încrucișări de histogramă MACD, golden/death cross pe EMA/SMA, spargeri de Bollinger Band, extreme de Stochastic, putere de trend pe ADX. Graficul randează lumânări pe temă închisă cu medii mobile, Bollinger Bands, VWAP, overlay-uri SMC (order blocks, FVG-uri, linii BOS/CHoCH, mături de lichiditate), panou de RSI și panou de MACD. PNG-uri de calitate de publicație la 1920×1080.

Modulele de indicatori și de semnale sunt gândite ca niște cărămizi. Imporți add_rsi(df) sau detect_signals(df) în propriile tale scripturi și folosești API-ul ca sursă de date. Tragi lumânări, aplici ce analiză vrei, pui tranzacții, toate dintr-un script de Python care rulează pe orice mașină.

Client de Go: GetRatesTA (v4.2+)

Clientul tipat de Go din clients/go/ împachetează endpointul wickworks cu propria lui metodă:

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},
        },
    },
)

Același client acoperă GetRates, GetTicks, GetAccount, CreateOrder, ListPositions, UpdatePosition, ClosePosition, endpointurile de istoric și apelurile de ciclu de viață al terminalului. Erorile se mapează pe sentinele tipate pe care poți da errors.Is(), aichteeteapee.ErrUnauthorized pentru 401, aichteeteapee.ErrBadRequest pentru 400, și un mt5httpapi.ErrNotInitialized dedicat pentru 503-ul pe care îl vezi cât timp VM-ul încă pornește și SDK-ul MT5 nu e gata.

Strategy Tester / Backtesting (v4.3+)

Să faci backtest unui EA pe MT5 înseamnă în mod normal să dai click prin fereastra de Strategy Tester ca un animal. v4.3 bagă toată treaba în API-ul HTTP: urci un INI, un expert .ex5, opțional un fișier de parametri .set, primești un jobId înapoi, interoghezi până se termină, tragi raportul HTML și logul terminalului. Același flux pe care l-ai rula în interfață, dar fără cap și scriptabil.

De ce mode: backtest pe propriul lui terminal. MT5 e single-instance per director portabil de date. Dacă terminal64.exe rulează deja acolo ca să susțină SDK-ul live, un subproces de Strategy Tester pe același director iese în tăcere cu codul 0 și nu primești nimic. Deci dedici un terminal în config.yaml cu mode: backtest, primește aceeași instalare portabilă, aceleași credențiale de broker, dar fără terminal64.exe pornit automat. Subprocesul de tester primește directorul de date în folosință exclusivă și chiar produce un raport. Îl rulezi lângă terminalele tale live; nu se văd între ele.

Flux în două etape. Întâi POST /backtest/build-ini transformă o specificație JSON mică într-un tester.ini complet format (fără credențiale, fără rezolvare de cale către expert, e un ajutor fără stare, poți să îți faci INI-ul și de mână). Apoi POST /backtest ia un upload multipart cu INI-ul plus expertul, bagă jobul la coadă și întoarce 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.log

Expertul și fișierul de set pot fi trimise inline (de preferat pentru rulări ad-hoc) sau referite după nume dintr-un pool ținut pe host, assets/experts/*.ex5 și assets/sets/*.set, montate read-only în VM la /shared/assets. Path traversal în expert_name / set_name e respins.

Ce face serverul pentru tine. [Common] Login / Password / Server din INI-ul urcat sunt suprascrise cu credențialele din config.yaml pentru brokerul/contul ăla, deci INI-ul pe care îl urci nu trebuie să știe credențialele tale reale și nu le comiți din greșeală într-un repo. Calea expertului e rescrisă la Uploaded<basename>. Fișierul de set primește un spațiu de nume per job ca să nu se ciocnească. Dacă brokerul tău folosește simboluri cu sufix (EURUSDp, EURUSD.p, EURUSD-mini), symbol_suffix-ul configurat se adaugă automat la [Tester].Symbol când lipsește.

Payload-ul de status. GET /backtest/<jobId> întoarce statusqueued / running / completed / failed. Când e gata, un obiect summary e parsat din raportul HTML, netProfit, profitFactor, recoveryFactor, expectedPayoff, sharpeRatio, maxDrawdown, totalTrades, profitTrades, lossTrades, ca să poți clasa rulările programatic fără să parsezi tu HTML.

Concurență. Rulează un singur tester odată per proces de API, serializat de un lock intern. Trimiterile în plus stau la coadă. Timeoutul implicit vine din backtest_timeout din config.yaml (implicit 6h dacă nu e setat); se suprascrie per job prin câmpul multipart timeout. Dacă API-ul repornește cu un backtest în zbor, jobul orfan e marcat failed cu API restarted before completion la următoarea pornire, fără zombi.

Optimizare și Instanțe de Terminal (v4.4)

v4.3 ți-a dat backtesturi într-o singură trecere cu raport HTML. Aia e jumătatea ușoară. Rulările de optimizare sunt altă mâncare de pește: Tester.Optimization pe 1 sau 2 scoate un raport XML de optimizare, iar modul 3, “toate simbolurile”, scoate un .symbols.xml plus un cache binar .opt pe care MT5 nu îl expune prin niciun API documentat. v4.4 le parsează pe amândouă.

POST /backtest/build-ini ia acum optimization (0..3) și optimizationCriterion (0..7) pe lângă câmpurile de o singură trecere din v4.3. Și există un nou POST /backtest/build-set care scoate un fișier .set de Strategy Tester dintr-o listă JSON de parametri, și valori fixe și intervale de optimizare (start / step / stop / optimize), în formatul nativ al MT5 name=value||.... Deci poți genera baleiajul de parametri programatic în loc să editezi fișiere de set de mână în interfață.

GET /backtest/<jobId> capătă trei câmpuri la rulările de optimizare: optimizationType (0..3), optimizationResults (primele N treceri, sortate) și optimizationCache, metadate din parsarea .opt: nume de profil, offseturi de octeți, build de MT5, număr de simboluri. Toate trei sunt None la rulările care nu sunt de optimizare. POST /backtest ia un topPasses opțional (1..500, implicit 50) ca să limiteze câte rânduri vin înapoi.

Parserul de cache e partea distractivă. mt5api/backtest/cache_parser.py face inginerie inversă pe formatul binar .opt al MT5. E condus de profiluri în loc să fie hardcodat: layouturile candidate de offseturi de octeți primesc un scor pe fișierul real și câștigă cel mai bun. Când MT5 scoate un build nou care mută layoutul, adaugi un candidat în OPT_CACHE_PROFILE_CANDIDATES, fără căi de cod per build, fără adulmecat versiuni.

Tail de log în direct. GET /backtest/<jobId>/tail?lines=N amestecă logul rulării (stdout-ul lui terminal64.exe), jurnalul terminalului MT5 și sub-logul de Strategy Tester într-un singur flux. Limitat la 10..1000 de linii. Merge cât jobul e la coadă, în rulare sau terminat, deci poți să te uiți la o optimizare de șase ore în loc să holbezi la status: running și să speri.

Instanțe de terminal. O intrare din terminals[] ia acum un nume opțional de instance, care lasă aceeași pereche broker/cont să apară de mai multe ori, cu porturi diferite și directoare portabile de date diferite. Ăsta e tot rostul: un terminal live poate coexista cu unul sau mai multe terminale de backtest pe același login. Îl omiți și se rezolvă la default, păstrând aliasul vechi de rută. Mai multe intrări pe aceeași pereche broker/cont au nevoie de valori distincte de instanță, altfel se ciocnesc pe terminals/<broker>/<account>/<instance>/.

Dimensionarea Poziției

Endpointul de simbol îți dă tot ce îți trebuie ca să calculezi dimensiuni corecte de poziție:

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_lot

Rotunjești în jos la volume_step, limitezi la [volume_min, volume_max]. Verificare de bun simț: volume * trade_contract_size * price ar trebui să aibă sens raportat la soldul tău. Un lot de EURUSD e 100.000 EUR, nu 1 EUR, trade_contract_size îți spune asta. Verifică înainte să îți dai din greșeală tot contul pe ceea ce credeai că e o poziție micro.

Skill de AI

Repo-ul vine cu un director .agents/skills/ care conține o definiție de skill pentru agenții de cod AI. Merge în orice citește .agents/skills/, inclusiv OpenClaw, și se instalează nativ în Claude Code și Codex dintr-un singur marketplace comun:

claude plugin marketplace add psyb0t/agents
claude plugin install mt5-httpapi@psyb0t

Codex folosește același marketplace cu alt verb, codex plugin marketplace add psyb0t/agents apoi codex plugin add mt5-httpapi@psyb0t, pentru că nu există codex plugin install. Codex mai ridică skill-ul și singur într-un checkout al repo-ului, pentru că scanează nativ .agents/skills/ și nu e implicată nicio instalare. Îl îndrepți spre instanța ta pornită prin MT5_API_URL, adaugi MT5_API_TOKEN dacă autentificarea e pornită, și agentul primește referința completă de API, checklistul de siguranță dinainte de tranzacție, formulele de dimensionare a poziției și tiparele de folosire. Știe ce endpointuri există, ce câmpuri să verifice înainte să pună o tranzacție și cum să interpreteze rezultatele.
Asta înseamnă că îi poți spune agentului tău de AI “cumpără 0.1 loturi de EURUSD cu un stop loss de 2 ATR” și are tot ce îi trebuie ca să tragă informațiile despre simbol, să calculeze prețul de SL, să pună ordinul și să verifice rezultatul. Fără citit manual de documentație de API, skill-ul îi dă manualul complet.

MCP: Un Singur Endpoint, Toate Terminalele

Un skill învață un agent să conducă REST API-ul. v4.7 a mers mai departe și l-a lăsat să sară complet peste stratul de HTTP: fiecare terminal montează propriul server MCP la /<broker>/<account>/mcp/, lângă REST API, în același proces. nginx taie prefixul și dă restul mai departe direct, deci URL-ul la care ajungi e pur și simplu baza normală a terminalului plus /mcp/.

A pornit ca un passthrough generic, o singură unealtă request care putea chema orice endpoint, iar v4.8 a înlocuit-o cu ~24 de unelte tipate dedicate grupate pe familii: date de piață, cont, poziții, ordine, istoric, terminal, backtest. Numele fiecărei unelte, parametrii ei tipați și descrierea sunt ce citește agentul, deci nu mai ghicește căi brute. Fiecare unealtă rulează exact același handler, aceeași autentificare și același locking de MT5 ca o cerere HTTP adevărată. request-ul generic plus un catalog endpoints au rămas ca plasă de siguranță. O omisiune intenționată: trimiterea unui backtest e un upload multipart, deci rămâne doar pe REST, get_backtest interoghează statusul, raportul și logul, dar rulările noi trec prin POST /backtest.

Ceea ce a lăsat o limitare sincer enervantă. O sesiune MCP are un catalog fix de unelte, deci nu există un loc per apel în care să numești un cont, o sesiune era legată permanent de terminalul la care s-a conectat. Șase terminale însemnau șase sesiuni. v4.9 rezolvă asta cu un serviciu mcpunifier: un container de Linux care stă lângă VM-ul de Windows și servește aceleași unelte la un singur /mcp cu parametri broker și account, plus o unealtă list_terminals ca apelantul să descopere ce e configurat și ce conturi sunt vii înainte să facă altceva.

URL-ul pe care îl dai clientului e ce îți decide raza de explozie:

http://host:8888/<broker>/<account>/mcp/   one terminal, no account param to get wrong
http://host:8888/mcp/                        every terminal, + broker/account + list_terminals

Unificatorul citește același config/config.yaml care generează rutarea de nginx, ceea ce înseamnă că fizic nu poate ruta undeva unde nginx nu rutează, și trimite direct către portul fiecărui terminal. Tabela de rutare se rezolvă o dată la pornire și nu se mai sondează niciodată, deci serviciul nu stă niciodată să aștepte un terminal, un terminal picat pică doar apelurile care îl numesc pe el în loc să le ia pe celelalte cu el, și fiecare răspuns reușit cară terminalul care chiar a răspuns. Ceri o pereche broker/cont care nu e configurată și ești refuzat cu lista a ce ai fi putut cere, în loc să fii rutat în tăcere spre ceva plauzibil dar greșit. Nimic din ce exista nu s-a schimbat: endpointurile per terminal și toată suprafața REST rămân neatinse.

v4.9.1 e genul de bug care merită scris undeva. Configurarea de nginx generată de v4.9.0 avea un proxy_pass http://mcpunifier:6600/ literal, iar nginx rezolvă un hostname literal de upstream în timp ce parsează configurarea, nu la momentul cererii. Deci pe orice instalare fără containerul ăla, nginx crăpa cu host not found in upstream "mcpunifier", și fiecare rută per terminal plus tot REST API-ul din spatele ei se duceau în 502. Cum docker-compose.yml e în gitignore, dacă trăgeai v4.9.0 primeai generatorul nou fără serviciul pe care îl referă, iar următoarea repornire îți punea stiva la pământ. Acum un unificator lipsă e doar un unificator lipsă. v4.9.2 a adăugat un harness cap-coadă pentru asta sub formă de script de shell, pe care v4.10 l-a retras apoi, cele șapte aserțiuni ale lui s-au mutat nemodificate în tests/integration/test_mcpunifier.py, așa că proiectul rulează un singur harness de integrare într-un singur limbaj în loc de un script de shell care stătea lângă el. Acum e make test-integration, o suită de pytest susținută de containere care pornește nginx adevărat pe configurarea generată, cu un VM lipsă intenționat.

Și s-a dovedit că bugul ăla de nginx avea un frate mai mare. Aceeași rezolvare la parsare a proxy_pass-ului literal care a pus stiva la pământ pentru un unificator lipsă făcea la fel și pentru rutele de terminal: un singur container de VM absent și nginx nu mai pornea deloc, târând după el rutele fiecărui VM sănătos, REST API-ul și /mcp/. v4.10 a făcut ca rutele de terminal să își rezolve upstream-ul per cerere, ceea ce a făcut posibil și lucrul următor.

v4.10 a trecut la mai multe VM-uri. Un vms.yaml declară resursele fiecărui VM de Windows și fiecare terminal se leagă de unul printr-un câmp nou vm:; config_helper.py generează rute de nginx îndreptate spre containerul VM-ului proprietar, run.sh parcurge fiecare VM pentru DNAT și fișiere de grup per VM, iar docker-compose.yml se randează dintr-un șablon Jinja. Lipsa lui vms.yaml înseamnă un singur VM, iar un terminal fără câmp vm: se rutează spre mt5, deci instalările existente nu observă nimic. Aceeași versiune a adăugat teste de contract pentru handlerele care mișcă bani: conduc rutele reale de Flask cu SDK-ul MT5 falsificat la cusătura m() și verifică exact cererea care ar ajunge la order_send, un BUY la piață prețuit la ask, un SELL la bid, o închidere parțială care trimite doar volumul cerut, o modificare doar de sl care păstrează tp-ul existent. Fiecare cale de eșec verifică și că order_send nu a fost chemat niciodată, pentru că un handler care crapă după ce a trimis a tranzacționat deja. CI-ul chiar rulează acum tot, la push-uri și PR-uri; pipeline.yml se declanșa înainte doar pe tag-uri v*, ceea ce însemna că suita din tests/ nu rulase niciodată în CI.

v4.11 a închis ultima gaură dintre cele două cataloage MCP: uneltele tipate per terminal și cele unificate expun acum parametri de interval from/to care se potrivesc, pentru tick-uri și pentru rate de TA, cu teste de schemă și de paritate care le acoperă pe amândouă.

Debloatul

VM-ul de Windows trece printr-un debloat agresiv la prima pornire. Se dezactivează toate animațiile, transparența, wallpaperul. Se omoară SysMain, audio, spooler, search, telemetria și vreo alte 50 de servicii inutile. Se scoate Windows Defender cu totul, nu se dezactivează, se scoate. Se ia în proprietate directoarele Defender și se șterg binarele. Se distruge toată mizeria care îți invadează intimitatea: advertising ID, istoricul de activitate, datele de diagnostic, toate permisiunile de capabilități. Se dezactivează fiecare sarcină programată de spionaj Microsoft. Se pune prioritatea de procesor pe foreground, se reduc timeouturile de omorâre, se dezactivează timestampurile NTFS.
Rezultatul e un Windows 11 care pornește rapid, stă jos în idle și nu sună acasă la Microsoft la fiecare 30 de secunde. Exact atât sistem de operare cât să ruleze MT5 și API-ul de Python.

Fiecare Binar Vendorizat Trebuie Acum Să Se Declare

Un repo care pornește un VM de Windows și îi smulge Defenderul adună executabile. Nu multe, dar cele pe care le are sunt exact alea pe care ți-ar plăcea cel mai puțin să le iei pe încredere, și stăteau în arbore neexaminate.

v4.12.0 a adăugat make verify-binaries. Fiecare executabil urmărit trebuie declarat în assets/binaries.lock.json cu sha256-ul lui, sursa lui din upstream și starea semnăturii. Un binar nedeclarat pică build-ul. Unul schimbat pică build-ul. O semnătură degradată pică build-ul. Rulează primul înăuntrul lui make test, deci CI-ul îl impune la fiecare PR în loc de atunci când își aduce cineva aminte.

Rostul unei porți ca asta nu e regula, e ce găsește regula în clipa în care o pornești. Aici a documentat imediat scripts/defender-remover/PowerRun.exe:

"path":      "scripts/defender-remover/PowerRun.exe",
"product":   "PowerRun",
"vendor":    "Sordum Software",
"signature": "malformed",
"note":      "REPACKED, NOT PRISTINE..."

A venit vendorizat înăuntrul trusei defender-remover, nu direct de la Sordum. Directorul lui de certificate nu e un WIN_CERTIFICATE bine format, o lungime declarată de 776284822, revizia 0xc496, tipul 14951, față de un 0x200 și 2 cerute, iar hashul lui nu se potrivește cu nicio versiune Sordum din upstream, deci semnătura nu poate fi verificată față de nimic.

Ca să fie clar ce înseamnă și ce nu înseamnă asta: nu se știe că ar fi rău intenționat. O grămadă de unelte reîmpachetate arată așa. Ce s-a schimbat e că nu mai e tăcut, starea e scrisă undeva, în repo, lângă fișier, iar CI-ul pică dacă se mișcă vreodată. Un binar neexplicat despre care știi e alt risc decât un binar neexplicat despre care nu știi.

Aceeași versiune a dus suita de unit teste de la 244 de teste la 379. Tot ce înainte rula doar pe un terminal viu rulează acum în CI, conducând aplicația Flask reală pe un SDK scriptat, deci mt5client, monitorul și clientul de Go au primit prima lor acoperire.

Loguri

Totul se scurge în data/metatrader5/logs/ pe host:

  • install.log: progresul instalării MT5
  • start-mt5.log: logul secvenței de pornire
  • pip.log: instalarea pachetelor de Python
  • api-<broker>-<account>.log: logurile de API per terminal
  • full.log: furtunul concatenat cu tot ce e mai sus plus intrările din Windows Event Log trase dinăuntrul VM-ului. Ăsta e cel care prinde omorârile de OOM și procesele ucise în tăcere de Defender, care nu apar nicăieri altundeva.

Un sidecar separat de rotație a logurilor rulează lângă VM și rotește tot zilnic cu retenție de 7 zile. Gata cu fișierele de log de 4 GB care îți mănâncă discul după o săptămână de stivă lăsată pornită. Când se strică ceva, full.log e primul loc unde te uiți, cronologic, un singur fișier, tot într-un singur flux.

Sidecar de Tailscale

Expunerea publică a unui API de tranzacționare înseamnă să ceri să fii jefuit. Majoritatea oamenilor vor chestia asta accesibilă de pe laptopul lor și de nicăieri altundeva. Așa că există un sidecar de Tailscale încorporat care intră în tailnetul tău și servește API-ul la un hostname MagicDNS simplu:

http://mt5-httpapi/roboforex/main/account
http://mt5-httpapi/roboforex/main/symbols/EURUSD/rates?count=100
http://mt5-httpapi/ftmo/challenge1/positions

Pui cheia de autentificare în config.yaml, decomentezi blocul tailscale din docker-compose.yml, make up. Merge și cu Tailscale de serie și cu Headscale self-hosted (pentru al doilea setezi login_server). HTTP simplu prin design, stratul de wireguard deja criptează tot înăuntrul tailnetului, iar hostname-urile MagicDNS simple oricum nu au certificate TLS potrivite.

Sidecarul rulează în propriul netns (mod bridge, nu rețeaua hostului), deci primește propria identitate în tailnet. ACL-urile tale se limitează doar la nodul sidecarului, Tailscale-ul hostului (dacă are unul) rămâne complet în afara poveștii, iar orice trafic din sidecar destinat tailnetului se duce pe propria lui interfață tailscale0, nu pe a hostului. Tailscale Serve ascultă pe portul 80 înăuntrul netns-ului și dă mai departe spre sidecarul de nginx mereu pornit, peste rețeaua internă a lui docker. Starea persistă în .data/tailscale/state/, deci make down / make up refolosește loginul existent, cheia de autentificare se consumă doar la primul login.

Tokenul de API (dacă e setat) se aplică în continuare peste, Tailscale controlează accesibilitatea la nivel de rețea, tokenul de bearer controlează accesul la aplicație. Apărare pe straturi.

Cloudflare Tunnel (Când Chiar Ai Nevoie de Public)

Dacă chiar ai nevoie ca treaba asta să fie accesibilă de pe internetul deschis, să zicem ca să o legi la un bot găzduit sau la un frontend pe Vercel, există o opțiune de Cloudflare Tunnel. cloudflared sună afară spre marginea Cloudflare și dă mai departe spre sidecarul de nginx mereu pornit. Un tunel, un hostname, fiecare terminal accesibil în spatele lui /<broker>/<account>/:

https://mt5-api.yourdomain.com/roboforex/main/account
https://mt5-api.yourdomain.com/ftmo/challenge1/positions

Fără porturi deschise în firewall. Fără găurit NAT. Fără certificate de administrat, Cloudflare termină TLS-ul la margine gratis, sub Universal SSL-ul lor. Instalare: pui cloudflared pe host o dată, creezi un tunel, rutezi un hostname spre el, arunci credențialele în .data/cloudflared/, decomentezi blocul cloudflared din compose, make up.

Tratează hostname-ul public ca pe unul ostil și setează mereu api_token în config.yaml când folosești asta. Cloudflare controlează accesibilitatea publică; tokenul de bearer controlează aplicația. Dacă sari peste token aici, oricine găsește hostname-ul îți poate goli contul.

Pe Scurt

MetaTrader 5 în Docker cu un REST API. VM Windows adevărat prin KVM, nu Wine. Mai mulți brokeri și mai multe conturi care merg simultan pe resurse minime. Date de piață complete, administrare de ordine, urmărire de poziții, istoric de tranzacții, TA pe server prin sidecarul wickworks și un pipeline complet de Strategy Tester peste HTTP, toate în spatele unui JSON simplu. Plus un client tipat de Go, un skill de agent AI și unelte MCP tipate, per terminal sau unificate peste toate terminalele deodată, ca să lași un LLM să conducă tranzacții pe API fără să îi dai documentația cu lingurița.
Fără MQL5. Fără desktop de Windows. Fără biblioteci MT5 pe partea de client. Doar curl și gata.
Ia-l de aici: github.com/psyb0t/mt5-httpapi
Licențiat sub WTFPL, pentru că tranzacționarea ar trebui să ceară un disclaimer, nu o licență de software.