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
Noneand alast_error()tuple. Nothing raises. - One account per terminal, one terminal per
initialize()call. You can pass apathto a specificterminal64.exeand switch accounts withlogin(), 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.
requestsorfetchand 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_copiedand the rest are pushed to your URL, HMAC-signed, instead of you polling a pipe. - Terminal health is a field.
statuson/v1/accounts/{id}saysconnectedor it doesn't. A502 upstream_errortells you the terminal is unreachable instead of handing youNone.
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()./v1places 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 fullMqlTradeRequest: no choice of filling mode, nomagicnumbers, 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
readonlyslot 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 package | REST API (MetaKit) | |
|---|---|---|
| OS | Windows only | Any |
| Terminal | Yours, same machine, must stay logged in | Hosted per account |
| Accounts per process | One | Any number |
| Order placement | Yes, order_send(), full request struct | Market / limit / stop, modify, close, cancel — with idempotency keys |
| Latency | Local pipe, milliseconds | Network hop, tens of ms |
| Symbol and tick depth | Everything the terminal knows | Symbols, ticks, candles, positions, orders, deals |
| Events | Poll | Signed webhooks |
| Deploy | Windows VPS or desktop | Container, serverless, anything |
| Cost | Free 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.