pmt5api 1.0.0__tar.gz → 1.2.0__tar.gz

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