MetaTrader 5 and Python: Official Package or Local Socket Bridge?

MetaTrader 5 and Python: Official Package or Local Socket Bridge?

There are two practical ways to connect MetaTrader 5 to Python:

  1. Use MetaQuotes’ official MetaTrader5 Python package to communicate with a local MetaTrader 5 terminal.
  2. Let an MQL5 Expert Advisor or script exchange messages with a Python service over a socket.

The official package is the shorter route when Python needs terminal state, account information, symbols, ticks, or bars. A socket bridge is useful when an Expert Advisor should keep control inside MetaTrader while Python provides a calculation or model result.

The examples below only read terminal information or return a neutral socket response. They do not place trades. Test all automation on a demo account first, and treat communication failure as a reason to take no action.

Current platform limits of the MetaTrader5 package

As checked on September 9, 2026, the current PyPI release of `MetaTrader5` is 5.0.6180. It provides CPython wheels for Windows x86-64 and no source distribution. In practical terms, install the package and run the MetaTrader 5 terminal on 64-bit Windows. A normal pip install MetaTrader5 on Linux or macOS does not have a matching official package artifact.

MetaQuotes describes the package as an interprocess connection to the MetaTrader 5 terminal. It is not a broker-independent market-data API. The symbols, history, account state, and permissions you see come through the connected terminal and its configured trading account.

Install it in a virtual environment rather than into the system Python:

py -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install MetaTrader5

If pip reports that no matching distribution exists, check that Python is 64-bit CPython and that its version has a wheel on the PyPI files page.

Connect to the terminal without embedding credentials

The simplest connection uses the account already selected in the terminal:

import MetaTrader5 as mt5

if not mt5.initialize():
    raise RuntimeError(f"initialize() failed: {mt5.last_error()}")

try:
    print("Python package:", mt5.__version__)
    print("Terminal version:", mt5.version())
finally:
    mt5.shutdown()

According to the `initialize()` reference, the call can launch the terminal when necessary. If several MetaTrader installations exist, pass the intended terminal executable explicitly:

import MetaTrader5 as mt5

terminal = r"C:\Program Files\MetaTrader 5\terminal64.exe"

if not mt5.initialize(terminal, timeout=60_000):
    raise RuntimeError(f"initialize() failed: {mt5.last_error()}")

try:
    print(mt5.version())
finally:
    mt5.shutdown()

Avoid putting account passwords in source code, notebooks, screenshots, or logs. If a script only needs the terminal’s saved session, do not pass login credentials at all.

Read recent bars safely

This example selects a symbol in Market Watch, requests bars, handles an empty result, and always closes the package connection:

from datetime import datetime, timezone

import MetaTrader5 as mt5

symbol = "EURUSD"
timeframe = mt5.TIMEFRAME_M1
start_position = 1  # skip the current, still-forming bar
count = 100

if not mt5.initialize():
    raise RuntimeError(f"initialize() failed: {mt5.last_error()}")

try:
    if not mt5.symbol_select(symbol, True):
        raise RuntimeError(
            f"symbol_select({symbol!r}) failed: {mt5.last_error()}"
        )

    rates = mt5.copy_rates_from_pos(
        symbol,
        timeframe,
        start_position,
        count,
    )

    if rates is None or len(rates) == 0:
        raise RuntimeError(f"no bars returned: {mt5.last_error()}")

    for row in rates[:5]:
        opened = datetime.fromtimestamp(int(row["time"]), tz=timezone.utc)
        print(opened.isoformat(), row["open"], row["high"], row["low"], row["close"])
finally:
    mt5.shutdown()

The broker’s symbol may be named differently, such as EURUSD.a, so inspect the terminal rather than assuming one exact name. In `copy_rates_from_pos()`, position 0 is the current bar; use position 1 when a calculation should only consume closed bars. Available history is also limited by the terminal’s Max. bars in chart setting. MetaQuotes documents bar and tick timestamps as UTC in its `copy_rates_from()` notes, so keep timezone-aware UTC values in Python.

Keep order submission out of the first test

The package exposes order_check() and order_send(), but a successful check does not guarantee that a trade will execute. Execution rules, filling modes, symbol names, market hours, volume steps, permissions, and return codes depend on the terminal, broker, account, and instrument.

Build the connection in stages:

  1. confirm the package and terminal versions;
  2. read terminal and symbol information;
  3. retrieve a small bar sample and verify its timestamps;
  4. log errors without recording credentials or unnecessary account identifiers;
  5. only then test an explicitly reviewed order request on a demo account.

For any order path, inspect both last_error() and the returned trade result. Use `order_check()` as one validation step, not as a promise of execution, and read the `order_send()` reference for the request and result structures.

When a socket bridge is the better boundary

A socket bridge keeps the MetaTrader-facing loop in MQL5 while Python acts as a separate service. This is useful when:

  • an existing Expert Advisor already owns timing and order state;
  • Python performs a calculation that is awkward to implement in MQL5;
  • the messages need a custom schema or versioned protocol;
  • the MQL5 side must be able to continue safely when Python is unavailable.

MetaTrader’s network-function documentation requires the destination address to be added manually under Tools > Options > Expert Advisors. Socket calls are available to Expert Advisors and scripts, not indicators. `SocketConnect()` requires a finite connection timeout. Use `SocketTimeouts()` to set send and receive timeouts too; their default zero values can wait indefinitely.

For a same-machine bridge, bind Python to 127.0.0.1, not 0.0.0.0. Plain TCP does not authenticate or encrypt messages. If the Python service must run on another machine, use a protected network design with authentication and TLS rather than exposing an unauthenticated trading-control port.

Here is a deliberately small one-request server for testing the message boundary:

import json
import socket

HOST = "127.0.0.1"
PORT = 9090
MAX_MESSAGE = 64 * 1024

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((HOST, PORT))
    server.listen(5)
    print(f"Listening on {HOST}:{PORT}")

    while True:
        conn, address = server.accept()
        with conn:
            conn.settimeout(5)
            with conn.makefile("rwb") as stream:
                try:
                    line = stream.readline(MAX_MESSAGE + 1)
                except OSError:
                    continue

                if not line or len(line) > MAX_MESSAGE or not line.endswith(b"\n"):
                    reply = {"status": "error", "action": "none"}
                else:
                    try:
                        request = json.loads(line.decode("utf-8"))
                        if not isinstance(request, dict):
                            raise ValueError("request must be a JSON object")
                        request_id = request.get("request_id")
                        if (
                            not isinstance(request_id, str)
                            or not request_id.strip()
                            or len(request_id) > 128
                        ):
                            raise ValueError("invalid request_id")
                        reply = {
                            "status": "ok",
                            "request_id": request_id,
                            "action": "none",
                        }
                    except (UnicodeDecodeError, json.JSONDecodeError, ValueError):
                        reply = {"status": "error", "action": "none"}

                try:
                    stream.write(json.dumps(reply).encode("utf-8") + b"\n")
                    stream.flush()
                except OSError:
                    continue

This is a framing demonstration, not a production trading service. A real protocol should include a version, request ID, timestamp, strict field validation, a message-size limit, and idempotency rules. The MQL5 side should use `SocketConnect()` and `SocketTimeouts()`, reject stale or malformed responses, and default to no action after a timeout or disconnect.

Which approach should you choose?

Use the official package when Python is running on supported Windows, the MetaTrader terminal is local, and Python needs direct access to terminal data or account state.

Use a socket bridge when an Expert Advisor should retain control, Python is a bounded calculation service, or a custom message contract is more important than the convenience of the package.

Whichever path you choose, start read-only, record the exact package and terminal versions, test with broker-specific symbol names, keep credentials out of code, and make communication failure produce no trade rather than an improvised fallback.

For project context rather than a copy-and-paste template, see MicroQuant’s MT5 client. It demonstrates terminal initialization and OHLC-bar retrieval with the official package, not the socket route. The file also contains live order paths, so audit it carefully and test only with a demo account.

Primary references

Checked September 9, 2026:

Leave a Reply