pmt4api 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.
- {pmt4api-1.1.2 → pmt4api-1.2.0}/CHANGELOG.md +39 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/PKG-INFO +28 -4
- {pmt4api-1.1.2 → pmt4api-1.2.0}/README.md +27 -3
- {pmt4api-1.1.2 → pmt4api-1.2.0}/examples/pnl.py +5 -3
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/__init__.py +1 -1
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/_account.py +54 -28
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/_coverage.py +4 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/_runtime.py +70 -8
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/_streams.py +5 -4
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/client.py +6 -1
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/diagnostics.py +8 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/docs/API.md +30 -12
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/docs/FROM-CSHARP.md +2 -1
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/errors.py +12 -1
- {pmt4api-1.1.2 → pmt4api-1.2.0}/LICENSE.txt +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/examples/bars.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/examples/connect.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/examples/servers.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/examples/sizing.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/examples/stream_async.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/examples/stream_quotes.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/examples/trade_demo.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/hatch_build.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/pyproject.toml +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/__main__.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/_base.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/_events.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/_market.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/_risk.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/_session.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/_trading.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/_util.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/bars.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/frames.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/models.py +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/py.typed +0 -0
- {pmt4api-1.1.2 → pmt4api-1.2.0}/src/pmt4api/servers.py +0 -0
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog — pmt4api
|
|
2
2
|
|
|
3
|
+
## [1.2.0] — 2026-11-06
|
|
4
|
+
|
|
5
|
+
Bundles **wmt4api 3.4.0**. 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 downloaded every order since 1970 for it, on every call).
|
|
11
|
+
A missing or reversed window raises `InvalidRequestError` before anything is sent. `ahistory` likewise.
|
|
12
|
+
- **One call covers at most `max_history_days` (366 by default).** A longer window raises `InvalidRequestError`
|
|
13
|
+
before a connection is opened or a licence request spent. Ask window by window and add the results up, or set
|
|
14
|
+
`Client(max_history_days=...)` / `c.max_history_days` when you mean one long download (`0` removes the limit; the
|
|
15
|
+
ends stay required). `equity_history(start)` runs to now, so its start is at most that long ago.
|
|
16
|
+
- What to change: `c.summary()` → `c.summary(start, end)`; `c.history()` → `c.history(start, end)` (for "up to now",
|
|
17
|
+
`end = c.server_time`).
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- `Client.max_history_days` (read and write) and the constructor argument of the same name.
|
|
21
|
+
|
|
22
|
+
### Fixed (this package)
|
|
23
|
+
- **The platform wheel could not start on a machine that also has a newer .NET installed on `PATH`** (for example a
|
|
24
|
+
system .NET 10 next to the bundled .NET 9): the loader under pythonnet asked that `dotnet` for its runtimes, took
|
|
25
|
+
the newest and looked for it in the bundled runtime — `RuntimeNotFoundError` ("needs the .NET 8 (or later)
|
|
26
|
+
runtime"), on 1.1.2 too. The runtime is now started at a version the chosen runtime folder holds, and the message
|
|
27
|
+
says which runtime it tried.
|
|
28
|
+
|
|
29
|
+
### Fixed (core wmt4api 3.4.0)
|
|
30
|
+
- **Order updates could reach `on_order_update` / `order_updates()` out of order** — a close before its open included
|
|
31
|
+
(seen on IC Markets: a commission modify before the open). They now arrive in the order the server sent them; the
|
|
32
|
+
same holds for quotes, order progress and connect / disconnect.
|
|
33
|
+
- **`equity_history()` returned an empty list for a window with no closed order.** It is a flat line at the balance
|
|
34
|
+
(plus the floating P/L of open positions), the last point the live balance and equity — as in `pmt5api`.
|
|
35
|
+
|
|
36
|
+
### Security (core wmt4api 3.4.0)
|
|
37
|
+
- `token_url` must be `https` (plain `http` only for a loopback host) and carry no user information, query or
|
|
38
|
+
fragment; redirects from the licence service are not followed (they would re-send the key).
|
|
39
|
+
- A login already bound to another trading account under your licence is refused with `LicenseError` reason
|
|
40
|
+
`AccountLimitReached` and a message that says so.
|
|
41
|
+
|
|
3
42
|
## [1.1.2] — 2026-10-15
|
|
4
43
|
|
|
5
44
|
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: pmt4api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
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
|
|
@@ -235,22 +235,38 @@ cent-account scaling, so check the first result against your terminal. Also: `re
|
|
|
235
235
|
| `balance`, `equity`, `margin`, `free_margin`, `margin_level`, `profit` | the same figures as cheap reads from the cache (no refresh; the margin may still wait for a conversion pair's first tick) |
|
|
236
236
|
| `orders()` / `positions()` / `pending_orders()` / `order(ticket)` | `list[Order]` working / open positions / pending / one |
|
|
237
237
|
| `history(start, end)` | `list[Order]` — closed orders and balance / credit entries, by close time |
|
|
238
|
-
| `summary(start
|
|
238
|
+
| `summary(start, end)` | `AccountSummary` — realized profit, deposits, withdrawals |
|
|
239
239
|
| `equity_history(start, "D1")` | `list[EquityPoint]` — balance (exact) and equity (reconstructed) curve with drawdowns |
|
|
240
240
|
|
|
241
241
|
MT4 keeps one record for open positions, pending orders, closed orders and ledger entries: `Order.order_type` tells
|
|
242
242
|
them apart (`Buy` / `Sell`, `BuyLimit` … , `Balance` / `Credit`), and `is_market`, `is_pending`, `is_capital_flow`
|
|
243
243
|
say it directly. Times are naive `datetime` values in the **broker's server time**; an unset time is `None`.
|
|
244
244
|
|
|
245
|
+
**History is asked for by window.** `history()` and `summary()` take a start and an end, and one call covers at most
|
|
246
|
+
`max_history_days` (366 by default; set it on the client, `0` removes the limit); `equity_history()` runs from its
|
|
247
|
+
start to now, so its start is at most that long ago. A missing, reversed or longer window raises
|
|
248
|
+
`InvalidRequestError` before anything is sent. For a longer period, ask window by window and add the results up:
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
from datetime import timedelta
|
|
252
|
+
|
|
253
|
+
end = c.server_time # server time; "up to now"
|
|
254
|
+
closed = c.history(end - timedelta(days=30), end)
|
|
255
|
+
year = c.summary(end - timedelta(days=365), end)
|
|
256
|
+
```
|
|
257
|
+
|
|
245
258
|
`history()`, `summary()` and `equity_history()` each open their own short-lived broker connection and cost one
|
|
246
259
|
licence request, which the licence service rate-limits per minute: fetch once and reuse the result. `history()`
|
|
247
|
-
takes a `threading.Event` as `cancel=` to abandon the wait.
|
|
260
|
+
takes a `threading.Event` as `cancel=` to abandon the wait. A window with no closed order in it gives a flat equity
|
|
261
|
+
curve at the balance, never an empty one.
|
|
248
262
|
|
|
249
263
|
## Events
|
|
250
264
|
|
|
251
265
|
`on_quote`, `on_order_update`, `on_order_progress`, `on_connect`, `on_disconnect`, `on_quote_history`,
|
|
252
266
|
`on_symbols_update` — each returns a function that removes the callback. `quotes()`, `order_updates()` and
|
|
253
|
-
`order_progress()` (and the `a…` async versions) turn a stream into an iterator.
|
|
267
|
+
`order_progress()` (and the `a…` async versions) turn a stream into an iterator. Events reach you in the order the
|
|
268
|
+
server sent them — a ticket's open before its modifications and its close — and callbacks run one at a time on the
|
|
269
|
+
client's own thread.
|
|
254
270
|
|
|
255
271
|
## Staying connected
|
|
256
272
|
|
|
@@ -274,12 +290,20 @@ restored, cached snapshots are not. `disconnect()` is final: it also stops a rec
|
|
|
274
290
|
| 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
291
|
| Reconnect | `auto_reconnect`, `reconnect_backoff` (3, doubling up to `reconnect_max_backoff`, 30), `reconnect_max_attempts` (0 = no limit), `servers=` | — |
|
|
276
292
|
| Orders | `placed_type=` (`manual`, `expert`, `dealer` …), `comment_encoding=` (default `latin-1`) | — |
|
|
293
|
+
| History window | `max_history_days=` (366; `0` = no limit) — also a client attribute | — |
|
|
277
294
|
| .NET runtime location | — | `DOTNET_ROOT` |
|
|
278
295
|
|
|
279
296
|
A core path you give (argument or `WMT4API_DLL`) is used exactly as given and is an error if it does not exist. The
|
|
280
297
|
.NET runtime is taken from `DOTNET_ROOT` if you set it, else the runtime bundled in the wheel, else an installed one.
|
|
281
298
|
The bundled runtime runs in invariant-culture mode on machines without ICU. The MT4 core has no proxy support.
|
|
282
299
|
|
|
300
|
+
**These two settings decide what code runs in your process.** `WMT4API_DLL` (or `dll_path=`) names the library that is
|
|
301
|
+
loaded, and `DOTNET_ROOT` (or, without it, the `dotnet` found on `PATH`) names the .NET runtime that is started;
|
|
302
|
+
both run with your rights. Set them only from configuration you control, and do not run with an environment
|
|
303
|
+
someone else can change. With `enable_core_logging()` switched on, the package logs once when either was chosen
|
|
304
|
+
by the environment. The licence service URL must be `https` (plain `http` only for a loopback test service), and
|
|
305
|
+
redirects from it are not followed.
|
|
306
|
+
|
|
283
307
|
### Diagnostics
|
|
284
308
|
|
|
285
309
|
```python
|
|
@@ -208,22 +208,38 @@ cent-account scaling, so check the first result against your terminal. Also: `re
|
|
|
208
208
|
| `balance`, `equity`, `margin`, `free_margin`, `margin_level`, `profit` | the same figures as cheap reads from the cache (no refresh; the margin may still wait for a conversion pair's first tick) |
|
|
209
209
|
| `orders()` / `positions()` / `pending_orders()` / `order(ticket)` | `list[Order]` working / open positions / pending / one |
|
|
210
210
|
| `history(start, end)` | `list[Order]` — closed orders and balance / credit entries, by close time |
|
|
211
|
-
| `summary(start
|
|
211
|
+
| `summary(start, end)` | `AccountSummary` — realized profit, deposits, withdrawals |
|
|
212
212
|
| `equity_history(start, "D1")` | `list[EquityPoint]` — balance (exact) and equity (reconstructed) curve with drawdowns |
|
|
213
213
|
|
|
214
214
|
MT4 keeps one record for open positions, pending orders, closed orders and ledger entries: `Order.order_type` tells
|
|
215
215
|
them apart (`Buy` / `Sell`, `BuyLimit` … , `Balance` / `Credit`), and `is_market`, `is_pending`, `is_capital_flow`
|
|
216
216
|
say it directly. Times are naive `datetime` values in the **broker's server time**; an unset time is `None`.
|
|
217
217
|
|
|
218
|
+
**History is asked for by window.** `history()` and `summary()` take a start and an end, and one call covers at most
|
|
219
|
+
`max_history_days` (366 by default; set it on the client, `0` removes the limit); `equity_history()` runs from its
|
|
220
|
+
start to now, so its start is at most that long ago. A missing, reversed or longer window raises
|
|
221
|
+
`InvalidRequestError` before anything is sent. For a longer period, ask window by window and add the results up:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from datetime import timedelta
|
|
225
|
+
|
|
226
|
+
end = c.server_time # server time; "up to now"
|
|
227
|
+
closed = c.history(end - timedelta(days=30), end)
|
|
228
|
+
year = c.summary(end - timedelta(days=365), end)
|
|
229
|
+
```
|
|
230
|
+
|
|
218
231
|
`history()`, `summary()` and `equity_history()` each open their own short-lived broker connection and cost one
|
|
219
232
|
licence request, which the licence service rate-limits per minute: fetch once and reuse the result. `history()`
|
|
220
|
-
takes a `threading.Event` as `cancel=` to abandon the wait.
|
|
233
|
+
takes a `threading.Event` as `cancel=` to abandon the wait. A window with no closed order in it gives a flat equity
|
|
234
|
+
curve at the balance, never an empty one.
|
|
221
235
|
|
|
222
236
|
## Events
|
|
223
237
|
|
|
224
238
|
`on_quote`, `on_order_update`, `on_order_progress`, `on_connect`, `on_disconnect`, `on_quote_history`,
|
|
225
239
|
`on_symbols_update` — each returns a function that removes the callback. `quotes()`, `order_updates()` and
|
|
226
|
-
`order_progress()` (and the `a…` async versions) turn a stream into an iterator.
|
|
240
|
+
`order_progress()` (and the `a…` async versions) turn a stream into an iterator. Events reach you in the order the
|
|
241
|
+
server sent them — a ticket's open before its modifications and its close — and callbacks run one at a time on the
|
|
242
|
+
client's own thread.
|
|
227
243
|
|
|
228
244
|
## Staying connected
|
|
229
245
|
|
|
@@ -247,12 +263,20 @@ restored, cached snapshots are not. `disconnect()` is final: it also stops a rec
|
|
|
247
263
|
| 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
264
|
| Reconnect | `auto_reconnect`, `reconnect_backoff` (3, doubling up to `reconnect_max_backoff`, 30), `reconnect_max_attempts` (0 = no limit), `servers=` | — |
|
|
249
265
|
| Orders | `placed_type=` (`manual`, `expert`, `dealer` …), `comment_encoding=` (default `latin-1`) | — |
|
|
266
|
+
| History window | `max_history_days=` (366; `0` = no limit) — also a client attribute | — |
|
|
250
267
|
| .NET runtime location | — | `DOTNET_ROOT` |
|
|
251
268
|
|
|
252
269
|
A core path you give (argument or `WMT4API_DLL`) is used exactly as given and is an error if it does not exist. The
|
|
253
270
|
.NET runtime is taken from `DOTNET_ROOT` if you set it, else the runtime bundled in the wheel, else an installed one.
|
|
254
271
|
The bundled runtime runs in invariant-culture mode on machines without ICU. The MT4 core has no proxy support.
|
|
255
272
|
|
|
273
|
+
**These two settings decide what code runs in your process.** `WMT4API_DLL` (or `dll_path=`) names the library that is
|
|
274
|
+
loaded, and `DOTNET_ROOT` (or, without it, the `dotnet` found on `PATH`) names the .NET runtime that is started;
|
|
275
|
+
both run with your rights. Set them only from configuration you control, and do not run with an environment
|
|
276
|
+
someone else can change. With `enable_core_logging()` switched on, the package logs once when either was chosen
|
|
277
|
+
by the environment. The licence service URL must be `https` (plain `http` only for a loopback test service), and
|
|
278
|
+
redirects from it are not followed.
|
|
279
|
+
|
|
256
280
|
### Diagnostics
|
|
257
281
|
|
|
258
282
|
```python
|
|
@@ -13,10 +13,12 @@ with Client(int(os.environ["PMT4_LOGIN"]), os.environ["PMT4_PASSWORD"], os.envir
|
|
|
13
13
|
print("balance %.2f equity %.2f floating %.2f margin %.2f free %.2f"
|
|
14
14
|
% (a.balance, a.equity, a.profit, a.margin, a.free_margin))
|
|
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
|
print("%s balance %.2f equity %.2f drawdown %.2f%%"
|
|
22
24
|
% (p.time.strftime("%Y-%m-%d %H:%M"), p.balance, p.equity, p.equity_drawdown_relative * 100))
|
|
@@ -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.2.0"
|
|
21
21
|
|
|
22
22
|
__all__ = [
|
|
23
23
|
"Client",
|
|
@@ -4,15 +4,22 @@ from __future__ import annotations
|
|
|
4
4
|
import asyncio
|
|
5
5
|
import functools
|
|
6
6
|
import threading
|
|
7
|
-
from datetime import datetime
|
|
7
|
+
from datetime import datetime
|
|
8
8
|
from typing import Any, List, Optional
|
|
9
9
|
|
|
10
10
|
from ._base import _Base
|
|
11
11
|
from ._util import enum_value, to_core_time, translated
|
|
12
|
-
from .errors import NotConnectedError, RequestCancelled
|
|
12
|
+
from .errors import InvalidRequestError, NotConnectedError, RequestCancelled
|
|
13
13
|
from .models import AccountInfo, AccountSummary, EquityPoint, Order, _time
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def _history_days(days: Any) -> int:
|
|
17
|
+
"""A ``max_history_days`` value: a whole number of days, 0 for no limit."""
|
|
18
|
+
if isinstance(days, bool) or not isinstance(days, int) or days < 0:
|
|
19
|
+
raise ValueError("max_history_days must be a whole number of days, or 0 for no limit (got %r)" % (days,))
|
|
20
|
+
return days
|
|
21
|
+
|
|
22
|
+
|
|
16
23
|
class _Cancellation:
|
|
17
24
|
"""Turns a :class:`threading.Event` into the core's ``CancellationToken`` for the duration of one call."""
|
|
18
25
|
|
|
@@ -173,27 +180,24 @@ class _AccountMixin(_Base):
|
|
|
173
180
|
# ---- history -----------------------------------------------------------------------------------------------
|
|
174
181
|
|
|
175
182
|
@translated
|
|
176
|
-
def history(self, start:
|
|
177
|
-
|
|
178
|
-
"""Closed orders and balance / credit entries between ``start`` and ``end
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
def history(self, start: datetime, end: datetime, timeout: Optional[float] = None,
|
|
184
|
+
cancel: Optional[threading.Event] = None) -> List[Order]:
|
|
185
|
+
"""Closed orders and balance / credit entries between ``start`` and ``end``, sorted by close time. Blocks until
|
|
186
|
+
the server has sent everything, up to ``timeout`` seconds (default: the client's ``history_timeout``); set the
|
|
187
|
+
``cancel`` event to abandon the wait early (:class:`RequestCancelled`).
|
|
188
|
+
|
|
189
|
+
Both ends are required and the window may be at most :attr:`max_history_days` long (366 by default): a missing,
|
|
190
|
+
reversed or longer window raises :class:`InvalidRequestError` before anything is sent. For a longer period,
|
|
191
|
+
call once per window and add the results up. For "up to now", pass ``c.server_time`` as ``end``.
|
|
182
192
|
|
|
183
193
|
Naive datetimes are used exactly as given, in the broker's server time; timezone-aware ones are converted to
|
|
184
|
-
it.
|
|
194
|
+
it.
|
|
185
195
|
|
|
186
196
|
Each call opens its own short-lived broker connection and costs one licence request, which the licence
|
|
187
197
|
service rate-limits per minute: fetch once and reuse the result rather than polling.
|
|
188
198
|
"""
|
|
189
199
|
self._require_connected()
|
|
190
|
-
|
|
191
|
-
end = self._server_naive(end)
|
|
192
|
-
start = self._server_naive(start)
|
|
193
|
-
if end is None:
|
|
194
|
-
end = now + timedelta(days=1) # nothing the server has booked is later than this
|
|
195
|
-
if start is None:
|
|
196
|
-
start = now - timedelta(days=30)
|
|
200
|
+
start, end = self._window(start, end, "history")
|
|
197
201
|
ms = int(timeout * 1000) if timeout is not None else int(self._api.DownloadOrderHistoryTimeoutMs)
|
|
198
202
|
if ms <= 0:
|
|
199
203
|
raise ValueError("timeout must be positive")
|
|
@@ -206,8 +210,7 @@ class _AccountMixin(_Base):
|
|
|
206
210
|
rows = self._api.DownloadOrderHistory(to_core_time(start), to_core_time(end), ms)
|
|
207
211
|
return [Order._from_core(o) for o in (rows or ())]
|
|
208
212
|
|
|
209
|
-
async def ahistory(self, start:
|
|
210
|
-
timeout: Optional[float] = None) -> List[Order]:
|
|
213
|
+
async def ahistory(self, start: datetime, end: datetime, timeout: Optional[float] = None) -> List[Order]:
|
|
211
214
|
"""Awaitable :meth:`history`. Cancelling the awaiting task abandons the download (the broker connection it
|
|
212
215
|
opened is closed), so ``asyncio.wait_for(c.ahistory(...), 10)`` does not leave a request running."""
|
|
213
216
|
cancel = threading.Event()
|
|
@@ -220,32 +223,55 @@ class _AccountMixin(_Base):
|
|
|
220
223
|
cancel.set()
|
|
221
224
|
raise
|
|
222
225
|
|
|
226
|
+
@property
|
|
227
|
+
def max_history_days(self) -> int:
|
|
228
|
+
"""The longest window, in days, one :meth:`history`, :meth:`summary` or :meth:`equity_history` call may ask
|
|
229
|
+
for (default 366). A longer window raises :class:`InvalidRequestError` before anything is sent: split the
|
|
230
|
+
period into several calls, or raise this when you mean one long download. ``0`` removes the limit (the start
|
|
231
|
+
and end are still required)."""
|
|
232
|
+
return int(self._api.MaxHistoryDays)
|
|
233
|
+
|
|
234
|
+
@max_history_days.setter
|
|
235
|
+
def max_history_days(self, days: int) -> None:
|
|
236
|
+
self._api.MaxHistoryDays = _history_days(days)
|
|
237
|
+
|
|
238
|
+
def _window(self, start: Optional[datetime], end: Optional[datetime], what: str) -> tuple:
|
|
239
|
+
"""``start`` and ``end`` in server time; both are required (the length is checked by the core)."""
|
|
240
|
+
if start is None or end is None:
|
|
241
|
+
raise InvalidRequestError("%s() needs a start and an end: history is always asked for by window "
|
|
242
|
+
"(at most max_history_days, %d days, per call)" % (what, self.max_history_days))
|
|
243
|
+
return self._server_naive(start), self._server_naive(end)
|
|
244
|
+
|
|
223
245
|
# ---- realized figures and curves ---------------------------------------------------------------------------
|
|
224
246
|
|
|
225
247
|
@translated
|
|
226
|
-
def summary(self, start:
|
|
227
|
-
"""Realized trading result, deposits and withdrawals
|
|
248
|
+
def summary(self, start: datetime, end: datetime) -> AccountSummary:
|
|
249
|
+
"""Realized trading result, deposits and withdrawals between ``start`` and ``end``, from the closed-order
|
|
250
|
+
ledger. Both ends are required; the window may be at most :attr:`max_history_days` long — for a longer period,
|
|
251
|
+
add up one summary per window (``balance`` is the account's current balance in each).
|
|
228
252
|
|
|
229
253
|
Downloads the history, so it blocks and costs one licence request; call it off your UI thread (or use
|
|
230
254
|
``asyncio.to_thread``).
|
|
231
255
|
"""
|
|
232
256
|
self._require_connected()
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if start is None or end is None:
|
|
236
|
-
raise ValueError("give both start and end, or neither")
|
|
237
|
-
return AccountSummary._from_core(self._api.GetAccountSummary(to_core_time(self._server_naive(start)),
|
|
238
|
-
to_core_time(self._server_naive(end))))
|
|
257
|
+
start, end = self._window(start, end, "summary")
|
|
258
|
+
return AccountSummary._from_core(self._api.GetAccountSummary(to_core_time(start), to_core_time(end)))
|
|
239
259
|
|
|
240
260
|
@translated
|
|
241
261
|
def equity_history(self, start: datetime, timeframe: str = "D1", exclude_same_bars: bool = True) -> List[EquityPoint]:
|
|
242
|
-
"""The balance and equity curve
|
|
262
|
+
"""The balance and equity curve from ``start`` to now (``timeframe`` is ``H1``, ``H4`` or ``D1``); ``start``
|
|
263
|
+
may be at most :attr:`max_history_days` ago.
|
|
243
264
|
|
|
244
265
|
``balance`` is exact; the historical ``equity`` is a reconstruction priced from the symbols' bars. With
|
|
245
266
|
``exclude_same_bars`` (the default, as in the C# library) consecutive points with an unchanged balance and
|
|
246
|
-
equity are dropped.
|
|
267
|
+
equity are dropped. A window with no closed order in it is a flat line at the balance (never empty); the last
|
|
268
|
+
point is the live balance and equity. Downloads the history and per-symbol bars, so it blocks and costs a
|
|
269
|
+
licence request.
|
|
247
270
|
"""
|
|
248
271
|
self._require_connected()
|
|
272
|
+
if start is None:
|
|
273
|
+
raise InvalidRequestError("equity_history() needs a start (at most max_history_days, %d days, ago)"
|
|
274
|
+
% self.max_history_days)
|
|
249
275
|
tf = enum_value(self._core.EquityTimeframe, timeframe, "equity timeframe")
|
|
250
276
|
with self._bars_lock: # it downloads bars through the same single-flight request as bars()
|
|
251
277
|
points = self._api.CalculateEquityHistory(to_core_time(self._server_naive(start)), tf,
|
|
@@ -42,6 +42,7 @@ COVERAGE: Dict[str, str] = {
|
|
|
42
42
|
"ConnectTimeout": "Client(connect_timeout)",
|
|
43
43
|
"GetQuoteTimeoutMs": "Client(quote_timeout)",
|
|
44
44
|
"DownloadOrderHistoryTimeoutMs": "Client(history_timeout)",
|
|
45
|
+
"MaxHistoryDays": "Client.max_history_days",
|
|
45
46
|
"DownloadQuoteHistoryTimeout": "Client(bars_timeout)",
|
|
46
47
|
"NoServerMessagesTimeout": "Client(no_message_timeout)",
|
|
47
48
|
"TradeTimeout": "Client(trade_timeout)",
|
|
@@ -182,6 +183,9 @@ NOTES: Dict[str, str] = {
|
|
|
182
183
|
"32767-bar limit; the server's own depth limit applies",
|
|
183
184
|
"DownloadOrderHistory": "`timeout=` and a `threading.Event` for `cancel=` replace the timeout and cancellation "
|
|
184
185
|
"token arguments; `ahistory` cancels with the awaiting task",
|
|
186
|
+
"GetAccountSummary": "the window overload only (`summary(start, end)`); the core's all-time overload is "
|
|
187
|
+
"obsolete and throws",
|
|
188
|
+
"MaxHistoryDays": "also a constructor argument, `Client(max_history_days=...)`",
|
|
185
189
|
"Servers": "give `servers=[(host, port), ...]` to the constructor or call `add_server`",
|
|
186
190
|
"Reconnect": "returns False when every server failed; with `auto_reconnect=True` the core does it by itself",
|
|
187
191
|
}
|
|
@@ -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 ``WMT4API_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("wmt4api") # 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 (``WMT4API_DLL``, ``DOTNET_ROOT``) chose when the core was loaded; empty if nothing."""
|
|
238
|
+
return list(_chosen_by_environment)
|
|
@@ -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.
|
|
@@ -80,6 +80,9 @@ class Client(_SessionMixin, _MarketMixin, _AccountMixin, _RiskMixin, _TradingMix
|
|
|
80
80
|
``comment_encoding`` is the codepage order comments are written in (default ``latin-1``); ``placed_type``
|
|
81
81
|
records how orders are placed (``"manual"``, ``"expert"``, ``"dealer"``; a per-call ``placed_type`` overrides it).
|
|
82
82
|
|
|
83
|
+
History is always asked for by window: :meth:`history` and :meth:`summary` take a start and an end, and one call
|
|
84
|
+
covers at most ``max_history_days`` (default 366; ``0`` removes the limit).
|
|
85
|
+
|
|
83
86
|
Callbacks run on a dispatcher thread of this client, not on the thread that created it.
|
|
84
87
|
"""
|
|
85
88
|
|
|
@@ -92,7 +95,7 @@ class Client(_SessionMixin, _MarketMixin, _AccountMixin, _RiskMixin, _TradingMix
|
|
|
92
95
|
auto_reconnect: bool = False, reconnect_backoff: Optional[float] = None,
|
|
93
96
|
reconnect_max_backoff: Optional[float] = None, reconnect_max_attempts: Optional[int] = None,
|
|
94
97
|
servers: Optional[Sequence[Tuple[str, int]]] = None, comment_encoding: str = "latin-1",
|
|
95
|
-
placed_type: Optional[str] = None):
|
|
98
|
+
placed_type: Optional[str] = None, max_history_days: Optional[int] = None):
|
|
96
99
|
core = _runtime.load(dll_path)
|
|
97
100
|
self._core = core
|
|
98
101
|
self._login = int(login)
|
|
@@ -136,6 +139,8 @@ class Client(_SessionMixin, _MarketMixin, _AccountMixin, _RiskMixin, _TradingMix
|
|
|
136
139
|
api.AutoReconnect = True
|
|
137
140
|
if placed_type:
|
|
138
141
|
self._trader.PlacedType = int(_placed(core.PlacedType, placed_type))
|
|
142
|
+
if max_history_days is not None:
|
|
143
|
+
self.max_history_days = max_history_days
|
|
139
144
|
self._comment_encoding = comment_encoding
|
|
140
145
|
api.Encoding = _net_encoding(comment_encoding)
|
|
141
146
|
for h, p in (servers or ()):
|
|
@@ -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 = "pmt4api.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, 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)
|
|
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, max_history_days=None)
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
A connection to one MetaTrader 4 account through the licensed core.
|
|
@@ -36,6 +36,9 @@ and call `connect()` in a loop. `servers` are extra `(host, port)` access server
|
|
|
36
36
|
`comment_encoding` is the codepage order comments are written in (default `latin-1`); `placed_type`
|
|
37
37
|
records how orders are placed (`"manual"`, `"expert"`, `"dealer"`; a per-call `placed_type` overrides it).
|
|
38
38
|
|
|
39
|
+
History is always asked for by window: `history` and `summary` take a start and an end, and one call
|
|
40
|
+
covers at most `max_history_days` (default 366; `0` removes the limit).
|
|
41
|
+
|
|
39
42
|
Callbacks run on a dispatcher thread of this client, not on the thread that created it.
|
|
40
43
|
|
|
41
44
|
## Connection and session
|
|
@@ -236,7 +239,7 @@ Account details. With `refresh` (the default) every open position's symbol is qu
|
|
|
236
239
|
derived on the client (the server does not push them), as the terminal does; see `equity` for the cheap
|
|
237
240
|
read of one figure.
|
|
238
241
|
|
|
239
|
-
#### `async ahistory(start
|
|
242
|
+
#### `async ahistory(start, end, timeout=None)`
|
|
240
243
|
|
|
241
244
|
Awaitable `history`. Cancelling the awaiting task abandons the download (the broker connection it
|
|
242
245
|
opened is closed), so `asyncio.wait_for(c.ahistory(...), 10)` does not leave a request running.
|
|
@@ -257,25 +260,31 @@ quotes.
|
|
|
257
260
|
|
|
258
261
|
#### `equity_history(start, timeframe='D1', exclude_same_bars=True)`
|
|
259
262
|
|
|
260
|
-
The balance and equity curve
|
|
263
|
+
The balance and equity curve from `start` to now (`timeframe` is `H1`, `H4` or `D1`); `start`
|
|
264
|
+
may be at most `max_history_days` ago.
|
|
261
265
|
|
|
262
266
|
`balance` is exact; the historical `equity` is a reconstruction priced from the symbols' bars. With
|
|
263
267
|
`exclude_same_bars` (the default, as in the C# library) consecutive points with an unchanged balance and
|
|
264
|
-
equity are dropped.
|
|
268
|
+
equity are dropped. A window with no closed order in it is a flat line at the balance (never empty); the last
|
|
269
|
+
point is the live balance and equity. Downloads the history and per-symbol bars, so it blocks and costs a
|
|
270
|
+
licence request.
|
|
265
271
|
|
|
266
272
|
#### `free_margin` *(property)*
|
|
267
273
|
|
|
268
274
|
Equity − margin.
|
|
269
275
|
|
|
270
|
-
#### `history(start
|
|
276
|
+
#### `history(start, end, timeout=None, cancel=None)`
|
|
277
|
+
|
|
278
|
+
Closed orders and balance / credit entries between `start` and `end`, sorted by close time. Blocks until
|
|
279
|
+
the server has sent everything, up to `timeout` seconds (default: the client's `history_timeout`); set the
|
|
280
|
+
`cancel` event to abandon the wait early (`RequestCancelled`).
|
|
271
281
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
(`RequestCancelled`).
|
|
282
|
+
Both ends are required and the window may be at most `max_history_days` long (366 by default): a missing,
|
|
283
|
+
reversed or longer window raises `InvalidRequestError` before anything is sent. For a longer period,
|
|
284
|
+
call once per window and add the results up. For "up to now", pass `c.server_time` as `end`.
|
|
276
285
|
|
|
277
286
|
Naive datetimes are used exactly as given, in the broker's server time; timezone-aware ones are converted to
|
|
278
|
-
it.
|
|
287
|
+
it.
|
|
279
288
|
|
|
280
289
|
Each call opens its own short-lived broker connection and costs one licence request, which the licence
|
|
281
290
|
service rate-limits per minute: fetch once and reuse the result rather than polling.
|
|
@@ -299,6 +308,13 @@ conversion quote.
|
|
|
299
308
|
|
|
300
309
|
Equity / margin × 100, as the terminal shows it; 0 when no margin is used.
|
|
301
310
|
|
|
311
|
+
#### `max_history_days` *(property)*
|
|
312
|
+
|
|
313
|
+
The longest window, in days, one `history`, `summary` or `equity_history` call may ask
|
|
314
|
+
for (default 366). A longer window raises `InvalidRequestError` before anything is sent: split the
|
|
315
|
+
period into several calls, or raise this when you mean one long download. `0` removes the limit (the start
|
|
316
|
+
and end are still required).
|
|
317
|
+
|
|
302
318
|
#### `order(ticket)`
|
|
303
319
|
|
|
304
320
|
One open order or position by ticket, or `None` when it is not open.
|
|
@@ -320,9 +336,11 @@ Open market positions only (`Buy` / `Sell`).
|
|
|
320
336
|
|
|
321
337
|
The floating P/L of the open positions (equity − balance − credit), from cached quotes.
|
|
322
338
|
|
|
323
|
-
#### `summary(start
|
|
339
|
+
#### `summary(start, end)`
|
|
324
340
|
|
|
325
|
-
Realized trading result, deposits and withdrawals
|
|
341
|
+
Realized trading result, deposits and withdrawals between `start` and `end`, from the closed-order
|
|
342
|
+
ledger. Both ends are required; the window may be at most `max_history_days` long — for a longer period,
|
|
343
|
+
add up one summary per window (`balance` is the account's current balance in each).
|
|
326
344
|
|
|
327
345
|
Downloads the history, so it blocks and costs one licence request; call it off your UI thread (or use
|
|
328
346
|
`asyncio.to_thread`).
|
|
@@ -29,7 +29,7 @@ Every public member of the C# `MT4API` and `OrderClient` classes and where it we
|
|
|
29
29
|
| `DownloadQuoteHistory` | `Client.bars` | `count=` bars ending at `end`, or everything since `start=`; pages past the wire's 32767-bar limit; the server's own depth limit applies |
|
|
30
30
|
| `DownloadQuoteHistoryTimeout` | `Client(bars_timeout)` | |
|
|
31
31
|
| `Encoding` | `Client(comment_encoding)` | |
|
|
32
|
-
| `GetAccountSummary` | `Client.summary` |
|
|
32
|
+
| `GetAccountSummary` | `Client.summary` | the window overload only (`summary(start, end)`); the core's all-time overload is obsolete and throws |
|
|
33
33
|
| `GetAskTickValue` | `Client.tick_value` | |
|
|
34
34
|
| `GetBidTickValue` | `Client.tick_value` | |
|
|
35
35
|
| `GetOpenedOrder` | `Client.order` | |
|
|
@@ -48,6 +48,7 @@ Every public member of the C# `MT4API` and `OrderClient` classes and where it we
|
|
|
48
48
|
| `LiveEquityCached` | `Client.equity` | |
|
|
49
49
|
| `LiveMarginCached` | `Client.margin` | |
|
|
50
50
|
| `LoadServersIni` | `pmt4api.load_servers_ini` | |
|
|
51
|
+
| `MaxHistoryDays` | `Client.max_history_days` | also a constructor argument, `Client(max_history_days=...)` |
|
|
51
52
|
| `NoServerMessagesTimeout` | `Client(no_message_timeout)` | |
|
|
52
53
|
| `OnConnect` | `Client.on_connect` | |
|
|
53
54
|
| `OnDisconnect` | `Client.on_disconnect` | |
|
|
@@ -171,6 +171,17 @@ def _python_words(text: str) -> str:
|
|
|
171
171
|
"pass token_url= to Client(...) or set the WMT4API_TOKEN_URL environment variable"))
|
|
172
172
|
|
|
173
173
|
|
|
174
|
+
def _window_words(text: str) -> str:
|
|
175
|
+
"""The core names the history-window arguments as a C# program sees them; say them as this package takes them."""
|
|
176
|
+
for core, ours in (("MaxHistoryDays", "max_history_days"), ("window (from)", "window (start)"),
|
|
177
|
+
("window (to)", "window (end)"), ("reversed (from ", "reversed (start "),
|
|
178
|
+
(" is after to ", " is after end ")):
|
|
179
|
+
text = text.replace(core, ours)
|
|
180
|
+
for name in ("from", "to"): # .NET appends the parameter name the text already gives
|
|
181
|
+
text = text.replace(" (Parameter '%s')" % name, "")
|
|
182
|
+
return text
|
|
183
|
+
|
|
184
|
+
|
|
174
185
|
def _status(text: str) -> Optional[int]:
|
|
175
186
|
"""The number in "... rejected (status N)" of the core's bare connect-phase refusals."""
|
|
176
187
|
if "(status " not in text:
|
|
@@ -227,7 +238,7 @@ def translate(exc: BaseException) -> Optional[WmtError]:
|
|
|
227
238
|
if kind == "cancelled":
|
|
228
239
|
return RequestCancelled(message)
|
|
229
240
|
if kind == "invalid":
|
|
230
|
-
return InvalidRequestError(message)
|
|
241
|
+
return InvalidRequestError(_window_words(message))
|
|
231
242
|
name = type(exc).__name__
|
|
232
243
|
module = getattr(type(exc), "__module__", "")
|
|
233
244
|
if name == "Exception" and module == "System":
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|