MetaTrader 5 running inside a real Windows VM (Docker + QEMU/KVM) with a REST API slapped on top for programmatic trading. No Wine bullshit, no janky workarounds - a legit Windows environment running the full MT5 terminal in portable mode.
Supports multiple brokers and multiple accounts on the same VM simultaneously. Each terminal gets its own Python API process on its own port. Run two FTMO challenges at once, or mix brokers - whatever you need.
This is a tool for automating trades. If you blow your account, that's on you. Use demo accounts first, test your shit, and don't come crying when your algo buys the top.
- Linux host with KVM enabled (
/dev/kvm) - Docker + Docker Compose
- ~20 GB disk (4 GB ISO + 11 GB VM + MT5 installs)
- 5 GB RAM (for the Windows VM)
The container is configured with a 512M memory limit but a 5G memswap limit — so the VM runs mostly on swap. Sounds cursed, works fine. Windows + MT5 are not latency-sensitive enough for this to matter. tiny11 + our debloat script idles at ~1.4 GB RAM, and MT5 + the Python API barely add anything on top of that. noVNC is there so you can watch the installation progress and confirm everything started up. After that, forget the UI exists and just hit the REST API.
This is 4 MT5 terminals (RoboForex, 2x TeleTrade, FTMO) running simultaneously on 2 virtual CPUs with only 512M of real RAM — the rest lives in swap. CPU spikes to 100% during startup while all terminals and APIs initialize at once, then drops to ~15% idle. Total memory usage: 2.1 GB, all comfortably handled by swap. You could easily run 10+ terminals in a single container like this.
# 1. Set up your broker account
cp config/accounts.json.example config/accounts.json
cp config/terminals.example.json config/terminals.json
# Edit both files with your broker credentials
# 2. Drop your broker's MT5 installer in mt5installers/
# Name it: mt5setup-<broker>.exe
cp ~/Downloads/mt5setup.exe mt5installers/mt5setup-roboforex.exe
# 3. Fire it up
make upFirst run downloads tiny11 (stripped-down Windows 11, ~4 GB), installs it (~10 min), then sets up Python + MT5 automatically. After that, boots in ~1 min. Go grab a coffee on the first run.
Your broker credentials. Organized by broker, then account name:
{
"roboforex": {
"main": {
"login": 12345678,
"password": "your_password",
"server": "RoboForex-Pro"
},
"demo": {
"login": 87654321,
"password": "demo_password",
"server": "RoboForex-Demo"
}
}
}Defines which terminals to run. Each entry gets its own MT5 terminal instance and its own API process on a dedicated port:
[
{
"broker": "roboforex",
"account": "main",
"port": 6542
},
{
"broker": "roboforex",
"account": "demo",
"port": 6543
}
]broker— matches the installer name (mt5setup-<broker>.exe) andaccounts.jsonkeyaccount— matches the account name inaccounts.jsonunder that brokerport— unique port for this terminal's HTTP API
Each terminal installs to <broker>/base/ and gets copied to <broker>/<account>/ at startup so multiple accounts of the same broker don't step on each other.
Extra Python packages you want in the VM. MetaTrader5 and flask are already in there.
Custom commands that run on every VM boot before MT5 starts. Shove whatever Windows setup shit you need in here.
Dump your broker MT5 installers here. Name them mt5setup-<broker>.exe and each one gets its own portable install automatically.
Each terminal's API runs on its configured port (from terminals.json). Default: http://localhost:6542
| Method | Endpoint | Description |
|---|---|---|
| GET | /ping |
Is this thing on? |
| GET | /error |
Last MT5 error |
GET /ping:
{ "status": "ok" }GET /error:
{ "code": 1, "message": "Success" }| Method | Endpoint | Description |
|---|---|---|
| GET | /terminal |
Terminal info |
| POST | /terminal/init |
Initialize MT5 connection |
| POST | /terminal/shutdown |
Kill MT5 |
GET /terminal:
{
"build": 5602,
"codepage": 0,
"commondata_path": "C:\\Users\\Docker\\AppData\\Roaming\\MetaQuotes\\Terminal\\Common",
"community_account": false,
"community_balance": 0.0,
"community_connection": false,
"company": "Your Broker Inc.",
"connected": true,
"data_path": "C:\\Users\\Docker\\Desktop\\Shared\\mybroker",
"dlls_allowed": true,
"email_enabled": false,
"ftp_enabled": false,
"language": "English",
"maxbars": 100000,
"mqid": false,
"name": "MyBroker MetaTrader 5",
"notifications_enabled": false,
"path": "C:\\Users\\Docker\\Desktop\\Shared\\mybroker",
"ping_last": 0,
"retransmission": 0.003,
"trade_allowed": true,
"tradeapi_disabled": false
}POST /terminal/init and POST /terminal/shutdown:
{ "success": true }The API auto-initializes on first request. You almost never need to call these manually.
| Method | Endpoint | Description |
|---|---|---|
| GET | /account |
Current account info |
GET /account:
{
"login": 12345678,
"name": "Your Name",
"server": "MyBroker-Server",
"company": "Your Broker Inc.",
"currency": "USD",
"currency_digits": 2,
"balance": 10000.0,
"credit": 0.0,
"profit": 0.0,
"equity": 10000.0,
"margin": 0.0,
"margin_free": 10000.0,
"margin_level": 0.0,
"margin_initial": 0.0,
"margin_maintenance": 0.0,
"margin_so_call": 70.0,
"margin_so_so": 20.0,
"margin_so_mode": 0,
"margin_mode": 2,
"assets": 0.0,
"liabilities": 0.0,
"commission_blocked": 0.0,
"leverage": 500,
"limit_orders": 0,
"trade_allowed": true,
"trade_expert": true,
"trade_mode": 0,
"fifo_close": false
}| Method | Endpoint | Description |
|---|---|---|
| GET | /symbols |
List symbols (?group=*USD*) |
| GET | /symbols/:symbol |
Symbol details |
| GET | /symbols/:symbol/tick |
Latest tick |
| GET | /symbols/:symbol/rates |
OHLCV candles (?timeframe=H1&count=100) |
| GET | /symbols/:symbol/ticks |
Tick data (?count=100) |
GET /symbols — array of symbol names:
["EURUSD", "GBPUSD", "ADAUSD", "BTCUSD", "..."]GET /symbols/:symbol — full symbol info:
{
"name": "EURUSD",
"description": "Euro vs US Dollar",
"path": "Markets\\Forex\\Major\\EURUSD",
"currency_base": "EUR",
"currency_profit": "USD",
"currency_margin": "EUR",
"digits": 5,
"point": 1e-05,
"spread": 30,
"spread_float": true,
"trade_contract_size": 100000.0,
"trade_tick_size": 1e-05,
"trade_tick_value": 1.0,
"trade_tick_value_profit": 1.0,
"trade_tick_value_loss": 1.0,
"volume_min": 0.01,
"volume_max": 100.0,
"volume_step": 0.01,
"volume_limit": 0.0,
"trade_mode": 4,
"trade_calc_mode": 0,
"trade_exemode": 2,
"trade_stops_level": 1,
"trade_freeze_level": 0,
"swap_long": -11.0,
"swap_short": 1.14064,
"swap_mode": 1,
"swap_rollover3days": 3,
"margin_initial": 0.0,
"margin_maintenance": 0.0,
"margin_hedged": 50000.0,
"filling_mode": 3,
"expiration_mode": 15,
"order_gtc_mode": 0,
"order_mode": 127,
"bid": 1.18672,
"ask": 1.18702,
"bidhigh": 1.18845,
"bidlow": 1.1847,
"askhigh": 1.1885,
"asklow": 1.18475,
"last": 0.0,
"time": 1771027139,
"select": true,
"visible": true,
"custom": false,
"session_deals": 0,
"session_buy_orders": 0,
"session_sell_orders": 0,
"session_open": 1.1869,
"session_close": 1.18698,
"price_change": -0.0219,
"bank": "",
"basis": "",
"category": "",
"exchange": "",
"isin": "",
"..."
}There's a shitload of fields — these are the ones you'll actually use:
| Field | What it is |
|---|---|
bid, ask |
Current prices |
digits |
Price decimal places |
point |
Smallest price change |
trade_tick_size |
Minimum price movement |
trade_tick_value |
Profit/loss per tick per 1 lot |
trade_contract_size |
Contract size (100000 for forex) |
volume_min, volume_max, volume_step |
Lot size constraints |
spread |
Current spread in points |
swap_long, swap_short |
Overnight swap rates |
trade_stops_level |
Min distance for SL/TP from price (points) |
GET /symbols/:symbol/tick:
{
"time": 1771150549,
"bid": 0.3001,
"ask": 0.3004,
"last": 0.0,
"volume": 0,
"time_msc": 1771150549145,
"flags": 1030,
"volume_real": 0.0
}GET /symbols/:symbol/rates — array of OHLCV candles:
Timeframes: M1 M2 M3 M4 M5 M6 M10 M12 M15 M20 M30 H1 H2 H3 H4 H6 H8 H12 D1 W1 MN1
{
"time": 1771128000,
"open": 0.2962,
"high": 0.3006,
"low": 0.2922,
"close": 0.2979,
"tick_volume": 4755,
"spread": 30,
"real_volume": 0
}time is the candle open time, unix epoch seconds.
GET /symbols/:symbol/ticks — array of ticks:
{
"time": 1771146325,
"bid": 0.2973,
"ask": 0.2976,
"last": 0.0,
"volume": 0,
"time_msc": 1771146325123,
"flags": 6,
"volume_real": 0.0
}| Method | Endpoint | Description |
|---|---|---|
| GET | /positions |
List open positions (?symbol=) |
| GET | /positions/:ticket |
Get position |
| PUT | /positions/:ticket |
Update SL/TP |
| DELETE | /positions/:ticket |
Close position |
GET /positions — array of position objects:
{
"ticket": 42094820,
"time": 1771150554,
"time_msc": 1771150554509,
"time_update": 1771150554,
"time_update_msc": 1771150554509,
"type": 0,
"magic": 0,
"identifier": 42094820,
"reason": 3,
"volume": 100.0,
"price_open": 0.3005,
"sl": 0.28,
"tp": 0.32,
"price_current": 0.3003,
"swap": 0.0,
"profit": -0.02,
"symbol": "ADAUSD",
"comment": "",
"external_id": ""
}type 0 = buy, 1 = sell. profit is unrealized P&L.
PUT /positions/:ticket — move your stop loss / take profit:
{
"sl": 0.27,
"tp": 0.36
}DELETE /positions/:ticket — close that shit:
{
"volume": 500,
"deviation": 20
}All fields optional. volume defaults to full position, deviation defaults to 20.
| Method | Endpoint | Description |
|---|---|---|
| GET | /orders |
List pending orders (?symbol=) |
| POST | /orders |
Place an order |
| GET | /orders/:ticket |
Get order |
| PUT | /orders/:ticket |
Modify order |
| DELETE | /orders/:ticket |
Cancel order |
GET /orders — array of pending order objects:
{
"ticket": 42094812,
"time_setup": 1771147800,
"time_setup_msc": 1771147800123,
"time_done": 0,
"time_done_msc": 0,
"time_expiration": 0,
"type": 2,
"type_time": 0,
"type_filling": 1,
"state": 1,
"magic": 0,
"position_id": 0,
"position_by_id": 0,
"reason": 3,
"volume_initial": 1000.0,
"volume_current": 1000.0,
"price_open": 0.28,
"sl": 0.25,
"tp": 0.35,
"price_current": 0.2989,
"price_stoplimit": 0.0,
"symbol": "ADAUSD",
"comment": "",
"external_id": ""
}type: 0=BUY, 1=SELL, 2=BUY_LIMIT, 3=SELL_LIMIT, 4=BUY_STOP, 5=SELL_STOP. state: 1=placed, 2=canceled, 3=partial, 4=filled, 5=rejected, 6=expired.
POST /orders — send it:
{
"symbol": "ADAUSD",
"type": "BUY",
"volume": 1000,
"price": 0.28,
"sl": 0.25,
"tp": 0.35,
"deviation": 20,
"magic": 0,
"comment": "",
"type_filling": "IOC",
"type_time": "GTC"
}Required: symbol, type, volume. Everything else is optional. price gets auto-filled for market orders.
Order types:
- Market:
BUY,SELL - Pending:
BUY_LIMIT,SELL_LIMIT,BUY_STOP,SELL_STOP,BUY_STOP_LIMIT,SELL_STOP_LIMIT
Fill policies: FOK, IOC (default), RETURN
Expiration types: GTC (default), DAY, SPECIFIED, SPECIFIED_DAY
PUT /orders/:ticket — change your mind on a pending order:
{
"price": 0.29,
"sl": 0.26,
"tp": 0.36,
"type_time": "GTC"
}All fields optional.
What comes back from POST/PUT/DELETE on orders and positions:
{
"retcode": 10009,
"deal": 40536203,
"order": 42094820,
"volume": 100.0,
"price": 0.3005,
"bid": 0.3002,
"ask": 0.3005,
"comment": "Request executed",
"request_id": 1549268253,
"retcode_external": 0
}retcode 10009 = you're good. Anything else = something went wrong.
| Method | Endpoint | Description |
|---|---|---|
| GET | /history/orders |
Order history (?from=TS&to=TS) |
| GET | /history/deals |
Deal history (?from=TS&to=TS) |
from and to are required, unix epoch seconds.
History order object (completed/cancelled orders):
{
"ticket": 42094820,
"time_setup": 1771150554,
"time_setup_msc": 1771150554509,
"time_done": 1771150554,
"time_done_msc": 1771150554509,
"time_expiration": 0,
"type": 0,
"type_time": 0,
"type_filling": 1,
"state": 4,
"magic": 0,
"position_id": 42094820,
"position_by_id": 0,
"reason": 3,
"volume_initial": 100.0,
"volume_current": 0.0,
"price_open": 0.3005,
"sl": 0.28,
"tp": 0.32,
"price_current": 0.3005,
"price_stoplimit": 0.0,
"symbol": "ADAUSD",
"comment": "Request executed",
"external_id": ""
}state 4 = filled, 2 = canceled, 5 = rejected, 6 = expired. volume_current 0 = fully filled.
Deal object (actual executed trades):
{
"ticket": 40536203,
"order": 42094820,
"time": 1771150554,
"time_msc": 1771150554509,
"type": 0,
"entry": 0,
"position_id": 42094820,
"symbol": "ADAUSD",
"volume": 100.0,
"price": 0.3005,
"commission": 0.0,
"swap": 0.0,
"profit": 0.0,
"fee": 0.0,
"magic": 0,
"reason": 3,
"comment": "",
"external_id": ""
}type: 0 = buy, 1 = sell. entry: 0 = opening, 1 = closing. profit is 0 for entries, actual realized P&L for exits.
# Check your balance
curl http://localhost:6542/account
# Grab some EURUSD H4 candles
curl "http://localhost:6542/symbols/EURUSD/rates?timeframe=H4&count=100"
# YOLO 1000 ADAUSD with SL and TP
curl -X POST http://localhost:6542/orders \
-H "Content-Type: application/json" \
-d '{"symbol": "ADAUSD", "type": "BUY", "volume": 1000, "sl": 0.25, "tp": 0.35}'
# Place a pending buy limit
curl -X POST http://localhost:6542/orders \
-H "Content-Type: application/json" \
-d '{"symbol": "ADAUSD", "type": "BUY_LIMIT", "volume": 1000, "price": 0.28, "sl": 0.25, "tp": 0.35}'
# Move your SL and TP
curl -X PUT http://localhost:6542/positions/12345 \
-H "Content-Type: application/json" \
-d '{"sl": 0.27, "tp": 0.36}'
# Close half
curl -X DELETE http://localhost:6542/positions/12345 \
-H "Content-Type: application/json" \
-d '{"volume": 500}'
# Close everything
curl -X DELETE http://localhost:6542/positions/12345
# Hit different terminals when running multi-terminal
curl http://localhost:6542/account # terminal 1
curl http://localhost:6543/account # terminal 2
# Get deal history for the last 24h
curl "http://localhost:6542/history/deals?from=$(date -d '1 day ago' +%s)&to=$(date +%s)"The API gives you raw market data — it doesn't do TA. If you need indicators, grab the candles from here and crunch them yourself. There's a full working example in examples/python/ using pandas-ta with ATR, RSI, MACD, Bollinger Bands, MFI, Stochastic, ADX, VWAP, and moving averages.
cd examples/python
pip install -r requirements.txt
# Default: EURUSD H4 200 candles
python ta.py
# Custom symbol/timeframe/count
python ta.py BTCUSD H1 100
python ta.py ADAUSD D1 200
# Custom API URL
MT5_API_URL=http://10.0.0.5:6542 python ta.py EURUSD D1
# Candlestick chart with TA overlays (1920x1080 PNG)
python chart.py ADAUSD
python chart.py BTCUSD H1 100
python chart.py EURUSD D1 200 -o eurusd.pngCheck out indicators.py for the individual indicator functions and signals.py for signal detection. Use them as building blocks for your own shit.
make up Fire up the VM (downloads ISO if needed)
make down Shut it down
make logs Tail the logs
make status Check VM and API status
make clean Nuke VM disk and state (keeps ISO)
make distclean Nuke everything including ISO
| Port | Service | Override |
|---|---|---|
| 8006 | noVNC (VM desktop) | NOVNC_PORT=9006 make up |
| 6542+ | HTTP API (per terminal) | Set in config/terminals.json |
API ports are determined by config/terminals.json. The run.sh script reads all configured ports, generates an .env file with API_PORT_RANGE, and docker-compose maps the range automatically. Each terminal's API process listens on its own port.
config/ Your config shit
accounts.json Broker credentials (gitignored)
terminals.json Multi-terminal config (gitignored)
terminals.example.json Example config
accounts.json.example Example credentials
requirements.txt Python packages for the VM
setup.bat Custom boot commands
scripts/ Scripts that run inside the Windows VM
oem-install.bat First-boot OEM script (creates startup entry)
install.bat Setup (Python, MT5, firewall) — runs every boot
start-mt5.bat Boot entrypoint (install + start terminals + APIs)
debloat.bat Windows debloat script
defender-remover/ Windows Defender removal tool
mt5api/ Python HTTP API server
handlers/ Route handlers
config.py Configuration (--broker, --account, --port CLI args)
mt5client.py MT5 wrapper
server.py Flask routes
examples/ Usage examples
python/ TA, charting, and API client modules
mt5installers/ Broker MT5 setup executables (gitignored)
data/ Generated/volatile data (gitignored)
win.iso Windows ISO
storage/ VM disk
metatrader5/ Shared folder with VM
<broker>/base/ Base MT5 install per broker
<broker>/<account>/ Per-account copy (created at startup)
oem/ First-boot scripts
Inside the VM's shared folder (data/metatrader5/logs/):
install.log- MT5 installation progress (install.bat)start-mt5.log- Boot sequence log (start-mt5.bat)pip.log- Python package installationapi-<broker>-<account>.log- Per-terminal API logs
When shit breaks, check these first.
WTFPL
