MetaTrader 5 only runs on Windows. The official Python library only works on Windows. The MQL5 scripting language is a C++ knockoff from 2005 that makes you want to gouge your eyes out with a rusty fork. And if you want to do anything programmatic with it — pull candles, place orders, check positions — you’re expected to either write MQL5 or run Python on a Windows box with the terminal open.
I wanted to hit an HTTP endpoint from any machine, in any language, and get JSON back. Like a normal human being.
So I built mt5-httpapi. A real Windows VM running inside Docker via QEMU/KVM, with the full MT5 terminal in portable mode and a Flask REST API on top. No Wine, no emulation hacks, no janky workarounds. A legit Windows 11 environment running the actual MetaTrader 5 binary, accessible over plain HTTP/JSON from anywhere.
Multiple brokers. Multiple accounts. Each terminal gets its own API process inside the VM, and an always-on nginx sidecar fronts them all behind a single host port at http://localhost:8888/<broker>/<account>/.... Run two FTMO challenges simultaneously, or mix brokers, or run ten terminals on one box — whatever you need.
How This Abomination Works
The container runs dockurr/windows — a Docker image that boots a full Windows VM using QEMU/KVM hardware virtualization. On first run, it downloads tiny11 (a stripped-down Windows 11, ~4 GB), installs it, then automatically sets up Python 3.12, installs MetaTrader5, debloats the living shit out of Windows, removes Defender entirely, and starts everything up.
After the first boot (~10 minutes), subsequent starts take about a minute. The container is configured with 2 vCPUs, 512 MB real RAM, and 5 GB swap. Sounds cursed, works fine. tiny11 plus the debloat script idles at ~1.4 GB, and MT5 plus the Python API barely add anything. Windows and MT5 are not latency-sensitive enough for swap to matter.
A shared folder between the host and the Windows VM (/shared → C:\Users\Docker\Desktop\Shared) holds everything — scripts, configs, broker installers, the API server code, and logs. The run.sh script syncs everything into this folder, generates iptables NAT rules for port forwarding from the container to the VM, and fires up docker-compose.
noVNC on port 8006 gives you a browser-based view of the Windows desktop. Useful for watching the install progress and confirming everything started. After that, forget the UI exists and just hit the REST API.
Multi-Terminal Behind One Port
Architecture update (v4.0+): the original layout exposed each terminal on its own host port (6542, 6543, 6544…). That’s gone. Everything now lives behind a single host port (default 127.0.0.1:8888) fronted by an always-on nginx sidecar that routes by path prefix:
http://localhost:8888/<broker>/<account>/...One port forwarded from the host, one TLS surface to worry about, one rule in your firewall. Each request hits nginx, the /<broker>/<account>/ prefix gets stripped, and the rest is proxied to that terminal’s Python API process inside the VM over the docker bridge. nginx config is auto-generated from config.yaml on every make up, so adding or removing terminals doesn’t need any manual reverse-proxy fiddling.
Config consolidation (v4.0): the old accounts.json + terminals.json split is gone. There’s now a single config/config.yaml (gitignored) as the source of truth:
# 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
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"The broker field matches both the accounts: key and the installer filename (mt5setup-roboforex.exe, mt5setup-ftmo.exe). Each broker’s terminal installs to <broker>/base/ once, then gets copied to <broker>/<account>/ at startup so multiple accounts of the same broker don’t step on each other.
utc_offset is per-terminal because brokers run on weird timezones (RoboForex/FTMO on UTC+3, TeleTrade on UTC+2). Every timestamp on the wire — candles, ticks, history, positions — gets normalized to real UTC server-side. Your client never has to think about broker-local time again. port is the container-internal port nginx talks to; it’s not exposed to the host.
reboot_interval is the new VM auto-reboot knob: MetaQuotes’ DWM/VirtIO-GPU stack accumulates kernel-side state over long uptimes and eventually gets weird, so the VM gets bounced on a schedule (default every 30 minutes). Set to 0 to disable.
Four terminals running simultaneously on 2 vCPUs with 512 MB real RAM — CPU spikes to 100% during startup while everything initializes, then drops to ~15% idle. Total memory: 2.1 GB, all handled by swap. You could run 10+ terminals like this without breaking a sweat — as long as you’re not deep-history-scraping all of them at once (MT5 caches every loaded chart and never releases it; deep backfills blow the 512 MB limit through the floor).
Setup
Requirements: Linux host with KVM enabled (/dev/kvm), Docker + Compose, ~20 GB disk, 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 upFirst run downloads the Windows ISO, installs it, debloats, installs MT5, reboots a couple times, then starts everything. After that:
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 ISOAuthentication
The API runs open by default. If you’re exposing it to a network (even a local one with other machines), set the token in config/config.yaml:
api_token: "$(openssl rand -hex 32)"If api_token is non-empty, every endpoint requires Authorization: Bearer <token>. Empty string = no auth — works fine for a single-machine setup where nothing else can reach the port.
With auth enabled, set the token in your shell and include it on every request:
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"}All the curl examples below assume no auth. If you’ve configured a token, prepend -H "Authorization: Bearer $MT5_API_TOKEN" to each one.
The API
All terminals are served behind a single host port via nginx. Default entry point: http://localhost:8888 (loopback-only). Each terminal lives at its own path prefix — http://localhost:8888/<broker>/<account>/.... Examples below use roboforex/main; substitute your own broker/account. GET for reading, POST for creating, PUT for modifying, DELETE for closing. All JSON.
Health and 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/restartThe API auto-initializes on first request. If MT5 isn’t connected yet, a background thread retries every 30 seconds. You almost never need to call /terminal/init manually.
A health monitor runs in the background — every 60 seconds it checks if the terminal is alive, logged in, and has algo trading enabled. If the terminal is dead for 5 consecutive checks, it auto-restarts: kills the process, relaunches the terminal, waits for the journal to confirm it’s up, and reconnects the API. You can also trigger a manual restart via POST /terminal/restart.
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
}Market Data
# 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"
# Tick history
curl "http://localhost:8888/roboforex/main/symbols/EURUSD/ticks?count=100"Timeframes: M1 M2 M3 M4 M5 M6 M10 M12 M15 M20 M30 H1 H2 H3 H4 H6 H8 H12 D1 W1 MN1. Candle time is the open time, unix epoch seconds.
Placing Orders
# 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}'Required fields: symbol, type, volume. Price auto-fills for market orders. Order types: BUY, SELL, BUY_LIMIT, SELL_LIMIT, BUY_STOP, SELL_STOP, BUY_STOP_LIMIT, SELL_STOP_LIMIT. Fill policies: FOK, IOC (default), RETURN. Expiration: GTC (default), DAY, SPECIFIED, SPECIFIED_DAY.
Every trade operation returns a result with retcode — 10009 means success, anything else means something went wrong. Use GET /error to debug.
Managing Positions and Orders
# 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/67890History
# 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 and to are required, unix epoch seconds. Deals have entry (0 = opening, 1 = closing) and profit (0 for entries, realized P&L for exits).
Technical Analysis
The API gives you raw market data — it doesn’t do TA. But there’s a full working example in examples/python/ that pulls candles and crunches them with pandas-ta and smartmoneyconcepts.
Indicators included: 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, and liquidity levels.
# 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.pngThe TA report prints the latest candle’s values for every indicator and then runs signal detection — RSI overbought/oversold, MACD histogram crossovers, EMA/SMA golden/death crosses, Bollinger Band breakouts, Stochastic extremes, ADX trend strength. The chart renders dark-themed candlesticks with moving averages, Bollinger Bands, VWAP, SMC overlays (order blocks, FVGs, BOS/CHoCH lines, liquidity sweeps), RSI panel, and MACD panel. Publication-quality PNGs at 1920×1080.
The indicator and signal modules are designed as building blocks. Import add_rsi(df) or detect_signals(df) into your own scripts and use the API as your data source. Pull candles, apply whatever analysis you want, place trades — all from a Python script running on any machine.
Position Sizing
The symbol endpoint gives you everything you need to calculate proper position sizes:
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_lotRound down to volume_step, clamp to [volume_min, volume_max]. Sanity check: volume * trade_contract_size * price should make sense relative to your balance. One lot of EURUSD is 100,000 EUR, not 1 EUR — trade_contract_size tells you this. Check it before you accidentally YOLO your entire account on what you thought was a micro position.
AI Skill
The repo ships with a .skills/ directory containing a skill definition for AI coding agents. Install it in OpenClaw or any other agent that supports the skills format, point it at your running instance via MT5_API_URL, and the agent gets the full API reference, pre-trade safety checklist, position sizing formulas, and usage patterns. It knows what endpoints exist, what fields to check before placing a trade, and how to interpret the results.
This means you can tell your AI agent “buy 0.1 lots of EURUSD with a 2 ATR stop loss” and it has everything it needs to pull the symbol info, calculate the SL price, place the order, and verify the result. No manual API documentation reading required — the skill gives it the complete playbook.
The Debloat
The Windows VM goes through an aggressive debloat on first boot. Disable all animations, transparency, wallpaper. Kill SysMain, audio, spooler, search, telemetry, and about 50 other useless services. Remove Windows Defender entirely — not disable, remove. Take ownership of the Defender directories and delete the binaries. Nuke all the privacy-invading bullshit: advertising ID, activity history, diagnostic data, all capability permissions. Disable every Microsoft spying scheduled task. Set processor priority to foreground, reduce kill timeouts, disable NTFS timestamps.
The result is a Windows 11 that boots fast, idles low, and doesn’t phone home to Microsoft every 30 seconds. Just enough OS to run MT5 and the Python API.
Logs
Everything funnels into data/metatrader5/logs/ on the host:
- install.log — MT5 installation progress
- start-mt5.log — boot sequence log
- pip.log — Python package installation
- api-<broker>-<account>.log — per-terminal API logs
- full.log — concatenated firehose of everything above plus Windows Event Log entries tailed from inside the VM. This is the one that catches the OOM kills and silent Defender-killed processes that don’t show up anywhere else.
A separate log rotation sidecar runs alongside the VM and rotates everything daily with 7-day retention. No more 4 GB log files eating your disk after a week of leaving the stack up. When shit breaks, full.log is the first place to look — chronological, single file, everything in one stream.
Tailscale Sidecar
Public exposure on a trading API is asking to get robbed. Most people want this thing reachable from their laptop and nothing else. So there’s a built-in Tailscale sidecar that joins your tailnet and serves the API at a bare MagicDNS hostname:
http://mt5-httpapi/roboforex/main/account
http://mt5-httpapi/roboforex/main/symbols/EURUSD/rates?count=100
http://mt5-httpapi/ftmo/challenge1/positionsSet the auth key in config.yaml, uncomment the tailscale block in docker-compose.yml, make up. Works with both stock Tailscale and self-hosted Headscale (set login_server for the latter). Plain HTTP by design — the wireguard layer already encrypts everything inside the tailnet, and bare MagicDNS hostnames don’t have matching TLS certs anyway.
The sidecar runs in its own netns (bridge mode, not host net) so it gets its own tailnet identity. Your ACLs scope to the sidecar’s node only, the host’s own Tailscale (if it has one) stays completely out of the picture, and any tailnet-bound traffic from inside the sidecar routes via its own tailscale0 interface — not the host’s. Tailscale Serve listens on port 80 inside the netns and proxies to the always-on nginx sidecar over docker’s internal network. State persists in .data/tailscale/state/, so make down / make up reuses the existing login — auth key is consumed on first login only.
The API token (if set) still applies on top — Tailscale gates network reachability, the bearer token gates application access. Layered defense.
Cloudflare Tunnel (When You Really Need Public)
If you actually need this thing reachable from the open internet — say, hooking it up to a hosted bot or a frontend on Vercel — there’s a Cloudflare Tunnel option. cloudflared dials out to Cloudflare’s edge and proxies to the always-on nginx sidecar. One tunnel, one hostname, every terminal reachable behind /<broker>/<account>/:
https://mt5-api.yourdomain.com/roboforex/main/account
https://mt5-api.yourdomain.com/ftmo/challenge1/positionsNo firewall ports opened. No NAT punching. No certs to manage — Cloudflare terminates TLS at the edge for free under their Universal SSL. Setup: install cloudflared on the host once, create a tunnel, route a hostname to it, drop the credentials into .data/cloudflared/, uncomment the cloudflared block in compose, make up.
Treat the public hostname as hostile and always set api_token in config.yaml when using this. Cloudflare gates the public reachability; the bearer token gates the application. If you skip the token here, anyone who finds the hostname can drain your account.
The Bottom Line
MetaTrader 5 in Docker with a REST API. Real Windows VM via KVM, not Wine. Multiple brokers and accounts running simultaneously on minimal resources. Full market data, order management, position tracking, and trade history — all over plain HTTP/JSON. Plus a TA example with 20+ indicators, Smart Money Concepts, signal detection, and publication-quality charting.
No MQL5. No Windows desktop. No MT5 libraries on the client side. Just curl and go.
Go grab it: github.com/psyb0t/mt5-httpapi
Licensed under WTFPL — because trading should require a disclaimer, not a software license.