Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mt5-python-bridge

A production-shaped bridge between MetaTrader 5 and Python: reconnection that actually detects a dead link, broker symbol resolution, order retries that handle the failures brokers really return, and timezone-correct history sync.

CI Python License


Why this exists

The MetaTrader5 package is a thin ctypes wrapper over the terminal. It works, and then it does not, in ways that are silent:

What the code does What actually happens
mt5.symbol_info("XAUUSD") Returns None — this broker calls it XAUUSD.m. The order is never sent and nothing raises.
Checks mt5.initialize() for health Keeps returning True after the server link drops. Quotes go stale, orders fail one by one.
Sends an order with one filling mode Retcode 10030. Which of FOK/IOC/RETURN a symbol accepts is a per-broker, per-symbol decision.
Retries a requote at the original price Fails again. The price that caused the requote is the one being resent.
Treats retcode 10010 as success It is a partial fill. The position is smaller than the code believes.
history_deals_get(datetime(2026,3,1), ...) Naive datetime read as broker server time, which is usually not local. The window silently shifts by hours.
Places a stop 5 points away Retcode 10016 — under the symbol's trade_stops_level.

Every row is a passing test in this repository.


Design: the terminal is an interface, not an import

Nothing outside real.py imports MetaTrader5. Everything programs against TerminalProtocol.

That is not architecture for its own sake. It buys three concrete things:

  1. The package installs and tests anywhere. MetaTrader5 is a Windows-only binary wheel; CI here runs on Linux, macOS and Windows.
  2. The nasty paths become ordinary tests. A requote, a rejected filling mode, a half fill, a terminal that drops mid-session — three lines of setup each, instead of waiting for production.
  3. Swapping backends touches one file.
terminal = FakeTerminal()
terminal.add_symbol("XAUUSD.m", bid=2400.00, ask=2400.30, point=0.01, digits=2)
terminal.supported_filling = (FillingMode.RETURN,)   # this broker refuses IOC and FOK
terminal.fail_next_orders(RETCODE_REQUOTE, times=2)  # then two requotes
terminal.partial_fill_next(0.5)                      # then a half fill

Install

pip install git+https://github.com/PTHAICAP/mt5-python-bridge.git

# On Windows, with a terminal installed, add the real backend:
pip install "mt5-python-bridge[live] @ git+https://github.com/PTHAICAP/mt5-python-bridge.git"

Usage

from mt5_bridge import (
    ConnectionConfig, MT5Connection, OrderExecutor, OrderRequest,
    OrderType, SymbolResolver,
)
from mt5_bridge.real import RealTerminal

terminal = RealTerminal()
conn = MT5Connection(terminal, ConnectionConfig(
    login=12345678, password=os.environ["MT5_PASSWORD"], server="Broker-Live",
))
conn.connect()

resolver = SymbolResolver(terminal)
executor = OrderExecutor(terminal, resolver)

# "XAUUSD" resolves to XAUUSD.m / GOLD / XAUUSD.raw — whatever this broker uses
result = executor.send(OrderRequest(
    symbol="XAUUSD",
    order_type=OrderType.BUY,
    volume=0.10,
    stop_loss=2380.00,
    deviation_points=20,
    comment="swing-gold",
))

if result.partial:
    remaining = 0.10 - result.volume       # your call whether to chase it

In a trading loop, one call keeps the link alive:

while running:
    conn.ensure_connected(probe_symbol="XAUUSD")   # reconnects if the link died
    ...

History, with the timezone handled:

from mt5_bridge import HistorySync, detect_server_offset, summarise

offset = detect_server_offset(terminal, "EURUSD")   # do not hard-code it: DST moves it
sync = HistorySync(terminal, server_utc_offset=offset)

deals = sync.fetch_since(datetime(2026, 3, 1, tzinfo=timezone.utc))
print(summarise(deals))
# {'count': 42, 'gross_profit': 1840.5, 'commission': -126.0,
#  'swap': -18.4, 'net_profit': 1696.1, 'symbols': ('EURUSD', 'XAUUSD')}

summarise reports gross and net separately on purpose: on a high-frequency account commission and swap routinely turn a positive gross into a negative net, and a summary that hides that is worse than none.


What it handles

Symbol resolution. 18 common broker suffixes (.m, m, .raw, .ecn, .pro, _i, .cent, …) plus outright renames — XAUUSDGOLD, USTECNAS100, USOILWTI. Resolved once, cached, and the symbol is added to Market Watch: a symbol that exists but is not selected returns no ticks forever, with no error.

Connection health. is_healthy() probes for a live quote instead of trusting initialize(). ensure_connected() reconnects with exponential backoff and jitter — jitter matters when several desks share one VPS, or every process retries in lockstep.

Order execution. Filling-mode fallback with caching, requote retries at a freshly read price, partial fills surfaced rather than silently retried, stop distances validated against trade_stops_level before sending, volumes rounded down to the broker's step, comments truncated to MT5's 31 characters.

Retryable vs terminal. Retcodes are classified. A requote is retried; a no money or market closed is raised immediately rather than burning three attempts on something that cannot succeed.

Closing. close_position() sends an opposite deal referencing the position ticket. A plain opposite order opens a hedge on hedging accounts, leaving both legs live.

Credentials. Nothing in the package reads a file, an environment variable or a keyring — the caller supplies them. ConnectionConfig.__repr__ masks the password so it cannot reach a log line or a traceback.


Testing

pip install -e ".[dev]"
pytest --cov=src/mt5_bridge

71 tests, 90% branch coverage, no MetaTrader 5 required. CI runs the full suite on Ubuntu, Windows and macOS across Python 3.10–3.12.

tests/test_symbols.py             suffix + alias resolution, Market Watch, stops levels
tests/test_orders.py              filling fallback, requotes, partial fills, close/modify
tests/test_connection_history.py  backoff, health probing, server timezone, dedup

real.py is excluded from coverage: exercising it needs a running Windows terminal, so pretending otherwise would be a dishonest number.


Scope

This is a connectivity layer. No signals, no strategy, no risk logic — for the risk side see trading-risk-monitor.

Not affiliated with MetaQuotes. MetaTrader5 is a trademark of MetaQuotes Ltd.


License

MIT — see LICENSE.

About

A production-shaped MetaTrader 5 bridge for Python: reconnection, symbol resolution, order retries, history sync

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages