pmt4api 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. pmt4api-1.0.0/CHANGELOG.md +17 -0
  2. pmt4api-1.0.0/LICENSE.txt +29 -0
  3. pmt4api-1.0.0/PKG-INFO +389 -0
  4. pmt4api-1.0.0/README.md +362 -0
  5. pmt4api-1.0.0/examples/bars.py +19 -0
  6. pmt4api-1.0.0/examples/connect.py +25 -0
  7. pmt4api-1.0.0/examples/pnl.py +22 -0
  8. pmt4api-1.0.0/examples/servers.py +16 -0
  9. pmt4api-1.0.0/examples/sizing.py +23 -0
  10. pmt4api-1.0.0/examples/stream_async.py +25 -0
  11. pmt4api-1.0.0/examples/stream_quotes.py +19 -0
  12. pmt4api-1.0.0/examples/trade_demo.py +28 -0
  13. pmt4api-1.0.0/hatch_build.py +12 -0
  14. pmt4api-1.0.0/pyproject.toml +49 -0
  15. pmt4api-1.0.0/src/pmt4api/__init__.py +32 -0
  16. pmt4api-1.0.0/src/pmt4api/__main__.py +78 -0
  17. pmt4api-1.0.0/src/pmt4api/_account.py +253 -0
  18. pmt4api-1.0.0/src/pmt4api/_base.py +24 -0
  19. pmt4api-1.0.0/src/pmt4api/_coverage.py +186 -0
  20. pmt4api-1.0.0/src/pmt4api/_events.py +189 -0
  21. pmt4api-1.0.0/src/pmt4api/_market.py +263 -0
  22. pmt4api-1.0.0/src/pmt4api/_risk.py +66 -0
  23. pmt4api-1.0.0/src/pmt4api/_runtime.py +154 -0
  24. pmt4api-1.0.0/src/pmt4api/_session.py +171 -0
  25. pmt4api-1.0.0/src/pmt4api/_streams.py +238 -0
  26. pmt4api-1.0.0/src/pmt4api/_trading.py +384 -0
  27. pmt4api-1.0.0/src/pmt4api/_util.py +78 -0
  28. pmt4api-1.0.0/src/pmt4api/bars.py +62 -0
  29. pmt4api-1.0.0/src/pmt4api/client.py +143 -0
  30. pmt4api-1.0.0/src/pmt4api/diagnostics.py +38 -0
  31. pmt4api-1.0.0/src/pmt4api/docs/API.md +702 -0
  32. pmt4api-1.0.0/src/pmt4api/docs/FROM-CSHARP.md +151 -0
  33. pmt4api-1.0.0/src/pmt4api/errors.py +233 -0
  34. pmt4api-1.0.0/src/pmt4api/frames.py +21 -0
  35. pmt4api-1.0.0/src/pmt4api/models.py +337 -0
  36. pmt4api-1.0.0/src/pmt4api/py.typed +0 -0
  37. pmt4api-1.0.0/src/pmt4api/servers.py +40 -0
@@ -0,0 +1,17 @@
1
+ # Changelog — pmt4api
2
+
3
+ ## [1.0.0] — 2026-10-12
4
+ First release: the complete Python interface to wmt4api 3.2.0 (the MetaTrader 4 client library).
5
+
6
+ - Every public member and type of the C# `MT4API` / `OrderClient` classes is mapped to a Python counterpart, or has a
7
+ stated reason for not having one (`pmt4api.COVERAGE`, `pmt4api.TYPES`; a test fails when the core gains an
8
+ unaccounted member).
9
+ - Market data (symbols, quotes, bars on the nine MT4 timeframes with paging), account, sizing and risk, order entry
10
+ (market, pending, modify, partial close, close-by, close-all-by, cancel; synchronous, with request ids and progress
11
+ events, and awaitable), history, summary and equity curve.
12
+ - Callbacks, sync and async iterators, one ordered dispatcher thread per client, with back-pressure accounting.
13
+ - A small error hierarchy; the MT4 trade-return codes by name; requotes carry the new prices.
14
+ - `auto_reconnect`, `servers=` failover, a silence watchdog, `reconnect()`; `disconnect()` is final.
15
+ - Refusals before anything is sent: a magic number the core cannot transmit, a volume the wire cannot carry, a
16
+ comment that would not read back.
17
+ - Wheels: Linux x86-64 with the core and a .NET runtime bundled, and a pure wheel that carries the core.
@@ -0,0 +1,29 @@
1
+ wmt5api — Commercial SDK License
2
+ ================================
3
+
4
+ Copyright © Sen Trading (mtapi.one). All rights reserved.
5
+
6
+ This software (the "SDK": wmt5api.dll, its documentation, and the accompanying
7
+ sample) is licensed, not sold. Your use is governed by the commercial license
8
+ agreement under which you obtained your API key.
9
+
10
+ Subject to that agreement and a valid, active license key, you are granted a
11
+ non-exclusive, non-transferable right to integrate and use the SDK in your own
12
+ applications.
13
+
14
+ You may NOT, except as expressly permitted by your agreement:
15
+ • disassemble the SDK, or attempt to discover its source code, internal structure, or algorithms by any means;
16
+ • redistribute, sublicense, rent, or resell the SDK or your API key;
17
+ • remove or alter any proprietary notices.
18
+
19
+ The SDK connects to a hosted licensing service to authorize each connection.
20
+ A license is valid for your subscription period; it stops authorizing new
21
+ connections when the subscription lapses or is cancelled.
22
+
23
+ THE SDK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
24
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
25
+ A PARTICULAR PURPOSE. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM,
26
+ DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF THE SDK.
27
+
28
+ For license terms, support, or a key: contact your account representative at
29
+ mtapi.one.
pmt4api-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,389 @@
1
+ Metadata-Version: 2.5
2
+ Name: pmt4api
3
+ Version: 1.0.0
4
+ Summary: Complete Python interface to the wmt4api MetaTrader 4 client library (market data, account, sizing, order entry, events).
5
+ Project-URL: Homepage, https://mtapi.one
6
+ Author: Sen Trading
7
+ License-File: LICENSE.txt
8
+ Keywords: market-data,metatrader,mt4,trading,wmt4api
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Financial and Insurance Industry
12
+ Classifier: License :: Other/Proprietary License
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.9
20
+ Requires-Dist: pythonnet>=3.0.3
21
+ Provides-Extra: pandas
22
+ Requires-Dist: pandas>=1.5; extra == 'pandas'
23
+ Provides-Extra: test
24
+ Requires-Dist: pandas>=1.5; extra == 'test'
25
+ Requires-Dist: pytest>=8; extra == 'test'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # pmt4api — Python for wmt4api
29
+
30
+ `pmt4api` is the complete Python interface to **wmt4api**, the MetaTrader 4 client library from
31
+ [mtapi.one](https://mtapi.one). The protocol, licensing and trading logic stay in the licensed core
32
+ (`wmt4api.dll`), which this package loads in-process; every public capability of the C# library is available here
33
+ under a Pythonic name, with plain data types, a small error hierarchy, context managers, and sync, async and
34
+ callback styles.
35
+
36
+ No MetaTrader terminal is needed — the core talks to your broker directly. It is the sibling of
37
+ `pmt5api` (MT5): the two share their conventions, so a system that trades
38
+ both reads alike.
39
+
40
+ **Parity with the C# library is measured, not claimed:** `pmt4api.COVERAGE` maps every public member of the C#
41
+ `MT4API` and `OrderClient` classes to its Python counterpart (or states why it has none), and the test suite fails
42
+ if the core gains a member that is not accounted for. [`docs/FROM-CSHARP.md`](src/pmt4api/docs/FROM-CSHARP.md) is
43
+ that table; [`docs/API.md`](src/pmt4api/docs/API.md) is the reference.
44
+
45
+ ## Install
46
+
47
+ The **Linux x86-64 wheel** carries everything — the core and a .NET runtime — so there is nothing else to install:
48
+
49
+ ```
50
+ pip install pmt4api-<version>-py3-none-manylinux_2_28_x86_64.whl
51
+ ```
52
+
53
+ On any other platform use the `py3-none-any` wheel, which carries the core and needs a .NET 8 (or later) runtime on
54
+ the machine (`dotnet --list-runtimes` should list `Microsoft.NETCore.App`).
55
+
56
+ You also need your **API key** (`wk_…`, the same key you use with wmt4api):
57
+
58
+ ```
59
+ export WMT4API_KEY=wk_... # or pass api_key=... to Client(...)
60
+ ```
61
+
62
+ | | |
63
+ |---|---|
64
+ | Python | 3.9 or newer |
65
+ | Platform | **Linux x86-64 verified** (glibc 2.28 or newer). Windows and macOS are not verified yet — see *Known limitations* |
66
+ | Optional | `pip install pandas` for `to_frame` |
67
+
68
+ ## Quick start
69
+
70
+ ```python
71
+ from pmt4api import Client
72
+
73
+ with Client(login=12345678, password="investor-password", host="broker-access-host", port=443) as c:
74
+ acct = c.account()
75
+ print(acct.name, acct.balance, acct.currency, "equity", acct.equity)
76
+
77
+ sym = c.find_symbol("EURUSD") # brokers add suffixes: EURUSDm, EURUSD.s ...
78
+ print(c.quote(sym)) # Quote(symbol='EURUSDm', bid=..., ask=..., time=...)
79
+
80
+ for p in c.positions():
81
+ print(p.ticket, p.symbol, p.order_type, p.lots, p.profit)
82
+ ```
83
+
84
+ Use an investor (read-only) password for anything that does not trade.
85
+
86
+ ## Market data
87
+
88
+ ```python
89
+ from datetime import datetime
90
+ import pmt4api
91
+
92
+ c.symbols() # dict[str, SymbolInfo]: digits, contract size, min/max/step lots, execution ...
93
+ c.find_symbols("XAU") # every name that starts with it: ['XAUUSDm', 'XAUEURm', ...]
94
+ c.symbol("XAUUSDm") # one symbol's specification
95
+
96
+ h1 = c.bars("EURUSDm", "H1", count=500) # the latest 500 H1 bars, oldest first
97
+ d1 = c.bars("XAUUSDm", "D1", start=datetime(2026, 1, 1)) # everything since a date the server still holds
98
+ df = pmt4api.to_frame(h1) # pandas DataFrame indexed by time
99
+ ```
100
+
101
+ Timeframes: `M1 M5 M15 M30 H1 H4 D1 W1 MN1` — the nine the MT4 server serves; it delivers each itself. A series holds
102
+ **completed** bars: the bar that is still forming is not part of what the server sends (read the live price with
103
+ `quote()`), and `end=` reads the series up to that moment with no look-ahead. The broker limits how far back it goes
104
+ (M1 is typically a few days), so a long request returns what exists; one request carries at most 32 767 bars and
105
+ longer requests page backwards for you. `volume` is the tick volume.
106
+
107
+ ### Live quotes
108
+
109
+ ```python
110
+ with Client(...) as c:
111
+ for q in c.quotes("EURUSDm", "XAUUSDm", timeout=30): # ends after 30 s without a tick
112
+ print(q.symbol, q.bid, q.ask, q.time)
113
+ ```
114
+
115
+ ```python
116
+ async with Client(...) as c: # connects without blocking the event loop
117
+ async with c.aquotes("EURUSDm") as quotes: # attached on entry, detached on exit
118
+ async for q in quotes:
119
+ print(q)
120
+ ```
121
+
122
+ ```python
123
+ remove = c.on_quote(lambda q: print(q)) # callback style
124
+ c.subscribe("EURUSDm")
125
+ ...
126
+ remove()
127
+ ```
128
+
129
+ Until you subscribe to something the server streams a default set of a few hundred symbols; the moment you do it
130
+ streams only what you subscribed. The core keeps quotes honest across that change: `quote()` and the tick-value
131
+ calculations subscribe a symbol they need and wait for a tick that arrives *after* the request instead of using a
132
+ price cached from the feed that stopped (if no tick comes — a closed market — the last price is returned, and
133
+ `Quote.time` says how old it is). A position's symbol is put on the feed as soon as the position exists and stays
134
+ there, because the position's profit and the account's equity are priced from it. `account()` reports
135
+ `priced_from`, the server time of the oldest tick its figures used: minutes old while the market is open means the
136
+ feed is not delivering, and the equity should not be trusted.
137
+
138
+ Callbacks are delivered by **one dispatcher thread per client**, one at a time, in the order the core handed the
139
+ events over — you need no locks between callbacks, and a slow callback does not stall the connection. (The core
140
+ raises each event on a thread-pool thread, so two events that happen within microseconds of each other can swap;
141
+ order updates of one ticket carry `Order.profit` and times you can compare.) If a callback falls more than 20,000 events
142
+ behind, the oldest events are dropped, a warning is logged and `c.events_dropped` counts them. An exception raised
143
+ inside a callback is logged (`logging.getLogger("pmt4api")`) and swallowed. The iterators attach when you create them,
144
+ drop the oldest item when the consumer falls behind, and detach on `close()` / `async with` / garbage collection.
145
+
146
+ ## Trading
147
+
148
+ ```python
149
+ o = c.buy("EURUSDm", 0.10, sl=1.0950, tp=1.1100, comment="bot-1") # market order at the ask; sell() at the bid
150
+ c.modify(o.ticket, sl=1.0970) # change stops; what you leave out keeps its value
151
+ c.close(o.ticket, 0.05) # close half; close(ticket) closes it all
152
+
153
+ p = c.place_order("EURUSDm", "buy_stop", 0.10, 1.1200, sl=1.1150,
154
+ expiration=timedelta(days=2)) # also buy_limit sell_limit sell_stop; a datetime works too
155
+ c.modify(p.ticket, price=1.1210) # a pending order's price; its expiration is kept
156
+ c.cancel(p.ticket)
157
+
158
+ c.close_by(a.ticket, b.ticket) # close one position against an opposite one (hedged accounts)
159
+ c.close_all_by("EURUSDm") # ... every opposite pair of the symbol
160
+ ```
161
+
162
+ `lots` must be a multiple of 0.01 (the MT4 wire carries hundredths of a lot); a finer volume is refused before
163
+ anything is sent. `slippage` is in points. For an instant-execution symbol the order goes at `price` (default: the
164
+ current ask or bid) and a move of more than `slippage` points raises `RequoteError` with the prices the server now
165
+ offers; a market-execution symbol ignores both and fills at its own price.
166
+
167
+ **Tagging orders: use `comment`, not `magic`.** The core accepts a magic number but does not transmit it, so an
168
+ order cannot carry one. Rather than let a strategy believe its orders are tagged, `magic=` other than 0 raises
169
+ `InvalidRequestError`. A comment (ASCII, at most 31 characters) comes back on `Order.comment`; `Order.magic` is still
170
+ read from orders that carry one placed elsewhere.
171
+
172
+ **Without waiting** — a request id now, the outcome through events:
173
+
174
+ ```python
175
+ c.on_order_progress(lambda p: print(p.request_id, p.stage, p.order and p.order.ticket)) # InProcess, Accepted, Opened ...
176
+ rid = c.buy_async("EURUSDm", 0.10)
177
+ for p in c.order_progress(request_id=rid, timeout=15): # or follow one request as an iterator
178
+ print(p.stage)
179
+ ```
180
+
181
+ **In asyncio** — the synchronous calls on a worker thread: `await c.abuy(...)`, `asell`, `aplace_order`, `amodify`,
182
+ `aclose`, `acancel`, `aclose_by`, `aclose_all_by`, `aquote`, `abars`, `ahistory`. Cancelling the `await` of a trade
183
+ (a `wait_for` timeout, a cancelled task) does not recall a request that is already on its way: the order may still be
184
+ sent, so check `orders()` afterwards. Cancelling `ahistory` does abandon the download.
185
+
186
+ **Test on a demo account first.** Nothing here is ever retried or resent for you: if the server does not confirm in
187
+ time the call raises `RequestTimeout` and the order **may have been accepted** — look at `orders()` before sending
188
+ again. `NoQuoteError` is different: no price was available, so nothing was sent. A market order needs a price, so
189
+ even the `*_async` calls first wait (up to `quote_timeout`) for the symbol's first quote. After a partial close the
190
+ remainder of the position may carry a new ticket: read `positions()`.
191
+
192
+ ## Sizing and risk
193
+
194
+ ```python
195
+ r = c.lots_for_risk("EURUSDm", entry_price=1.1000, stop_price=1.0950, risk_amount=50.0)
196
+ for w in r.warnings:
197
+ print("warning:", w) # ok can still be True with a caveat (the broker's minimum volume risks more)
198
+ if r.ok:
199
+ c.buy("EURUSDm", r.lots, sl=1.0950)
200
+ else:
201
+ print("not sizing:", r.reason)
202
+ ```
203
+
204
+ The value of a price unit comes from `tick_value()` (converted through the account currency's cross rates), the
205
+ broker's volume step, minimum and maximum from the account's own group; a size that cannot be justified is refused,
206
+ not guessed, and the volume is rounded **down**. Amounts are in the currency the account reports; there is no
207
+ cent-account scaling, so check the first result against your terminal. Also: `required_margin(symbol, lots)`,
208
+ `order_profit(symbol, open, close, lots)`, `tick_value(symbol, side)`, `contract_size(symbol)`.
209
+
210
+ ## Account, history and curves
211
+
212
+ | Call | Returns |
213
+ |---|---|
214
+ | `account(refresh=True)` | `AccountInfo` — balance, credit, equity, floating profit, margin, free margin, margin level, leverage, currency, type |
215
+ | `balance`, `equity`, `margin`, `free_margin`, `margin_level`, `profit` | the same figures as cheap reads from the cache (no refresh; the margin may still wait for a conversion pair's first tick) |
216
+ | `orders()` / `positions()` / `pending_orders()` / `order(ticket)` | `list[Order]` working / open positions / pending / one |
217
+ | `history(start, end)` | `list[Order]` — closed orders and balance / credit entries, by close time |
218
+ | `summary(start=None, end=None)` | `AccountSummary` — realized profit, deposits, withdrawals |
219
+ | `equity_history(start, "D1")` | `list[EquityPoint]` — balance (exact) and equity (reconstructed) curve with drawdowns |
220
+
221
+ MT4 keeps one record for open positions, pending orders, closed orders and ledger entries: `Order.order_type` tells
222
+ them apart (`Buy` / `Sell`, `BuyLimit` … , `Balance` / `Credit`), and `is_market`, `is_pending`, `is_capital_flow`
223
+ say it directly. Times are naive `datetime` values in the **broker's server time**; an unset time is `None`.
224
+
225
+ `history()`, `summary()` and `equity_history()` each open their own short-lived broker connection and cost one
226
+ licence request, which the licence service rate-limits per minute: fetch once and reuse the result. `history()`
227
+ takes a `threading.Event` as `cancel=` to abandon the wait.
228
+
229
+ ## Events
230
+
231
+ `on_quote`, `on_order_update`, `on_order_progress`, `on_connect`, `on_disconnect`, `on_quote_history`,
232
+ `on_symbols_update` — each returns a function that removes the callback. `quotes()`, `order_updates()` and
233
+ `order_progress()` (and the `a…` async versions) turn a stream into an iterator.
234
+
235
+ ## Staying connected
236
+
237
+ Pass `auto_reconnect=True` to let the core re-establish a dropped session (exponential backoff up to 60 s, failover
238
+ across the primary server, `servers=[(host, port), ...]` and the access servers the broker published, re-subscription).
239
+ Watch `on_disconnect` (its `error` says why: `ConnectionLostError`, and `None` when you closed the client) and
240
+ `on_connect`. A session that carries nothing for `no_message_timeout` (default 30 s; the server answers a ping every
241
+ few seconds) is declared dead and closed, so a half-open link is noticed instead of lingering. A refusal retrying
242
+ cannot cure (an expired licence, a rejected login) stops the reconnecting. Do not poll `connected` and call
243
+ `connect()` in a loop — brokers rate-limit logins that hammer their access servers. `reconnect()` re-establishes by
244
+ hand and returns whether it worked. After a reconnect, re-query `account()` and `orders()`; subscriptions are
245
+ restored, cached snapshots are not. `disconnect()` is final: it also stops a reconnect that is in flight.
246
+
247
+ ## Configuration
248
+
249
+ | Setting | Argument | Environment variable |
250
+ |---|---|---|
251
+ | API key | `api_key=` | `WMT4API_KEY` (then `WMTAPI_KEY`) |
252
+ | Licence service URL (leave at its default) | `token_url=` | `WMT4API_TOKEN_URL` |
253
+ | Core library (only if you do not use a wheel that carries it) | `dll_path=` | `WMT4API_DLL` |
254
+ | Timeouts (seconds, each > 0) | `connect_timeout` (one attempt on one server), `token_timeout`, `trade_timeout` (≥ 1), `quote_timeout` / `history_timeout` / `bars_timeout` (the defaults of `quote`, `history`, `bars`), `no_message_timeout` | — |
255
+ | Reconnect | `auto_reconnect`, `reconnect_backoff`, `reconnect_max_attempts`, `servers=` | — |
256
+ | Orders | `placed_type=` (`manual`, `expert`, `dealer` …), `comment_encoding=` (default `latin-1`) | — |
257
+ | .NET runtime location | — | `DOTNET_ROOT` |
258
+
259
+ A core path you give (argument or `WMT4API_DLL`) is used exactly as given and is an error if it does not exist. The
260
+ .NET runtime is taken from `DOTNET_ROOT` if you set it, else the runtime bundled in the wheel, else an installed one.
261
+ The bundled runtime runs in invariant-culture mode on machines without ICU. The MT4 core has no proxy support.
262
+
263
+ ### Diagnostics
264
+
265
+ ```python
266
+ import logging
267
+ logging.basicConfig(level=logging.DEBUG)
268
+ pmt4api.enable_core_logging() # the core's own messages -> logging.getLogger("pmt4api.core")
269
+ ```
270
+
271
+ ### Offline helpers
272
+
273
+ `load_servers_ini(path_or_bytes)` / `save_servers_ini(records)` read and write a terminal's `servers.ini`.
274
+ `python -m pmt4api` prints what is installed and whether it works here (`--connect` also reads an account, using
275
+ `PMT4_LOGIN`, `PMT4_PASSWORD`, `PMT4_HOST`); the output holds no secrets.
276
+
277
+ ## Errors
278
+
279
+ Everything the core raises reaches you as one of these, all subclasses of `pmt4api.WmtError`; the original
280
+ exception stays available as `__cause__`.
281
+
282
+ | Exception | Meaning |
283
+ |---|---|
284
+ | `LicenseError` | The licence service refused the key. `.reason` is one of `NoKey`, `Invalid`, `Expired`, `Revoked`, `RateLimited`, `LoginNotAllowed`, `AccountLimitReached`, `Unknown`; `.status` is its HTTP status. |
285
+ | `TokenServiceError` | The licence service could not be reached or answered unusably — retry, don't re-prompt for a key. |
286
+ | `LoginRejectedError` | The broker rejected the account number or password (`.code` is usually `INVALID_ACCOUNT`, 65). A `ServerError`. |
287
+ | `ConnectError` | No access server could be reached (or none answered within `connect_timeout`). |
288
+ | `ConnectionLostError` | An established connection dropped; with a trade in flight, whether it was applied is unknown. |
289
+ | `ServerError` | The broker refused a request. `.code` is the MT4 return code by name (`NOT_ENOUGH_MONEY`, `INVALID_STOPS`, `TRADE_DISABLED` …) and `.code_value` its number (`pmt4api.TRADE_CODES`). |
290
+ | `RequoteError` | An instant-execution price moved: `.bid` / `.ask` are the prices now offered. A `ServerError`; nothing was filled. |
291
+ | `InvestorModeError` | A read-only (investor) login tried to trade. Nothing was sent. A `ServerError`. |
292
+ | `InvalidRequestError` | The request was rejected before it was sent, e.g. an unknown symbol or a `magic` number (also a `ValueError`). |
293
+ | `BusyError` | A bar download for the same symbol and timeframe is already running. |
294
+ | `RequestTimeout` | No answer in time (also a built-in `TimeoutError`). For a trade call the request **was sent**: the order may have been accepted. |
295
+ | `NoQuoteError` | No quote arrived in time to read or to trade against. Nothing was sent. |
296
+ | `RequestCancelled` | You cancelled the wait (`history(cancel=...)`). |
297
+ | `NotConnectedError` | The call needs a connection and the client is not connected. |
298
+ | `ProtocolError` | The server sent something the core could not decode. |
299
+ | `CoreNotFoundError` / `RuntimeNotFoundError` | `wmt4api.dll` or the .NET runtime was not found; the message says where it looked. |
300
+
301
+ Calls made through `client.raw` bypass this translation and raise the core's own exceptions.
302
+
303
+ ```python
304
+ from pmt4api import Client, LicenseError, LoginRejectedError, RequoteError, TokenServiceError
305
+
306
+ try:
307
+ c = Client(...).connect()
308
+ except LicenseError as e:
309
+ print("licence refused:", e.reason)
310
+ except LoginRejectedError as e:
311
+ print("the broker rejected the login:", e.code)
312
+ except TokenServiceError as e:
313
+ print("licence service unreachable:", e)
314
+ ```
315
+
316
+ ## What has been verified
317
+
318
+ Run against real demo accounts (not only unit-tested), through a licence authority of its own, from clean containers
319
+ with no .NET installed, on three brokers (Exness, ICMarkets, DPrime — all market-execution): connect, licensing errors
320
+ (bad key, unreachable licence service, wrong password, unreachable host); account, symbols and trading rules; quotes
321
+ and streams, including that a quote stays current after the feed is narrowed; bars on all nine timeframes (identical to
322
+ the core's own download), paging and windows; history, summary and an equity curve reconciled against the history
323
+ rows; sizing; order entry — market, pending with expiration, modify, partial close, close-by, close-all-by, cancel,
324
+ asynchronous with progress events — with the server's own rejections arriving as typed errors; failing over from a
325
+ dead primary to a backup server; dropping the connection twice mid-stream, a broker that refuses connections for a
326
+ while and a link that goes silent without closing — each recovered with subscriptions restored — a manual reconnect,
327
+ and a `disconnect()` that stays final; concurrent subscribing, quoting, bar and account reads from several threads
328
+ without an error; and every shipped example.
329
+
330
+ Two unattended runs on demo accounts: **three hours** (5 symbols, 212 333 ticks, the account read every minute,
331
+ 17 bar and 17 history downloads; the connection cut three times — recovered in 3.7–4.0 s — and made silent twice —
332
+ noticed at the 20 s setting, back after 24 s) with no error, no event dropped, resident memory flat (~147 MB, −0.7 MB
333
+ between the halves), threads and file descriptors flat — on a build of the core that preceded the last review fixes;
334
+ and a **ninety-minute** run of the released wheel in a container (DPrime demo): 88 account reads, 8 bar and 8 history
335
+ downloads, 17 far-away pending orders placed and cancelled, 7 real positions opened, checked against the feed and
336
+ closed (10 more market orders were refused by the broker with `TRADE_DISABLED`, which the run counts as skipped), the
337
+ connection cut four times (recovered in about 3 s) and made silent twice (noticed at the 20 s setting, back in under
338
+ 24 s), which finished with no error and no growth check tripped (resident memory 104 MB at one minute, 140 MB at half an
339
+ hour, 145 MB at the hour; threads 19–21, file descriptors 100).
340
+
341
+ ## Known limitations
342
+
343
+ - **Linux x86-64 is the verified platform.** Windows, macOS, arm64 and musl-based Linux have not been verified.
344
+ - **No magic number**: the core does not transmit it (see *Trading*). Tag orders with `comment`.
345
+ - **Investor-mode refusal** (`InvestorModeError`, and `is_investor` / `can_trade` reading the account record's
346
+ read-only flag) and **requotes** (`RequoteError`) are covered by unit tests; they have not been provoked live
347
+ (the available demo accounts are market-execution and no investor password was at hand).
348
+ - **Comments are ASCII**, at most 31 characters: orders are read back through an ASCII parser.
349
+ - **`history()`, `summary()` and `equity_history()` use the primary server only**: each opens its own short
350
+ connection to `host:port` and does not fail over to `servers=`.
351
+ - **The connect handshake and the first data load give up after 30 s** whatever `connect_timeout` says (that one
352
+ bounds the TCP dial of each server); a server that stalls there is skipped and the next one tried.
353
+ - **`trade_timeout` counts whole seconds** (the core truncates it). A `ServerError` with code `TRADE_TIMEOUT` (128)
354
+ is the server's own timeout while processing: the outcome is not known — look at `orders()` before sending again.
355
+ - **The server's UTC offset is read once per session**; a session that spans the broker's daylight-saving change has
356
+ `server_time`, `bars(end=now)` and a relative `expiration` off by an hour until it reconnects.
357
+ - **The silence watchdog's premise** — the server answers each ping within seconds — was measured on a quiet
358
+ session (one answer per ping); a market-closed weekend has not been observed. `no_message_timeout=0` switches it
359
+ off.
360
+ - The server lists a default feed of its own until you subscribe; `is_subscribed()` reports your subscriptions, and
361
+ `quote()` answers for any symbol either way.
362
+ - One `Client` is one broker connection. Its methods can be called from several threads, but a trading call is
363
+ neither queued nor serialized for you; two threads acting on the same ticket is your responsibility.
364
+ - `TradeTimeout` below one second makes the core give up straight after sending, so `trade_timeout` must be ≥ 1.
365
+ - A folder named `wmt4api` in your working directory is handled automatically (it would otherwise hide the core's
366
+ .NET namespace).
367
+
368
+ ## Licensing and security
369
+
370
+ Your connection is authorised per session by the hosted licence service, using your API key; the core talks to your
371
+ broker directly. What is sent to the licence service and what is not, and the security notes, are in the core's own
372
+ documents, which travel with the wheel: `pmt4api/_native/docs/LICENSING.md`, `SECURITY.md` and `ERRORS.md`
373
+ (and the core's `CHANGELOG.md` next to them). `python -m pmt4api` prints where they are.
374
+
375
+ ## Third-party components
376
+
377
+ The Linux wheel includes the Microsoft .NET runtime (MIT licence; its notices are in `pmt4api/_dotnet/`) and the
378
+ wmt4api core with its own notices (`pmt4api/_native/THIRD-PARTY-NOTICES.txt`).
379
+
380
+ ## Trademarks
381
+
382
+ MetaTrader, MT4 and MT5 are trademarks of MetaQuotes Ltd. wmt4api/wmt5api/pmt4api/pmt5api are independent products
383
+ of Sen Trading ([mtapi.one](https://mtapi.one)); they are not affiliated with, endorsed by or sponsored by MetaQuotes
384
+ Ltd. All other names are the property of their respective owners.
385
+
386
+ ## Licence and support
387
+
388
+ See `LICENSE.txt`. Use of the core library is governed by the wmt4api commercial SDK licence. For keys, support
389
+ and licence terms contact mtapi.one.