pmt5api 1.0.0__tar.gz → 1.2.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.
- pmt5api-1.2.0/CHANGELOG.md +133 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/PKG-INFO +83 -18
- {pmt5api-1.0.0 → pmt5api-1.2.0}/README.md +82 -17
- {pmt5api-1.0.0 → pmt5api-1.2.0}/examples/pnl.py +6 -4
- {pmt5api-1.0.0 → pmt5api-1.2.0}/examples/sizing.py +3 -3
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/__init__.py +3 -3
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/__main__.py +4 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/_account.py +69 -34
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/_base.py +6 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/_coverage.py +2 -1
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/_events.py +2 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/_market.py +23 -9
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/_risk.py +18 -4
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/_runtime.py +92 -8
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/_session.py +37 -2
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/_streams.py +44 -10
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/_trading.py +71 -4
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/_util.py +5 -3
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/bars.py +26 -12
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/client.py +13 -2
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/diagnostics.py +8 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/docs/API.md +196 -24
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/docs/FROM-CSHARP.md +2 -1
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/errors.py +23 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/models.py +138 -3
- pmt5api-1.0.0/CHANGELOG.md +0 -30
- {pmt5api-1.0.0 → pmt5api-1.2.0}/LICENSE.txt +0 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/examples/bars.py +0 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/examples/connect.py +0 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/examples/servers.py +0 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/examples/stream_async.py +0 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/examples/stream_quotes.py +0 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/examples/trade_demo.py +0 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/hatch_build.py +0 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/pyproject.toml +0 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/frames.py +0 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/py.typed +0 -0
- {pmt5api-1.0.0 → pmt5api-1.2.0}/src/pmt5api/servers.py +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Changelog — pmt5api
|
|
2
|
+
|
|
3
|
+
## [1.2.0] — 2026-11-06
|
|
4
|
+
|
|
5
|
+
Bundles **wmt5api 3.2.1**. A minor release with a **Breaking** section, like the core's: history is always asked for
|
|
6
|
+
by a window you choose.
|
|
7
|
+
|
|
8
|
+
### Breaking — history is always asked for by window
|
|
9
|
+
- **`history(start, end)` and `summary(start, end)` need both ends.** `history()` no longer defaults to the last 30
|
|
10
|
+
days and `summary()` no longer means "all time" (the core read every deal since 1970 for it, on every call). A
|
|
11
|
+
missing or reversed window raises `InvalidRequestError` before anything is sent; `request_history(start, end)`
|
|
12
|
+
likewise.
|
|
13
|
+
- **One call covers at most `max_history_days` (366 by default).** A longer window raises `InvalidRequestError`
|
|
14
|
+
before anything is sent. Ask window by window and add the results up, or set `Client(max_history_days=...)` /
|
|
15
|
+
`c.max_history_days` when you mean one long download (`0` removes the limit; the ends stay required).
|
|
16
|
+
`equity_history(start)` and `calibrate_money_scale(start)` run to now, so their start is at most that long ago (an
|
|
17
|
+
older one makes `calibrate_money_scale` return `False`, the reason in `money_scale_evidence`).
|
|
18
|
+
- What to change: `c.summary()` → `c.summary(start, end)`; `c.history()` → `c.history(start, end)` (for "up to now",
|
|
19
|
+
`end = c.server_time`); a calibration over two years → over at most one.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `Client.max_history_days` (read and write) and the constructor argument of the same name.
|
|
23
|
+
- `Client.server_name`: the name of the trade server the session logged in to (for example `ICMarketsSC-Demo`).
|
|
24
|
+
|
|
25
|
+
### Changed (core wmt5api 3.2.0 / 3.2.1)
|
|
26
|
+
- `history()` asks the server for your window only (the server answers in whole calendar months; before, three more
|
|
27
|
+
months were asked for on every call). A position closed in the window but held longer than three months before it
|
|
28
|
+
is now listed in `orders` (up to a year back); a position whose deals arrived in different pages is no longer
|
|
29
|
+
dropped.
|
|
30
|
+
- Two `history()` calls at once on one client run one after the other instead of failing with `BusyError`.
|
|
31
|
+
|
|
32
|
+
### Fixed (this package)
|
|
33
|
+
- **The platform wheel could not start on a machine that also has a newer .NET installed on `PATH`** (for example a
|
|
34
|
+
system .NET 10 next to the bundled .NET 9): the loader under pythonnet asked that `dotnet` for its runtimes, took
|
|
35
|
+
the newest and looked for it in the bundled runtime — `RuntimeNotFoundError` ("needs the .NET 8 (or later)
|
|
36
|
+
runtime"), on 1.1.2 too. The runtime is now started at a version the chosen runtime folder holds, and the message
|
|
37
|
+
says which runtime it tried.
|
|
38
|
+
|
|
39
|
+
### Fixed (core wmt5api 3.2.0 / 3.2.1)
|
|
40
|
+
- **Events could reach callbacks out of order** — an order's close before its open, an older price after a newer one,
|
|
41
|
+
a disconnect before the connect. Quotes, order updates, order progress, connect progress and history chunks now
|
|
42
|
+
arrive in the order the client received them; a throwing handler no longer drops the rest of a batch.
|
|
43
|
+
- **A market order or close with the default fill policy could use a filling mode the symbol does not accept**, and
|
|
44
|
+
the broker refused it (`UNSUPPORTED_FILLING_MODE`): the symbol's declared modes are a bit mask and were read as one
|
|
45
|
+
value. The default now picks a declared mode (fill-or-kill if declared, else immediate-or-cancel, else return on an
|
|
46
|
+
exchange).
|
|
47
|
+
- `Order.fill_policy` of a market order filled immediate-or-cancel read fill-or-kill; it reports the mode the server
|
|
48
|
+
confirmed.
|
|
49
|
+
- `equity_history()`: the documentation no longer says "empty" for a window with no deal (it is a flat line).
|
|
50
|
+
|
|
51
|
+
### Security (core wmt5api 3.2.0)
|
|
52
|
+
- `token_url` must be `https` (plain `http` only for a loopback host) and carry no user information, query or
|
|
53
|
+
fragment; redirects from the licence service are not followed (they would re-send the key).
|
|
54
|
+
- A login already bound to another trading account under your licence is refused with `LicenseError` reason
|
|
55
|
+
`AccountLimitReached` and a message that says so.
|
|
56
|
+
|
|
57
|
+
## [1.1.2] — 2026-10-15
|
|
58
|
+
|
|
59
|
+
From the delta re-test of 1.1.1 (no S1 or S2). Python layer only: the cores are unchanged.
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
- A `bars()` timeout says the budget of the call (`not complete within 0.5 s`), not what was left for the last block.
|
|
63
|
+
- `Order` and `Deal` money fields (profit, swap, commission, fee) carry no binary noise (`1.3900000000000001` is `1.39`).
|
|
64
|
+
- README: a connect to an unreachable licence service waits about twice `token_timeout`.
|
|
65
|
+
|
|
66
|
+
## [1.1.1] — 2026-10-14
|
|
67
|
+
|
|
68
|
+
From the Windows re-test of 1.1.0 (13 notes, one S2). Bundles wmt5api 3.1.2.
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
- **`equity_history()` returned `[]` for a window with no deal** (an idle account): the balance curve is now a flat line
|
|
72
|
+
at the current balance (core `wmt5api 3.1.2`). A script that read `eq[-1]` on an idle account raised `IndexError`.
|
|
73
|
+
- Account money figures carry no binary noise (`1635341.7899999998` is `1635341.79`).
|
|
74
|
+
- A bars timeout states a sub-second wait as it is (`0.5 s`, not `0 s`).
|
|
75
|
+
- A missing .NET runtime on a **32-bit** Python says that 32-bit Python needs the 32-bit runtime.
|
|
76
|
+
- `SymbolInfo` has a meaning for every field in the reference.
|
|
77
|
+
- README: ticks and their `time`, that the server enforces volume min/step, the minute rounding of an expiry, Ctrl+C
|
|
78
|
+
during `connect()`, the licence request retry.
|
|
79
|
+
|
|
80
|
+
## [1.1.0] — 2026-10-13
|
|
81
|
+
|
|
82
|
+
Found by a customer-style test on Windows Server 2022 / Python 3.10 (28 findings); every one is addressed. Bundles
|
|
83
|
+
wmt5api 3.1.1. Additive: nothing that worked in 1.0.0 is removed.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
- **Two packages could not start the .NET runtime from two threads at once** (`RuntimeNotFoundError` in one of them,
|
|
87
|
+
every time, on Linux as well). One lock now covers every `pmt*api` package in the process.
|
|
88
|
+
- **`bars()` re-aggregated the whole series after every block** (MT5): a deep request was quadratic and timed out
|
|
89
|
+
(`D1` × 1000 took over two minutes of CPU). It counts as the blocks arrive and aggregates once; the default budget
|
|
90
|
+
is `bars_timeout` plus a second per two weeks of one-minute bars the request needs.
|
|
91
|
+
- **A closed client left a thread behind** for every client that had used callbacks or `quotes()`: the callback
|
|
92
|
+
thread now ends at `disconnect()` (and when the client is dropped) and returns with the next event.
|
|
93
|
+
- **Type checkers saw every method as `(*Any, **Any) -> Any`**: the decorator that translates errors now keeps the
|
|
94
|
+
signature.
|
|
95
|
+
- Messages: a missing .NET runtime names the runtime and where to get it; the no-key text speaks `api_key=`; a bars
|
|
96
|
+
timeout states its real wait; a trade refusal names its code.
|
|
97
|
+
- `python -m pmt4api --connect` / `pmt5api --connect` accept `host:port` in `PMT*_HOST`.
|
|
98
|
+
- The reference gains a meaning for every field of the main data types.
|
|
99
|
+
- `close()`, `modify()` and `cancel()` return the order they acted on, the same on both packages (`pmt5api`: `close()` returned an empty record and `modify()` `None`; `pmt4api`: `cancel()` returned `None`).
|
|
100
|
+
- The core fixes: see wmt5api 3.1.1 — **`Deal.price` / `Deal.open_price` were swapped on a closing deal**, a pending sell order reported `DealBuy`, and `history()` omitted cancelled and expired pending orders.
|
|
101
|
+
|
|
102
|
+
### Added
|
|
103
|
+
- `lots_for_risk(..., side="buy" | "sell")`: a stop on the wrong side of the entry is refused instead of being sized by its distance.
|
|
104
|
+
- `LoginRejectedError` (a `ServerError`): the broker refused the login — `connect()` raises it for a wrong account or password instead of a bare `ServerError`; a dial that times out is a `ConnectError`, as on MT4.
|
|
105
|
+
|
|
106
|
+
## [1.0.0] — 2026-10-12
|
|
107
|
+
|
|
108
|
+
First release. `pmt5api` exposes every public capability of the wmt5api 3.x C# library to Python.
|
|
109
|
+
|
|
110
|
+
### Capabilities
|
|
111
|
+
- Connection and session: licensing, failover, `auto_reconnect`, proxies (SOCKS4/SOCKS5/HTTPS), client certificate,
|
|
112
|
+
all timeouts, connect-progress events, server time and account mode.
|
|
113
|
+
- Market data: symbols with trading rules and sessions, commissions, quotes and subscriptions (callbacks, iterators,
|
|
114
|
+
asyncio), bars M1–D1 with tick volume, tick value and contract size.
|
|
115
|
+
- Account: balances and margins (with and without swap/commission), orders, positions, closed orders, history and
|
|
116
|
+
deal history (blocking and event-driven), realized summary, balance/equity curve.
|
|
117
|
+
- Sizing and risk: money-scale calibration, lots-for-risk with evidence, required margin, order profit.
|
|
118
|
+
- Order entry: market, pending (limit, stop, stop-limit), expiration, fill policy, modify, partial close, close-by,
|
|
119
|
+
cancel — synchronous, with request ids and progress events, and awaitable; magic number on orders and deals.
|
|
120
|
+
- Offline helpers and administration: `servers.dat` read and write, demo account request, password change.
|
|
121
|
+
- Diagnostics: the core's log into Python `logging`.
|
|
122
|
+
- Parity manifest `pmt5api.COVERAGE`, checked against the DLL by the test suite; generated API reference and a
|
|
123
|
+
C#-to-Python table.
|
|
124
|
+
|
|
125
|
+
### Packaging
|
|
126
|
+
- Linux x86-64 wheel carrying the core and the .NET 9 runtime (`manylinux_2_28`); a pure wheel carrying the core;
|
|
127
|
+
an sdist.
|
|
128
|
+
|
|
129
|
+
### Verified
|
|
130
|
+
Against real demo accounts, from clean containers without .NET (Python 3.9, 3.12, 3.13; glibc 2.28 and 2.41):
|
|
131
|
+
read-only and trading suites, reconnects (drops, a refused window, a silent link), every shipped example, and a
|
|
132
|
+
three-hour unattended run (4 recovered drops, no errors, memory flat after warm-up).
|
|
133
|
+
Not verified: Windows, macOS, arm64, musl; netting accounts; client certificates.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pmt5api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Complete Python interface to the wmt5api MetaTrader 5 client library (market data, account, sizing, order entry, events).
|
|
5
5
|
Project-URL: Homepage, https://mtapi.one
|
|
6
6
|
Author: Sen Trading
|
|
@@ -37,19 +37,25 @@ No MetaTrader terminal is needed — the core talks to your broker directly.
|
|
|
37
37
|
|
|
38
38
|
**Parity with the C# library is measured, not claimed:** `pmt5api.COVERAGE` maps every public member of the C#
|
|
39
39
|
`MT5API` class to its Python counterpart (or states why it has none), and the test suite fails if the core gains a
|
|
40
|
-
member that is not accounted for.
|
|
41
|
-
|
|
40
|
+
member that is not accounted for. `pmt5api/docs/FROM-CSHARP.md` (inside the installed package) is that table;
|
|
41
|
+
`pmt5api/docs/API.md` (inside the installed package; `python -m pmt5api` prints its folder) is the reference.
|
|
42
42
|
|
|
43
43
|
## Install
|
|
44
44
|
|
|
45
|
-
The **Linux x86-64 wheel** carries everything — the core and a .NET runtime — so there is nothing else to install:
|
|
46
|
-
|
|
47
45
|
```
|
|
48
|
-
pip install pmt5api
|
|
46
|
+
pip install pmt5api
|
|
49
47
|
```
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
the
|
|
49
|
+
pip chooses the wheel for your platform. The **Linux x86-64 wheel** carries everything — the core and a .NET runtime —
|
|
50
|
+
so there is nothing else to install. On any other platform the `py3-none-any` wheel carries the core and needs a .NET 8
|
|
51
|
+
(or later) runtime on the machine (`dotnet --list-runtimes` should list `Microsoft.NETCore.App`). From a downloaded
|
|
52
|
+
file: `pip install pmt5api-<version>-py3-none-<platform>.whl`.
|
|
53
|
+
|
|
54
|
+
**Windows (x64).** Install the .NET 8 runtime once (`winget install Microsoft.DotNet.Runtime.8`, or the installer from
|
|
55
|
+
https://dotnet.microsoft.com/download/dotnet/8.0), then `pip install pmt5api`. Check it with `python -m pmt5api`: it prints
|
|
56
|
+
the versions, finds the runtime and constructs a client offline. Set the key in the shell that runs your program —
|
|
57
|
+
PowerShell: `$env:WMTAPI_KEY = "wk_..."`, cmd: `set WMTAPI_KEY=wk_...`. A string the broker sent can hold characters your
|
|
58
|
+
console's code page cannot print: set `PYTHONUTF8=1` (or run `chcp 65001`) to avoid a `UnicodeEncodeError`.
|
|
53
59
|
|
|
54
60
|
You also need your **API key** (`wk_…`, the same key you use with wmt5api):
|
|
55
61
|
|
|
@@ -101,6 +107,16 @@ bars; higher timeframes are built from them, aligned to the server's calendar (a
|
|
|
101
107
|
bar at 00, 04, 08, … server time, a week on Sunday, a month on the 1st), with `tick_volume` summed. Minutes after
|
|
102
108
|
`end` are not used; the last bar may still be forming. `spread` is present on M1 bars only.
|
|
103
109
|
|
|
110
|
+
**What a request costs.** The server hands the one-minute bars over in blocks of about two weeks, so the time of a
|
|
111
|
+
`bars()` call grows with how far back it reaches, not with the timeframe. Measured from a datacentre against a
|
|
112
|
+
broker's demo server: 5 000 one-minute bars 0.4 s; 100 `D1` bars 3 s; 5 000 `H1` bars 9 s; 500 `D1` bars 16 s;
|
|
113
|
+
1 000 `D1` bars 32 s (your link and your broker will differ). The default budget is `bars_timeout` plus a second per
|
|
114
|
+
block the request needs, so these finish without a `timeout=`; pass one to bound the call yourself.
|
|
115
|
+
|
|
116
|
+
**MT4 and MT5 differ here.** On MT5 `end=` includes the bar stamped exactly `end` and the bar that is still forming; the
|
|
117
|
+
MT4 package returns completed bars only and excludes a bar that opens at `end`. Code that runs on both has to drop
|
|
118
|
+
the forming MT5 bar itself.
|
|
119
|
+
|
|
104
120
|
### Live quotes
|
|
105
121
|
|
|
106
122
|
```python
|
|
@@ -123,19 +139,25 @@ c.subscribe("EURUSD")
|
|
|
123
139
|
remove()
|
|
124
140
|
```
|
|
125
141
|
|
|
126
|
-
Callbacks are delivered by **one dispatcher thread per client**, one at a time, in
|
|
142
|
+
Callbacks are delivered by **one dispatcher thread per client**, one at a time, in the order the events arrived — you need no locks
|
|
127
143
|
between callbacks, and a slow callback does not stall the connection. If a callback falls more than 20,000 events
|
|
128
144
|
behind, the oldest events are dropped, a warning is logged and `c.events_dropped` counts them. An exception raised
|
|
129
145
|
inside a callback is logged (`logging.getLogger("pmt5api")`) and swallowed. Order updates keep the state they had at
|
|
130
146
|
the moment they arrived (they are snapshots). The iterators attach when you create them, drop the oldest item when the
|
|
131
147
|
consumer falls behind, and detach on `close()` / `async with` / garbage collection.
|
|
132
148
|
|
|
149
|
+
A tick's `time` is the server's: on some brokers about one tick in a hundred carries a time slightly earlier than the
|
|
150
|
+
previous tick of the same symbol (the feed is merged from several sources). Delivery order is arrival order; sort by
|
|
151
|
+
`time` if you need the market's order.
|
|
152
|
+
|
|
133
153
|
## Trading
|
|
134
154
|
|
|
135
155
|
```python
|
|
156
|
+
from datetime import datetime
|
|
157
|
+
|
|
136
158
|
o = c.buy("EURUSD", 0.10, sl=1.0950, tp=1.1100, magic=424242) # market order at the ask; sell() at the bid
|
|
137
|
-
c.modify(o.ticket, sl=1.0970) # change stops
|
|
138
|
-
c.close(o.ticket, 0.05) # close half; close(ticket) closes it all
|
|
159
|
+
c.modify(o.ticket, sl=1.0970) # change stops (returns the order as the server holds it)
|
|
160
|
+
c.close(o.ticket, 0.05) # close half (returns the closing record); close(ticket) closes it all
|
|
139
161
|
|
|
140
162
|
p = c.place_order("EURUSD", "buy_stop_limit", 0.10, 1.1200, stop_limit=1.1190,
|
|
141
163
|
expiration=datetime(2026, 12, 31, 23, 0)) # also buy_limit sell_limit buy_stop sell_stop
|
|
@@ -159,6 +181,12 @@ rid = c.buy_async("EURUSD", 0.10)
|
|
|
159
181
|
`aclose`, `acancel`, `aquote`. Cancelling the `await` (a `wait_for` timeout, a cancelled task) does not recall a request
|
|
160
182
|
that is already on its way: the order may still be sent, so check `orders()` afterwards.
|
|
161
183
|
|
|
184
|
+
**The server, not the library, enforces volume rules.** A volume below the minimum or off the step is sent and refused
|
|
185
|
+
(`ServerError` `INVALID_VOLUME`); `symbol(name)` has `min_lots`, `max_lots` and `lots_step` to check against first. A
|
|
186
|
+
pending order's expiration is stored to the minute (rounded **down**), so an order can expire up to a minute before the
|
|
187
|
+
time you asked for. Pressing Ctrl+C while `connect()` is dialling is honoured when the dial ends (up to
|
|
188
|
+
`connect_timeout`), not at once.
|
|
189
|
+
|
|
162
190
|
**Test on a demo account first.** Nothing here is ever retried or resent for you: if the server does not confirm in
|
|
163
191
|
time the call raises `RequestTimeout` and the order **may have been accepted** — look at `orders()` before sending
|
|
164
192
|
again. `NoQuoteError` is different: no price was available, so nothing was sent. `orders()` can lag a moment behind
|
|
@@ -174,8 +202,10 @@ Do not size a trade from a one-line formula: what a price unit is worth depends
|
|
|
174
202
|
from your own closed deals and refuses to size without that evidence:
|
|
175
203
|
|
|
176
204
|
```python
|
|
177
|
-
|
|
178
|
-
|
|
205
|
+
from datetime import timedelta
|
|
206
|
+
|
|
207
|
+
if c.calibrate_money_scale(c.server_time - timedelta(days=365)): # read-only; places no order; at most a year back
|
|
208
|
+
r = c.lots_for_risk("EURUSD", entry_price=1.1000, stop_price=1.0950, risk_amount=50.0, side="buy")
|
|
179
209
|
for w in r.warnings:
|
|
180
210
|
print("warning:", w) # read these: ok can still be True with a caveat
|
|
181
211
|
if r.ok and r.expected_loss_at_stop <= 50.0 * 1.05:
|
|
@@ -198,12 +228,29 @@ to be checked. Also: `required_margin(symbol, lots)`, `order_profit(symbol, open
|
|
|
198
228
|
| `account(refresh=True)` | `AccountInfo` — balance, equity, floating P/L (with and without swap/commission), margin, free margin, margin level, leverage, account mode, contact details |
|
|
199
229
|
| `orders()` / `positions()` / `closed_orders()` | `list[Order]` — working / open positions only / what this session saw close |
|
|
200
230
|
| `history(start, end)` / `deal_history(year, month)` | `History` — `.orders` and `.deals` (the ledger, including deposits and withdrawals) |
|
|
201
|
-
| `summary(start
|
|
231
|
+
| `summary(start, end)` | `AccountSummary` — realized profit, deposits, withdrawals, from the deal ledger |
|
|
202
232
|
| `equity_history(start, "D1")` | `list[EquityPoint]` — balance (exact) and equity (reconstructed) curve with drawdowns |
|
|
203
233
|
| `request_history(...)`, `request_bars(...)` | without waiting; the data arrives through `on_history` / `on_quote_history` |
|
|
204
234
|
|
|
205
235
|
Times are naive `datetime` values in the **broker's server time**, as the server reports them; an unset time is `None`.
|
|
206
236
|
|
|
237
|
+
**History is asked for by window.** `history()` and `summary()` take a start and an end, and one call covers at most
|
|
238
|
+
`max_history_days` (366 by default; set it on the client, `0` removes the limit); `equity_history()` and
|
|
239
|
+
`calibrate_money_scale()` run from their start to now, so the start is at most that long ago. A missing, reversed or longer window raises
|
|
240
|
+
`InvalidRequestError` before anything is sent. For a longer period, ask window by window and add the results up:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
from datetime import timedelta
|
|
244
|
+
|
|
245
|
+
end = c.server_time # server time; "up to now"
|
|
246
|
+
h = c.history(end - timedelta(days=30), end) # .orders and .deals
|
|
247
|
+
year = c.summary(end - timedelta(days=365), end)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The server answers history in whole calendar months; `history()` keeps your window, and lists a position closed in it
|
|
251
|
+
whose opening deal is earlier (up to a year before the window). A window with no deal in it gives a flat equity curve
|
|
252
|
+
at the balance, never an empty one.
|
|
253
|
+
|
|
207
254
|
`account()` first quotes each open position's symbol once and recalculates profits, so equity, profit and margin
|
|
208
255
|
level are current (a cold read would otherwise show equity equal to balance). Pass `refresh=False` to skip that.
|
|
209
256
|
|
|
@@ -211,7 +258,9 @@ level are current (a cold read would otherwise show equity equal to balance). Pa
|
|
|
211
258
|
|
|
212
259
|
`on_quote`, `on_order_update`, `on_order_progress`, `on_connect_progress`, `on_history`, `on_quote_history`,
|
|
213
260
|
`on_symbols_update`, `on_symbol_update` — each returns a function that removes the callback. `quotes()` and
|
|
214
|
-
`order_updates()` (and the `a…` async versions) turn a stream into an iterator.
|
|
261
|
+
`order_updates()` (and the `a…` async versions) turn a stream into an iterator. Events reach you in the order the
|
|
262
|
+
client received them — an order's open before its close, ticks in time order — and callbacks run one at a time on the
|
|
263
|
+
client's own thread.
|
|
215
264
|
|
|
216
265
|
## Staying connected
|
|
217
266
|
|
|
@@ -228,16 +277,24 @@ subscriptions are restored, cached snapshots are not.
|
|
|
228
277
|
| API key | `api_key=` | `WMTAPI_KEY` |
|
|
229
278
|
| Licence service URL (leave at its default) | `token_url=` | `WMTAPI_TOKEN_URL` |
|
|
230
279
|
| Core library (only if you do not use a wheel that carries it) | `dll_path=` | `WMTAPI_DLL` |
|
|
231
|
-
| Timeouts (seconds, each > 0) | `connect_timeout` (
|
|
232
|
-
| Reconnect | `auto_reconnect`, `reconnect_backoff
|
|
280
|
+
| Timeouts (seconds, each > 0) | `connect_timeout` (the whole connect; default 30), `cluster_member_timeout` (per server while failing over; 10), `token_timeout` (10), `execution_timeout` (a trade confirmation; 30), `quote_timeout` (10) / `history_timeout` (30) / `bars_timeout` (10, plus a second per two weeks of one-minute bars a `bars()` call needs) — the defaults of `quote`, `history`, `bars`, `ping_interval` (20), `no_message_timeout` (30) | — |
|
|
281
|
+
| Reconnect | `auto_reconnect`, `reconnect_backoff` (3, doubling up to `reconnect_max_backoff`, 30), `reconnect_max_attempts` (0 = no limit) | — |
|
|
233
282
|
| Proxy | `proxy=Proxy("socks5" \| "socks4" \| "https", host, port, user, password)` — `host` must be an IP address | — |
|
|
234
283
|
| Client certificate / common key | `certificate=`, `certificate_password=`, `common_key=` | — |
|
|
284
|
+
| History window | `max_history_days=` (366; `0` = no limit) — also a client attribute | — |
|
|
235
285
|
| .NET runtime location | — | `DOTNET_ROOT` |
|
|
236
286
|
|
|
237
287
|
A core path you give (argument or `WMTAPI_DLL`) is used exactly as given and is an error if it does not exist. The
|
|
238
288
|
.NET runtime is taken from `DOTNET_ROOT` if you set it, else the runtime bundled in the wheel, else an installed one.
|
|
239
289
|
The bundled runtime runs in invariant-culture mode on machines without ICU.
|
|
240
290
|
|
|
291
|
+
**These two settings decide what code runs in your process.** `WMTAPI_DLL` (or `dll_path=`) names the library that is
|
|
292
|
+
loaded, and `DOTNET_ROOT` (or, without it, the `dotnet` found on `PATH`) names the .NET runtime that is started;
|
|
293
|
+
both run with your rights. Set them only from configuration you control, and do not run with an environment
|
|
294
|
+
someone else can change. With `enable_core_logging()` switched on, the package logs once when either was chosen
|
|
295
|
+
by the environment. The licence service URL must be `https` (plain `http` only for a loopback test service), and
|
|
296
|
+
redirects from it are not followed.
|
|
297
|
+
|
|
241
298
|
### Diagnostics
|
|
242
299
|
|
|
243
300
|
```python
|
|
@@ -252,6 +309,10 @@ pmt5api.enable_core_logging() # the core's own messages -> logging.getL
|
|
|
252
309
|
`request_demo_account(DemoAccountRequest(...), host, port)` asks a broker for a demo account (it creates a real
|
|
253
310
|
account at that broker: use it deliberately); `c.change_password(new, investor=False)`.
|
|
254
311
|
|
|
312
|
+
`python -m pmt5api` prints what is installed and whether it works here (`--connect` also reads an account, using
|
|
313
|
+
`PMT5_LOGIN`, `PMT5_PASSWORD`, `PMT5_HOST` — a bare host, or `host:port`, or the port in `PMT5_PORT` — and the key in
|
|
314
|
+
`WMTAPI_KEY`); the output holds no secrets and is what to send when asking for help.
|
|
315
|
+
|
|
255
316
|
## Errors
|
|
256
317
|
|
|
257
318
|
Everything the core raises reaches you as one of these, all subclasses of `pmt5api.WmtError`; the original
|
|
@@ -260,7 +321,7 @@ exception stays available as `__cause__`.
|
|
|
260
321
|
| Exception | Meaning |
|
|
261
322
|
|---|---|
|
|
262
323
|
| `LicenseError` | The licence service refused the key. `.reason` is one of `NoKey`, `Invalid`, `Expired`, `Revoked`, `RateLimited`, `LoginNotAllowed`, `AccountLimitReached`, `Unknown`. |
|
|
263
|
-
| `TokenServiceError` | The licence service could not be reached or answered unusably — retry, don't re-prompt for a key. |
|
|
324
|
+
| `TokenServiceError` | The licence service could not be reached or answered unusably — retry, don't re-prompt for a key. (`token_timeout` bounds one request and a failed request is tried again once, so a connect to an unreachable licence service can wait about twice that.) |
|
|
264
325
|
| `ConnectError` | The broker's access server (or the network, or a proxy) could not be reached. |
|
|
265
326
|
| `ConnectionLostError` | An established connection dropped (the core's `Disconnected`). |
|
|
266
327
|
| `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`, …). |
|
|
@@ -304,6 +365,10 @@ moved by 4 MB), threads and file descriptors stayed flat. Every shipped example
|
|
|
304
365
|
|
|
305
366
|
## Known limitations
|
|
306
367
|
|
|
368
|
+
- **Sizing needs evidence from the account's own closed deals.** On an account whose deals all carry swap or
|
|
369
|
+
commission, or whose deals do not have the shape the calibration can read (a Tickmill demo, for one), `calibrate_money_scale`
|
|
370
|
+
finds no symbol to measure from and `lots_for_risk` refuses: that is the intended answer, not an error. `tick_value()`
|
|
371
|
+
and `required_margin()` still work, and the refusal says what was missing.
|
|
307
372
|
- **Linux x86-64 is the verified platform.** Windows, macOS, arm64 and musl-based Linux have not been verified.
|
|
308
373
|
- Netting accounts have not been verified (the available demo accounts are all hedging).
|
|
309
374
|
- Client certificates (`certificate=`), `common_key=` and `request_demo_account` have been unit-tested but not run
|
|
@@ -10,19 +10,25 @@ No MetaTrader terminal is needed — the core talks to your broker directly.
|
|
|
10
10
|
|
|
11
11
|
**Parity with the C# library is measured, not claimed:** `pmt5api.COVERAGE` maps every public member of the C#
|
|
12
12
|
`MT5API` class to its Python counterpart (or states why it has none), and the test suite fails if the core gains a
|
|
13
|
-
member that is not accounted for.
|
|
14
|
-
|
|
13
|
+
member that is not accounted for. `pmt5api/docs/FROM-CSHARP.md` (inside the installed package) is that table;
|
|
14
|
+
`pmt5api/docs/API.md` (inside the installed package; `python -m pmt5api` prints its folder) is the reference.
|
|
15
15
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
18
|
-
The **Linux x86-64 wheel** carries everything — the core and a .NET runtime — so there is nothing else to install:
|
|
19
|
-
|
|
20
18
|
```
|
|
21
|
-
pip install pmt5api
|
|
19
|
+
pip install pmt5api
|
|
22
20
|
```
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
the
|
|
22
|
+
pip chooses the wheel for your platform. The **Linux x86-64 wheel** carries everything — the core and a .NET runtime —
|
|
23
|
+
so there is nothing else to install. On any other platform the `py3-none-any` wheel carries the core and needs a .NET 8
|
|
24
|
+
(or later) runtime on the machine (`dotnet --list-runtimes` should list `Microsoft.NETCore.App`). From a downloaded
|
|
25
|
+
file: `pip install pmt5api-<version>-py3-none-<platform>.whl`.
|
|
26
|
+
|
|
27
|
+
**Windows (x64).** Install the .NET 8 runtime once (`winget install Microsoft.DotNet.Runtime.8`, or the installer from
|
|
28
|
+
https://dotnet.microsoft.com/download/dotnet/8.0), then `pip install pmt5api`. Check it with `python -m pmt5api`: it prints
|
|
29
|
+
the versions, finds the runtime and constructs a client offline. Set the key in the shell that runs your program —
|
|
30
|
+
PowerShell: `$env:WMTAPI_KEY = "wk_..."`, cmd: `set WMTAPI_KEY=wk_...`. A string the broker sent can hold characters your
|
|
31
|
+
console's code page cannot print: set `PYTHONUTF8=1` (or run `chcp 65001`) to avoid a `UnicodeEncodeError`.
|
|
26
32
|
|
|
27
33
|
You also need your **API key** (`wk_…`, the same key you use with wmt5api):
|
|
28
34
|
|
|
@@ -74,6 +80,16 @@ bars; higher timeframes are built from them, aligned to the server's calendar (a
|
|
|
74
80
|
bar at 00, 04, 08, … server time, a week on Sunday, a month on the 1st), with `tick_volume` summed. Minutes after
|
|
75
81
|
`end` are not used; the last bar may still be forming. `spread` is present on M1 bars only.
|
|
76
82
|
|
|
83
|
+
**What a request costs.** The server hands the one-minute bars over in blocks of about two weeks, so the time of a
|
|
84
|
+
`bars()` call grows with how far back it reaches, not with the timeframe. Measured from a datacentre against a
|
|
85
|
+
broker's demo server: 5 000 one-minute bars 0.4 s; 100 `D1` bars 3 s; 5 000 `H1` bars 9 s; 500 `D1` bars 16 s;
|
|
86
|
+
1 000 `D1` bars 32 s (your link and your broker will differ). The default budget is `bars_timeout` plus a second per
|
|
87
|
+
block the request needs, so these finish without a `timeout=`; pass one to bound the call yourself.
|
|
88
|
+
|
|
89
|
+
**MT4 and MT5 differ here.** On MT5 `end=` includes the bar stamped exactly `end` and the bar that is still forming; the
|
|
90
|
+
MT4 package returns completed bars only and excludes a bar that opens at `end`. Code that runs on both has to drop
|
|
91
|
+
the forming MT5 bar itself.
|
|
92
|
+
|
|
77
93
|
### Live quotes
|
|
78
94
|
|
|
79
95
|
```python
|
|
@@ -96,19 +112,25 @@ c.subscribe("EURUSD")
|
|
|
96
112
|
remove()
|
|
97
113
|
```
|
|
98
114
|
|
|
99
|
-
Callbacks are delivered by **one dispatcher thread per client**, one at a time, in
|
|
115
|
+
Callbacks are delivered by **one dispatcher thread per client**, one at a time, in the order the events arrived — you need no locks
|
|
100
116
|
between callbacks, and a slow callback does not stall the connection. If a callback falls more than 20,000 events
|
|
101
117
|
behind, the oldest events are dropped, a warning is logged and `c.events_dropped` counts them. An exception raised
|
|
102
118
|
inside a callback is logged (`logging.getLogger("pmt5api")`) and swallowed. Order updates keep the state they had at
|
|
103
119
|
the moment they arrived (they are snapshots). The iterators attach when you create them, drop the oldest item when the
|
|
104
120
|
consumer falls behind, and detach on `close()` / `async with` / garbage collection.
|
|
105
121
|
|
|
122
|
+
A tick's `time` is the server's: on some brokers about one tick in a hundred carries a time slightly earlier than the
|
|
123
|
+
previous tick of the same symbol (the feed is merged from several sources). Delivery order is arrival order; sort by
|
|
124
|
+
`time` if you need the market's order.
|
|
125
|
+
|
|
106
126
|
## Trading
|
|
107
127
|
|
|
108
128
|
```python
|
|
129
|
+
from datetime import datetime
|
|
130
|
+
|
|
109
131
|
o = c.buy("EURUSD", 0.10, sl=1.0950, tp=1.1100, magic=424242) # market order at the ask; sell() at the bid
|
|
110
|
-
c.modify(o.ticket, sl=1.0970) # change stops
|
|
111
|
-
c.close(o.ticket, 0.05) # close half; close(ticket) closes it all
|
|
132
|
+
c.modify(o.ticket, sl=1.0970) # change stops (returns the order as the server holds it)
|
|
133
|
+
c.close(o.ticket, 0.05) # close half (returns the closing record); close(ticket) closes it all
|
|
112
134
|
|
|
113
135
|
p = c.place_order("EURUSD", "buy_stop_limit", 0.10, 1.1200, stop_limit=1.1190,
|
|
114
136
|
expiration=datetime(2026, 12, 31, 23, 0)) # also buy_limit sell_limit buy_stop sell_stop
|
|
@@ -132,6 +154,12 @@ rid = c.buy_async("EURUSD", 0.10)
|
|
|
132
154
|
`aclose`, `acancel`, `aquote`. Cancelling the `await` (a `wait_for` timeout, a cancelled task) does not recall a request
|
|
133
155
|
that is already on its way: the order may still be sent, so check `orders()` afterwards.
|
|
134
156
|
|
|
157
|
+
**The server, not the library, enforces volume rules.** A volume below the minimum or off the step is sent and refused
|
|
158
|
+
(`ServerError` `INVALID_VOLUME`); `symbol(name)` has `min_lots`, `max_lots` and `lots_step` to check against first. A
|
|
159
|
+
pending order's expiration is stored to the minute (rounded **down**), so an order can expire up to a minute before the
|
|
160
|
+
time you asked for. Pressing Ctrl+C while `connect()` is dialling is honoured when the dial ends (up to
|
|
161
|
+
`connect_timeout`), not at once.
|
|
162
|
+
|
|
135
163
|
**Test on a demo account first.** Nothing here is ever retried or resent for you: if the server does not confirm in
|
|
136
164
|
time the call raises `RequestTimeout` and the order **may have been accepted** — look at `orders()` before sending
|
|
137
165
|
again. `NoQuoteError` is different: no price was available, so nothing was sent. `orders()` can lag a moment behind
|
|
@@ -147,8 +175,10 @@ Do not size a trade from a one-line formula: what a price unit is worth depends
|
|
|
147
175
|
from your own closed deals and refuses to size without that evidence:
|
|
148
176
|
|
|
149
177
|
```python
|
|
150
|
-
|
|
151
|
-
|
|
178
|
+
from datetime import timedelta
|
|
179
|
+
|
|
180
|
+
if c.calibrate_money_scale(c.server_time - timedelta(days=365)): # read-only; places no order; at most a year back
|
|
181
|
+
r = c.lots_for_risk("EURUSD", entry_price=1.1000, stop_price=1.0950, risk_amount=50.0, side="buy")
|
|
152
182
|
for w in r.warnings:
|
|
153
183
|
print("warning:", w) # read these: ok can still be True with a caveat
|
|
154
184
|
if r.ok and r.expected_loss_at_stop <= 50.0 * 1.05:
|
|
@@ -171,12 +201,29 @@ to be checked. Also: `required_margin(symbol, lots)`, `order_profit(symbol, open
|
|
|
171
201
|
| `account(refresh=True)` | `AccountInfo` — balance, equity, floating P/L (with and without swap/commission), margin, free margin, margin level, leverage, account mode, contact details |
|
|
172
202
|
| `orders()` / `positions()` / `closed_orders()` | `list[Order]` — working / open positions only / what this session saw close |
|
|
173
203
|
| `history(start, end)` / `deal_history(year, month)` | `History` — `.orders` and `.deals` (the ledger, including deposits and withdrawals) |
|
|
174
|
-
| `summary(start
|
|
204
|
+
| `summary(start, end)` | `AccountSummary` — realized profit, deposits, withdrawals, from the deal ledger |
|
|
175
205
|
| `equity_history(start, "D1")` | `list[EquityPoint]` — balance (exact) and equity (reconstructed) curve with drawdowns |
|
|
176
206
|
| `request_history(...)`, `request_bars(...)` | without waiting; the data arrives through `on_history` / `on_quote_history` |
|
|
177
207
|
|
|
178
208
|
Times are naive `datetime` values in the **broker's server time**, as the server reports them; an unset time is `None`.
|
|
179
209
|
|
|
210
|
+
**History is asked for by window.** `history()` and `summary()` take a start and an end, and one call covers at most
|
|
211
|
+
`max_history_days` (366 by default; set it on the client, `0` removes the limit); `equity_history()` and
|
|
212
|
+
`calibrate_money_scale()` run from their start to now, so the start is at most that long ago. A missing, reversed or longer window raises
|
|
213
|
+
`InvalidRequestError` before anything is sent. For a longer period, ask window by window and add the results up:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from datetime import timedelta
|
|
217
|
+
|
|
218
|
+
end = c.server_time # server time; "up to now"
|
|
219
|
+
h = c.history(end - timedelta(days=30), end) # .orders and .deals
|
|
220
|
+
year = c.summary(end - timedelta(days=365), end)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The server answers history in whole calendar months; `history()` keeps your window, and lists a position closed in it
|
|
224
|
+
whose opening deal is earlier (up to a year before the window). A window with no deal in it gives a flat equity curve
|
|
225
|
+
at the balance, never an empty one.
|
|
226
|
+
|
|
180
227
|
`account()` first quotes each open position's symbol once and recalculates profits, so equity, profit and margin
|
|
181
228
|
level are current (a cold read would otherwise show equity equal to balance). Pass `refresh=False` to skip that.
|
|
182
229
|
|
|
@@ -184,7 +231,9 @@ level are current (a cold read would otherwise show equity equal to balance). Pa
|
|
|
184
231
|
|
|
185
232
|
`on_quote`, `on_order_update`, `on_order_progress`, `on_connect_progress`, `on_history`, `on_quote_history`,
|
|
186
233
|
`on_symbols_update`, `on_symbol_update` — each returns a function that removes the callback. `quotes()` and
|
|
187
|
-
`order_updates()` (and the `a…` async versions) turn a stream into an iterator.
|
|
234
|
+
`order_updates()` (and the `a…` async versions) turn a stream into an iterator. Events reach you in the order the
|
|
235
|
+
client received them — an order's open before its close, ticks in time order — and callbacks run one at a time on the
|
|
236
|
+
client's own thread.
|
|
188
237
|
|
|
189
238
|
## Staying connected
|
|
190
239
|
|
|
@@ -201,16 +250,24 @@ subscriptions are restored, cached snapshots are not.
|
|
|
201
250
|
| API key | `api_key=` | `WMTAPI_KEY` |
|
|
202
251
|
| Licence service URL (leave at its default) | `token_url=` | `WMTAPI_TOKEN_URL` |
|
|
203
252
|
| Core library (only if you do not use a wheel that carries it) | `dll_path=` | `WMTAPI_DLL` |
|
|
204
|
-
| Timeouts (seconds, each > 0) | `connect_timeout` (
|
|
205
|
-
| Reconnect | `auto_reconnect`, `reconnect_backoff
|
|
253
|
+
| Timeouts (seconds, each > 0) | `connect_timeout` (the whole connect; default 30), `cluster_member_timeout` (per server while failing over; 10), `token_timeout` (10), `execution_timeout` (a trade confirmation; 30), `quote_timeout` (10) / `history_timeout` (30) / `bars_timeout` (10, plus a second per two weeks of one-minute bars a `bars()` call needs) — the defaults of `quote`, `history`, `bars`, `ping_interval` (20), `no_message_timeout` (30) | — |
|
|
254
|
+
| Reconnect | `auto_reconnect`, `reconnect_backoff` (3, doubling up to `reconnect_max_backoff`, 30), `reconnect_max_attempts` (0 = no limit) | — |
|
|
206
255
|
| Proxy | `proxy=Proxy("socks5" \| "socks4" \| "https", host, port, user, password)` — `host` must be an IP address | — |
|
|
207
256
|
| Client certificate / common key | `certificate=`, `certificate_password=`, `common_key=` | — |
|
|
257
|
+
| History window | `max_history_days=` (366; `0` = no limit) — also a client attribute | — |
|
|
208
258
|
| .NET runtime location | — | `DOTNET_ROOT` |
|
|
209
259
|
|
|
210
260
|
A core path you give (argument or `WMTAPI_DLL`) is used exactly as given and is an error if it does not exist. The
|
|
211
261
|
.NET runtime is taken from `DOTNET_ROOT` if you set it, else the runtime bundled in the wheel, else an installed one.
|
|
212
262
|
The bundled runtime runs in invariant-culture mode on machines without ICU.
|
|
213
263
|
|
|
264
|
+
**These two settings decide what code runs in your process.** `WMTAPI_DLL` (or `dll_path=`) names the library that is
|
|
265
|
+
loaded, and `DOTNET_ROOT` (or, without it, the `dotnet` found on `PATH`) names the .NET runtime that is started;
|
|
266
|
+
both run with your rights. Set them only from configuration you control, and do not run with an environment
|
|
267
|
+
someone else can change. With `enable_core_logging()` switched on, the package logs once when either was chosen
|
|
268
|
+
by the environment. The licence service URL must be `https` (plain `http` only for a loopback test service), and
|
|
269
|
+
redirects from it are not followed.
|
|
270
|
+
|
|
214
271
|
### Diagnostics
|
|
215
272
|
|
|
216
273
|
```python
|
|
@@ -225,6 +282,10 @@ pmt5api.enable_core_logging() # the core's own messages -> logging.getL
|
|
|
225
282
|
`request_demo_account(DemoAccountRequest(...), host, port)` asks a broker for a demo account (it creates a real
|
|
226
283
|
account at that broker: use it deliberately); `c.change_password(new, investor=False)`.
|
|
227
284
|
|
|
285
|
+
`python -m pmt5api` prints what is installed and whether it works here (`--connect` also reads an account, using
|
|
286
|
+
`PMT5_LOGIN`, `PMT5_PASSWORD`, `PMT5_HOST` — a bare host, or `host:port`, or the port in `PMT5_PORT` — and the key in
|
|
287
|
+
`WMTAPI_KEY`); the output holds no secrets and is what to send when asking for help.
|
|
288
|
+
|
|
228
289
|
## Errors
|
|
229
290
|
|
|
230
291
|
Everything the core raises reaches you as one of these, all subclasses of `pmt5api.WmtError`; the original
|
|
@@ -233,7 +294,7 @@ exception stays available as `__cause__`.
|
|
|
233
294
|
| Exception | Meaning |
|
|
234
295
|
|---|---|
|
|
235
296
|
| `LicenseError` | The licence service refused the key. `.reason` is one of `NoKey`, `Invalid`, `Expired`, `Revoked`, `RateLimited`, `LoginNotAllowed`, `AccountLimitReached`, `Unknown`. |
|
|
236
|
-
| `TokenServiceError` | The licence service could not be reached or answered unusably — retry, don't re-prompt for a key. |
|
|
297
|
+
| `TokenServiceError` | The licence service could not be reached or answered unusably — retry, don't re-prompt for a key. (`token_timeout` bounds one request and a failed request is tried again once, so a connect to an unreachable licence service can wait about twice that.) |
|
|
237
298
|
| `ConnectError` | The broker's access server (or the network, or a proxy) could not be reached. |
|
|
238
299
|
| `ConnectionLostError` | An established connection dropped (the core's `Disconnected`). |
|
|
239
300
|
| `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`, …). |
|
|
@@ -277,6 +338,10 @@ moved by 4 MB), threads and file descriptors stayed flat. Every shipped example
|
|
|
277
338
|
|
|
278
339
|
## Known limitations
|
|
279
340
|
|
|
341
|
+
- **Sizing needs evidence from the account's own closed deals.** On an account whose deals all carry swap or
|
|
342
|
+
commission, or whose deals do not have the shape the calibration can read (a Tickmill demo, for one), `calibrate_money_scale`
|
|
343
|
+
finds no symbol to measure from and `lots_for_risk` refuses: that is the intended answer, not an error. `tick_value()`
|
|
344
|
+
and `required_margin()` still work, and the refusal says what was missing.
|
|
280
345
|
- **Linux x86-64 is the verified platform.** Windows, macOS, arm64 and musl-based Linux have not been verified.
|
|
281
346
|
- Netting accounts have not been verified (the available demo accounts are all hedging).
|
|
282
347
|
- Client certificates (`certificate=`), `common_key=` and `request_demo_account` have been unit-tested but not run
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
PMT5_LOGIN=... PMT5_PASSWORD=... PMT5_HOST=... python pnl.py
|
|
4
4
|
"""
|
|
5
5
|
import os
|
|
6
|
-
from datetime import
|
|
6
|
+
from datetime import timedelta
|
|
7
7
|
|
|
8
8
|
from pmt5api import Client
|
|
9
9
|
|
|
@@ -13,10 +13,12 @@ with Client(int(os.environ["PMT5_LOGIN"]), os.environ["PMT5_PASSWORD"], os.envir
|
|
|
13
13
|
print("balance %.2f equity %.2f floating %.2f (swap %.2f, commission %.2f)"
|
|
14
14
|
% (a.balance, a.equity, a.floating_pl, a.swap, a.commission))
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
now = c.server_time # history windows are in the broker's server time
|
|
17
|
+
s = c.summary(now - timedelta(days=365), now) # the last year (at most max_history_days per call); blocks
|
|
18
|
+
print("last 365 days: realized %.2f deposits %.2f withdrawals %.2f"
|
|
19
|
+
% (s.realized_profit, s.deposit, s.withdrawal))
|
|
18
20
|
|
|
19
|
-
curve = c.equity_history(
|
|
21
|
+
curve = c.equity_history(now - timedelta(days=30), "D1")
|
|
20
22
|
for p in curve[-5:]:
|
|
21
23
|
flag = " (incomplete)" if p.incomplete else ""
|
|
22
24
|
print("%s balance %.2f equity %.2f drawdown %.2f%%%s"
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"""
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
|
-
from datetime import
|
|
7
|
+
from datetime import timedelta
|
|
8
8
|
|
|
9
9
|
from pmt5api import Client
|
|
10
10
|
|
|
@@ -19,8 +19,8 @@ with Client(int(os.environ["PMT5_LOGIN"]), os.environ["PMT5_PASSWORD"], os.envir
|
|
|
19
19
|
acct = c.account()
|
|
20
20
|
print("account currency %s, equity %.2f" % (acct.currency, acct.equity))
|
|
21
21
|
|
|
22
|
-
# 1. measure the account's own conventions from its closed deals (read-only; places no order)
|
|
23
|
-
if not c.calibrate_money_scale(
|
|
22
|
+
# 1. measure the account's own conventions from its closed deals of the last year (read-only; places no order)
|
|
23
|
+
if not c.calibrate_money_scale(c.server_time - timedelta(days=365)):
|
|
24
24
|
sys.exit("cannot size yet: " + c.money_scale_evidence)
|
|
25
25
|
print("money scale %s" % c.money_scale)
|
|
26
26
|
|
|
@@ -8,7 +8,7 @@ from ._coverage import COVERAGE, TYPES
|
|
|
8
8
|
from .bars import Bar, QuoteHistoryEvent, TIMEFRAMES, aggregate
|
|
9
9
|
from .client import Client, Proxy
|
|
10
10
|
from .diagnostics import enable_core_logging
|
|
11
|
-
from .errors import (BusyError, ConnectError, NoQuoteError, ConnectionLostError, CoreNotFoundError, InvalidRequestError, InvestorModeError,
|
|
11
|
+
from .errors import (BusyError, ConnectError, NoQuoteError, ConnectionLostError, CoreNotFoundError, InvalidRequestError, InvestorModeError, LoginRejectedError,
|
|
12
12
|
LicenseError, NotConnectedError, ProxyError, RequestTimeout, RuntimeNotFoundError,
|
|
13
13
|
ServerError, TokenServiceError, WmtError)
|
|
14
14
|
from .frames import to_frame
|
|
@@ -17,7 +17,7 @@ from .models import (AccountInfo, AccountSummary, BaseInfo, CommissionInfo, Comm
|
|
|
17
17
|
OrderUpdate, Quote, ServerRecord, SymbolInfo, SymbolSessions, TradingSession)
|
|
18
18
|
from .servers import load_servers_dat, request_demo_account, save_servers_dat, tick_value_from_deal
|
|
19
19
|
|
|
20
|
-
__version__ = "1.
|
|
20
|
+
__version__ = "1.2.0"
|
|
21
21
|
|
|
22
22
|
__all__ = [
|
|
23
23
|
"Client", "Proxy",
|
|
@@ -28,7 +28,7 @@ __all__ = [
|
|
|
28
28
|
"Bar", "TIMEFRAMES", "aggregate", "to_frame",
|
|
29
29
|
"load_servers_dat", "save_servers_dat", "request_demo_account", "tick_value_from_deal", "enable_core_logging",
|
|
30
30
|
"WmtError", "LicenseError", "TokenServiceError", "ConnectError", "ConnectionLostError", "NotConnectedError",
|
|
31
|
-
"ServerError", "InvestorModeError", "ProxyError", "RequestTimeout", "NoQuoteError", "InvalidRequestError", "BusyError",
|
|
31
|
+
"ServerError", "LoginRejectedError", "InvestorModeError", "ProxyError", "RequestTimeout", "NoQuoteError", "InvalidRequestError", "BusyError",
|
|
32
32
|
"CoreNotFoundError",
|
|
33
33
|
"RuntimeNotFoundError", "COVERAGE", "TYPES", "__version__",
|
|
34
34
|
]
|