pmt4api 1.0.0__tar.gz → 1.1.2__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.
- pmt4api-1.1.2/CHANGELOG.md +63 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/PKG-INFO +43 -15
- {pmt4api-1.0.0 → pmt4api-1.1.2}/README.md +42 -14
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/__init__.py +1 -1
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/__main__.py +4 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/_base.py +6 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/_coverage.py +1 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/_events.py +2 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/_risk.py +14 -2
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/_runtime.py +24 -2
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/_session.py +2 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/_streams.py +39 -6
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/_trading.py +5 -3
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/_util.py +5 -3
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/bars.py +7 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/client.py +7 -4
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/docs/API.md +83 -11
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/docs/FROM-CSHARP.md +1 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/errors.py +13 -1
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/models.py +78 -2
- pmt4api-1.0.0/CHANGELOG.md +0 -17
- {pmt4api-1.0.0 → pmt4api-1.1.2}/LICENSE.txt +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/examples/bars.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/examples/connect.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/examples/pnl.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/examples/servers.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/examples/sizing.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/examples/stream_async.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/examples/stream_quotes.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/examples/trade_demo.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/hatch_build.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/pyproject.toml +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/_account.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/_market.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/diagnostics.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/frames.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/py.typed +0 -0
- {pmt4api-1.0.0 → pmt4api-1.1.2}/src/pmt4api/servers.py +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Changelog — pmt4api
|
|
2
|
+
|
|
3
|
+
## [1.1.2] — 2026-10-15
|
|
4
|
+
|
|
5
|
+
From the delta re-test of 1.1.1 (no S1 or S2). Python layer only: the cores are unchanged.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- `Order` money fields (profit, swap, commission) carry no binary noise.
|
|
9
|
+
- README: a connect to an unreachable licence service waits several times `token_timeout` (about 44 s at the default 10: the retry, and each server of the list).
|
|
10
|
+
|
|
11
|
+
## [1.1.1] — 2026-10-14
|
|
12
|
+
|
|
13
|
+
From the Windows re-test of 1.1.0 (13 notes, one S2). Bundles wmt4api 3.3.1.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- A licence request no longer parks its connection (core `wmt4api 3.3.1`): repeated `history()` calls held about four
|
|
17
|
+
handles each until the next garbage collection.
|
|
18
|
+
- A missing .NET runtime on a **32-bit** Python says that 32-bit Python needs the 32-bit runtime.
|
|
19
|
+
- A quote-history timeout says how long it waited.
|
|
20
|
+
- README: how to read why a position closed (`[sl]` / `[tp]` in the comment), Ctrl+C during `connect()`, the licence
|
|
21
|
+
request retry.
|
|
22
|
+
|
|
23
|
+
## [1.1.0] — 2026-10-13
|
|
24
|
+
|
|
25
|
+
Found by a customer-style test on Windows Server 2022 / Python 3.10 (28 findings); every one is addressed. Bundles
|
|
26
|
+
wmt4api 3.3.0. Additive: nothing that worked in 1.0.0 is removed.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **Two packages could not start the .NET runtime from two threads at once** (`RuntimeNotFoundError` in one of them,
|
|
30
|
+
every time, on Linux as well). One lock now covers every `pmt*api` package in the process.
|
|
31
|
+
- **`bars()` re-aggregated the whole series after every block** (MT5): a deep request was quadratic and timed out
|
|
32
|
+
(`D1` × 1000 took over two minutes of CPU). It counts as the blocks arrive and aggregates once; the default budget
|
|
33
|
+
is `bars_timeout` plus a second per two weeks of one-minute bars the request needs.
|
|
34
|
+
- **A closed client left a thread behind** for every client that had used callbacks or `quotes()`: the callback
|
|
35
|
+
thread now ends at `disconnect()` (and when the client is dropped) and returns with the next event.
|
|
36
|
+
- **Type checkers saw every method as `(*Any, **Any) -> Any`**: the decorator that translates errors now keeps the
|
|
37
|
+
signature.
|
|
38
|
+
- Messages: a missing .NET runtime names the runtime and where to get it; the no-key text speaks `api_key=`; a bars
|
|
39
|
+
timeout states its real wait; a trade refusal names its code.
|
|
40
|
+
- `python -m pmt4api --connect` / `pmt5api --connect` accept `host:port` in `PMT*_HOST`.
|
|
41
|
+
- The reference gains a meaning for every field of the main data types.
|
|
42
|
+
- `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`).
|
|
43
|
+
- The core fixes: see wmt4api 3.3.0 — **the account balance and credit now follow closes, deposits and withdrawals in the session** (they stayed at their value from connect).
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- `lots_for_risk(..., side="buy" | "sell")`: a stop on the wrong side of the entry is refused instead of being sized by its distance.
|
|
47
|
+
- `reconnect_max_backoff` (seconds, default 30): the longest delay between reconnect attempts.
|
|
48
|
+
|
|
49
|
+
## [1.0.0] — 2026-10-12
|
|
50
|
+
First release: the complete Python interface to wmt4api 3.2.0 (the MetaTrader 4 client library).
|
|
51
|
+
|
|
52
|
+
- Every public member and type of the C# `MT4API` / `OrderClient` classes is mapped to a Python counterpart, or has a
|
|
53
|
+
stated reason for not having one (`pmt4api.COVERAGE`, `pmt4api.TYPES`; a test fails when the core gains an
|
|
54
|
+
unaccounted member).
|
|
55
|
+
- Market data (symbols, quotes, bars on the nine MT4 timeframes with paging), account, sizing and risk, order entry
|
|
56
|
+
(market, pending, modify, partial close, close-by, close-all-by, cancel; synchronous, with request ids and progress
|
|
57
|
+
events, and awaitable), history, summary and equity curve.
|
|
58
|
+
- Callbacks, sync and async iterators, one ordered dispatcher thread per client, with back-pressure accounting.
|
|
59
|
+
- A small error hierarchy; the MT4 trade-return codes by name; requotes carry the new prices.
|
|
60
|
+
- `auto_reconnect`, `servers=` failover, a silence watchdog, `reconnect()`; `disconnect()` is final.
|
|
61
|
+
- Refusals before anything is sent: a magic number the core cannot transmit, a volume the wire cannot carry, a
|
|
62
|
+
comment that would not read back.
|
|
63
|
+
- Wheels: Linux x86-64 with the core and a .NET runtime bundled, and a pure wheel that carries the core.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pmt4api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.2
|
|
4
4
|
Summary: Complete Python interface to the wmt4api MetaTrader 4 client library (market data, account, sizing, order entry, events).
|
|
5
5
|
Project-URL: Homepage, https://mtapi.one
|
|
6
6
|
Author: Sen Trading
|
|
@@ -39,19 +39,33 @@ both reads alike.
|
|
|
39
39
|
|
|
40
40
|
**Parity with the C# library is measured, not claimed:** `pmt4api.COVERAGE` maps every public member of the C#
|
|
41
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.
|
|
43
|
-
that table;
|
|
42
|
+
if the core gains a member that is not accounted for. `pmt4api/docs/FROM-CSHARP.md` (inside the installed package) is
|
|
43
|
+
that table; `pmt4api/docs/API.md` (inside the installed package; `python -m pmt4api` prints its folder) is the reference.
|
|
44
44
|
|
|
45
45
|
## Install
|
|
46
46
|
|
|
47
|
-
The **Linux x86-64 wheel** carries everything — the core and a .NET runtime — so there is nothing else to install:
|
|
48
|
-
|
|
49
47
|
```
|
|
50
|
-
pip install pmt4api
|
|
48
|
+
pip install pmt4api
|
|
51
49
|
```
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
the
|
|
51
|
+
pip chooses the wheel for your platform. The **Linux x86-64 wheel** carries everything — the core and a .NET runtime —
|
|
52
|
+
so there is nothing else to install. On any other platform the `py3-none-any` wheel carries the core and needs a .NET 8
|
|
53
|
+
(or later) runtime on the machine (`dotnet --list-runtimes` should list `Microsoft.NETCore.App`). From a downloaded
|
|
54
|
+
file: `pip install pmt4api-<version>-py3-none-<platform>.whl`.
|
|
55
|
+
|
|
56
|
+
**Windows (x64).** Install the .NET 8 runtime once (`winget install Microsoft.DotNet.Runtime.8`, or the installer from
|
|
57
|
+
https://dotnet.microsoft.com/download/dotnet/8.0), then `pip install pmt4api`. Check it with `python -m pmt4api`: it prints
|
|
58
|
+
the versions, finds the runtime and constructs a client offline. Set the key in the shell that runs your program —
|
|
59
|
+
PowerShell: `$env:WMT4API_KEY = "wk_..."`, cmd: `set WMT4API_KEY=wk_...`. A string the broker sent can hold characters your
|
|
60
|
+
console's code page cannot print: set `PYTHONUTF8=1` (or run `chcp 65001`) to avoid a `UnicodeEncodeError`.
|
|
61
|
+
|
|
62
|
+
**Windows (x64).** Use the `py3-none-any` wheel from PyPI — `pip install pmt4api` picks it — and install the .NET 8
|
|
63
|
+
runtime once (`winget install Microsoft.DotNet.Runtime.8`, or the installer from
|
|
64
|
+
https://dotnet.microsoft.com/download/dotnet/8.0); `dotnet --list-runtimes` must list `Microsoft.NETCore.App 8.x` or
|
|
65
|
+
later. Check the install with `python -m pmt4api`: it prints the versions, finds the runtime and constructs a client
|
|
66
|
+
offline. Set the key in the shell that runs your program — PowerShell: `$env:WMT4API_KEY = "wk_..."`, cmd:
|
|
67
|
+
`set WMT4API_KEY=wk_...`. A library string can hold characters your console code page cannot print: set
|
|
68
|
+
`PYTHONUTF8=1` (or run `chcp 65001`) to avoid a `UnicodeEncodeError` when you print what the broker sent.
|
|
55
69
|
|
|
56
70
|
You also need your **API key** (`wk_…`, the same key you use with wmt4api):
|
|
57
71
|
|
|
@@ -104,6 +118,10 @@ Timeframes: `M1 M5 M15 M30 H1 H4 D1 W1 MN1` — the nine the MT4 server serves;
|
|
|
104
118
|
(M1 is typically a few days), so a long request returns what exists; one request carries at most 32 767 bars and
|
|
105
119
|
longer requests page backwards for you. `volume` is the tick volume.
|
|
106
120
|
|
|
121
|
+
**MT4 and MT5 differ here.** The MT4 package returns completed bars only and excludes a bar that opens at `end=`; on
|
|
122
|
+
MT5 `end=` includes the bar stamped exactly `end` and the bar still forming. Code that runs on both has to drop the
|
|
123
|
+
forming MT5 bar itself.
|
|
124
|
+
|
|
107
125
|
### Live quotes
|
|
108
126
|
|
|
109
127
|
```python
|
|
@@ -146,14 +164,16 @@ drop the oldest item when the consumer falls behind, and detach on `close()` / `
|
|
|
146
164
|
## Trading
|
|
147
165
|
|
|
148
166
|
```python
|
|
167
|
+
from datetime import timedelta
|
|
168
|
+
|
|
149
169
|
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
|
|
151
|
-
c.close(o.ticket, 0.05) # close half; close(ticket) closes it all
|
|
170
|
+
c.modify(o.ticket, sl=1.0970) # change stops (returns the order as the server holds it)
|
|
171
|
+
c.close(o.ticket, 0.05) # close half (returns the closing record); close(ticket) closes it all
|
|
152
172
|
|
|
153
173
|
p = c.place_order("EURUSDm", "buy_stop", 0.10, 1.1200, sl=1.1150,
|
|
154
174
|
expiration=timedelta(days=2)) # also buy_limit sell_limit sell_stop; a datetime works too
|
|
155
175
|
c.modify(p.ticket, price=1.1210) # a pending order's price; its expiration is kept
|
|
156
|
-
c.cancel(p.ticket)
|
|
176
|
+
c.cancel(p.ticket) # returns the order that was deleted
|
|
157
177
|
|
|
158
178
|
c.close_by(a.ticket, b.ticket) # close one position against an opposite one (hedged accounts)
|
|
159
179
|
c.close_all_by("EURUSDm") # ... every opposite pair of the symbol
|
|
@@ -192,7 +212,7 @@ remainder of the position may carry a new ticket: read `positions()`.
|
|
|
192
212
|
## Sizing and risk
|
|
193
213
|
|
|
194
214
|
```python
|
|
195
|
-
r = c.lots_for_risk("EURUSDm", entry_price=1.1000, stop_price=1.0950, risk_amount=50.0)
|
|
215
|
+
r = c.lots_for_risk("EURUSDm", entry_price=1.1000, stop_price=1.0950, risk_amount=50.0, side="buy")
|
|
196
216
|
for w in r.warnings:
|
|
197
217
|
print("warning:", w) # ok can still be True with a caveat (the broker's minimum volume risks more)
|
|
198
218
|
if r.ok:
|
|
@@ -251,8 +271,8 @@ restored, cached snapshots are not. `disconnect()` is final: it also stops a rec
|
|
|
251
271
|
| API key | `api_key=` | `WMT4API_KEY` (then `WMTAPI_KEY`) |
|
|
252
272
|
| Licence service URL (leave at its default) | `token_url=` | `WMT4API_TOKEN_URL` |
|
|
253
273
|
| 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
|
|
255
|
-
| Reconnect | `auto_reconnect`, `reconnect_backoff`, `reconnect_max_attempts
|
|
274
|
+
| Timeouts (seconds, each > 0) | `connect_timeout` (one attempt on one server; default 6), `token_timeout` (10), `trade_timeout` (30; ≥ 1), `quote_timeout` (5) / `history_timeout` (30) / `bars_timeout` (30) — the defaults of `quote`, `history`, `bars`, `no_message_timeout` (30; ≥ 10, or 0 for off) | — |
|
|
275
|
+
| Reconnect | `auto_reconnect`, `reconnect_backoff` (3, doubling up to `reconnect_max_backoff`, 30), `reconnect_max_attempts` (0 = no limit), `servers=` | — |
|
|
256
276
|
| Orders | `placed_type=` (`manual`, `expert`, `dealer` …), `comment_encoding=` (default `latin-1`) | — |
|
|
257
277
|
| .NET runtime location | — | `DOTNET_ROOT` |
|
|
258
278
|
|
|
@@ -272,7 +292,15 @@ pmt4api.enable_core_logging() # the core's own messages -> logging.getL
|
|
|
272
292
|
|
|
273
293
|
`load_servers_ini(path_or_bytes)` / `save_servers_ini(records)` read and write a terminal's `servers.ini`.
|
|
274
294
|
`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
|
|
295
|
+
`PMT4_LOGIN`, `PMT4_PASSWORD`, `PMT4_HOST` — a bare host, or `host:port`, or the port in `PMT4_PORT`); the output holds
|
|
296
|
+
no secrets.
|
|
297
|
+
|
|
298
|
+
## What a closed order says about why
|
|
299
|
+
|
|
300
|
+
An MT4 server pushes the same `PositionClose` update for a stop-loss, a take-profit and a close you asked for; the
|
|
301
|
+
reason is in the order's `comment`, which the server suffixes with `[sl]` or `[tp]` for a stop or a take-profit hit.
|
|
302
|
+
Pressing Ctrl+C while `connect()` is dialling is honoured when the dial ends (up to `connect_timeout`), not at once.
|
|
303
|
+
`token_timeout` bounds one licence request; a failed request is tried again and a connect tries each server of the list, so a connect to an unreachable licence service can wait several times that (about 44 s at the default 10).
|
|
276
304
|
|
|
277
305
|
## Errors
|
|
278
306
|
|
|
@@ -12,19 +12,33 @@ both reads alike.
|
|
|
12
12
|
|
|
13
13
|
**Parity with the C# library is measured, not claimed:** `pmt4api.COVERAGE` maps every public member of the C#
|
|
14
14
|
`MT4API` and `OrderClient` classes to its Python counterpart (or states why it has none), and the test suite fails
|
|
15
|
-
if the core gains a member that is not accounted for.
|
|
16
|
-
that table;
|
|
15
|
+
if the core gains a member that is not accounted for. `pmt4api/docs/FROM-CSHARP.md` (inside the installed package) is
|
|
16
|
+
that table; `pmt4api/docs/API.md` (inside the installed package; `python -m pmt4api` prints its folder) is the reference.
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
20
|
-
The **Linux x86-64 wheel** carries everything — the core and a .NET runtime — so there is nothing else to install:
|
|
21
|
-
|
|
22
20
|
```
|
|
23
|
-
pip install pmt4api
|
|
21
|
+
pip install pmt4api
|
|
24
22
|
```
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
the
|
|
24
|
+
pip chooses the wheel for your platform. The **Linux x86-64 wheel** carries everything — the core and a .NET runtime —
|
|
25
|
+
so there is nothing else to install. On any other platform the `py3-none-any` wheel carries the core and needs a .NET 8
|
|
26
|
+
(or later) runtime on the machine (`dotnet --list-runtimes` should list `Microsoft.NETCore.App`). From a downloaded
|
|
27
|
+
file: `pip install pmt4api-<version>-py3-none-<platform>.whl`.
|
|
28
|
+
|
|
29
|
+
**Windows (x64).** Install the .NET 8 runtime once (`winget install Microsoft.DotNet.Runtime.8`, or the installer from
|
|
30
|
+
https://dotnet.microsoft.com/download/dotnet/8.0), then `pip install pmt4api`. Check it with `python -m pmt4api`: it prints
|
|
31
|
+
the versions, finds the runtime and constructs a client offline. Set the key in the shell that runs your program —
|
|
32
|
+
PowerShell: `$env:WMT4API_KEY = "wk_..."`, cmd: `set WMT4API_KEY=wk_...`. A string the broker sent can hold characters your
|
|
33
|
+
console's code page cannot print: set `PYTHONUTF8=1` (or run `chcp 65001`) to avoid a `UnicodeEncodeError`.
|
|
34
|
+
|
|
35
|
+
**Windows (x64).** Use the `py3-none-any` wheel from PyPI — `pip install pmt4api` picks it — and install the .NET 8
|
|
36
|
+
runtime once (`winget install Microsoft.DotNet.Runtime.8`, or the installer from
|
|
37
|
+
https://dotnet.microsoft.com/download/dotnet/8.0); `dotnet --list-runtimes` must list `Microsoft.NETCore.App 8.x` or
|
|
38
|
+
later. Check the install with `python -m pmt4api`: it prints the versions, finds the runtime and constructs a client
|
|
39
|
+
offline. Set the key in the shell that runs your program — PowerShell: `$env:WMT4API_KEY = "wk_..."`, cmd:
|
|
40
|
+
`set WMT4API_KEY=wk_...`. A library string can hold characters your console code page cannot print: set
|
|
41
|
+
`PYTHONUTF8=1` (or run `chcp 65001`) to avoid a `UnicodeEncodeError` when you print what the broker sent.
|
|
28
42
|
|
|
29
43
|
You also need your **API key** (`wk_…`, the same key you use with wmt4api):
|
|
30
44
|
|
|
@@ -77,6 +91,10 @@ Timeframes: `M1 M5 M15 M30 H1 H4 D1 W1 MN1` — the nine the MT4 server serves;
|
|
|
77
91
|
(M1 is typically a few days), so a long request returns what exists; one request carries at most 32 767 bars and
|
|
78
92
|
longer requests page backwards for you. `volume` is the tick volume.
|
|
79
93
|
|
|
94
|
+
**MT4 and MT5 differ here.** The MT4 package returns completed bars only and excludes a bar that opens at `end=`; on
|
|
95
|
+
MT5 `end=` includes the bar stamped exactly `end` and the bar still forming. Code that runs on both has to drop the
|
|
96
|
+
forming MT5 bar itself.
|
|
97
|
+
|
|
80
98
|
### Live quotes
|
|
81
99
|
|
|
82
100
|
```python
|
|
@@ -119,14 +137,16 @@ drop the oldest item when the consumer falls behind, and detach on `close()` / `
|
|
|
119
137
|
## Trading
|
|
120
138
|
|
|
121
139
|
```python
|
|
140
|
+
from datetime import timedelta
|
|
141
|
+
|
|
122
142
|
o = c.buy("EURUSDm", 0.10, sl=1.0950, tp=1.1100, comment="bot-1") # market order at the ask; sell() at the bid
|
|
123
|
-
c.modify(o.ticket, sl=1.0970) # change stops
|
|
124
|
-
c.close(o.ticket, 0.05) # close half; close(ticket) closes it all
|
|
143
|
+
c.modify(o.ticket, sl=1.0970) # change stops (returns the order as the server holds it)
|
|
144
|
+
c.close(o.ticket, 0.05) # close half (returns the closing record); close(ticket) closes it all
|
|
125
145
|
|
|
126
146
|
p = c.place_order("EURUSDm", "buy_stop", 0.10, 1.1200, sl=1.1150,
|
|
127
147
|
expiration=timedelta(days=2)) # also buy_limit sell_limit sell_stop; a datetime works too
|
|
128
148
|
c.modify(p.ticket, price=1.1210) # a pending order's price; its expiration is kept
|
|
129
|
-
c.cancel(p.ticket)
|
|
149
|
+
c.cancel(p.ticket) # returns the order that was deleted
|
|
130
150
|
|
|
131
151
|
c.close_by(a.ticket, b.ticket) # close one position against an opposite one (hedged accounts)
|
|
132
152
|
c.close_all_by("EURUSDm") # ... every opposite pair of the symbol
|
|
@@ -165,7 +185,7 @@ remainder of the position may carry a new ticket: read `positions()`.
|
|
|
165
185
|
## Sizing and risk
|
|
166
186
|
|
|
167
187
|
```python
|
|
168
|
-
r = c.lots_for_risk("EURUSDm", entry_price=1.1000, stop_price=1.0950, risk_amount=50.0)
|
|
188
|
+
r = c.lots_for_risk("EURUSDm", entry_price=1.1000, stop_price=1.0950, risk_amount=50.0, side="buy")
|
|
169
189
|
for w in r.warnings:
|
|
170
190
|
print("warning:", w) # ok can still be True with a caveat (the broker's minimum volume risks more)
|
|
171
191
|
if r.ok:
|
|
@@ -224,8 +244,8 @@ restored, cached snapshots are not. `disconnect()` is final: it also stops a rec
|
|
|
224
244
|
| API key | `api_key=` | `WMT4API_KEY` (then `WMTAPI_KEY`) |
|
|
225
245
|
| Licence service URL (leave at its default) | `token_url=` | `WMT4API_TOKEN_URL` |
|
|
226
246
|
| Core library (only if you do not use a wheel that carries it) | `dll_path=` | `WMT4API_DLL` |
|
|
227
|
-
| Timeouts (seconds, each > 0) | `connect_timeout` (one attempt on one server), `token_timeout
|
|
228
|
-
| Reconnect | `auto_reconnect`, `reconnect_backoff`, `reconnect_max_attempts
|
|
247
|
+
| Timeouts (seconds, each > 0) | `connect_timeout` (one attempt on one server; default 6), `token_timeout` (10), `trade_timeout` (30; ≥ 1), `quote_timeout` (5) / `history_timeout` (30) / `bars_timeout` (30) — the defaults of `quote`, `history`, `bars`, `no_message_timeout` (30; ≥ 10, or 0 for off) | — |
|
|
248
|
+
| Reconnect | `auto_reconnect`, `reconnect_backoff` (3, doubling up to `reconnect_max_backoff`, 30), `reconnect_max_attempts` (0 = no limit), `servers=` | — |
|
|
229
249
|
| Orders | `placed_type=` (`manual`, `expert`, `dealer` …), `comment_encoding=` (default `latin-1`) | — |
|
|
230
250
|
| .NET runtime location | — | `DOTNET_ROOT` |
|
|
231
251
|
|
|
@@ -245,7 +265,15 @@ pmt4api.enable_core_logging() # the core's own messages -> logging.getL
|
|
|
245
265
|
|
|
246
266
|
`load_servers_ini(path_or_bytes)` / `save_servers_ini(records)` read and write a terminal's `servers.ini`.
|
|
247
267
|
`python -m pmt4api` prints what is installed and whether it works here (`--connect` also reads an account, using
|
|
248
|
-
`PMT4_LOGIN`, `PMT4_PASSWORD`, `PMT4_HOST`); the output holds
|
|
268
|
+
`PMT4_LOGIN`, `PMT4_PASSWORD`, `PMT4_HOST` — a bare host, or `host:port`, or the port in `PMT4_PORT`); the output holds
|
|
269
|
+
no secrets.
|
|
270
|
+
|
|
271
|
+
## What a closed order says about why
|
|
272
|
+
|
|
273
|
+
An MT4 server pushes the same `PositionClose` update for a stop-loss, a take-profit and a close you asked for; the
|
|
274
|
+
reason is in the order's `comment`, which the server suffixes with `[sl]` or `[tp]` for a stop or a take-profit hit.
|
|
275
|
+
Pressing Ctrl+C while `connect()` is dialling is honoured when the dial ends (up to `connect_timeout`), not at once.
|
|
276
|
+
`token_timeout` bounds one licence request; a failed request is tried again and a connect tries each server of the list, so a connect to an unreachable licence service can wait several times that (about 44 s at the default 10).
|
|
249
277
|
|
|
250
278
|
## Errors
|
|
251
279
|
|
|
@@ -17,7 +17,7 @@ from .models import (AccountInfo, AccountSummary, ConnectEvent, EquityPoint, Lot
|
|
|
17
17
|
OrderUpdate, Quote, ServerRecord, SymbolGroup, SymbolInfo)
|
|
18
18
|
from .servers import load_servers_ini, save_servers_ini
|
|
19
19
|
|
|
20
|
-
__version__ = "1.
|
|
20
|
+
__version__ = "1.1.2"
|
|
21
21
|
|
|
22
22
|
__all__ = [
|
|
23
23
|
"Client",
|
|
@@ -62,6 +62,10 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
62
62
|
print("connect SKIPPED: environment variable %s is not set" % e)
|
|
63
63
|
return 0
|
|
64
64
|
port = int(os.environ.get("PMT4_PORT", "443"))
|
|
65
|
+
if ":" in host: # an account file lists "host:port"
|
|
66
|
+
host, _, given = host.rpartition(":")
|
|
67
|
+
if given.isdigit():
|
|
68
|
+
port = int(given)
|
|
65
69
|
try:
|
|
66
70
|
with Client(login, password, host, port, connect_timeout=45) as live:
|
|
67
71
|
a = live.account()
|
|
@@ -11,6 +11,12 @@ class _Base:
|
|
|
11
11
|
_api: Any # the core's client object (MT5API / MT4API)
|
|
12
12
|
_core: Any # the core's namespace (types and enums)
|
|
13
13
|
|
|
14
|
+
def _stop_events(self) -> None:
|
|
15
|
+
"""End the callback thread (it restarts by itself with the next event): a closed client keeps no thread."""
|
|
16
|
+
d = getattr(self, "_dispatch", None)
|
|
17
|
+
if d is not None:
|
|
18
|
+
d.stop()
|
|
19
|
+
|
|
14
20
|
def _require_connected(self) -> None:
|
|
15
21
|
if not self._api.Connected:
|
|
16
22
|
raise NotConnectedError("not connected; call connect() first")
|
|
@@ -47,6 +47,7 @@ COVERAGE: Dict[str, str] = {
|
|
|
47
47
|
"TradeTimeout": "Client(trade_timeout)",
|
|
48
48
|
"AutoReconnect": "Client(auto_reconnect)",
|
|
49
49
|
"ReconnectBackoffMs": "Client(reconnect_backoff)",
|
|
50
|
+
"ReconnectMaxBackoffMs": "Client(reconnect_max_backoff)",
|
|
50
51
|
"ReconnectMaxAttempts": "Client(reconnect_max_attempts)",
|
|
51
52
|
"Encoding": "Client(comment_encoding)",
|
|
52
53
|
"PlacedType": "Client(placed_type)",
|
|
@@ -7,6 +7,7 @@ to a callback that fell too far behind.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
|
+
import weakref
|
|
10
11
|
from typing import Any, AsyncIterator, Callable, Iterator, Optional
|
|
11
12
|
|
|
12
13
|
from ._base import _Base
|
|
@@ -59,6 +60,7 @@ class _EventsMixin(_Base):
|
|
|
59
60
|
d = getattr(self, "_dispatch", None)
|
|
60
61
|
if d is None:
|
|
61
62
|
d = self._dispatch = _Dispatcher()
|
|
63
|
+
weakref.finalize(self, d.stop) # a client that is dropped without disconnect() leaves no thread
|
|
62
64
|
return d
|
|
63
65
|
|
|
64
66
|
def _wanted(self, symbols: Any) -> Any:
|
|
@@ -15,16 +15,28 @@ class _RiskMixin(_Base):
|
|
|
15
15
|
def lots_for_risk(self, symbol: str, entry_price: float, stop_price: float, risk_amount: float, *,
|
|
16
16
|
lot_step: Optional[float] = None, min_lot: Optional[float] = None,
|
|
17
17
|
max_ratio: Optional[float] = None, max_equity_share: Optional[float] = None,
|
|
18
|
-
max_lot: Optional[float] = None) -> LotSizing:
|
|
18
|
+
max_lot: Optional[float] = None, side: Optional[str] = None) -> LotSizing:
|
|
19
19
|
"""The volume that loses ``risk_amount`` (account currency) if ``stop_price`` is hit, with the evidence.
|
|
20
20
|
|
|
21
21
|
The value of a price unit comes from :meth:`tick_value` and the broker's step / minimum / maximum volume from
|
|
22
22
|
the account's own group (pass one to override it); a size that cannot be justified is refused, not guessed.
|
|
23
23
|
The volume is rounded **down** to the step. Check ``ok``: when false, ``lots`` is 0 and ``reason`` says which
|
|
24
|
-
check failed.
|
|
24
|
+
check failed. Give ``side`` (``"buy"`` or ``"sell"``) and a stop on the wrong side of the entry is refused instead
|
|
25
|
+
of sized by its distance. Amounts are in the currency the account reports (no cent-account scaling is applied: check the
|
|
25
26
|
first result against your terminal).
|
|
26
27
|
"""
|
|
27
28
|
self._require_connected()
|
|
29
|
+
if side is not None:
|
|
30
|
+
key = str(side).strip().lower()
|
|
31
|
+
if key not in ("buy", "sell"):
|
|
32
|
+
raise ValueError("side must be 'buy' or 'sell'")
|
|
33
|
+
wrong = (stop_price >= entry_price) if key == "buy" else (stop_price <= entry_price)
|
|
34
|
+
if wrong: # a stop on the wrong side of the entry is not a stop
|
|
35
|
+
return LotSizing(ok=False, lots=0.0, expected_loss_at_stop=0.0, risk_requested=float(risk_amount),
|
|
36
|
+
minimum_trade_cost=0.0, ratio=0.0,
|
|
37
|
+
reason="a %s needs its stop %s the entry price (entry %g, stop %g)"
|
|
38
|
+
% (key, "below" if key == "buy" else "above", entry_price, stop_price),
|
|
39
|
+
warnings=())
|
|
28
40
|
kwargs = {}
|
|
29
41
|
for key, value in (("lotStep", lot_step), ("minLot", min_lot), ("maxRatio", max_ratio),
|
|
30
42
|
("maxEquityShare", max_equity_share), ("maxLot", max_lot)):
|
|
@@ -5,6 +5,7 @@ import glob
|
|
|
5
5
|
import importlib
|
|
6
6
|
import os
|
|
7
7
|
import shutil
|
|
8
|
+
import struct
|
|
8
9
|
import sys
|
|
9
10
|
import threading
|
|
10
11
|
from pathlib import Path
|
|
@@ -17,8 +18,25 @@ ENV_DLL = "WMT4API_DLL"
|
|
|
17
18
|
|
|
18
19
|
_NAMESPACE = "wmt4api" # the .NET namespace of the core; NOT the name of this Python package
|
|
19
20
|
_PKG = Path(__file__).resolve().parent
|
|
21
|
+
_BITNESS_HINT = (" This is a 32-bit Python: it needs the 32-bit (x86) .NET runtime, and 64-bit Python with the 64-bit "
|
|
22
|
+
"runtime is the supported combination.")
|
|
20
23
|
_BUNDLED_RUNTIME = _PKG / "_dotnet" # present in the platform wheels: host/fxr + shared/Microsoft.NETCore.App
|
|
21
|
-
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _process_lock() -> "threading.RLock":
|
|
27
|
+
"""ONE lock for every pmt*api package in the process. Each package has its own copy of this module, but the .NET
|
|
28
|
+
runtime is one per process: two packages starting it from two threads at once raced inside pythonnet. The lock
|
|
29
|
+
lives in ``sys.modules`` under a fixed name, which both copies see."""
|
|
30
|
+
holder = sys.modules.get("_pmt_runtime_lock")
|
|
31
|
+
if holder is None:
|
|
32
|
+
import types
|
|
33
|
+
fresh = types.ModuleType("_pmt_runtime_lock")
|
|
34
|
+
fresh.lock = threading.RLock() # type: ignore[attr-defined]
|
|
35
|
+
holder = sys.modules.setdefault("_pmt_runtime_lock", fresh)
|
|
36
|
+
return holder.lock # type: ignore[attr-defined]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_lock = _process_lock()
|
|
22
40
|
_core = None
|
|
23
41
|
_dll: Optional[Path] = None
|
|
24
42
|
|
|
@@ -137,7 +155,11 @@ def load(dll_path=None):
|
|
|
137
155
|
_load("coreclr")
|
|
138
156
|
import clr # noqa: F401 (provided by pythonnet)
|
|
139
157
|
except Exception as e: # noqa: BLE001 — anything here means the runtime could not start
|
|
140
|
-
raise RuntimeNotFoundError(
|
|
158
|
+
raise RuntimeNotFoundError(
|
|
159
|
+
"could not start the .NET runtime. The core needs the .NET 8 (or later) runtime, Microsoft.NETCore.App: "
|
|
160
|
+
"install it from https://dotnet.microsoft.com/download/dotnet/8.0 (`dotnet --list-runtimes` lists what "
|
|
161
|
+
"this machine has) or point DOTNET_ROOT at a folder that holds it.%s Detail: %s: %s"
|
|
162
|
+
% (_BITNESS_HINT if struct.calcsize("P") == 4 else "", type(e).__name__, str(e)[:240])) from e
|
|
141
163
|
import clr as _clr
|
|
142
164
|
from System.Reflection import Assembly # available once the runtime is up
|
|
143
165
|
try:
|
|
@@ -53,6 +53,8 @@ class _SessionMixin(_Base):
|
|
|
53
53
|
except Exception as exc: # noqa: BLE001
|
|
54
54
|
if type(exc).__name__ != "NullReferenceException": # never connected: nothing to close
|
|
55
55
|
raise
|
|
56
|
+
finally:
|
|
57
|
+
self._stop_events() # the callback thread ends once what is queued is delivered
|
|
56
58
|
|
|
57
59
|
@property
|
|
58
60
|
def connected(self) -> bool:
|
|
@@ -26,21 +26,35 @@ log = logging.getLogger(_PACKAGE)
|
|
|
26
26
|
EVENT_QUEUE = 20000
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
_STOP = object() # put on the dispatcher's queue: end the thread once everything queued before it is delivered
|
|
30
|
+
|
|
31
|
+
|
|
29
32
|
class _Dispatcher:
|
|
30
|
-
"""One thread that runs a client's callbacks in arrival order.
|
|
33
|
+
"""One thread that runs a client's callbacks in arrival order.
|
|
34
|
+
|
|
35
|
+
The thread exists only while there is something to deliver or the client is connected: it is started by the first
|
|
36
|
+
event, ended by :meth:`stop` (a client's ``disconnect()``, or the client being garbage-collected), and started again
|
|
37
|
+
by the next event. A client that is dropped therefore leaves no thread behind."""
|
|
31
38
|
|
|
32
39
|
def __init__(self) -> None:
|
|
33
|
-
self._q: "queue.Queue[
|
|
40
|
+
self._q: "queue.Queue[Any]" = queue.Queue(maxsize=EVENT_QUEUE)
|
|
34
41
|
self.dropped = 0
|
|
35
42
|
self._count_lock = threading.Lock()
|
|
36
|
-
self.
|
|
37
|
-
self._thread.
|
|
43
|
+
self._thread_lock = threading.Lock()
|
|
44
|
+
self._thread: Optional[threading.Thread] = None
|
|
45
|
+
|
|
46
|
+
def _ensure_thread(self) -> None:
|
|
47
|
+
with self._thread_lock:
|
|
48
|
+
if self._thread is None:
|
|
49
|
+
t = threading.Thread(target=self._run, name=_PACKAGE + "-events", daemon=True)
|
|
50
|
+
self._thread = t
|
|
51
|
+
t.start()
|
|
38
52
|
|
|
39
53
|
def submit(self, fn: Callable[[Any], None], arg: Any, event: str) -> None:
|
|
40
54
|
while True:
|
|
41
55
|
try:
|
|
42
56
|
self._q.put_nowait((fn, arg, event))
|
|
43
|
-
|
|
57
|
+
break
|
|
44
58
|
except queue.Full:
|
|
45
59
|
try:
|
|
46
60
|
self._q.get_nowait()
|
|
@@ -51,10 +65,29 @@ class _Dispatcher:
|
|
|
51
65
|
total = self.dropped
|
|
52
66
|
if total == 1 or total % 1000 == 0:
|
|
53
67
|
log.warning("event queue full: %d event(s) dropped so far; a callback is too slow", total)
|
|
68
|
+
self._ensure_thread()
|
|
69
|
+
|
|
70
|
+
def stop(self) -> None:
|
|
71
|
+
"""End the thread after it has delivered what is queued. Safe to call at any time and more than once; events
|
|
72
|
+
that arrive afterwards start a new thread."""
|
|
73
|
+
with self._thread_lock:
|
|
74
|
+
running = self._thread is not None
|
|
75
|
+
if running:
|
|
76
|
+
try:
|
|
77
|
+
self._q.put_nowait(_STOP)
|
|
78
|
+
except queue.Full: # a full queue means the thread is busy: it ends when it next finds the queue empty
|
|
79
|
+
pass
|
|
54
80
|
|
|
55
81
|
def _run(self) -> None:
|
|
56
82
|
while True:
|
|
57
|
-
|
|
83
|
+
item = self._q.get()
|
|
84
|
+
if item is _STOP:
|
|
85
|
+
with self._thread_lock:
|
|
86
|
+
if self._q.empty(): # nothing arrived behind the marker: the thread can end
|
|
87
|
+
self._thread = None
|
|
88
|
+
return
|
|
89
|
+
continue # events were queued after stop(): keep going, they need delivering
|
|
90
|
+
fn, arg, event = item
|
|
58
91
|
try:
|
|
59
92
|
fn(arg)
|
|
60
93
|
except Exception: # noqa: BLE001 — keep the dispatcher alive
|
|
@@ -243,11 +243,13 @@ class _TradingMixin(_Base):
|
|
|
243
243
|
return self._confirmed(self._api.OrderClose(str(o.Symbol), int(o.Ticket), amount, px, int(slippage)))
|
|
244
244
|
|
|
245
245
|
@translated
|
|
246
|
-
def cancel(self, ticket: int) ->
|
|
247
|
-
"""Delete a pending order."""
|
|
246
|
+
def cancel(self, ticket: int) -> Order:
|
|
247
|
+
"""Delete a pending order. Returns the order that was deleted, as it stood."""
|
|
248
248
|
self._require_connected()
|
|
249
249
|
o = self._open_pending(ticket)
|
|
250
|
+
gone = Order._from_core(o)
|
|
250
251
|
self._api.OrderCancel(int(o.Ticket), o.Type, str(o.Symbol), float(o.Lots), float(o.OpenPrice))
|
|
252
|
+
return gone
|
|
251
253
|
|
|
252
254
|
@translated
|
|
253
255
|
def close_by(self, ticket: int, opposite_ticket: int) -> None:
|
|
@@ -371,7 +373,7 @@ class _TradingMixin(_Base):
|
|
|
371
373
|
"""Awaitable :meth:`close`."""
|
|
372
374
|
return await self._run(self.close, *args, **kwargs)
|
|
373
375
|
|
|
374
|
-
async def acancel(self, *args: Any, **kwargs: Any) ->
|
|
376
|
+
async def acancel(self, *args: Any, **kwargs: Any) -> Order:
|
|
375
377
|
"""Awaitable :meth:`cancel`."""
|
|
376
378
|
return await self._run(self.cancel, *args, **kwargs)
|
|
377
379
|
|
|
@@ -4,12 +4,14 @@ from __future__ import annotations
|
|
|
4
4
|
import functools
|
|
5
5
|
import math
|
|
6
6
|
from datetime import datetime
|
|
7
|
-
from typing import Any, Callable, Optional
|
|
7
|
+
from typing import Any, Callable, Optional, TypeVar, cast
|
|
8
8
|
|
|
9
9
|
from .errors import WmtError, translate
|
|
10
10
|
|
|
11
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
|
|
14
|
+
def translated(fn: F) -> F:
|
|
13
15
|
"""Re-raise the core's exceptions as the pmt4api hierarchy (the core exception stays as ``__cause__``)."""
|
|
14
16
|
@functools.wraps(fn)
|
|
15
17
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
@@ -22,7 +24,7 @@ def translated(fn: Callable) -> Callable:
|
|
|
22
24
|
if mapped is None:
|
|
23
25
|
raise
|
|
24
26
|
raise mapped from exc
|
|
25
|
-
return wrapper
|
|
27
|
+
return cast(F, wrapper) # the same signature, so type checkers and IDEs keep the real parameters
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
def to_core_time(value: datetime) -> Any:
|
|
@@ -60,3 +60,10 @@ class QuoteHistoryEvent:
|
|
|
60
60
|
@classmethod
|
|
61
61
|
def _from_core(cls, e: Any) -> "QuoteHistoryEvent":
|
|
62
62
|
return cls(symbol=str(e.Symbol), timeframe=str(e.Timeframe), bars=tuple(Bar._from_core(b) for b in (e.Bars or ())))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
Bar.FIELD_DOC = {
|
|
66
|
+
"time": "the start of the period, broker server time",
|
|
67
|
+
"open": "first price of the period", "high": "highest price", "low": "lowest price", "close": "last price",
|
|
68
|
+
"volume": "the number of ticks in the period (MT4 reports tick volume only)",
|
|
69
|
+
}
|
|
@@ -68,11 +68,13 @@ class Client(_SessionMixin, _MarketMixin, _AccountMixin, _RiskMixin, _TradingMix
|
|
|
68
68
|
seconds, so a longer silence means the link stopped carrying traffic). Every other timeout must be positive; the
|
|
69
69
|
quote, history and bars timeouts are the defaults of :meth:`quote`, :meth:`history` and :meth:`bars`. The
|
|
70
70
|
handshake with the broker has its own fixed 30 s limit, whatever ``connect_timeout`` says (that one bounds the
|
|
71
|
-
TCP dial of each server).
|
|
71
|
+
TCP dial of each server). **Defaults:** ``connect_timeout`` 6 s, ``token_timeout`` 10 s, ``trade_timeout`` 30 s,
|
|
72
|
+
``quote_timeout`` 5 s, ``history_timeout`` 30 s, ``bars_timeout`` 30 s, ``no_message_timeout`` 30 s,
|
|
73
|
+
``reconnect_backoff`` 3 s (it doubles up to ``reconnect_max_backoff``, 30 s).
|
|
72
74
|
|
|
73
75
|
Reconnecting: ``auto_reconnect=True`` turns on the core's watchdog (exponential backoff, failover across the
|
|
74
76
|
primary server, ``servers`` and the broker's published list, re-subscription); tune it with ``reconnect_backoff``
|
|
75
|
-
(seconds) and ``reconnect_max_attempts`` (0 = until reconnected or ``disconnect()``). Do not poll ``connected``
|
|
77
|
+
and ``reconnect_max_backoff`` (seconds) and ``reconnect_max_attempts`` (0 = until reconnected or ``disconnect()``). Do not poll ``connected``
|
|
76
78
|
and call ``connect()`` in a loop. ``servers`` are extra ``(host, port)`` access servers to fall back to.
|
|
77
79
|
|
|
78
80
|
``comment_encoding`` is the codepage order comments are written in (default ``latin-1``); ``placed_type``
|
|
@@ -88,7 +90,7 @@ class Client(_SessionMixin, _MarketMixin, _AccountMixin, _RiskMixin, _TradingMix
|
|
|
88
90
|
history_timeout: Optional[float] = None, bars_timeout: Optional[float] = None,
|
|
89
91
|
no_message_timeout: Optional[float] = None,
|
|
90
92
|
auto_reconnect: bool = False, reconnect_backoff: Optional[float] = None,
|
|
91
|
-
reconnect_max_attempts: Optional[int] = None,
|
|
93
|
+
reconnect_max_backoff: Optional[float] = None, reconnect_max_attempts: Optional[int] = None,
|
|
92
94
|
servers: Optional[Sequence[Tuple[str, int]]] = None, comment_encoding: str = "latin-1",
|
|
93
95
|
placed_type: Optional[str] = None):
|
|
94
96
|
core = _runtime.load(dll_path)
|
|
@@ -108,7 +110,8 @@ class Client(_SessionMixin, _MarketMixin, _AccountMixin, _RiskMixin, _TradingMix
|
|
|
108
110
|
api.TokenServiceUrl = token_url
|
|
109
111
|
for attr, value in (("ConnectTimeout", connect_timeout), ("TokenServiceTimeout", token_timeout),
|
|
110
112
|
("GetQuoteTimeoutMs", quote_timeout), ("DownloadOrderHistoryTimeoutMs", history_timeout),
|
|
111
|
-
("DownloadQuoteHistoryTimeout", bars_timeout), ("ReconnectBackoffMs", reconnect_backoff)
|
|
113
|
+
("DownloadQuoteHistoryTimeout", bars_timeout), ("ReconnectBackoffMs", reconnect_backoff),
|
|
114
|
+
("ReconnectMaxBackoffMs", reconnect_max_backoff)):
|
|
112
115
|
ms = seconds_to_ms(value, attr)
|
|
113
116
|
if ms is not None:
|
|
114
117
|
setattr(api, attr, ms)
|
|
@@ -5,7 +5,7 @@ Generated from the code (`tools/gen_docs.py`); do not edit by hand. Times are na
|
|
|
5
5
|
## Constructing a client
|
|
6
6
|
|
|
7
7
|
```python
|
|
8
|
-
Client(login, password, host, port=443, *, api_key=None, token_url=None, dll_path=None, connect_timeout=None, token_timeout=None, trade_timeout=None, quote_timeout=None, history_timeout=None, bars_timeout=None, no_message_timeout=None, auto_reconnect=False, reconnect_backoff=None, reconnect_max_attempts=None, servers=None, comment_encoding='latin-1', placed_type=None)
|
|
8
|
+
Client(login, password, host, port=443, *, api_key=None, token_url=None, dll_path=None, connect_timeout=None, token_timeout=None, trade_timeout=None, quote_timeout=None, history_timeout=None, bars_timeout=None, no_message_timeout=None, auto_reconnect=False, reconnect_backoff=None, reconnect_max_backoff=None, reconnect_max_attempts=None, servers=None, comment_encoding='latin-1', placed_type=None)
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
A connection to one MetaTrader 4 account through the licensed core.
|
|
@@ -24,11 +24,13 @@ declared dead and closed: default 30 s, at least 10 s, `0` switches it off; the
|
|
|
24
24
|
seconds, so a longer silence means the link stopped carrying traffic). Every other timeout must be positive; the
|
|
25
25
|
quote, history and bars timeouts are the defaults of `quote`, `history` and `bars`. The
|
|
26
26
|
handshake with the broker has its own fixed 30 s limit, whatever `connect_timeout` says (that one bounds the
|
|
27
|
-
TCP dial of each server).
|
|
27
|
+
TCP dial of each server). **Defaults:** `connect_timeout` 6 s, `token_timeout` 10 s, `trade_timeout` 30 s,
|
|
28
|
+
`quote_timeout` 5 s, `history_timeout` 30 s, `bars_timeout` 30 s, `no_message_timeout` 30 s,
|
|
29
|
+
`reconnect_backoff` 3 s (it doubles up to `reconnect_max_backoff`, 30 s).
|
|
28
30
|
|
|
29
31
|
Reconnecting: `auto_reconnect=True` turns on the core's watchdog (exponential backoff, failover across the
|
|
30
32
|
primary server, `servers` and the broker's published list, re-subscription); tune it with `reconnect_backoff`
|
|
31
|
-
(seconds) and `reconnect_max_attempts` (0 = until reconnected or `disconnect()`). Do not poll `connected`
|
|
33
|
+
and `reconnect_max_backoff` (seconds) and `reconnect_max_attempts` (0 = until reconnected or `disconnect()`). Do not poll `connected`
|
|
32
34
|
and call `connect()` in a loop. `servers` are extra `(host, port)` access servers to fall back to.
|
|
33
35
|
|
|
34
36
|
`comment_encoding` is the codepage order comments are written in (default `latin-1`); `placed_type`
|
|
@@ -336,14 +338,15 @@ Blocks up to `quote_timeout` per symbol.
|
|
|
336
338
|
|
|
337
339
|
## Sizing and risk
|
|
338
340
|
|
|
339
|
-
#### `lots_for_risk(symbol, entry_price, stop_price, risk_amount, *, lot_step=None, min_lot=None, max_ratio=None, max_equity_share=None, max_lot=None)`
|
|
341
|
+
#### `lots_for_risk(symbol, entry_price, stop_price, risk_amount, *, lot_step=None, min_lot=None, max_ratio=None, max_equity_share=None, max_lot=None, side=None)`
|
|
340
342
|
|
|
341
343
|
The volume that loses `risk_amount` (account currency) if `stop_price` is hit, with the evidence.
|
|
342
344
|
|
|
343
345
|
The value of a price unit comes from `tick_value` and the broker's step / minimum / maximum volume from
|
|
344
346
|
the account's own group (pass one to override it); a size that cannot be justified is refused, not guessed.
|
|
345
347
|
The volume is rounded **down** to the step. Check `ok`: when false, `lots` is 0 and `reason` says which
|
|
346
|
-
check failed.
|
|
348
|
+
check failed. Give `side` (`"buy"` or `"sell"`) and a stop on the wrong side of the entry is refused instead
|
|
349
|
+
of sized by its distance. Amounts are in the currency the account reports (no cent-account scaling is applied: check the
|
|
347
350
|
first result against your terminal).
|
|
348
351
|
|
|
349
352
|
#### `order_profit(symbol, open_price, close_price, lots, buy=True)`
|
|
@@ -407,7 +410,7 @@ through `on_order_progress` (and `on_order_update`).
|
|
|
407
410
|
|
|
408
411
|
#### `cancel(ticket)`
|
|
409
412
|
|
|
410
|
-
Delete a pending order.
|
|
413
|
+
Delete a pending order. Returns the order that was deleted, as it stood.
|
|
411
414
|
|
|
412
415
|
#### `cancel_async(ticket)`
|
|
413
416
|
|
|
@@ -566,13 +569,38 @@ A list of bars is indexed by its `time`; anything else keeps a plain integer ind
|
|
|
566
569
|
A price. `time` is the server time of the tick (use it to judge how old the price is); `high` and `low`
|
|
567
570
|
are the session's range once the server has sent it, and `None` before that.
|
|
568
571
|
|
|
569
|
-
|
|
572
|
+
| Field | Type | Meaning |
|
|
573
|
+
|---|---|---|
|
|
574
|
+
| `symbol` | `str` | the symbol as the broker names it |
|
|
575
|
+
| `bid` | `float` | the price a sell is made at |
|
|
576
|
+
| `ask` | `float` | the price a buy is made at |
|
|
577
|
+
| `time` | `Optional[datetime]` | the server time of the tick: how old the price is |
|
|
578
|
+
| `high` | `Optional[float]` | the session's high once the server has sent it (`None` before) |
|
|
579
|
+
| `low` | `Optional[float]` | the session's low once the server has sent it (`None` before) |
|
|
570
580
|
|
|
571
581
|
#### `SymbolInfo`
|
|
572
582
|
|
|
573
583
|
A symbol's specification and trading rules, as the server publishes them.
|
|
574
584
|
|
|
575
|
-
|
|
585
|
+
| Field | Type | Meaning |
|
|
586
|
+
|---|---|---|
|
|
587
|
+
| `name` | `str` | the symbol as the broker names it |
|
|
588
|
+
| `digits` | `int` | the price digits |
|
|
589
|
+
| `point` | `float` | the smallest price step: 10 to the power of −digits |
|
|
590
|
+
| `currency` | `str` | the instrument's own currency: the base currency of a forex pair (EUR for EURUSD); profit is realised in `profit_currency` |
|
|
591
|
+
| `profit_mode` | `str` | how profit is calculated: Forex, CFD or Futures |
|
|
592
|
+
| `contract_size` | `float` | units of the instrument per 1.0 lot |
|
|
593
|
+
| `margin_divider` | `float` | the divider of the margin formula of a CFD symbol |
|
|
594
|
+
| `tick_size` | `float` | the smallest price change the server quotes |
|
|
595
|
+
| `execution` | `str` | how orders execute: Request, Instant, Market or Exchange |
|
|
596
|
+
| `security_index` | `int` | the account group's security (symbol class) the volume rules are keyed on |
|
|
597
|
+
| `min_lots` | `float` | the smallest volume |
|
|
598
|
+
| `max_lots` | `float` | the largest volume |
|
|
599
|
+
| `lots_step` | `float` | the volume step |
|
|
600
|
+
| `code` | `int` | the feed code the server uses for the symbol |
|
|
601
|
+
| `raw_tick_value` | `float` | the server's own tick value (brokers often report 0: use `Client.tick_value`) |
|
|
602
|
+
| `raw_bid_tick_value` | `float` | the server's tick value for a sell (often 0) |
|
|
603
|
+
| `raw_ask_tick_value` | `float` | the server's tick value for a buy (often 0) |
|
|
576
604
|
|
|
577
605
|
#### `SymbolGroup`
|
|
578
606
|
|
|
@@ -587,7 +615,24 @@ are derived on the client from the live quotes (the server does not push them),
|
|
|
587
615
|
`priced_from` is the server time of the oldest tick those figures were priced from (`None` with no open
|
|
588
616
|
position): minutes old while the market is open means the feed is not delivering — do not trust the equity.
|
|
589
617
|
|
|
590
|
-
|
|
618
|
+
| Field | Type | Meaning |
|
|
619
|
+
|---|---|---|
|
|
620
|
+
| `login` | `int` | the account number |
|
|
621
|
+
| `name` | `str` | the account holder's name as the broker holds it (personal data: keep it out of logs) |
|
|
622
|
+
| `company` | `str` | the broker's company name |
|
|
623
|
+
| `group` | `str` | the account's group on the server |
|
|
624
|
+
| `currency` | `str` | the deposit currency; every money figure below is in it |
|
|
625
|
+
| `leverage` | `int` | the N in 1:N |
|
|
626
|
+
| `balance` | `float` | closed-trade funds, floating profit excluded; it moves with every close and every deposit or withdrawal the session sees |
|
|
627
|
+
| `credit` | `float` | credit the broker granted |
|
|
628
|
+
| `equity` | `float` | balance + credit + floating profit (swap and commission of the open positions included) |
|
|
629
|
+
| `profit` | `float` | floating profit of the open positions including swap and commission |
|
|
630
|
+
| `margin` | `float` | the margin the open positions hold, derived on the client as the terminal does |
|
|
631
|
+
| `free_margin` | `float` | equity − margin |
|
|
632
|
+
| `margin_level` | `float` | equity ÷ margin × 100; 0 when no margin is used |
|
|
633
|
+
| `account_type` | `str` | the server's class of the account: Real, Demo or Contest |
|
|
634
|
+
| `is_investor` | `bool` | the login is the read-only investor password |
|
|
635
|
+
| `priced_from` | `Optional[datetime]` | the server time of the oldest quote the floating figures used (old while the market is open means the feed is not delivering) |
|
|
591
636
|
|
|
592
637
|
#### `AccountSummary`
|
|
593
638
|
|
|
@@ -614,13 +659,40 @@ Fields: `ok`, `lots`, `expected_loss_at_stop`, `risk_requested`, `minimum_trade_
|
|
|
614
659
|
|
|
615
660
|
One MT4 order: an open position, a pending order, a closed order of the history, or a balance / credit entry.
|
|
616
661
|
|
|
617
|
-
|
|
662
|
+
| Field | Type | Meaning |
|
|
663
|
+
|---|---|---|
|
|
664
|
+
| `ticket` | `int` | the order's ticket (a partial close leaves the remainder under a new ticket) |
|
|
665
|
+
| `symbol` | `str` | the symbol as the broker names it |
|
|
666
|
+
| `order_type` | `str` | `Buy`, `Sell` (a position), `BuyLimit`, `SellLimit`, `BuyStop`, `SellStop` (pending), or `Balance` / `Credit` for a capital entry |
|
|
667
|
+
| `lots` | `float` | the volume in lots |
|
|
668
|
+
| `open_time` | `Optional[datetime]` | when it opened or was placed, broker server time |
|
|
669
|
+
| `open_price` | `float` | the price the position opened at, or a pending order's price |
|
|
670
|
+
| `stop_loss` | `float` | stop-loss price (0 = none) |
|
|
671
|
+
| `take_profit` | `float` | take-profit price (0 = none) |
|
|
672
|
+
| `close_time` | `Optional[datetime]` | when it closed, broker server time (`None` while open) |
|
|
673
|
+
| `close_price` | `float` | the price it closed at; meaningful on a closed order only |
|
|
674
|
+
| `expiration` | `Optional[datetime]` | when a pending order expires (`None` = good till cancelled) |
|
|
675
|
+
| `magic` | `int` | always 0: the core does not carry a magic number; tag orders with `comment` |
|
|
676
|
+
| `swap` | `float` | swap in the deposit currency |
|
|
677
|
+
| `commission` | `float` | commission in the deposit currency (charged when the position closes) |
|
|
678
|
+
| `comment` | `str` | the order's comment (ASCII, 31 characters at most) |
|
|
679
|
+
| `profit` | `float` | profit in the deposit currency before swap and commission |
|
|
680
|
+
| `rate_open` | `float` | the conversion rate at open, as the server reports it |
|
|
681
|
+
| `rate_close` | `float` | the conversion rate at close, as the server reports it |
|
|
682
|
+
| `rate_margin` | `float` | the conversion rate used for the margin, as the server reports it |
|
|
618
683
|
|
|
619
684
|
#### `Bar`
|
|
620
685
|
|
|
621
686
|
Bar(time: 'datetime', open: 'float', high: 'float', low: 'float', close: 'float', volume: 'float')
|
|
622
687
|
|
|
623
|
-
|
|
688
|
+
| Field | Type | Meaning |
|
|
689
|
+
|---|---|---|
|
|
690
|
+
| `time` | `datetime` | the start of the period, broker server time |
|
|
691
|
+
| `open` | `float` | first price of the period |
|
|
692
|
+
| `high` | `float` | highest price |
|
|
693
|
+
| `low` | `float` | lowest price |
|
|
694
|
+
| `close` | `float` | last price |
|
|
695
|
+
| `volume` | `float` | the number of ticks in the period (MT4 reports tick volume only) |
|
|
624
696
|
|
|
625
697
|
#### `OrderUpdate`
|
|
626
698
|
|
|
@@ -79,6 +79,7 @@ Every public member of the C# `MT4API` and `OrderClient` classes and where it we
|
|
|
79
79
|
| `Reconnect` | `Client.reconnect` | returns False when every server failed; with `auto_reconnect=True` the core does it by itself |
|
|
80
80
|
| `ReconnectBackoffMs` | `Client(reconnect_backoff)` | |
|
|
81
81
|
| `ReconnectMaxAttempts` | `Client(reconnect_max_attempts)` | |
|
|
82
|
+
| `ReconnectMaxBackoffMs` | `Client(reconnect_max_backoff)` | |
|
|
82
83
|
| `RequiredMargin` | `Client.required_margin` | |
|
|
83
84
|
| `SaveServersIni` | `pmt4api.save_servers_ini` | |
|
|
84
85
|
| `Servers` | `Client.servers` | give `servers=[(host, port), ...]` to the constructor or call `add_server` |
|
|
@@ -163,6 +163,14 @@ _BARE_MESSAGES = (
|
|
|
163
163
|
_WRAPPERS = ("AggregateException", "TargetInvocationException")
|
|
164
164
|
|
|
165
165
|
|
|
166
|
+
def _python_words(text: str) -> str:
|
|
167
|
+
"""The core words its configuration for a C# program; say it as this package takes it."""
|
|
168
|
+
return (text.replace("set MT4API.ApiKey or the WMT4API_KEY environment variable",
|
|
169
|
+
"pass api_key= to Client(...) or set the WMT4API_KEY environment variable")
|
|
170
|
+
.replace("set MT4API.TokenServiceUrl or the WMT4API_TOKEN_URL environment variable",
|
|
171
|
+
"pass token_url= to Client(...) or set the WMT4API_TOKEN_URL environment variable"))
|
|
172
|
+
|
|
173
|
+
|
|
166
174
|
def _status(text: str) -> Optional[int]:
|
|
167
175
|
"""The number in "... rejected (status N)" of the core's bare connect-phase refusals."""
|
|
168
176
|
if "(status " not in text:
|
|
@@ -189,6 +197,7 @@ def translate(exc: BaseException) -> Optional[WmtError]:
|
|
|
189
197
|
continue
|
|
190
198
|
message = str(getattr(exc, "Message", None) or exc)
|
|
191
199
|
if kind == "license":
|
|
200
|
+
message = _python_words(message)
|
|
192
201
|
reason = getattr(exc, "Reason", None)
|
|
193
202
|
status = getattr(exc, "HttpStatus", None)
|
|
194
203
|
detail = getattr(exc, "ServerError", None)
|
|
@@ -209,7 +218,10 @@ def translate(exc: BaseException) -> Optional[WmtError]:
|
|
|
209
218
|
if kind == "requote":
|
|
210
219
|
return RequoteError(message, float(exc.Bid), float(exc.Ask), value if value is not None else 138)
|
|
211
220
|
cls = {"login": LoginRejectedError, "server": ServerError, "investor": InvestorModeError}[kind]
|
|
212
|
-
|
|
221
|
+
name = code_name(value)
|
|
222
|
+
if value is not None and name and ("(code %d)" % value) in message:
|
|
223
|
+
message = message.replace("(code %d)" % value, "(code %d: %s)" % (value, name))
|
|
224
|
+
return cls(message, name, value)
|
|
213
225
|
if kind == "timeout":
|
|
214
226
|
return RequestTimeout(message)
|
|
215
227
|
if kind == "cancelled":
|
|
@@ -52,6 +52,11 @@ def _unset_if_zero(value: Any) -> Optional[float]:
|
|
|
52
52
|
|
|
53
53
|
# ---- market data ------------------------------------------------------------------------------------------------
|
|
54
54
|
|
|
55
|
+
|
|
56
|
+
def _m(value: Any) -> float:
|
|
57
|
+
"""A money amount without the binary noise of the core's sums (1.3900000000000001 becomes 1.39)."""
|
|
58
|
+
return round(float(value), 8)
|
|
59
|
+
|
|
55
60
|
@dataclass(frozen=True)
|
|
56
61
|
class Quote:
|
|
57
62
|
"""A price. ``time`` is the server time of the tick (use it to judge how old the price is); ``high`` and ``low``
|
|
@@ -280,8 +285,8 @@ class Order:
|
|
|
280
285
|
return cls(ticket=int(o.Ticket), symbol=str(o.Symbol or ""), order_type=_name(o.Type), lots=float(o.Lots),
|
|
281
286
|
open_time=_time(o.OpenTime), open_price=float(o.OpenPrice), stop_loss=float(o.StopLoss),
|
|
282
287
|
take_profit=float(o.TakeProfit), close_time=_time(o.CloseTime), close_price=float(o.ClosePrice),
|
|
283
|
-
expiration=_time(o.ExpirationTime), magic=int(o.ExpertId), swap=
|
|
284
|
-
commission=
|
|
288
|
+
expiration=_time(o.ExpirationTime), magic=int(o.ExpertId), swap=_m(o.Swap),
|
|
289
|
+
commission=_m(o.Commission), comment=str(o.Comment or ""), profit=_m(o.Profit),
|
|
285
290
|
rate_open=float(o.RateOpen), rate_close=float(o.RateClose), rate_margin=float(o.RateMargin))
|
|
286
291
|
|
|
287
292
|
|
|
@@ -335,3 +340,74 @@ class ServerRecord:
|
|
|
335
340
|
return cls(name=str(r.Name or ""), company=str(r.Company or ""), is_demo=bool(r.IsDemo),
|
|
336
341
|
main_host=str(r.MainHost or ""),
|
|
337
342
|
access=tuple((str(a.Item1), int(a.Item2)) for a in (r.Access or ())))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# ---- what each field means (rendered into docs/API.md by tools/gen_docs.py) ------------------------------------------
|
|
346
|
+
AccountInfo.FIELD_DOC = {
|
|
347
|
+
"login": "the account number",
|
|
348
|
+
"name": "the account holder's name as the broker holds it (personal data: keep it out of logs)",
|
|
349
|
+
"company": "the broker's company name",
|
|
350
|
+
"group": "the account's group on the server",
|
|
351
|
+
"currency": "the deposit currency; every money figure below is in it",
|
|
352
|
+
"leverage": "the N in 1:N",
|
|
353
|
+
"balance": "closed-trade funds, floating profit excluded; it moves with every close and every deposit or "
|
|
354
|
+
"withdrawal the session sees",
|
|
355
|
+
"credit": "credit the broker granted",
|
|
356
|
+
"equity": "balance + credit + floating profit (swap and commission of the open positions included)",
|
|
357
|
+
"profit": "floating profit of the open positions including swap and commission",
|
|
358
|
+
"margin": "the margin the open positions hold, derived on the client as the terminal does",
|
|
359
|
+
"free_margin": "equity − margin",
|
|
360
|
+
"margin_level": "equity ÷ margin × 100; 0 when no margin is used",
|
|
361
|
+
"account_type": "the server's class of the account: Real, Demo or Contest",
|
|
362
|
+
"is_investor": "the login is the read-only investor password",
|
|
363
|
+
"priced_from": "the server time of the oldest quote the floating figures used (old while the market is open means "
|
|
364
|
+
"the feed is not delivering)",
|
|
365
|
+
}
|
|
366
|
+
Order.FIELD_DOC = {
|
|
367
|
+
"ticket": "the order's ticket (a partial close leaves the remainder under a new ticket)",
|
|
368
|
+
"symbol": "the symbol as the broker names it",
|
|
369
|
+
"order_type": "`Buy`, `Sell` (a position), `BuyLimit`, `SellLimit`, `BuyStop`, `SellStop` (pending), or `Balance` / "
|
|
370
|
+
"`Credit` for a capital entry",
|
|
371
|
+
"lots": "the volume in lots",
|
|
372
|
+
"open_time": "when it opened or was placed, broker server time",
|
|
373
|
+
"open_price": "the price the position opened at, or a pending order's price",
|
|
374
|
+
"stop_loss": "stop-loss price (0 = none)",
|
|
375
|
+
"take_profit": "take-profit price (0 = none)",
|
|
376
|
+
"close_time": "when it closed, broker server time (`None` while open)",
|
|
377
|
+
"close_price": "the price it closed at; meaningful on a closed order only",
|
|
378
|
+
"expiration": "when a pending order expires (`None` = good till cancelled)",
|
|
379
|
+
"magic": "always 0: the core does not carry a magic number; tag orders with `comment`",
|
|
380
|
+
"swap": "swap in the deposit currency",
|
|
381
|
+
"commission": "commission in the deposit currency (charged when the position closes)",
|
|
382
|
+
"comment": "the order's comment (ASCII, 31 characters at most)",
|
|
383
|
+
"profit": "profit in the deposit currency before swap and commission",
|
|
384
|
+
"rate_open": "the conversion rate at open, as the server reports it",
|
|
385
|
+
"rate_close": "the conversion rate at close, as the server reports it",
|
|
386
|
+
"rate_margin": "the conversion rate used for the margin, as the server reports it",
|
|
387
|
+
}
|
|
388
|
+
SymbolInfo.FIELD_DOC = {
|
|
389
|
+
"name": "the symbol as the broker names it",
|
|
390
|
+
"digits": "the price digits",
|
|
391
|
+
"point": "the smallest price step: 10 to the power of −digits",
|
|
392
|
+
"currency": "the instrument's own currency: the base currency of a forex pair (EUR for EURUSD); profit is "
|
|
393
|
+
"realised in `profit_currency`",
|
|
394
|
+
"profit_mode": "how profit is calculated: Forex, CFD or Futures",
|
|
395
|
+
"contract_size": "units of the instrument per 1.0 lot",
|
|
396
|
+
"margin_divider": "the divider of the margin formula of a CFD symbol",
|
|
397
|
+
"tick_size": "the smallest price change the server quotes",
|
|
398
|
+
"execution": "how orders execute: Request, Instant, Market or Exchange",
|
|
399
|
+
"security_index": "the account group's security (symbol class) the volume rules are keyed on",
|
|
400
|
+
"min_lots": "the smallest volume", "max_lots": "the largest volume", "lots_step": "the volume step",
|
|
401
|
+
"code": "the feed code the server uses for the symbol",
|
|
402
|
+
"raw_tick_value": "the server's own tick value (brokers often report 0: use `Client.tick_value`)",
|
|
403
|
+
"raw_bid_tick_value": "the server's tick value for a sell (often 0)",
|
|
404
|
+
"raw_ask_tick_value": "the server's tick value for a buy (often 0)",
|
|
405
|
+
}
|
|
406
|
+
Quote.FIELD_DOC = {
|
|
407
|
+
"symbol": "the symbol as the broker names it",
|
|
408
|
+
"bid": "the price a sell is made at",
|
|
409
|
+
"ask": "the price a buy is made at",
|
|
410
|
+
"time": "the server time of the tick: how old the price is",
|
|
411
|
+
"high": "the session's high once the server has sent it (`None` before)",
|
|
412
|
+
"low": "the session's low once the server has sent it (`None` before)",
|
|
413
|
+
}
|
pmt4api-1.0.0/CHANGELOG.md
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|