pmt5api 1.1.2__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.1.2 → pmt5api-1.2.0}/CHANGELOG.md +54 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/PKG-INFO +32 -5
- {pmt5api-1.1.2 → pmt5api-1.2.0}/README.md +31 -4
- {pmt5api-1.1.2 → pmt5api-1.2.0}/examples/pnl.py +6 -4
- {pmt5api-1.1.2 → pmt5api-1.2.0}/examples/sizing.py +3 -3
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/__init__.py +1 -1
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/_account.py +58 -29
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/_coverage.py +2 -1
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/_risk.py +4 -2
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/_runtime.py +70 -8
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/_session.py +7 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/_streams.py +5 -4
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/client.py +9 -2
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/diagnostics.py +8 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/docs/API.md +42 -14
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/docs/FROM-CSHARP.md +2 -1
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/errors.py +13 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/LICENSE.txt +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/examples/bars.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/examples/connect.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/examples/servers.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/examples/stream_async.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/examples/stream_quotes.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/examples/trade_demo.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/hatch_build.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/pyproject.toml +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/__main__.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/_base.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/_events.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/_market.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/_trading.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/_util.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/bars.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/frames.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/models.py +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/py.typed +0 -0
- {pmt5api-1.1.2 → pmt5api-1.2.0}/src/pmt5api/servers.py +0 -0
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Changelog — pmt5api
|
|
2
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
|
+
|
|
3
57
|
## [1.1.2] — 2026-10-15
|
|
4
58
|
|
|
5
59
|
From the delta re-test of 1.1.1 (no S1 or S2). Python layer only: the cores are unchanged.
|
|
@@ -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
|
|
@@ -202,9 +202,9 @@ Do not size a trade from a one-line formula: what a price unit is worth depends
|
|
|
202
202
|
from your own closed deals and refuses to size without that evidence:
|
|
203
203
|
|
|
204
204
|
```python
|
|
205
|
-
from datetime import
|
|
205
|
+
from datetime import timedelta
|
|
206
206
|
|
|
207
|
-
if c.calibrate_money_scale(
|
|
207
|
+
if c.calibrate_money_scale(c.server_time - timedelta(days=365)): # read-only; places no order; at most a year back
|
|
208
208
|
r = c.lots_for_risk("EURUSD", entry_price=1.1000, stop_price=1.0950, risk_amount=50.0, side="buy")
|
|
209
209
|
for w in r.warnings:
|
|
210
210
|
print("warning:", w) # read these: ok can still be True with a caveat
|
|
@@ -228,12 +228,29 @@ to be checked. Also: `required_margin(symbol, lots)`, `order_profit(symbol, open
|
|
|
228
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 |
|
|
229
229
|
| `orders()` / `positions()` / `closed_orders()` | `list[Order]` — working / open positions only / what this session saw close |
|
|
230
230
|
| `history(start, end)` / `deal_history(year, month)` | `History` — `.orders` and `.deals` (the ledger, including deposits and withdrawals) |
|
|
231
|
-
| `summary(start
|
|
231
|
+
| `summary(start, end)` | `AccountSummary` — realized profit, deposits, withdrawals, from the deal ledger |
|
|
232
232
|
| `equity_history(start, "D1")` | `list[EquityPoint]` — balance (exact) and equity (reconstructed) curve with drawdowns |
|
|
233
233
|
| `request_history(...)`, `request_bars(...)` | without waiting; the data arrives through `on_history` / `on_quote_history` |
|
|
234
234
|
|
|
235
235
|
Times are naive `datetime` values in the **broker's server time**, as the server reports them; an unset time is `None`.
|
|
236
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
|
+
|
|
237
254
|
`account()` first quotes each open position's symbol once and recalculates profits, so equity, profit and margin
|
|
238
255
|
level are current (a cold read would otherwise show equity equal to balance). Pass `refresh=False` to skip that.
|
|
239
256
|
|
|
@@ -241,7 +258,9 @@ level are current (a cold read would otherwise show equity equal to balance). Pa
|
|
|
241
258
|
|
|
242
259
|
`on_quote`, `on_order_update`, `on_order_progress`, `on_connect_progress`, `on_history`, `on_quote_history`,
|
|
243
260
|
`on_symbols_update`, `on_symbol_update` — each returns a function that removes the callback. `quotes()` and
|
|
244
|
-
`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.
|
|
245
264
|
|
|
246
265
|
## Staying connected
|
|
247
266
|
|
|
@@ -262,12 +281,20 @@ subscriptions are restored, cached snapshots are not.
|
|
|
262
281
|
| Reconnect | `auto_reconnect`, `reconnect_backoff` (3, doubling up to `reconnect_max_backoff`, 30), `reconnect_max_attempts` (0 = no limit) | — |
|
|
263
282
|
| Proxy | `proxy=Proxy("socks5" \| "socks4" \| "https", host, port, user, password)` — `host` must be an IP address | — |
|
|
264
283
|
| Client certificate / common key | `certificate=`, `certificate_password=`, `common_key=` | — |
|
|
284
|
+
| History window | `max_history_days=` (366; `0` = no limit) — also a client attribute | — |
|
|
265
285
|
| .NET runtime location | — | `DOTNET_ROOT` |
|
|
266
286
|
|
|
267
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
|
|
268
288
|
.NET runtime is taken from `DOTNET_ROOT` if you set it, else the runtime bundled in the wheel, else an installed one.
|
|
269
289
|
The bundled runtime runs in invariant-culture mode on machines without ICU.
|
|
270
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
|
+
|
|
271
298
|
### Diagnostics
|
|
272
299
|
|
|
273
300
|
```python
|
|
@@ -175,9 +175,9 @@ Do not size a trade from a one-line formula: what a price unit is worth depends
|
|
|
175
175
|
from your own closed deals and refuses to size without that evidence:
|
|
176
176
|
|
|
177
177
|
```python
|
|
178
|
-
from datetime import
|
|
178
|
+
from datetime import timedelta
|
|
179
179
|
|
|
180
|
-
if c.calibrate_money_scale(
|
|
180
|
+
if c.calibrate_money_scale(c.server_time - timedelta(days=365)): # read-only; places no order; at most a year back
|
|
181
181
|
r = c.lots_for_risk("EURUSD", entry_price=1.1000, stop_price=1.0950, risk_amount=50.0, side="buy")
|
|
182
182
|
for w in r.warnings:
|
|
183
183
|
print("warning:", w) # read these: ok can still be True with a caveat
|
|
@@ -201,12 +201,29 @@ to be checked. Also: `required_margin(symbol, lots)`, `order_profit(symbol, open
|
|
|
201
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 |
|
|
202
202
|
| `orders()` / `positions()` / `closed_orders()` | `list[Order]` — working / open positions only / what this session saw close |
|
|
203
203
|
| `history(start, end)` / `deal_history(year, month)` | `History` — `.orders` and `.deals` (the ledger, including deposits and withdrawals) |
|
|
204
|
-
| `summary(start
|
|
204
|
+
| `summary(start, end)` | `AccountSummary` — realized profit, deposits, withdrawals, from the deal ledger |
|
|
205
205
|
| `equity_history(start, "D1")` | `list[EquityPoint]` — balance (exact) and equity (reconstructed) curve with drawdowns |
|
|
206
206
|
| `request_history(...)`, `request_bars(...)` | without waiting; the data arrives through `on_history` / `on_quote_history` |
|
|
207
207
|
|
|
208
208
|
Times are naive `datetime` values in the **broker's server time**, as the server reports them; an unset time is `None`.
|
|
209
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
|
+
|
|
210
227
|
`account()` first quotes each open position's symbol once and recalculates profits, so equity, profit and margin
|
|
211
228
|
level are current (a cold read would otherwise show equity equal to balance). Pass `refresh=False` to skip that.
|
|
212
229
|
|
|
@@ -214,7 +231,9 @@ level are current (a cold read would otherwise show equity equal to balance). Pa
|
|
|
214
231
|
|
|
215
232
|
`on_quote`, `on_order_update`, `on_order_progress`, `on_connect_progress`, `on_history`, `on_quote_history`,
|
|
216
233
|
`on_symbols_update`, `on_symbol_update` — each returns a function that removes the callback. `quotes()` and
|
|
217
|
-
`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.
|
|
218
237
|
|
|
219
238
|
## Staying connected
|
|
220
239
|
|
|
@@ -235,12 +254,20 @@ subscriptions are restored, cached snapshots are not.
|
|
|
235
254
|
| Reconnect | `auto_reconnect`, `reconnect_backoff` (3, doubling up to `reconnect_max_backoff`, 30), `reconnect_max_attempts` (0 = no limit) | — |
|
|
236
255
|
| Proxy | `proxy=Proxy("socks5" \| "socks4" \| "https", host, port, user, password)` — `host` must be an IP address | — |
|
|
237
256
|
| Client certificate / common key | `certificate=`, `certificate_password=`, `common_key=` | — |
|
|
257
|
+
| History window | `max_history_days=` (366; `0` = no limit) — also a client attribute | — |
|
|
238
258
|
| .NET runtime location | — | `DOTNET_ROOT` |
|
|
239
259
|
|
|
240
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
|
|
241
261
|
.NET runtime is taken from `DOTNET_ROOT` if you set it, else the runtime bundled in the wheel, else an installed one.
|
|
242
262
|
The bundled runtime runs in invariant-culture mode on machines without ICU.
|
|
243
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
|
+
|
|
244
271
|
### Diagnostics
|
|
245
272
|
|
|
246
273
|
```python
|
|
@@ -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
|
|
|
@@ -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",
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
"""The account: balances, positions, orders, history, and the curves derived from them."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from datetime import datetime
|
|
5
|
-
from typing import List, Optional
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, List, Optional
|
|
6
6
|
|
|
7
7
|
from ._base import _Base
|
|
8
8
|
from ._util import enum_value, to_core_time, translated
|
|
9
|
-
from .errors import NotConnectedError
|
|
9
|
+
from .errors import InvalidRequestError, NotConnectedError
|
|
10
10
|
from .models import AccountInfo, AccountSummary, EquityPoint, History, Order
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def _history_days(days: Any) -> int:
|
|
14
|
+
"""A ``max_history_days`` value: a whole number of days, 0 for no limit."""
|
|
15
|
+
if isinstance(days, bool) or not isinstance(days, int) or days < 0:
|
|
16
|
+
raise ValueError("max_history_days must be a whole number of days, or 0 for no limit (got %r)" % (days,))
|
|
17
|
+
return days
|
|
18
|
+
|
|
19
|
+
|
|
13
20
|
def _money(value) -> float:
|
|
14
21
|
"""A money figure without the binary noise the core's sums carry (1635341.7899999998 becomes 1635341.79)."""
|
|
15
22
|
return round(float(value), 8)
|
|
@@ -82,22 +89,20 @@ class _AccountMixin(_Base):
|
|
|
82
89
|
# ---- history -----------------------------------------------------------------------------------------------
|
|
83
90
|
|
|
84
91
|
@translated
|
|
85
|
-
def history(self, start:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
def history(self, start: datetime, end: datetime, timeout: Optional[float] = None) -> History:
|
|
93
|
+
"""Orders and deals between ``start`` and ``end``. Blocks until the server has sent everything, up to
|
|
94
|
+
``timeout`` seconds (default: the client's ``history_timeout``).
|
|
95
|
+
|
|
96
|
+
Both ends are required and the window may be at most :attr:`max_history_days` long (366 by default): a missing,
|
|
97
|
+
reversed or longer window raises :class:`InvalidRequestError` before anything is sent. For a longer period,
|
|
98
|
+
call once per window and add the results up. For "up to now", pass ``c.server_time`` as ``end``. ``orders``
|
|
99
|
+
also lists a position closed in the window whose opening deal is earlier (up to a year before the window).
|
|
89
100
|
|
|
90
101
|
Naive datetimes are used exactly as given, in the broker's server time; timezone-aware ones are converted to
|
|
91
|
-
it.
|
|
102
|
+
it. Two calls at once on one client run one after the other (the wait counts toward the second one's timeout).
|
|
92
103
|
"""
|
|
93
104
|
self._require_connected()
|
|
94
|
-
|
|
95
|
-
end = self._server_naive(end)
|
|
96
|
-
start = self._server_naive(start)
|
|
97
|
-
if end is None:
|
|
98
|
-
end = now + timedelta(days=1) # nothing the server has booked is later than this
|
|
99
|
-
if start is None:
|
|
100
|
-
start = now - timedelta(days=30)
|
|
105
|
+
start, end = self._window(start, end, "history")
|
|
101
106
|
ms = int(timeout * 1000) if timeout is not None else int(self._api.DownloadOrderHistoryTimeoutMs)
|
|
102
107
|
return History._from_core(self._api.DownloadOrderHistory(to_core_time(start), to_core_time(end), ms))
|
|
103
108
|
|
|
@@ -110,10 +115,12 @@ class _AccountMixin(_Base):
|
|
|
110
115
|
@translated
|
|
111
116
|
def request_history(self, start: datetime, end: datetime) -> None:
|
|
112
117
|
"""Ask for history without waiting; it arrives through :meth:`on_history`. The server's answer is not trimmed
|
|
113
|
-
to ``start`` / ``end`` (it
|
|
114
|
-
:meth:`history` does trim.
|
|
118
|
+
to ``start`` / ``end`` (it comes in whole calendar months, in pages): filter by time. The blocking
|
|
119
|
+
:meth:`history` does trim. The window rules of :meth:`history` apply; refused while a :meth:`history`
|
|
120
|
+
download runs on this client (:class:`BusyError`), because the answers would mix."""
|
|
115
121
|
self._require_connected()
|
|
116
|
-
self.
|
|
122
|
+
start, end = self._window(start, end, "request_history")
|
|
123
|
+
self._api.RequestOrderHistory(to_core_time(start), to_core_time(end))
|
|
117
124
|
|
|
118
125
|
@translated
|
|
119
126
|
def request_history_month(self, year: int, month: int) -> None:
|
|
@@ -121,32 +128,54 @@ class _AccountMixin(_Base):
|
|
|
121
128
|
self._require_connected()
|
|
122
129
|
self._api.RequestOrderHistory(int(year), int(month))
|
|
123
130
|
|
|
131
|
+
@property
|
|
132
|
+
def max_history_days(self) -> int:
|
|
133
|
+
"""The longest window, in days, one :meth:`history`, :meth:`request_history`, :meth:`summary`,
|
|
134
|
+
:meth:`equity_history` or :meth:`calibrate_money_scale` call may ask for (default 366). A longer window raises
|
|
135
|
+
:class:`InvalidRequestError` before anything is sent: split the period into several calls, or raise this when
|
|
136
|
+
you mean one long download. ``0`` removes the limit (the start and end are still required)."""
|
|
137
|
+
return int(self._api.MaxHistoryDays)
|
|
138
|
+
|
|
139
|
+
@max_history_days.setter
|
|
140
|
+
def max_history_days(self, days: int) -> None:
|
|
141
|
+
self._api.MaxHistoryDays = _history_days(days)
|
|
142
|
+
|
|
143
|
+
def _window(self, start: Optional[datetime], end: Optional[datetime], what: str) -> tuple:
|
|
144
|
+
"""``start`` and ``end`` in server time; both are required (the length is checked by the core)."""
|
|
145
|
+
if start is None or end is None:
|
|
146
|
+
raise InvalidRequestError("%s() needs a start and an end: history is always asked for by window "
|
|
147
|
+
"(at most max_history_days, %d days, per call)" % (what, self.max_history_days))
|
|
148
|
+
return self._server_naive(start), self._server_naive(end)
|
|
149
|
+
|
|
124
150
|
# ---- realized figures and curves ---------------------------------------------------------------------------
|
|
125
151
|
|
|
126
152
|
@translated
|
|
127
|
-
def summary(self, start:
|
|
128
|
-
"""Realized trading result, deposits and withdrawals
|
|
153
|
+
def summary(self, start: datetime, end: datetime) -> AccountSummary:
|
|
154
|
+
"""Realized trading result, deposits and withdrawals between ``start`` and ``end``, from the deal ledger. Both
|
|
155
|
+
ends are required; the window may be at most :attr:`max_history_days` long — for a longer period, add up one
|
|
156
|
+
summary per window (``balance`` is the account's current balance in each).
|
|
129
157
|
|
|
130
158
|
Downloads the deal history, so it blocks; call it off your UI thread (or use ``asyncio.to_thread``).
|
|
131
159
|
"""
|
|
132
160
|
self._require_connected()
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if start is None or end is None:
|
|
136
|
-
raise ValueError("give both start and end, or neither")
|
|
137
|
-
return AccountSummary._from_core(self._api.GetAccountSummary(to_core_time(self._server_naive(start)),
|
|
138
|
-
to_core_time(self._server_naive(end))))
|
|
161
|
+
start, end = self._window(start, end, "summary")
|
|
162
|
+
return AccountSummary._from_core(self._api.GetAccountSummary(to_core_time(start), to_core_time(end)))
|
|
139
163
|
|
|
140
164
|
@translated
|
|
141
165
|
def equity_history(self, start: datetime, timeframe: str = "D1", exclude_same_bars: bool = True) -> List[EquityPoint]:
|
|
142
|
-
"""The balance and equity curve
|
|
166
|
+
"""The balance and equity curve from ``start`` to now (``timeframe`` is ``H1``, ``H4`` or ``D1``); ``start``
|
|
167
|
+
may be at most :attr:`max_history_days` ago.
|
|
143
168
|
|
|
144
169
|
``balance`` is exact; the historical ``equity`` is a bar-priced reconstruction and a point with
|
|
145
170
|
``incomplete`` set has understated floating P/L. With ``exclude_same_bars`` (the default, as in the C# library)
|
|
146
|
-
consecutive points with an unchanged balance and equity are dropped.
|
|
147
|
-
|
|
171
|
+
consecutive points with an unchanged balance and equity are dropped. A window with no deal in it is a flat line
|
|
172
|
+
at the balance (never empty); the last point is the live balance and equity. Downloads deals and per-symbol
|
|
173
|
+
bars, so it blocks.
|
|
148
174
|
"""
|
|
149
175
|
self._require_connected()
|
|
176
|
+
if start is None:
|
|
177
|
+
raise InvalidRequestError("equity_history() needs a start (at most max_history_days, %d days, ago)"
|
|
178
|
+
% self.max_history_days)
|
|
150
179
|
tf = enum_value(self._core.EquityTimeframe, timeframe, "equity timeframe")
|
|
151
180
|
points = self._api.CalculateEquityHistory(to_core_time(self._server_naive(start)), tf, bool(exclude_same_bars))
|
|
152
181
|
return [EquityPoint._from_core(p) for p in (points or ())]
|
|
@@ -36,7 +36,7 @@ COVERAGE: Dict[str, str] = {
|
|
|
36
36
|
"User": "Client.login",
|
|
37
37
|
"Password": "Client(password)",
|
|
38
38
|
"Host": "Client.host",
|
|
39
|
-
"Server": "Client.
|
|
39
|
+
"Server": "Client.server_name",
|
|
40
40
|
"Port": "Client.port",
|
|
41
41
|
"Id": "not applicable: a free-form tag for the caller's own use; keep it in your own code",
|
|
42
42
|
"ProcessServerMessagesInThread": "not applicable: documented by the core as reserved and unused",
|
|
@@ -52,6 +52,7 @@ COVERAGE: Dict[str, str] = {
|
|
|
52
52
|
"ExecutionTimeout": "Client(execution_timeout)",
|
|
53
53
|
"GetQuoteTimeoutMs": "Client(quote_timeout)",
|
|
54
54
|
"DownloadOrderHistoryTimeoutMs": "Client(history_timeout)",
|
|
55
|
+
"MaxHistoryDays": "Client.max_history_days",
|
|
55
56
|
"DownloadQuoteHistoryTimeout": "Client(bars_timeout)",
|
|
56
57
|
"PingTime": "Client(ping_interval)",
|
|
57
58
|
"NoServerMessagesTimeout": "Client(no_message_timeout)",
|
|
@@ -16,7 +16,9 @@ class _RiskMixin(_Base):
|
|
|
16
16
|
def calibrate_money_scale(self, start: datetime, *, min_deals_per_symbol: Optional[int] = None,
|
|
17
17
|
max_self_scatter: Optional[float] = None, max_disagreement: Optional[float] = None,
|
|
18
18
|
max_snap_distance: Optional[float] = None) -> bool:
|
|
19
|
-
"""Measure what a price unit is worth *on this account* from its own closed deals
|
|
19
|
+
"""Measure what a price unit is worth *on this account* from its own closed deals from ``start`` to now;
|
|
20
|
+
``start`` may be at most :attr:`max_history_days` ago (an older one returns ``False``, the reason in
|
|
21
|
+
:attr:`money_scale_evidence`).
|
|
20
22
|
|
|
21
23
|
Read-only; places no order. Cent handling is a per-broker configuration, not a property of the currency, so
|
|
22
24
|
sizing is refused until this has succeeded. Returns ``False`` when the scale cannot be established (read
|
|
@@ -42,7 +44,7 @@ class _RiskMixin(_Base):
|
|
|
42
44
|
@property
|
|
43
45
|
def money_scale_evidence(self) -> str:
|
|
44
46
|
"""Why the scale is what it is, or why it could not be established."""
|
|
45
|
-
return str(self._api.MoneyScaleEvidence or "")
|
|
47
|
+
return str(self._api.MoneyScaleEvidence or "").replace("MaxHistoryDays", "max_history_days")
|
|
46
48
|
|
|
47
49
|
@translated
|
|
48
50
|
def lots_for_risk(self, symbol: str, entry_price: float, stop_price: float, risk_amount: float, *,
|
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
"""Locates the .NET runtime and the licensed core library and loads them once per process.
|
|
1
|
+
"""Locates the .NET runtime and the licensed core library and loads them once per process.
|
|
2
|
+
|
|
3
|
+
Trust: whatever this module loads runs inside your Python process with your rights. The core library comes from
|
|
4
|
+
``dll_path=``, else the ``WMTAPI_DLL`` environment variable, else the wheel; the .NET runtime from ``DOTNET_ROOT``, else
|
|
5
|
+
the wheel, else an installed one found through ``PATH``. Each of these decides which native and managed code is
|
|
6
|
+
executed, so set them only from configuration you control, and do not run with an environment that someone else
|
|
7
|
+
can change. ``enable_core_logging()`` reports once when the core or the runtime was chosen by the environment.
|
|
8
|
+
"""
|
|
2
9
|
from __future__ import annotations
|
|
3
10
|
|
|
4
11
|
import glob
|
|
5
12
|
import importlib
|
|
13
|
+
import json
|
|
6
14
|
import os
|
|
7
15
|
import shutil
|
|
8
16
|
import struct
|
|
9
17
|
import sys
|
|
18
|
+
import tempfile
|
|
10
19
|
import threading
|
|
11
20
|
from pathlib import Path
|
|
12
|
-
from typing import Iterator, Optional
|
|
21
|
+
from typing import Iterator, List, Optional
|
|
13
22
|
|
|
14
23
|
from .errors import CoreNotFoundError, RuntimeNotFoundError
|
|
15
24
|
|
|
@@ -39,6 +48,8 @@ def _process_lock() -> "threading.RLock":
|
|
|
39
48
|
_lock = _process_lock()
|
|
40
49
|
_core = None
|
|
41
50
|
_dll: Optional[Path] = None
|
|
51
|
+
_chosen_by_environment: List[str] = [] # what the environment decided at load time (see trust_notes)
|
|
52
|
+
_root_we_set: Optional[str] = None # the DOTNET_ROOT value this module chose itself, if any
|
|
42
53
|
|
|
43
54
|
|
|
44
55
|
def find_dll(explicit=None) -> Path:
|
|
@@ -94,18 +105,56 @@ def _ensure_dotnet_root() -> None:
|
|
|
94
105
|
os.environ.setdefault("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1")
|
|
95
106
|
if os.environ.get("DOTNET_ROOT"):
|
|
96
107
|
return
|
|
108
|
+
global _root_we_set
|
|
97
109
|
if (_BUNDLED_RUNTIME / "shared" / "Microsoft.NETCore.App").is_dir():
|
|
98
|
-
os.environ["DOTNET_ROOT"] = str(_BUNDLED_RUNTIME)
|
|
110
|
+
os.environ["DOTNET_ROOT"] = _root_we_set = str(_BUNDLED_RUNTIME)
|
|
99
111
|
return
|
|
100
112
|
for root in _dotnet_root_candidates():
|
|
101
113
|
if (root / "shared" / "Microsoft.NETCore.App").is_dir():
|
|
102
|
-
os.environ["DOTNET_ROOT"] = str(root)
|
|
114
|
+
os.environ["DOTNET_ROOT"] = _root_we_set = str(root)
|
|
103
115
|
return
|
|
104
116
|
raise RuntimeNotFoundError(
|
|
105
117
|
"No .NET runtime was found. Install the .NET 8 (or later) runtime, or set DOTNET_ROOT to its install directory."
|
|
106
118
|
)
|
|
107
119
|
|
|
108
120
|
|
|
121
|
+
def _runtime_version(root: Path) -> Optional[str]:
|
|
122
|
+
"""The newest ``Microsoft.NETCore.App`` version installed under ``root`` (``shared/Microsoft.NETCore.App/<ver>``)."""
|
|
123
|
+
best = None
|
|
124
|
+
try:
|
|
125
|
+
entries = list((root / "shared" / "Microsoft.NETCore.App").iterdir())
|
|
126
|
+
except OSError:
|
|
127
|
+
return None
|
|
128
|
+
for d in entries:
|
|
129
|
+
try:
|
|
130
|
+
key = tuple(int(n) for n in d.name.split("-", 1)[0].split(".")[:3])
|
|
131
|
+
except ValueError:
|
|
132
|
+
continue
|
|
133
|
+
if d.is_dir() and (best is None or key > best[0]):
|
|
134
|
+
best = (key, d.name)
|
|
135
|
+
return best[1] if best else None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _start_coreclr(start, tmp: str) -> str:
|
|
139
|
+
"""Start CoreCLR from the root in ``DOTNET_ROOT`` at a version that root holds; returns what was used.
|
|
140
|
+
|
|
141
|
+
Left to itself the loader asks a ``dotnet`` found on ``PATH`` for the installed runtimes and takes the newest — one
|
|
142
|
+
that only that other installation may have (a system .NET 10 next to the bundled 9 fails with
|
|
143
|
+
FrameworkMissingFailure). The runtime configuration written here names the root's own newest version instead.
|
|
144
|
+
"""
|
|
145
|
+
root = Path(os.environ["DOTNET_ROOT"])
|
|
146
|
+
version = _runtime_version(root)
|
|
147
|
+
if version is None: # not a layout we can read: let the loader decide, as before
|
|
148
|
+
start("coreclr")
|
|
149
|
+
return "a runtime chosen by pythonnet"
|
|
150
|
+
cfg = Path(tmp) / "runtimeconfig.json"
|
|
151
|
+
cfg.write_text(json.dumps({"runtimeOptions": {
|
|
152
|
+
"tfm": "net" + ".".join(version.split("-", 1)[0].split(".")[:2]),
|
|
153
|
+
"framework": {"name": "Microsoft.NETCore.App", "version": version}}}), encoding="utf-8")
|
|
154
|
+
start("coreclr", runtime_config=str(cfg), dotnet_root=str(root))
|
|
155
|
+
return "Microsoft.NETCore.App %s in %s" % (version, root)
|
|
156
|
+
|
|
157
|
+
|
|
109
158
|
def runtime_root() -> Optional[Path]:
|
|
110
159
|
"""The .NET root in use (after ``load``), or None."""
|
|
111
160
|
root = os.environ.get("DOTNET_ROOT")
|
|
@@ -149,17 +198,24 @@ def load(dll_path=None):
|
|
|
149
198
|
"so dll_path=%s was not loaded" % (_dll, dll_path))
|
|
150
199
|
return _core
|
|
151
200
|
dll = find_dll(dll_path)
|
|
201
|
+
notes = []
|
|
202
|
+
if dll_path is None and os.environ.get(ENV_DLL):
|
|
203
|
+
notes.append("core library %s was named by the %s environment variable" % (dll, ENV_DLL))
|
|
204
|
+
if os.environ.get("DOTNET_ROOT") and os.environ["DOTNET_ROOT"] != _root_we_set:
|
|
205
|
+
notes.append(".NET runtime was taken from DOTNET_ROOT=%s, set by the environment" % os.environ["DOTNET_ROOT"])
|
|
152
206
|
_ensure_dotnet_root()
|
|
207
|
+
tried = "the runtime in DOTNET_ROOT=%s" % os.environ.get("DOTNET_ROOT")
|
|
153
208
|
try:
|
|
154
209
|
from pythonnet import load as _load
|
|
155
|
-
|
|
156
|
-
|
|
210
|
+
with tempfile.TemporaryDirectory(prefix="pmt-runtime-") as tmp:
|
|
211
|
+
tried = _start_coreclr(_load, tmp)
|
|
212
|
+
import clr # noqa: F401 (provided by pythonnet; the configuration is read by now)
|
|
157
213
|
except Exception as e: # noqa: BLE001 — anything here means the runtime could not start
|
|
158
214
|
raise RuntimeNotFoundError(
|
|
159
215
|
"could not start the .NET runtime. The core needs the .NET 8 (or later) runtime, Microsoft.NETCore.App: "
|
|
160
216
|
"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
|
|
217
|
+
"this machine has) or point DOTNET_ROOT at a folder that holds it.%s Tried: %s. Detail: %s: %s"
|
|
218
|
+
% (_BITNESS_HINT if struct.calcsize("P") == 4 else "", tried, type(e).__name__, str(e)[:240])) from e
|
|
163
219
|
import clr as _clr
|
|
164
220
|
from System.Reflection import Assembly # available once the runtime is up
|
|
165
221
|
try:
|
|
@@ -169,8 +225,14 @@ def load(dll_path=None):
|
|
|
169
225
|
_clr.AddReference("wmt5api") # now by name: registers the namespace for `import`
|
|
170
226
|
_core = _import_namespace()
|
|
171
227
|
_dll = dll
|
|
228
|
+
_chosen_by_environment[:] = notes
|
|
172
229
|
return _core
|
|
173
230
|
|
|
174
231
|
|
|
175
232
|
def loaded_dll() -> Optional[Path]:
|
|
176
233
|
return _dll
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def trust_notes() -> List[str]:
|
|
237
|
+
"""What the environment (``WMTAPI_DLL``, ``DOTNET_ROOT``) chose when the core was loaded; empty if nothing."""
|
|
238
|
+
return list(_chosen_by_environment)
|
|
@@ -103,6 +103,13 @@ class _SessionMixin(_Base):
|
|
|
103
103
|
"""The access server this client was created for (it may fail over to a cluster member)."""
|
|
104
104
|
return str(self._api.Host or self._host)
|
|
105
105
|
|
|
106
|
+
@property
|
|
107
|
+
def server_name(self) -> Optional[str]:
|
|
108
|
+
"""The name of the trade server the session logged in to (for example ``ICMarketsSC-Demo``), as the server
|
|
109
|
+
reports it while connecting; ``None`` before the first connect. The address dialled is :attr:`host`."""
|
|
110
|
+
name = self._api.Server
|
|
111
|
+
return str(name) if name else None
|
|
112
|
+
|
|
106
113
|
@property
|
|
107
114
|
def port(self) -> int:
|
|
108
115
|
"""The access-server port."""
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Delivery machinery shared by the event API: one dispatcher thread per client, and the sync / async iterators.
|
|
2
2
|
|
|
3
|
-
The core
|
|
4
|
-
|
|
5
|
-
arrives (a snapshot — the core's objects are shared and mutable) and hands it
|
|
6
|
-
which calls the callbacks one at a time, in arrival order
|
|
3
|
+
The core raises each kind of event one at a time, in the order it received them (cores before wmt4api 3.4.0 / wmt5api
|
|
4
|
+
3.2.0 used one thread-pool task per event, so events could arrive concurrently and out of order). The client converts
|
|
5
|
+
every event to a plain value the moment it arrives (a snapshot — the core's objects are shared and mutable) and hands it
|
|
6
|
+
to ONE dispatcher thread per client, which calls the callbacks one at a time, in arrival order — so a slow callback
|
|
7
|
+
never holds up the core's own delivery. A callback may take its time; if it falls more than
|
|
7
8
|
``EVENT_QUEUE`` events behind, the oldest events are dropped (a warning is logged, and ``Client.events_dropped``
|
|
8
9
|
counts them). An exception raised by a callback is logged (under the package's own logger name) and swallowed so it
|
|
9
10
|
cannot break the connection.
|
|
@@ -61,7 +61,11 @@ class Client(_SessionMixin, _MarketMixin, _AccountMixin, _RiskMixin, _TradingMix
|
|
|
61
61
|
or ``"expert"``; a per-call ``placed_type="manual"`` equals the default and so takes the client's value). ``all_order_updates=True`` also delivers the intermediate order states to
|
|
62
62
|
:meth:`on_order_update`.
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
History is always asked for by window: :meth:`history` and :meth:`summary` take a start and an end, and one call
|
|
65
|
+
covers at most ``max_history_days`` (default 366; ``0`` removes the limit).
|
|
66
|
+
|
|
67
|
+
Callbacks run on a dispatcher thread of this client, one at a time and in the order the events arrived — not on
|
|
68
|
+
the thread that created the client.
|
|
65
69
|
"""
|
|
66
70
|
|
|
67
71
|
def __init__(self, login: int, password: str, host: str, port: int = 443, *,
|
|
@@ -75,7 +79,8 @@ class Client(_SessionMixin, _MarketMixin, _AccountMixin, _RiskMixin, _TradingMix
|
|
|
75
79
|
reconnect_max_backoff: Optional[float] = None, reconnect_max_attempts: Optional[int] = None,
|
|
76
80
|
proxy: Optional[Proxy] = None, certificate: Optional[bytes] = None,
|
|
77
81
|
certificate_password: Optional[str] = None, common_key: Optional[bytes] = None,
|
|
78
|
-
placed_type: Optional[str] = None, all_order_updates: bool = False
|
|
82
|
+
placed_type: Optional[str] = None, all_order_updates: bool = False,
|
|
83
|
+
max_history_days: Optional[int] = None):
|
|
79
84
|
core = _runtime.load(dll_path)
|
|
80
85
|
self._core = core
|
|
81
86
|
self._login = int(login)
|
|
@@ -109,6 +114,8 @@ class Client(_SessionMixin, _MarketMixin, _AccountMixin, _RiskMixin, _TradingMix
|
|
|
109
114
|
api.EnableAllOrderUpdates = True
|
|
110
115
|
if placed_type:
|
|
111
116
|
api.PlacedType = _placed(core.PlacedType, placed_type)
|
|
117
|
+
if max_history_days is not None:
|
|
118
|
+
self.max_history_days = max_history_days
|
|
112
119
|
if proxy is not None:
|
|
113
120
|
api.ProxyEnable = True
|
|
114
121
|
api.ProxyType = enum_value(core.ProxyTypes, proxy.kind, "proxy kind")
|
|
@@ -6,6 +6,8 @@ from typing import Any, Callable
|
|
|
6
6
|
|
|
7
7
|
from . import _runtime
|
|
8
8
|
|
|
9
|
+
_trust_reported = False
|
|
10
|
+
|
|
9
11
|
_LEVELS = {"Trace": logging.DEBUG, "Debug": logging.DEBUG, "Info": logging.INFO, "Warn": logging.WARNING,
|
|
10
12
|
"Error": logging.ERROR, "Exception": logging.ERROR}
|
|
11
13
|
|
|
@@ -27,6 +29,12 @@ def enable_core_logging(logger_name: str = "pmt5api.core") -> Callable[[], None]
|
|
|
27
29
|
if logger.isEnabledFor(logging.DEBUG):
|
|
28
30
|
logger.debug("%s", message)
|
|
29
31
|
|
|
32
|
+
global _trust_reported
|
|
33
|
+
if not _trust_reported:
|
|
34
|
+
_trust_reported = True
|
|
35
|
+
for note in _runtime.trust_notes(): # once per process: what the environment chose to load
|
|
36
|
+
logger.info("%s", note)
|
|
37
|
+
|
|
30
38
|
core.Logger.OnMsgEx += on_msg
|
|
31
39
|
sink = System.Action[System.String](on_diag)
|
|
32
40
|
previous = core.Diag.Sink
|
|
@@ -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, cluster_member_timeout=None, token_timeout=None, execution_timeout=None, quote_timeout=None, history_timeout=None, bars_timeout=None, ping_interval=None, no_message_timeout=None, auto_reconnect=False, reconnect_backoff=None, reconnect_max_backoff=None, reconnect_max_attempts=None, proxy=None, certificate=None, certificate_password=None, common_key=None, placed_type=None, all_order_updates=False)
|
|
8
|
+
Client(login, password, host, port=443, *, api_key=None, token_url=None, dll_path=None, connect_timeout=None, cluster_member_timeout=None, token_timeout=None, execution_timeout=None, quote_timeout=None, history_timeout=None, bars_timeout=None, ping_interval=None, no_message_timeout=None, auto_reconnect=False, reconnect_backoff=None, reconnect_max_backoff=None, reconnect_max_attempts=None, proxy=None, certificate=None, certificate_password=None, common_key=None, placed_type=None, all_order_updates=False, max_history_days=None)
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
A connection to one MetaTrader 5 account through the licensed core.
|
|
@@ -38,7 +38,11 @@ require one. `placed_type` records how orders are placed (`"manual"`
|
|
|
38
38
|
or `"expert"`; a per-call `placed_type="manual"` equals the default and so takes the client's value). `all_order_updates=True` also delivers the intermediate order states to
|
|
39
39
|
`on_order_update`.
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
History is always asked for by window: `history` and `summary` take a start and an end, and one call
|
|
42
|
+
covers at most `max_history_days` (default 366; `0` removes the limit).
|
|
43
|
+
|
|
44
|
+
Callbacks run on a dispatcher thread of this client, one at a time and in the order the events arrived — not on
|
|
45
|
+
the thread that created the client.
|
|
42
46
|
|
|
43
47
|
## Connection and session
|
|
44
48
|
|
|
@@ -131,6 +135,11 @@ The access-server port.
|
|
|
131
135
|
The underlying core object, for anything this layer does not wrap. Calls made on it raise the core's own
|
|
132
136
|
exceptions, not pmt5api's.
|
|
133
137
|
|
|
138
|
+
#### `server_name` *(property)*
|
|
139
|
+
|
|
140
|
+
The name of the trade server the session logged in to (for example `ICMarketsSC-Demo`), as the server
|
|
141
|
+
reports it while connecting; `None` before the first connect. The address dialled is `host`.
|
|
142
|
+
|
|
134
143
|
#### `server_time` *(property)*
|
|
135
144
|
|
|
136
145
|
The server's clock, kept current by incoming quotes (server time, naive); `None` before the first
|
|
@@ -273,20 +282,34 @@ One calendar month of history (a convenience over `history`).
|
|
|
273
282
|
|
|
274
283
|
#### `equity_history(start, timeframe='D1', exclude_same_bars=True)`
|
|
275
284
|
|
|
276
|
-
The balance and equity curve
|
|
285
|
+
The balance and equity curve from `start` to now (`timeframe` is `H1`, `H4` or `D1`); `start`
|
|
286
|
+
may be at most `max_history_days` ago.
|
|
277
287
|
|
|
278
288
|
`balance` is exact; the historical `equity` is a bar-priced reconstruction and a point with
|
|
279
289
|
`incomplete` set has understated floating P/L. With `exclude_same_bars` (the default, as in the C# library)
|
|
280
|
-
consecutive points with an unchanged balance and equity are dropped.
|
|
281
|
-
|
|
290
|
+
consecutive points with an unchanged balance and equity are dropped. A window with no deal in it is a flat line
|
|
291
|
+
at the balance (never empty); the last point is the live balance and equity. Downloads deals and per-symbol
|
|
292
|
+
bars, so it blocks.
|
|
282
293
|
|
|
283
|
-
#### `history(start
|
|
294
|
+
#### `history(start, end, timeout=None)`
|
|
284
295
|
|
|
285
|
-
Orders and deals between `start` and `end
|
|
286
|
-
|
|
296
|
+
Orders and deals between `start` and `end`. Blocks until the server has sent everything, up to
|
|
297
|
+
`timeout` seconds (default: the client's `history_timeout`).
|
|
298
|
+
|
|
299
|
+
Both ends are required and the window may be at most `max_history_days` long (366 by default): a missing,
|
|
300
|
+
reversed or longer window raises `InvalidRequestError` before anything is sent. For a longer period,
|
|
301
|
+
call once per window and add the results up. For "up to now", pass `c.server_time` as `end`. `orders`
|
|
302
|
+
also lists a position closed in the window whose opening deal is earlier (up to a year before the window).
|
|
287
303
|
|
|
288
304
|
Naive datetimes are used exactly as given, in the broker's server time; timezone-aware ones are converted to
|
|
289
|
-
it.
|
|
305
|
+
it. Two calls at once on one client run one after the other (the wait counts toward the second one's timeout).
|
|
306
|
+
|
|
307
|
+
#### `max_history_days` *(property)*
|
|
308
|
+
|
|
309
|
+
The longest window, in days, one `history`, `request_history`, `summary`,
|
|
310
|
+
`equity_history` or `calibrate_money_scale` call may ask for (default 366). A longer window raises
|
|
311
|
+
`InvalidRequestError` before anything is sent: split the period into several calls, or raise this when
|
|
312
|
+
you mean one long download. `0` removes the limit (the start and end are still required).
|
|
290
313
|
|
|
291
314
|
#### `orders()`
|
|
292
315
|
|
|
@@ -302,16 +325,19 @@ order's `state` is not used: the core can leave a freshly opened position in `St
|
|
|
302
325
|
#### `request_history(start, end)`
|
|
303
326
|
|
|
304
327
|
Ask for history without waiting; it arrives through `on_history`. The server's answer is not trimmed
|
|
305
|
-
to `start` / `end` (it
|
|
306
|
-
`history` does trim.
|
|
328
|
+
to `start` / `end` (it comes in whole calendar months, in pages): filter by time. The blocking
|
|
329
|
+
`history` does trim. The window rules of `history` apply; refused while a `history`
|
|
330
|
+
download runs on this client (`BusyError`), because the answers would mix.
|
|
307
331
|
|
|
308
332
|
#### `request_history_month(year, month)`
|
|
309
333
|
|
|
310
334
|
Ask for one calendar month of history without waiting; it arrives through `on_history`.
|
|
311
335
|
|
|
312
|
-
#### `summary(start
|
|
336
|
+
#### `summary(start, end)`
|
|
313
337
|
|
|
314
|
-
Realized trading result, deposits and withdrawals
|
|
338
|
+
Realized trading result, deposits and withdrawals between `start` and `end`, from the deal ledger. Both
|
|
339
|
+
ends are required; the window may be at most `max_history_days` long — for a longer period, add up one
|
|
340
|
+
summary per window (`balance` is the account's current balance in each).
|
|
315
341
|
|
|
316
342
|
Downloads the deal history, so it blocks; call it off your UI thread (or use `asyncio.to_thread`).
|
|
317
343
|
|
|
@@ -324,7 +350,9 @@ use `account(refresh=True)`, which does both).
|
|
|
324
350
|
|
|
325
351
|
#### `calibrate_money_scale(start, *, min_deals_per_symbol=None, max_self_scatter=None, max_disagreement=None, max_snap_distance=None)`
|
|
326
352
|
|
|
327
|
-
Measure what a price unit is worth *on this account* from its own closed deals
|
|
353
|
+
Measure what a price unit is worth *on this account* from its own closed deals from `start` to now;
|
|
354
|
+
`start` may be at most `max_history_days` ago (an older one returns `False`, the reason in
|
|
355
|
+
`money_scale_evidence`).
|
|
328
356
|
|
|
329
357
|
Read-only; places no order. Cent handling is a per-broker configuration, not a property of the currency, so
|
|
330
358
|
sizing is refused until this has succeeded. Returns `False` when the scale cannot be established (read
|
|
@@ -66,6 +66,7 @@ Every public member of the C# `MT5API` class and where it went. Generated from `
|
|
|
66
66
|
| `LastServerMessageTime` | `Client.last_server_message_time` |
|
|
67
67
|
| `LedgerValuePerUnitLot` | `Client.ledger_value_per_lot` |
|
|
68
68
|
| `LoadServersDat` | `pmt5api.load_servers_dat` |
|
|
69
|
+
| `MaxHistoryDays` | `Client.max_history_days` |
|
|
69
70
|
| `MoneyScale` | `Client.money_scale` |
|
|
70
71
|
| `MoneyScaleEvidence` | `Client.money_scale_evidence` |
|
|
71
72
|
| `MoneyScaleFromDeal` | `Client.money_scale_from_deal` |
|
|
@@ -114,7 +115,7 @@ Every public member of the C# `MT5API` class and where it went. Generated from `
|
|
|
114
115
|
| `RequestQuoteHistoryToday` | `Client.request_bars` |
|
|
115
116
|
| `RequiredMargin` | `Client.required_margin` |
|
|
116
117
|
| `SaveServersDat` | `pmt5api.save_servers_dat` |
|
|
117
|
-
| `Server` | `Client.
|
|
118
|
+
| `Server` | `Client.server_name` |
|
|
118
119
|
| `ServerSymBuild` | `Client.symbol_build` |
|
|
119
120
|
| `ServerTime` | `Client.server_time` |
|
|
120
121
|
| `ServerTimeZoneInMinutes` | `Client.server_utc_offset` |
|
|
@@ -120,6 +120,17 @@ _BARE_MESSAGES = (
|
|
|
120
120
|
_WRAPPERS = ("AggregateException", "TargetInvocationException")
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
def _window_words(text: str) -> str:
|
|
124
|
+
"""The core names the history-window arguments as a C# program sees them; say them as this package takes them."""
|
|
125
|
+
for core, ours in (("MaxHistoryDays", "max_history_days"), ("window (from)", "window (start)"),
|
|
126
|
+
("window (to)", "window (end)"), ("reversed (from ", "reversed (start "),
|
|
127
|
+
(" is after to ", " is after end ")):
|
|
128
|
+
text = text.replace(core, ours)
|
|
129
|
+
for name in ("from", "to"): # .NET appends the parameter name the text already gives
|
|
130
|
+
text = text.replace(" (Parameter '%s')" % name, "")
|
|
131
|
+
return text
|
|
132
|
+
|
|
133
|
+
|
|
123
134
|
def translate(exc: BaseException) -> Optional[WmtError]:
|
|
124
135
|
"""Return the pmt5api error that corresponds to a core exception, or ``None`` if it is not one of ours.
|
|
125
136
|
|
|
@@ -151,6 +162,8 @@ def translate(exc: BaseException) -> Optional[WmtError]:
|
|
|
151
162
|
return cls(message, str(code) if code is not None else None, value)
|
|
152
163
|
if cls is ConnectError and message.startswith("Disconnected"):
|
|
153
164
|
return ConnectionLostError(message)
|
|
165
|
+
if cls is InvalidRequestError:
|
|
166
|
+
message = _window_words(message)
|
|
154
167
|
return cls(message)
|
|
155
168
|
if type(exc).__name__ == "Exception" and getattr(type(exc), "__module__", "") == "System":
|
|
156
169
|
text = str(getattr(exc, "Message", None) or exc)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|