MetaTrader5 Python package vs a REST API

8 min readMetaKit

pip install MetaTrader5 succeeds on Linux. That's the cruel part. The wheel installs, the import works, and then mt5.initialize() returns False with last_error() reading (-10005, 'IPC timeout') or similar, because there is no terminal on this machine to talk to and there never will be.

Most people who search for a MetaTrader5 Python package alternative got there through that exact error. So: what the package actually is, where it's the better tool, where a REST API is, and the same script written both ways.

What the MetaTrader5 Python package actually is

It is not a client library in the sense that boto3 or stripe are. It's an IPC bridge. You run a MetaTrader 5 terminal on a Windows machine, logged in to one account, and the package opens a local pipe to that process. Every call (account_info(), positions_get(), copy_rates_range(), order_send()) is marshalled into the terminal and answered by it.

Three consequences fall out of that design:

  • Windows only. The IPC is Windows-native and the terminal is a Windows binary. MetaQuotes publishes no Linux or macOS build of the package.
  • The terminal is part of your process's dependency tree. If it's not running, logged in, and connected, the package returns None and a last_error() tuple. Nothing raises.
  • One account per terminal, one terminal per initialize() call. You can pass a path to a specific terminal64.exe and switch accounts with login(), but you can't hold two accounts open in one Python process at the same time.

The Wine and Docker workarounds

Yes, you can run the Windows build of Python and the terminal under Wine on Linux, and people do, including us. We run each MetaKit account in exactly that shape inside a container, and wrote up what it takes. It works. It's also fragile in the specific way of things that were never supported: a Wine update, a terminal auto-update, or a Python minor version bump can each break the pipe, and when it breaks the error is IPC timeout again, with nothing to tell you which layer moved. Fine for a platform team whose job is maintaining it. Miserable as a side quest.

Where the package genuinely wins

Credit where due, because the package is good at what it's for.

Depth of access. Every symbol property (symbol_info("XAUUSD") returns about ninety fields), every tick with flags, copy_rates_from_pos for bars by index, history_orders_get and history_deals_get with a position or ticket filter. If the terminal knows it, the package will hand it to you as a named tuple or a NumPy structured array, ready for pandas.DataFrame().

Order placement. order_send() with a full MqlTradeRequest: fill mode, deviation, magic number, expiration, the lot. You can place, modify, and close from Python with the same control an EA has. This is the single biggest reason to stay on the package, and I'll come back to it.

No network hop. A local pipe on the same box is measured in single-digit milliseconds. For a research loop hammering copy_ticks_range across a year of data, that matters.

It's free, apart from the Windows machine you already had.

If you're one developer, on one Windows PC, doing research or running one personal account, stop reading and use the package. It's the right call.

What the package costs you

The bill arrives when the script needs to run somewhere other than your desk.

The OS constraint is total. Your deploy target is a Windows box: a VPS you maintain, or a Windows worker in an otherwise-Linux fleet. The rest of your stack lives in containers; this piece lives in an RDP session. The VPS post has the full cost breakdown; the short version is that the hours dwarf the hosting.

Concurrency across accounts means multiple terminals. Ten client accounts is ten terminal processes, each a few hundred megabytes of RAM, each with its own login state to watch. The package can't multiplex them; your supervisor has to.

Failure is silent. positions_get() returning None looks identical whether the account has no positions or the terminal died. You learn to check last_error() after every call and wrap the whole thing in retry logic. Then you learn a retry doesn't help when a modal dialog is waiting for a click.

Deployment is bespoke. No Docker image you'd want in production, no serverless, no CI runner that has a logged-in terminal. Every environment is hand-built.

What a REST API gives you, and what it doesn't

The REST approach moves the terminal to someone else's infrastructure and gives you HTTP. At MetaKit, each connected account runs in its own hosted terminal, and you talk to https://api.metakit.cloud/v1 from anything that can make a request.

What that buys:

  • Any OS, any language. Linux container, Lambda, a cron job on a Raspberry Pi, a TypeScript service. requests or fetch and you're done.
  • Many accounts, one process. Accounts are ids. Reading twenty of them is twenty GET requests, not twenty terminals.
  • Webhooks. trade.position.opened, account.error, copier.trade_copied and the rest are pushed to your URL, HMAC-signed, instead of you polling a pipe.
  • Terminal health is a field. status on /v1/accounts/{id} says connected or it doesn't. A 502 upstream_error tells you the terminal is unreachable instead of handing you None.

What it costs, plainly:

  • A network hop. Tens of milliseconds per call, more from far away. For dashboards and analytics it's nothing. For a tick-by-tick research loop, use candles at the timeframe you need (/v1/accounts/{id}/candles) rather than pulling raw ticks through HTTP.
  • Order placement is narrower than order_send(). /v1 places market, limit and stop orders, moves stops, closes and cancels, and that covers most "Python decides, Python places the order" designs. What it does not give you is the full MqlTradeRequest: no choice of filling mode, no magic numbers, no position-by-position close-by. It also validates volume and stops against the symbol spec and rejects off-step lots rather than rounding them, which is stricter than the package. If you need the raw request struct, the package still wins.
  • A hosted service holds credentials. Passwords are write-only and never returned, but they are held. Use the investor password and a readonly slot when you only read.
  • A subscription. $5 a month per read-only account, $10 per full account.

The same script, both ways

The task: fetch the last 30 days of deals and compute net P&L.

With the package, on Windows, terminal running:

import MetaTrader5 as mt5
from datetime import datetime, timedelta
 
if not mt5.initialize():
    raise SystemExit(f"initialize() failed: {mt5.last_error()}")
 
to = datetime.now()
frm = to - timedelta(days=30)
deals = mt5.history_deals_get(frm, to)
mt5.shutdown()
 
if deals is None:
    raise SystemExit("history_deals_get returned None; check last_error()")
 
# Only trading deals: skip balance operations like deposits.
net = sum(
    d.profit + d.commission + d.swap
    for d in deals
    if d.type in (mt5.DEAL_TYPE_BUY, mt5.DEAL_TYPE_SELL)
)
print(f"Net P&L (30d): {net:,.2f}")

Over REST, from anywhere:

import os
from datetime import datetime, timedelta, timezone
 
import requests
 
BASE = "https://api.metakit.cloud"
HEADERS = {"Authorization": f"Bearer {os.environ['METAKIT_KEY']}"}
ACCOUNT_ID = 2
 
to = datetime.now(timezone.utc)
frm = to - timedelta(days=30)
 
net = 0.0
page = 1
while True:
    res = requests.get(
        f"{BASE}/v1/accounts/{ACCOUNT_ID}/deals",
        params={
            "from": frm.isoformat(timespec="seconds"),
            "to": to.isoformat(timespec="seconds"),
            "page": page,
            "limit": 100,
        },
        headers=HEADERS,
    )
    if not res.ok:
        err = res.json()["error"]
        raise SystemExit(f"{err['code']}: {err['message']}")
 
    body = res.json()
    # Trading deals carry a positionId; balance operations don't.
    net += sum(
        d["profit"] + d["commission"] + d["swap"] + d["fee"]
        for d in body["data"]
        if d.get("positionId")
    )
 
    if page >= body["total_pages"]:
        break
    page += 1
 
print(f"Net P&L (30d): {net:,.2f}")

Two things to notice. The package hands you every deal in one call and you sum in memory; the API paginates at 100 per page, so you loop on total_pages. And the package's failure mode is None plus last_error(), while the API's is an HTTP status and an error envelope with a code you branch on (account_not_running, upstream_error, unauthorized).

Either way, remember that a trade is two deals sharing a positionId, and profit lands on the exit. If you want per-trade stats rather than a sum, either group by that id yourself or call /v1/accounts/{id}/performance, which does the grouping and hands back net_profit, win_rate, profit_factor, and max_drawdown directly. The orders, deals, positions post covers why that grouping isn't optional.

Decision table

MetaTrader5 packageREST API (MetaKit)
OSWindows onlyAny
TerminalYours, same machine, must stay logged inHosted per account
Accounts per processOneAny number
Order placementYes, order_send(), full request structMarket / limit / stop, modify, close, cancel — with idempotency keys
LatencyLocal pipe, millisecondsNetwork hop, tens of ms
Symbol and tick depthEverything the terminal knowsSymbols, ticks, candles, positions, orders, deals
EventsPollSigned webhooks
DeployWindows VPS or desktopContainer, serverless, anything
CostFree plus the Windows box$5 or $10 per account per month

Read the order placement row twice. It decides more choices than any other.

Which one you should use

Use the package if you're placing orders from Python, if you need the full symbol_info and tick detail for research, or if you have one account on one Windows machine and no reason to change.

Use a REST API if the code runs on Linux, if it reads more than one account, if it needs to stay up without you, or if you'd rather express "did the terminal die" as a status field than as a None. The Python connect walkthrough gets you from zero to a connected account in five steps, and llms.txt is the full reference.

Plenty of teams run both: the package on a Windows box for the one thing that places orders, and the API for everything that reads. That's not a compromise. That's using each for what it's built for.