pmt4api 1.0.0__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pmt4api-1.2.0/CHANGELOG.md +102 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/PKG-INFO +70 -18
- {pmt4api-1.0.0 → pmt4api-1.2.0}/README.md +69 -17
- {pmt4api-1.0.0 → pmt4api-1.2.0}/examples/pnl.py +5 -3
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/__init__.py +1 -1
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/__main__.py +4 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/_account.py +54 -28
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/_base.py +6 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/_coverage.py +5 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/_events.py +2 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/_risk.py +14 -2
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/_runtime.py +92 -8
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/_session.py +2 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/_streams.py +44 -10
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/_trading.py +5 -3
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/_util.py +5 -3
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/bars.py +7 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/client.py +13 -5
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/diagnostics.py +8 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/docs/API.md +112 -22
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/docs/FROM-CSHARP.md +3 -1
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/errors.py +25 -2
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/models.py +78 -2
- pmt4api-1.0.0/CHANGELOG.md +0 -17
- {pmt4api-1.0.0 → pmt4api-1.2.0}/LICENSE.txt +0 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/examples/bars.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/examples/connect.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/examples/servers.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/examples/sizing.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/examples/stream_async.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/examples/stream_quotes.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/examples/trade_demo.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/hatch_build.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/pyproject.toml +0 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/_market.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/frames.py +0 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/py.typed +0 -0
- {pmt4api-1.0.0 → pmt4api-1.2.0}/src/pmt4api/servers.py +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Changelog — pmt4api
|
|
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
|
+
|
|
42
|
+
## [1.1.2] — 2026-10-15
|
|
43
|
+
|
|
44
|
+
From the delta re-test of 1.1.1 (no S1 or S2). Python layer only: the cores are unchanged.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
- `Order` money fields (profit, swap, commission) carry no binary noise.
|
|
48
|
+
- README: a connect to an unreachable licence service waits several times `token_timeout` (about 44 s at the default 10: the retry, and each server of the list).
|
|
49
|
+
|
|
50
|
+
## [1.1.1] — 2026-10-14
|
|
51
|
+
|
|
52
|
+
From the Windows re-test of 1.1.0 (13 notes, one S2). Bundles wmt4api 3.3.1.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
- A licence request no longer parks its connection (core `wmt4api 3.3.1`): repeated `history()` calls held about four
|
|
56
|
+
handles each until the next garbage collection.
|
|
57
|
+
- A missing .NET runtime on a **32-bit** Python says that 32-bit Python needs the 32-bit runtime.
|
|
58
|
+
- A quote-history timeout says how long it waited.
|
|
59
|
+
- README: how to read why a position closed (`[sl]` / `[tp]` in the comment), Ctrl+C during `connect()`, the licence
|
|
60
|
+
request retry.
|
|
61
|
+
|
|
62
|
+
## [1.1.0] — 2026-10-13
|
|
63
|
+
|
|
64
|
+
Found by a customer-style test on Windows Server 2022 / Python 3.10 (28 findings); every one is addressed. Bundles
|
|
65
|
+
wmt4api 3.3.0. Additive: nothing that worked in 1.0.0 is removed.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
- **Two packages could not start the .NET runtime from two threads at once** (`RuntimeNotFoundError` in one of them,
|
|
69
|
+
every time, on Linux as well). One lock now covers every `pmt*api` package in the process.
|
|
70
|
+
- **`bars()` re-aggregated the whole series after every block** (MT5): a deep request was quadratic and timed out
|
|
71
|
+
(`D1` × 1000 took over two minutes of CPU). It counts as the blocks arrive and aggregates once; the default budget
|
|
72
|
+
is `bars_timeout` plus a second per two weeks of one-minute bars the request needs.
|
|
73
|
+
- **A closed client left a thread behind** for every client that had used callbacks or `quotes()`: the callback
|
|
74
|
+
thread now ends at `disconnect()` (and when the client is dropped) and returns with the next event.
|
|
75
|
+
- **Type checkers saw every method as `(*Any, **Any) -> Any`**: the decorator that translates errors now keeps the
|
|
76
|
+
signature.
|
|
77
|
+
- Messages: a missing .NET runtime names the runtime and where to get it; the no-key text speaks `api_key=`; a bars
|
|
78
|
+
timeout states its real wait; a trade refusal names its code.
|
|
79
|
+
- `python -m pmt4api --connect` / `pmt5api --connect` accept `host:port` in `PMT*_HOST`.
|
|
80
|
+
- The reference gains a meaning for every field of the main data types.
|
|
81
|
+
- `close()`, `modify()` and `cancel()` return the order they acted on, the same on both packages (`pmt5api`: `close()` returned an empty record and `modify()` `None`; `pmt4api`: `cancel()` returned `None`).
|
|
82
|
+
- The core fixes: see wmt4api 3.3.0 — **the account balance and credit now follow closes, deposits and withdrawals in the session** (they stayed at their value from connect).
|
|
83
|
+
|
|
84
|
+
### Added
|
|
85
|
+
- `lots_for_risk(..., side="buy" | "sell")`: a stop on the wrong side of the entry is refused instead of being sized by its distance.
|
|
86
|
+
- `reconnect_max_backoff` (seconds, default 30): the longest delay between reconnect attempts.
|
|
87
|
+
|
|
88
|
+
## [1.0.0] — 2026-10-12
|
|
89
|
+
First release: the complete Python interface to wmt4api 3.2.0 (the MetaTrader 4 client library).
|
|
90
|
+
|
|
91
|
+
- Every public member and type of the C# `MT4API` / `OrderClient` classes is mapped to a Python counterpart, or has a
|
|
92
|
+
stated reason for not having one (`pmt4api.COVERAGE`, `pmt4api.TYPES`; a test fails when the core gains an
|
|
93
|
+
unaccounted member).
|
|
94
|
+
- Market data (symbols, quotes, bars on the nine MT4 timeframes with paging), account, sizing and risk, order entry
|
|
95
|
+
(market, pending, modify, partial close, close-by, close-all-by, cancel; synchronous, with request ids and progress
|
|
96
|
+
events, and awaitable), history, summary and equity curve.
|
|
97
|
+
- Callbacks, sync and async iterators, one ordered dispatcher thread per client, with back-pressure accounting.
|
|
98
|
+
- A small error hierarchy; the MT4 trade-return codes by name; requotes carry the new prices.
|
|
99
|
+
- `auto_reconnect`, `servers=` failover, a silence watchdog, `reconnect()`; `disconnect()` is final.
|
|
100
|
+
- Refusals before anything is sent: a magic number the core cannot transmit, a volume the wire cannot carry, a
|
|
101
|
+
comment that would not read back.
|
|
102
|
+
- Wheels: Linux x86-64 with the core and a .NET runtime bundled, and a pure wheel that carries the core.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pmt4api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.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
|
|
@@ -39,19 +39,33 @@ both reads alike.
|
|
|
39
39
|
|
|
40
40
|
**Parity with the C# library is measured, not claimed:** `pmt4api.COVERAGE` maps every public member of the C#
|
|
41
41
|
`MT4API` and `OrderClient` classes to its Python counterpart (or states why it has none), and the test suite fails
|
|
42
|
-
if the core gains a member that is not accounted for.
|
|
43
|
-
that table;
|
|
42
|
+
if the core gains a member that is not accounted for. `pmt4api/docs/FROM-CSHARP.md` (inside the installed package) is
|
|
43
|
+
that table; `pmt4api/docs/API.md` (inside the installed package; `python -m pmt4api` prints its folder) is the reference.
|
|
44
44
|
|
|
45
45
|
## Install
|
|
46
46
|
|
|
47
|
-
The **Linux x86-64 wheel** carries everything — the core and a .NET runtime — so there is nothing else to install:
|
|
48
|
-
|
|
49
47
|
```
|
|
50
|
-
pip install pmt4api
|
|
48
|
+
pip install pmt4api
|
|
51
49
|
```
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
the
|
|
51
|
+
pip chooses the wheel for your platform. The **Linux x86-64 wheel** carries everything — the core and a .NET runtime —
|
|
52
|
+
so there is nothing else to install. On any other platform the `py3-none-any` wheel carries the core and needs a .NET 8
|
|
53
|
+
(or later) runtime on the machine (`dotnet --list-runtimes` should list `Microsoft.NETCore.App`). From a downloaded
|
|
54
|
+
file: `pip install pmt4api-<version>-py3-none-<platform>.whl`.
|
|
55
|
+
|
|
56
|
+
**Windows (x64).** Install the .NET 8 runtime once (`winget install Microsoft.DotNet.Runtime.8`, or the installer from
|
|
57
|
+
https://dotnet.microsoft.com/download/dotnet/8.0), then `pip install pmt4api`. Check it with `python -m pmt4api`: it prints
|
|
58
|
+
the versions, finds the runtime and constructs a client offline. Set the key in the shell that runs your program —
|
|
59
|
+
PowerShell: `$env:WMT4API_KEY = "wk_..."`, cmd: `set WMT4API_KEY=wk_...`. A string the broker sent can hold characters your
|
|
60
|
+
console's code page cannot print: set `PYTHONUTF8=1` (or run `chcp 65001`) to avoid a `UnicodeEncodeError`.
|
|
61
|
+
|
|
62
|
+
**Windows (x64).** Use the `py3-none-any` wheel from PyPI — `pip install pmt4api` picks it — and install the .NET 8
|
|
63
|
+
runtime once (`winget install Microsoft.DotNet.Runtime.8`, or the installer from
|
|
64
|
+
https://dotnet.microsoft.com/download/dotnet/8.0); `dotnet --list-runtimes` must list `Microsoft.NETCore.App 8.x` or
|
|
65
|
+
later. Check the install with `python -m pmt4api`: it prints the versions, finds the runtime and constructs a client
|
|
66
|
+
offline. Set the key in the shell that runs your program — PowerShell: `$env:WMT4API_KEY = "wk_..."`, cmd:
|
|
67
|
+
`set WMT4API_KEY=wk_...`. A library string can hold characters your console code page cannot print: set
|
|
68
|
+
`PYTHONUTF8=1` (or run `chcp 65001`) to avoid a `UnicodeEncodeError` when you print what the broker sent.
|
|
55
69
|
|
|
56
70
|
You also need your **API key** (`wk_…`, the same key you use with wmt4api):
|
|
57
71
|
|
|
@@ -104,6 +118,10 @@ Timeframes: `M1 M5 M15 M30 H1 H4 D1 W1 MN1` — the nine the MT4 server serves;
|
|
|
104
118
|
(M1 is typically a few days), so a long request returns what exists; one request carries at most 32 767 bars and
|
|
105
119
|
longer requests page backwards for you. `volume` is the tick volume.
|
|
106
120
|
|
|
121
|
+
**MT4 and MT5 differ here.** The MT4 package returns completed bars only and excludes a bar that opens at `end=`; on
|
|
122
|
+
MT5 `end=` includes the bar stamped exactly `end` and the bar still forming. Code that runs on both has to drop the
|
|
123
|
+
forming MT5 bar itself.
|
|
124
|
+
|
|
107
125
|
### Live quotes
|
|
108
126
|
|
|
109
127
|
```python
|
|
@@ -146,14 +164,16 @@ drop the oldest item when the consumer falls behind, and detach on `close()` / `
|
|
|
146
164
|
## Trading
|
|
147
165
|
|
|
148
166
|
```python
|
|
167
|
+
from datetime import timedelta
|
|
168
|
+
|
|
149
169
|
o = c.buy("EURUSDm", 0.10, sl=1.0950, tp=1.1100, comment="bot-1") # market order at the ask; sell() at the bid
|
|
150
|
-
c.modify(o.ticket, sl=1.0970) # change stops
|
|
151
|
-
c.close(o.ticket, 0.05) # close half; close(ticket) closes it all
|
|
170
|
+
c.modify(o.ticket, sl=1.0970) # change stops (returns the order as the server holds it)
|
|
171
|
+
c.close(o.ticket, 0.05) # close half (returns the closing record); close(ticket) closes it all
|
|
152
172
|
|
|
153
173
|
p = c.place_order("EURUSDm", "buy_stop", 0.10, 1.1200, sl=1.1150,
|
|
154
174
|
expiration=timedelta(days=2)) # also buy_limit sell_limit sell_stop; a datetime works too
|
|
155
175
|
c.modify(p.ticket, price=1.1210) # a pending order's price; its expiration is kept
|
|
156
|
-
c.cancel(p.ticket)
|
|
176
|
+
c.cancel(p.ticket) # returns the order that was deleted
|
|
157
177
|
|
|
158
178
|
c.close_by(a.ticket, b.ticket) # close one position against an opposite one (hedged accounts)
|
|
159
179
|
c.close_all_by("EURUSDm") # ... every opposite pair of the symbol
|
|
@@ -192,7 +212,7 @@ remainder of the position may carry a new ticket: read `positions()`.
|
|
|
192
212
|
## Sizing and risk
|
|
193
213
|
|
|
194
214
|
```python
|
|
195
|
-
r = c.lots_for_risk("EURUSDm", entry_price=1.1000, stop_price=1.0950, risk_amount=50.0)
|
|
215
|
+
r = c.lots_for_risk("EURUSDm", entry_price=1.1000, stop_price=1.0950, risk_amount=50.0, side="buy")
|
|
196
216
|
for w in r.warnings:
|
|
197
217
|
print("warning:", w) # ok can still be True with a caveat (the broker's minimum volume risks more)
|
|
198
218
|
if r.ok:
|
|
@@ -215,22 +235,38 @@ cent-account scaling, so check the first result against your terminal. Also: `re
|
|
|
215
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) |
|
|
216
236
|
| `orders()` / `positions()` / `pending_orders()` / `order(ticket)` | `list[Order]` working / open positions / pending / one |
|
|
217
237
|
| `history(start, end)` | `list[Order]` — closed orders and balance / credit entries, by close time |
|
|
218
|
-
| `summary(start
|
|
238
|
+
| `summary(start, end)` | `AccountSummary` — realized profit, deposits, withdrawals |
|
|
219
239
|
| `equity_history(start, "D1")` | `list[EquityPoint]` — balance (exact) and equity (reconstructed) curve with drawdowns |
|
|
220
240
|
|
|
221
241
|
MT4 keeps one record for open positions, pending orders, closed orders and ledger entries: `Order.order_type` tells
|
|
222
242
|
them apart (`Buy` / `Sell`, `BuyLimit` … , `Balance` / `Credit`), and `is_market`, `is_pending`, `is_capital_flow`
|
|
223
243
|
say it directly. Times are naive `datetime` values in the **broker's server time**; an unset time is `None`.
|
|
224
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
|
+
|
|
225
258
|
`history()`, `summary()` and `equity_history()` each open their own short-lived broker connection and cost one
|
|
226
259
|
licence request, which the licence service rate-limits per minute: fetch once and reuse the result. `history()`
|
|
227
|
-
takes a `threading.Event` as `cancel=` to abandon the wait.
|
|
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.
|
|
228
262
|
|
|
229
263
|
## Events
|
|
230
264
|
|
|
231
265
|
`on_quote`, `on_order_update`, `on_order_progress`, `on_connect`, `on_disconnect`, `on_quote_history`,
|
|
232
266
|
`on_symbols_update` — each returns a function that removes the callback. `quotes()`, `order_updates()` and
|
|
233
|
-
`order_progress()` (and the `a…` async versions) turn a stream into an iterator.
|
|
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.
|
|
234
270
|
|
|
235
271
|
## Staying connected
|
|
236
272
|
|
|
@@ -251,15 +287,23 @@ restored, cached snapshots are not. `disconnect()` is final: it also stops a rec
|
|
|
251
287
|
| API key | `api_key=` | `WMT4API_KEY` (then `WMTAPI_KEY`) |
|
|
252
288
|
| Licence service URL (leave at its default) | `token_url=` | `WMT4API_TOKEN_URL` |
|
|
253
289
|
| Core library (only if you do not use a wheel that carries it) | `dll_path=` | `WMT4API_DLL` |
|
|
254
|
-
| Timeouts (seconds, each > 0) | `connect_timeout` (one attempt on one server), `token_timeout
|
|
255
|
-
| Reconnect | `auto_reconnect`, `reconnect_backoff`, `reconnect_max_attempts
|
|
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) | — |
|
|
291
|
+
| Reconnect | `auto_reconnect`, `reconnect_backoff` (3, doubling up to `reconnect_max_backoff`, 30), `reconnect_max_attempts` (0 = no limit), `servers=` | — |
|
|
256
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 | — |
|
|
257
294
|
| .NET runtime location | — | `DOTNET_ROOT` |
|
|
258
295
|
|
|
259
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
|
|
260
297
|
.NET runtime is taken from `DOTNET_ROOT` if you set it, else the runtime bundled in the wheel, else an installed one.
|
|
261
298
|
The bundled runtime runs in invariant-culture mode on machines without ICU. The MT4 core has no proxy support.
|
|
262
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
|
+
|
|
263
307
|
### Diagnostics
|
|
264
308
|
|
|
265
309
|
```python
|
|
@@ -272,7 +316,15 @@ pmt4api.enable_core_logging() # the core's own messages -> logging.getL
|
|
|
272
316
|
|
|
273
317
|
`load_servers_ini(path_or_bytes)` / `save_servers_ini(records)` read and write a terminal's `servers.ini`.
|
|
274
318
|
`python -m pmt4api` prints what is installed and whether it works here (`--connect` also reads an account, using
|
|
275
|
-
`PMT4_LOGIN`, `PMT4_PASSWORD`, `PMT4_HOST`); the output holds
|
|
319
|
+
`PMT4_LOGIN`, `PMT4_PASSWORD`, `PMT4_HOST` — a bare host, or `host:port`, or the port in `PMT4_PORT`); the output holds
|
|
320
|
+
no secrets.
|
|
321
|
+
|
|
322
|
+
## What a closed order says about why
|
|
323
|
+
|
|
324
|
+
An MT4 server pushes the same `PositionClose` update for a stop-loss, a take-profit and a close you asked for; the
|
|
325
|
+
reason is in the order's `comment`, which the server suffixes with `[sl]` or `[tp]` for a stop or a take-profit hit.
|
|
326
|
+
Pressing Ctrl+C while `connect()` is dialling is honoured when the dial ends (up to `connect_timeout`), not at once.
|
|
327
|
+
`token_timeout` bounds one licence request; a failed request is tried again and a connect tries each server of the list, so a connect to an unreachable licence service can wait several times that (about 44 s at the default 10).
|
|
276
328
|
|
|
277
329
|
## Errors
|
|
278
330
|
|
|
@@ -12,19 +12,33 @@ both reads alike.
|
|
|
12
12
|
|
|
13
13
|
**Parity with the C# library is measured, not claimed:** `pmt4api.COVERAGE` maps every public member of the C#
|
|
14
14
|
`MT4API` and `OrderClient` classes to its Python counterpart (or states why it has none), and the test suite fails
|
|
15
|
-
if the core gains a member that is not accounted for.
|
|
16
|
-
that table;
|
|
15
|
+
if the core gains a member that is not accounted for. `pmt4api/docs/FROM-CSHARP.md` (inside the installed package) is
|
|
16
|
+
that table; `pmt4api/docs/API.md` (inside the installed package; `python -m pmt4api` prints its folder) is the reference.
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
20
|
-
The **Linux x86-64 wheel** carries everything — the core and a .NET runtime — so there is nothing else to install:
|
|
21
|
-
|
|
22
20
|
```
|
|
23
|
-
pip install pmt4api
|
|
21
|
+
pip install pmt4api
|
|
24
22
|
```
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
the
|
|
24
|
+
pip chooses the wheel for your platform. The **Linux x86-64 wheel** carries everything — the core and a .NET runtime —
|
|
25
|
+
so there is nothing else to install. On any other platform the `py3-none-any` wheel carries the core and needs a .NET 8
|
|
26
|
+
(or later) runtime on the machine (`dotnet --list-runtimes` should list `Microsoft.NETCore.App`). From a downloaded
|
|
27
|
+
file: `pip install pmt4api-<version>-py3-none-<platform>.whl`.
|
|
28
|
+
|
|
29
|
+
**Windows (x64).** Install the .NET 8 runtime once (`winget install Microsoft.DotNet.Runtime.8`, or the installer from
|
|
30
|
+
https://dotnet.microsoft.com/download/dotnet/8.0), then `pip install pmt4api`. Check it with `python -m pmt4api`: it prints
|
|
31
|
+
the versions, finds the runtime and constructs a client offline. Set the key in the shell that runs your program —
|
|
32
|
+
PowerShell: `$env:WMT4API_KEY = "wk_..."`, cmd: `set WMT4API_KEY=wk_...`. A string the broker sent can hold characters your
|
|
33
|
+
console's code page cannot print: set `PYTHONUTF8=1` (or run `chcp 65001`) to avoid a `UnicodeEncodeError`.
|
|
34
|
+
|
|
35
|
+
**Windows (x64).** Use the `py3-none-any` wheel from PyPI — `pip install pmt4api` picks it — and install the .NET 8
|
|
36
|
+
runtime once (`winget install Microsoft.DotNet.Runtime.8`, or the installer from
|
|
37
|
+
https://dotnet.microsoft.com/download/dotnet/8.0); `dotnet --list-runtimes` must list `Microsoft.NETCore.App 8.x` or
|
|
38
|
+
later. Check the install with `python -m pmt4api`: it prints the versions, finds the runtime and constructs a client
|
|
39
|
+
offline. Set the key in the shell that runs your program — PowerShell: `$env:WMT4API_KEY = "wk_..."`, cmd:
|
|
40
|
+
`set WMT4API_KEY=wk_...`. A library string can hold characters your console code page cannot print: set
|
|
41
|
+
`PYTHONUTF8=1` (or run `chcp 65001`) to avoid a `UnicodeEncodeError` when you print what the broker sent.
|
|
28
42
|
|
|
29
43
|
You also need your **API key** (`wk_…`, the same key you use with wmt4api):
|
|
30
44
|
|
|
@@ -77,6 +91,10 @@ Timeframes: `M1 M5 M15 M30 H1 H4 D1 W1 MN1` — the nine the MT4 server serves;
|
|
|
77
91
|
(M1 is typically a few days), so a long request returns what exists; one request carries at most 32 767 bars and
|
|
78
92
|
longer requests page backwards for you. `volume` is the tick volume.
|
|
79
93
|
|
|
94
|
+
**MT4 and MT5 differ here.** The MT4 package returns completed bars only and excludes a bar that opens at `end=`; on
|
|
95
|
+
MT5 `end=` includes the bar stamped exactly `end` and the bar still forming. Code that runs on both has to drop the
|
|
96
|
+
forming MT5 bar itself.
|
|
97
|
+
|
|
80
98
|
### Live quotes
|
|
81
99
|
|
|
82
100
|
```python
|
|
@@ -119,14 +137,16 @@ drop the oldest item when the consumer falls behind, and detach on `close()` / `
|
|
|
119
137
|
## Trading
|
|
120
138
|
|
|
121
139
|
```python
|
|
140
|
+
from datetime import timedelta
|
|
141
|
+
|
|
122
142
|
o = c.buy("EURUSDm", 0.10, sl=1.0950, tp=1.1100, comment="bot-1") # market order at the ask; sell() at the bid
|
|
123
|
-
c.modify(o.ticket, sl=1.0970) # change stops
|
|
124
|
-
c.close(o.ticket, 0.05) # close half; close(ticket) closes it all
|
|
143
|
+
c.modify(o.ticket, sl=1.0970) # change stops (returns the order as the server holds it)
|
|
144
|
+
c.close(o.ticket, 0.05) # close half (returns the closing record); close(ticket) closes it all
|
|
125
145
|
|
|
126
146
|
p = c.place_order("EURUSDm", "buy_stop", 0.10, 1.1200, sl=1.1150,
|
|
127
147
|
expiration=timedelta(days=2)) # also buy_limit sell_limit sell_stop; a datetime works too
|
|
128
148
|
c.modify(p.ticket, price=1.1210) # a pending order's price; its expiration is kept
|
|
129
|
-
c.cancel(p.ticket)
|
|
149
|
+
c.cancel(p.ticket) # returns the order that was deleted
|
|
130
150
|
|
|
131
151
|
c.close_by(a.ticket, b.ticket) # close one position against an opposite one (hedged accounts)
|
|
132
152
|
c.close_all_by("EURUSDm") # ... every opposite pair of the symbol
|
|
@@ -165,7 +185,7 @@ remainder of the position may carry a new ticket: read `positions()`.
|
|
|
165
185
|
## Sizing and risk
|
|
166
186
|
|
|
167
187
|
```python
|
|
168
|
-
r = c.lots_for_risk("EURUSDm", entry_price=1.1000, stop_price=1.0950, risk_amount=50.0)
|
|
188
|
+
r = c.lots_for_risk("EURUSDm", entry_price=1.1000, stop_price=1.0950, risk_amount=50.0, side="buy")
|
|
169
189
|
for w in r.warnings:
|
|
170
190
|
print("warning:", w) # ok can still be True with a caveat (the broker's minimum volume risks more)
|
|
171
191
|
if r.ok:
|
|
@@ -188,22 +208,38 @@ cent-account scaling, so check the first result against your terminal. Also: `re
|
|
|
188
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) |
|
|
189
209
|
| `orders()` / `positions()` / `pending_orders()` / `order(ticket)` | `list[Order]` working / open positions / pending / one |
|
|
190
210
|
| `history(start, end)` | `list[Order]` — closed orders and balance / credit entries, by close time |
|
|
191
|
-
| `summary(start
|
|
211
|
+
| `summary(start, end)` | `AccountSummary` — realized profit, deposits, withdrawals |
|
|
192
212
|
| `equity_history(start, "D1")` | `list[EquityPoint]` — balance (exact) and equity (reconstructed) curve with drawdowns |
|
|
193
213
|
|
|
194
214
|
MT4 keeps one record for open positions, pending orders, closed orders and ledger entries: `Order.order_type` tells
|
|
195
215
|
them apart (`Buy` / `Sell`, `BuyLimit` … , `Balance` / `Credit`), and `is_market`, `is_pending`, `is_capital_flow`
|
|
196
216
|
say it directly. Times are naive `datetime` values in the **broker's server time**; an unset time is `None`.
|
|
197
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
|
+
|
|
198
231
|
`history()`, `summary()` and `equity_history()` each open their own short-lived broker connection and cost one
|
|
199
232
|
licence request, which the licence service rate-limits per minute: fetch once and reuse the result. `history()`
|
|
200
|
-
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.
|
|
201
235
|
|
|
202
236
|
## Events
|
|
203
237
|
|
|
204
238
|
`on_quote`, `on_order_update`, `on_order_progress`, `on_connect`, `on_disconnect`, `on_quote_history`,
|
|
205
239
|
`on_symbols_update` — each returns a function that removes the callback. `quotes()`, `order_updates()` and
|
|
206
|
-
`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.
|
|
207
243
|
|
|
208
244
|
## Staying connected
|
|
209
245
|
|
|
@@ -224,15 +260,23 @@ restored, cached snapshots are not. `disconnect()` is final: it also stops a rec
|
|
|
224
260
|
| API key | `api_key=` | `WMT4API_KEY` (then `WMTAPI_KEY`) |
|
|
225
261
|
| Licence service URL (leave at its default) | `token_url=` | `WMT4API_TOKEN_URL` |
|
|
226
262
|
| Core library (only if you do not use a wheel that carries it) | `dll_path=` | `WMT4API_DLL` |
|
|
227
|
-
| Timeouts (seconds, each > 0) | `connect_timeout` (one attempt on one server), `token_timeout
|
|
228
|
-
| Reconnect | `auto_reconnect`, `reconnect_backoff`, `reconnect_max_attempts
|
|
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) | — |
|
|
264
|
+
| Reconnect | `auto_reconnect`, `reconnect_backoff` (3, doubling up to `reconnect_max_backoff`, 30), `reconnect_max_attempts` (0 = no limit), `servers=` | — |
|
|
229
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 | — |
|
|
230
267
|
| .NET runtime location | — | `DOTNET_ROOT` |
|
|
231
268
|
|
|
232
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
|
|
233
270
|
.NET runtime is taken from `DOTNET_ROOT` if you set it, else the runtime bundled in the wheel, else an installed one.
|
|
234
271
|
The bundled runtime runs in invariant-culture mode on machines without ICU. The MT4 core has no proxy support.
|
|
235
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
|
+
|
|
236
280
|
### Diagnostics
|
|
237
281
|
|
|
238
282
|
```python
|
|
@@ -245,7 +289,15 @@ pmt4api.enable_core_logging() # the core's own messages -> logging.getL
|
|
|
245
289
|
|
|
246
290
|
`load_servers_ini(path_or_bytes)` / `save_servers_ini(records)` read and write a terminal's `servers.ini`.
|
|
247
291
|
`python -m pmt4api` prints what is installed and whether it works here (`--connect` also reads an account, using
|
|
248
|
-
`PMT4_LOGIN`, `PMT4_PASSWORD`, `PMT4_HOST`); the output holds
|
|
292
|
+
`PMT4_LOGIN`, `PMT4_PASSWORD`, `PMT4_HOST` — a bare host, or `host:port`, or the port in `PMT4_PORT`); the output holds
|
|
293
|
+
no secrets.
|
|
294
|
+
|
|
295
|
+
## What a closed order says about why
|
|
296
|
+
|
|
297
|
+
An MT4 server pushes the same `PositionClose` update for a stop-loss, a take-profit and a close you asked for; the
|
|
298
|
+
reason is in the order's `comment`, which the server suffixes with `[sl]` or `[tp]` for a stop or a take-profit hit.
|
|
299
|
+
Pressing Ctrl+C while `connect()` is dialling is honoured when the dial ends (up to `connect_timeout`), not at once.
|
|
300
|
+
`token_timeout` bounds one licence request; a failed request is tried again and a connect tries each server of the list, so a connect to an unreachable licence service can wait several times that (about 44 s at the default 10).
|
|
249
301
|
|
|
250
302
|
## Errors
|
|
251
303
|
|
|
@@ -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",
|
|
@@ -62,6 +62,10 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
62
62
|
print("connect SKIPPED: environment variable %s is not set" % e)
|
|
63
63
|
return 0
|
|
64
64
|
port = int(os.environ.get("PMT4_PORT", "443"))
|
|
65
|
+
if ":" in host: # an account file lists "host:port"
|
|
66
|
+
host, _, given = host.rpartition(":")
|
|
67
|
+
if given.isdigit():
|
|
68
|
+
port = int(given)
|
|
65
69
|
try:
|
|
66
70
|
with Client(login, password, host, port, connect_timeout=45) as live:
|
|
67
71
|
a = live.account()
|
|
@@ -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,
|