Skip to main content
Yanked

This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Consider using release 1.2.0 instead.
Reason given by maintainers: Superseded by 1.2.0 — see the changelog

pmt5api — Python for wmt5api

pmt5api is the complete Python interface to wmt5api, the MetaTrader 5 client library from mtapi.one. The protocol, licensing and trading logic stay in the licensed core (wmt5api.dll), which this package loads in-process; every public capability of the C# library is available here under a Pythonic name, with plain data types, a small error hierarchy, context managers, and sync, async and callback styles.

No MetaTrader terminal is needed — the core talks to your broker directly.

Parity with the C# library is measured, not claimed: pmt5api.COVERAGE maps every public member of the C# MT5API class to its Python counterpart (or states why it has none), and the test suite fails if the core gains a member that is not accounted for. docs/FROM-CSHARP.md is that table; docs/API.md is the reference.

Install

The Linux x86-64 wheel carries everything — the core and a .NET runtime — so there is nothing else to install:

pip install pmt5api-<version>-py3-none-manylinux_2_28_x86_64.whl

On any other platform use the py3-none-any wheel, which carries the core and needs a .NET 8 (or later) runtime on the machine (dotnet --list-runtimes should list Microsoft.NETCore.App).

You also need your API key (wk_…, the same key you use with wmt5api):

export WMTAPI_KEY=wk_...          # or pass api_key=... to Client(...)
Python 3.9, 3.12 and 3.13 verified; 3.9 or newer required
Platform Linux x86-64 verified (glibc 2.28 or newer). Windows and macOS are not verified yet — see Known limitations
Optional pip install pandas for to_frame

Quick start

from pmt5api import Client

with Client(login=12345678, password="investor-password", host="broker-access-host", port=443) as c:
    acct = c.account()
    print(acct.name, acct.balance, acct.currency, "equity", acct.equity)

    print(c.quote("EURUSD"))                       # Quote(symbol='EURUSD', bid=..., ask=..., time=...)

    for p in c.positions():
        print(p.ticket, p.symbol, p.order_type, p.lots, p.profit)

Use an investor (read-only) password for anything that does not trade.

Market data

from datetime import datetime
import pmt5api

c.symbols()                       # dict[str, SymbolInfo]: digits, contract size, min/max/step lots, swaps, ...
c.symbol("EURUSD")                # one symbol; find_symbol("EURUSD") resolves a suffixed name such as EURUSDm
c.sessions("EURUSD")              # when it is quoted and traded, per weekday
c.is_trade_session("EURUSD")      # inside a trade session right now?

h1 = c.bars("EURUSD", "H1", count=500)                   # the latest 500 H1 bars, oldest first
d1 = c.bars("XAUUSD", "D1", start=datetime(2026, 1, 1))  # everything since a date
df = pmt5api.to_frame(h1)                                # pandas DataFrame indexed by time

Timeframes: M1 M2 M3 M4 M5 M6 M10 M12 M15 M20 M30 H1 H2 H3 H4 H6 H8 H12 D1 W1 MN1. The server delivers one-minute bars; higher timeframes are built from them, aligned to the server's calendar (a day starts at server midnight, an H4 bar at 00, 04, 08, … server time, a week on Sunday, a month on the 1st), with tick_volume summed. Minutes after end are not used; the last bar may still be forming. spread is present on M1 bars only.

Live quotes

with Client(...) as c:
    for q in c.quotes("EURUSD", "XAUUSD", timeout=30):    # ends after 30 s without a tick
        print(q.symbol, q.bid, q.ask)
async with Client(...) as c:                               # connects without blocking the event loop
    async with c.aquotes("EURUSD") as quotes:              # attached on entry, detached on exit
        async for q in quotes:
            print(q)
remove = c.on_quote(lambda q: print(q))                    # callback style
c.subscribe("EURUSD")
...
remove()

Callbacks are delivered by one dispatcher thread per client, one at a time, in arrival order — you need no locks between callbacks, and a slow callback does not stall the connection. If a callback falls more than 20,000 events behind, the oldest events are dropped, a warning is logged and c.events_dropped counts them. An exception raised inside a callback is logged (logging.getLogger("pmt5api")) and swallowed. Order updates keep the state they had at the moment they arrived (they are snapshots). The iterators attach when you create them, drop the oldest item when the consumer falls behind, and detach on close() / async with / garbage collection.

Trading

o = c.buy("EURUSD", 0.10, sl=1.0950, tp=1.1100, magic=424242)   # market order at the ask; sell() at the bid
c.modify(o.ticket, sl=1.0970)                     # change stops; what you leave out keeps its value
c.close(o.ticket, 0.05)                           # close half; close(ticket) closes it all

p = c.place_order("EURUSD", "buy_stop_limit", 0.10, 1.1200, stop_limit=1.1190,
                  expiration=datetime(2026, 12, 31, 23, 0))   # also buy_limit sell_limit buy_stop sell_stop
c.modify(p.ticket, expiration="gtc")
c.cancel(p.ticket)

c.close(a.ticket, close_by=b.ticket)              # close one position against an opposite one (hedging accounts)

magic is your own number for the order: it comes back on Order.magic and Deal.magic (including the deals that close the position), so a bot can find its own orders and history after a restart.

Without waiting — a request id now, the outcome through events:

c.on_order_progress(lambda p: print(p.request_id, p.status, p.ticket))     # REQUEST_ACCEPTED, REQUEST_EXECUTED, ...
rid = c.buy_async("EURUSD", 0.10)

In asyncio — the synchronous calls on a worker thread: await c.abuy(...), asell, aplace_order, amodify, aclose, acancel, aquote. Cancelling the await (a wait_for timeout, a cancelled task) does not recall a request that is already on its way: the order may still be sent, so check orders() afterwards.

Test on a demo account first. Nothing here is ever retried or resent for you: if the server does not confirm in time the call raises RequestTimeout and the order may have been accepted — look at orders() before sending again. NoQuoteError is different: no price was available, so nothing was sent. orders() can lag a moment behind a call that has just returned; close() and modify() read the position's volume and stops from it, so wait for a partial close to show up before acting on the same position again. positions() lists open positions only (orders of type Buy or Sell); orders() also lists pending orders. A market order needs a price, so even the *_async calls first wait (up to quote_timeout) for the symbol's first quote.

Sizing and risk

Do not size a trade from a one-line formula: what a price unit is worth depends on how your broker configured your account (two accounts in the same cent currency can differ by a factor of one hundred). The core measures it from your own closed deals and refuses to size without that evidence:

if c.calibrate_money_scale(datetime(2025, 1, 1)):                 # read-only; places no order
    r = c.lots_for_risk("EURUSD", entry_price=1.1000, stop_price=1.0950, risk_amount=50.0)
    for w in r.warnings:
        print("warning:", w)              # read these: ok can still be True with a caveat
    if r.ok and r.expected_loss_at_stop <= 50.0 * 1.05:
        c.buy("EURUSD", r.lots, sl=1.0950)
    else:
        print("not sizing:", r.reason)
else:
    print(c.money_scale_evidence)

ok is true even when the smallest allowed volume risks more than you asked for (up to max_ratio, default 2×), or when nothing independent corroborated the price unit — which is why warnings and expected_loss_at_stop are there to be checked. Also: required_margin(symbol, lots), order_profit(symbol, open, close, lots), tick_value(symbol), ledger_value_per_lot(symbol), money_scale_from_deal(deal).

Account, history and curves

Call Returns
account(refresh=True) AccountInfo — balance, equity, floating P/L (with and without swap/commission), margin, free margin, margin level, leverage, account mode, contact details
orders() / positions() / closed_orders() list[Order] — working / open positions only / what this session saw close
history(start, end) / deal_history(year, month) History — .orders and .deals (the ledger, including deposits and withdrawals)
summary(start=None, end=None) AccountSummary — realized profit, deposits, withdrawals, from the deal ledger
equity_history(start, "D1") list[EquityPoint] — balance (exact) and equity (reconstructed) curve with drawdowns
request_history(...), request_bars(...) without waiting; the data arrives through on_history / on_quote_history

Times are naive datetime values in the broker's server time, as the server reports them; an unset time is None.

account() first quotes each open position's symbol once and recalculates profits, so equity, profit and margin level are current (a cold read would otherwise show equity equal to balance). Pass refresh=False to skip that.

Events

on_quote, on_order_update, on_order_progress, on_connect_progress, on_history, on_quote_history, on_symbols_update, on_symbol_update — each returns a function that removes the callback. quotes() and order_updates() (and the a… async versions) turn a stream into an iterator.

Staying connected

Pass auto_reconnect=True to let the core re-establish a dropped session (exponential backoff with jitter, failover to the broker's cluster members, re-subscription). Watch on_connect_progress to tell a permanent failure from a transient one, and bound it with reconnect_max_attempts. Do not poll connected and call connect() in a loop — brokers rate-limit logins that hammer their access servers. After a reconnect, re-query account() and orders(); subscriptions are restored, cached snapshots are not.

Configuration

Setting Argument Environment variable
API key api_key= WMTAPI_KEY
Licence service URL (leave at its default) token_url= WMTAPI_TOKEN_URL
Core library (only if you do not use a wheel that carries it) dll_path= WMTAPI_DLL
Timeouts (seconds, each > 0) connect_timeout (one attempt on one server), cluster_member_timeout (per server while failing over), token_timeout, execution_timeout, quote_timeout / history_timeout / bars_timeout (the defaults of quote, history, bars), ping_interval, no_message_timeout —
Reconnect auto_reconnect, reconnect_backoff, reconnect_max_backoff, reconnect_max_attempts —
Proxy proxy=Proxy("socks5" | "socks4" | "https", host, port, user, password) — host must be an IP address —
Client certificate / common key certificate=, certificate_password=, common_key= —
.NET runtime location — DOTNET_ROOT

A core path you give (argument or WMTAPI_DLL) is used exactly as given and is an error if it does not exist. The .NET runtime is taken from DOTNET_ROOT if you set it, else the runtime bundled in the wheel, else an installed one. The bundled runtime runs in invariant-culture mode on machines without ICU.

Diagnostics

import logging
logging.basicConfig(level=logging.DEBUG)
pmt5api.enable_core_logging()          # the core's own messages -> logging.getLogger("pmt5api.core")

Offline helpers and account administration

load_servers_dat(path_or_bytes) / save_servers_dat(records) read and write a terminal's servers.dat; request_demo_account(DemoAccountRequest(...), host, port) asks a broker for a demo account (it creates a real account at that broker: use it deliberately); c.change_password(new, investor=False).

Errors

Everything the core raises reaches you as one of these, all subclasses of pmt5api.WmtError; the original exception stays available as __cause__.

Exception Meaning
LicenseError The licence service refused the key. .reason is one of NoKey, Invalid, Expired, Revoked, RateLimited, LoginNotAllowed, AccountLimitReached, Unknown.
TokenServiceError The licence service could not be reached or answered unusably — retry, don't re-prompt for a key.
ConnectError The broker's access server (or the network, or a proxy) could not be reached.
ConnectionLostError An established connection dropped (the core's Disconnected).
ServerError / InvestorModeError The broker refused the request (wrong login or password, a rejected order / a read-only login tried to trade). .code is the server's return code by name (REQUOTE, NO_MONEY, INVALID_PRICE, …).
InvalidRequestError The request was rejected before it was sent, e.g. an unknown symbol (also a ValueError).
BusyError The core allows one request of this kind at a time and one is still running (a history or bars download).
ProxyError The configured proxy failed.
RequestTimeout No answer in time (also a built-in TimeoutError). For a trade call the request was sent: the order may have been accepted.
NoQuoteError No quote arrived in time to read or to trade against. Nothing was sent.
NotConnectedError The call needs a connection and the client is not connected.
CoreNotFoundError / RuntimeNotFoundError wmt5api.dll or the .NET runtime was not found; the message says where it looked.

Calls made through client.raw bypass this translation and raise the core's own exceptions.

from pmt5api import Client, LicenseError, TokenServiceError

try:
    c = Client(...).connect()
except LicenseError as e:
    print("licence refused:", e.reason)
except TokenServiceError as e:
    print("licence service unreachable:", e)

What has been verified

Each of these was run against real demo accounts (not only unit-tested), from a clean container with no .NET installed: connect and licensing (including the production HTTPS licence path); account, symbols and trading rules; quotes and streams; bars (H1 identical to the core's own conversion on every period compared); history; the sizing chain (calibration, sizing, refusal of a zero-distance stop, cross-check from a closed deal); summary reconciled against the deal ledger and an equity curve ending at the account's balance; order entry — market, pending, stop-limit with expiration, modify, partial close, close-by, cancel, asynchronous with progress events — with the magic number round-tripping through every deal; a rejected order; connecting through SOCKS5 and HTTPS proxies; dropping the connection twice mid-stream and recovering with subscriptions restored; a broker that refuses connections for a while; a link that goes silent without closing (noticed at no_message_timeout, then recovered); and a three-hour unattended run on a demo account — five symbols streamed (147 732 ticks), the account polled every minute (179 reads), 17 bar and 17 history downloads, 17 far-away pending orders placed and cancelled, the connection cut four times (each recovered in 2.0–2.5 s with the subscriptions restored) — which finished with no error. Resident memory settled at about 200 MB (it rises to ~190 MB in the first half hour; between the two halves of the run it moved by 4 MB), threads and file descriptors stayed flat. Every shipped example was run against a real account too.

Known limitations

  • Linux x86-64 is the verified platform. Windows, macOS, arm64 and musl-based Linux have not been verified.
  • Netting accounts have not been verified (the available demo accounts are all hedging).
  • Client certificates (certificate=), common_key= and request_demo_account have been unit-tested but not run against a broker that requires or serves them.
  • One Client is one broker connection. Its methods can be called from several threads, but a trading call is neither queued nor serialized for you; two threads acting on the same ticket is your responsibility.
  • The state of an order is the core's value and is not a reliable way to tell a position from a pending order (a freshly opened position can stay Started). Use positions(), or the order's type, for that.
  • A folder named wmt5api in your working directory is handled automatically (it would otherwise hide the core's .NET namespace).

Licensing and security

Your connection is authorised per session by the hosted licence service, using your API key; the core talks to your broker directly. What is sent to the licence service and what is not, and the security notes, are in the core's own documents, which travel with the wheel: pmt5api/_native/docs/LICENSING.md, SECURITY.md and ERRORS.md (and the core's CHANGELOG.md next to them). python -m pmt5api prints where they are.

Third-party components

The Linux wheel includes the Microsoft .NET runtime (MIT licence; its notices are in pmt5api/_dotnet/) and the wmt5api core with its own notices (pmt5api/_native/THIRD-PARTY-NOTICES.txt).

Trademarks

MetaTrader, MT4 and MT5 are trademarks of MetaQuotes Ltd. wmt4api/wmt5api/pmt5api are independent products of Sen Trading (mtapi.one); they are not affiliated with, endorsed by or sponsored by MetaQuotes Ltd. All other names are the property of their respective owners.

Licence and support

See LICENSE.txt. Use of the core library is governed by the wmt5api commercial SDK licence. For keys, support and licence terms contact mtapi.one.

Metadata

Release files for pmt5api 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pmt5api 1.0.0
File Size Uploaded Provenance Actions
pmt5api-1.0.0.tar.gz 61.0 kB Without provenance Details for pmt5api-1.0.0.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for pmt5api 1.0.0
File Interpreter ABI Platform Actions
pmt5api-1.0.0-py3-none-manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.28+ x86-64 Details for pmt5api-1.0.0-py3-none-manylinux_2_28_x86_64.whl
pmt5api-1.0.0-py3-none-any.whl Python 3 none any Details for pmt5api-1.0.0-py3-none-any.whl

Total release size: 34.0 MB

Release files / pmt5api-1.0.0.tar.gz

Download URL pmt5api-1.0.0.tar.gz
Size 61.0 kB
Tags Source
SHA-256 checksum
How to use checksums
804d8747822fdd77ce853bf630d3a4e1bdace8c2f15f59cd15abc46928350d41
BLAKE2b-256 checksum
How to use checksums
93e9b712fea2d729aff713a038ecc642b014887f65cd4b0e6ba6dfa74e72cd09
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / pmt5api-1.0.0-py3-none-manylinux_2_28_x86_64.whl

Download URL pmt5api-1.0.0-py3-none-manylinux_2_28_x86_64.whl
Size 33.7 MB
Tags Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
1988ad9b6d0c6b75a31473b46bcb9479cfe71d7e7e699c5cf7607700e1d05d0a
BLAKE2b-256 checksum
How to use checksums
6854c36fa3a26276ef7920e05249d0077b15cc13d58eb13df5b56c64b8cb0aae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / pmt5api-1.0.0-py3-none-any.whl

Download URL pmt5api-1.0.0-py3-none-any.whl
Size 195.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ad4cfbe6396867bf06e5e368aaabe2d746fad575e90954fc47e42a2fb9d9f44f
BLAKE2b-256 checksum
How to use checksums
5af92ef222f8191785f1ca93628cbdba83be2a79e3b7f6b3099e3bb40a23b6e6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for pmt5api 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.0 This release

3 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page