pmt5api 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pmt5api/__init__.py +34 -0
- pmt5api/__main__.py +78 -0
- pmt5api/_account.py +146 -0
- pmt5api/_base.py +24 -0
- pmt5api/_coverage.py +216 -0
- pmt5api/_events.py +147 -0
- pmt5api/_market.py +296 -0
- pmt5api/_native/CHANGELOG.md +284 -0
- pmt5api/_native/THIRD-PARTY-NOTICES.txt +92 -0
- pmt5api/_native/docs/ERRORS.md +143 -0
- pmt5api/_native/docs/LICENSING.md +67 -0
- pmt5api/_native/docs/SECURITY.md +50 -0
- pmt5api/_native/wmt5api.dll +0 -0
- pmt5api/_risk.py +116 -0
- pmt5api/_runtime.py +154 -0
- pmt5api/_session.py +175 -0
- pmt5api/_streams.py +238 -0
- pmt5api/_trading.py +367 -0
- pmt5api/_util.py +78 -0
- pmt5api/bars.py +98 -0
- pmt5api/client.py +129 -0
- pmt5api/diagnostics.py +38 -0
- pmt5api/docs/API.md +717 -0
- pmt5api/docs/FROM-CSHARP.md +131 -0
- pmt5api/errors.py +150 -0
- pmt5api/frames.py +21 -0
- pmt5api/models.py +522 -0
- pmt5api/py.typed +0 -0
- pmt5api/servers.py +73 -0
- pmt5api-1.0.0.dist-info/METADATA +339 -0
- pmt5api-1.0.0.dist-info/RECORD +33 -0
- pmt5api-1.0.0.dist-info/WHEEL +4 -0
- pmt5api-1.0.0.dist-info/licenses/LICENSE.txt +29 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Errors & troubleshooting — wmt5api
|
|
2
|
+
|
|
3
|
+
Applies to wmt5api 3.1.0.
|
|
4
|
+
|
|
5
|
+
Errors are **typed** — catch what you care about. A licence error is raised during `Connect()`, before any
|
|
6
|
+
account data is read (the broker login handshake has already taken place).
|
|
7
|
+
|
|
8
|
+
```csharp
|
|
9
|
+
try
|
|
10
|
+
{
|
|
11
|
+
api.Connect();
|
|
12
|
+
}
|
|
13
|
+
catch (LicenseException ex) { /* licence refused — inspect ex.Reason */ }
|
|
14
|
+
catch (TokenServiceException ex) { /* licence service unreachable or unusable reply — retry */ }
|
|
15
|
+
catch (TimeoutException ex) { /* the host did not reply within ConnectTimeout */ }
|
|
16
|
+
catch (ConnectException ex) { /* cannot connect, or the connection was lost */ }
|
|
17
|
+
catch (ServerException ex) { /* the broker refused the login — ex.Code is the MT5 return code */ }
|
|
18
|
+
|
|
19
|
+
try
|
|
20
|
+
{
|
|
21
|
+
var q = api.GetQuote(symbol, 5000);
|
|
22
|
+
api.OrderSend(symbol, lots, q.Ask, OrderType.Buy);
|
|
23
|
+
}
|
|
24
|
+
catch (TimeoutException ex) { /* no quote in time, or the trade was NOT CONFIRMED in time — it may still
|
|
25
|
+
have been accepted: check GetOpenedOrders() before resending */ }
|
|
26
|
+
catch (InvestorModeException ex) { /* the login is read-only (investor) */ }
|
|
27
|
+
catch (ServerException ex) { /* the broker rejected the request — ex.Code is the MT5 return code */ }
|
|
28
|
+
catch (ConnectException ex) { /* not connected, or the connection was lost */ }
|
|
29
|
+
catch (ArgumentException ex) { /* pre-flight validation, for example an unknown symbol */ }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`TimeoutException` means different things at different steps — see the table below. In particular, **never
|
|
33
|
+
resend an order just because a trade call timed out.**
|
|
34
|
+
|
|
35
|
+
`Connect()` throws the typed exception **directly** — it is never wrapped in an `AggregateException`.
|
|
36
|
+
|
|
37
|
+
## The exception types
|
|
38
|
+
|
|
39
|
+
| Type | Derives from | Raised when |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `LicenseException` | `Exception` | The licence service refused the licence. `.Reason`, `.ServerError`, `.HttpStatus`. |
|
|
42
|
+
| `TokenServiceException` | `Exception` | The licence service could not be reached, timed out, or returned an unusable reply. |
|
|
43
|
+
| `ServerException` | `Exception` | The broker rejected the login or a trading request. `.Code` is the MT5 return code (`Msg`). |
|
|
44
|
+
| `ConnectException` | `Exception` | The client is not connected, a connection could not be established, or it was lost. |
|
|
45
|
+
| `InvestorModeException` | `Exception` | A trading call was made on a read-only (investor) login. |
|
|
46
|
+
| `ProxyException` | `Exception` | The configured SOCKS or HTTPS proxy refused or failed the connection. |
|
|
47
|
+
| `ConnectionException` | `ApplicationException` | A client certificate (`PfxFile`) was supplied but holds no RSA private key. |
|
|
48
|
+
| `ArgumentException` | `SystemException` | Client-side validation: an unknown symbol, an unknown group, a `CommonKey` that is not 15 bytes. |
|
|
49
|
+
| `TimeoutException` | `SystemException` | The host did not reply within `ConnectTimeout`; a quote did not arrive within its timeout; a history download did not return within `DownloadOrderHistoryTimeoutMs`. |
|
|
50
|
+
| `Exception` (plain) | — | A few state errors, listed [below](#plain-systemexception-state-errors). |
|
|
51
|
+
|
|
52
|
+
## LicenseException — the licence service refused
|
|
53
|
+
|
|
54
|
+
`ex.Reason` is a `LicenseReason`:
|
|
55
|
+
|
|
56
|
+
| Reason | Meaning | What to do |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `NoKey` | No API key was supplied | Set `ApiKey` or the `WMTAPI_KEY` environment variable |
|
|
59
|
+
| `Invalid` | The key is not recognised | Check the key; it may be for another product |
|
|
60
|
+
| `Expired` | The licence period ended | Renew the subscription |
|
|
61
|
+
| `Revoked` | The subscription was cancelled or the key revoked | Re-subscribe or obtain a new key |
|
|
62
|
+
| `RateLimited` | Too many requests this minute | Back off and retry |
|
|
63
|
+
| `LoginNotAllowed` | This login is not on the key's allow-list | Use an allowed login, or ask for the key to be widened |
|
|
64
|
+
| `AccountLimitReached` | The licence already has its maximum number of distinct broker logins and this is a new one | Connect with an already-enrolled login, or upgrade the plan |
|
|
65
|
+
| `Unknown` | The service refused for a reason this version does not recognise | Read `ex.ServerError` and `ex.HttpStatus`, and contact support |
|
|
66
|
+
|
|
67
|
+
`ex.ServerError` is the raw error string the service returned (null for a client-side `NoKey`);
|
|
68
|
+
`ex.HttpStatus` is the HTTP status (0 when the failure was detected client-side).
|
|
69
|
+
|
|
70
|
+
## TokenServiceException — infrastructure, not a licence decision
|
|
71
|
+
|
|
72
|
+
The licence service was unreachable, timed out, or returned a reply that could not be used. Transport
|
|
73
|
+
failures are retried a few times before this is thrown. **Retry**, and check your network — do **not**
|
|
74
|
+
re-prompt the user for a key. `TokenServiceTimeout` (default 10 000 ms) bounds each request.
|
|
75
|
+
|
|
76
|
+
## ServerException — the broker rejected a login or a trade
|
|
77
|
+
|
|
78
|
+
`ex.Code` is the MT5 return code (the `Msg` enum); the member name is the meaning. The ones you typically
|
|
79
|
+
branch on after an order:
|
|
80
|
+
|
|
81
|
+
| Code | Meaning |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `DONE` / `REQUEST_EXECUTED` | success |
|
|
84
|
+
| `REQUOTE` / `PRICE_CHANGED` | the price moved — re-quote and resend |
|
|
85
|
+
| `NO_MONEY` | insufficient margin |
|
|
86
|
+
| `MARKET_CLOSED` | the symbol's session is closed |
|
|
87
|
+
| `INVALID_VOLUME` / `INVALID_PRICE` / `INVALID_STOPS` | bad request parameters |
|
|
88
|
+
| `TRADE_DISABLED` / `AUTOTRADE_DISABLE_SERVER` | trading is not permitted |
|
|
89
|
+
| `TOO_MANY_TRADE_REQUESTS` / `TOO_FREQUENT_REQUEST` | slow down |
|
|
90
|
+
|
|
91
|
+
On login the codes you will meet include `INVALID_ACCOUNT`, `ACCOUNT_DISABLED`, `UNALLOWED_IP_ADDRESS` and
|
|
92
|
+
`OLD_VERSION`. The full `Msg` enum is in IntelliSense.
|
|
93
|
+
|
|
94
|
+
## What each call can throw
|
|
95
|
+
|
|
96
|
+
| Call | Typed exceptions |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `Connect()` | `LicenseException`, `TokenServiceException`, `ServerException`, `ConnectException` (including "cannot connect to host:port", with the socket error as `InnerException`), `TimeoutException`, `ProxyException`, `ConnectionException` |
|
|
99
|
+
| `OrderSend`, `OrderClose`, `OrderModify`, `OrderCancel` and their `…AsyncTask` variants | `ArgumentException` (unknown symbol), `ConnectException`, `InvestorModeException`, `ServerException`, `TimeoutException` (the server did not confirm the request within `ExecutionTimeout`, default 30 000 ms — the order may still have been accepted, so check `GetOpenedOrders()` before resending); plain `Exception` (see below). The `…AsyncTask` variants surface these when you `await` the task. |
|
|
100
|
+
| `OrderSendAsync`, `OrderCloseAsync`, `OrderModifyAsync`, `OrderCancelAsync` | The same pre-flight errors are thrown; a broker rejection arrives through `OnOrderProgress`, not as an exception. |
|
|
101
|
+
| `Subscribe`, `GetQuote` | `ArgumentException` (unknown symbol), `TimeoutException` (`GetQuote` with a timeout); both connect first if the client is not connected, so they can also throw the `Connect()` errors. |
|
|
102
|
+
| `Symbols.GetInfo`, `Symbols.GetGroup`, `Symbols.GetGroupByName`, `Symbols.GetComission` | `ConnectException` (symbol list not received yet), `ArgumentException` (no such symbol or group) |
|
|
103
|
+
| `GetTickValue`, `GetAskTickValue`, `GetBidTickValue`, `CalculateOrderProfit` | `double.NaN` when unresolved. `GetTickValue` also throws `ArgumentException`/`ConnectException` from the symbol lookup. |
|
|
104
|
+
| `RequiredMargin` | `double.NaN` if the price does not arrive in time; `ArgumentException` if the broker publishes no route between the two currencies |
|
|
105
|
+
| `CalibrateMoneyScale`, `CalculateLots` | Do not throw for a failed measurement: `CalibrateMoneyScale` returns `false` (read `MoneyScaleEvidence`) and `CalculateLots` returns `Ok = false` with a `Reason`. |
|
|
106
|
+
| `DownloadOrderHistory`, `GetAccountSummary`, `CalculateEquityHistory` | `TimeoutException`; plain `Exception` (see below) |
|
|
107
|
+
| `LoadServersDat` | The usual file errors; `NotImplementedException` if the file holds a record type this version does not know |
|
|
108
|
+
| `RequestDemoAccount` | `ConnectException`/`TimeoutException` from the connection; plain `Exception` carrying the broker's refusal status |
|
|
109
|
+
|
|
110
|
+
## Plain `System.Exception` state errors
|
|
111
|
+
|
|
112
|
+
These are raised as a plain `System.Exception`, not a typed exception, so you can match them only by type
|
|
113
|
+
`Exception` and message. Treat each as a state error and do not retry blindly.
|
|
114
|
+
|
|
115
|
+
| Message | Raised by | Meaning |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `Not connected` | `Unsubscribe`, `Subscriptions`, `RequestOrderHistory(from, to)`, `DownloadOrderHistory` | The client is not connected. Call `Connect()` first. |
|
|
118
|
+
| `Download Order History Running` | `DownloadOrderHistory`, `RequestOrderHistory` | Another history download is in progress on this client. Wait for it to finish. |
|
|
119
|
+
| `Request Lots = 0` | `OrderClose`, `OrderCancel` (and their async variants) | The volume rounds to zero. |
|
|
120
|
+
| `Previous request still running` | `DownloadQuoteHistoryToday`, `DownloadQuoteHistoryMonth` | A download for that symbol is already in progress. |
|
|
121
|
+
| `Cannot DownloadQuoteHistory in N ms` | `DownloadQuoteHistoryToday`, `DownloadQuoteHistoryMonth` | The bars did not arrive within `DownloadQuoteHistoryTimeout`. |
|
|
122
|
+
| `If timeframe > 60 it should be in whole hours` | `ConvertToTimeframe`, `DownloadQuoteHistory…` | Use a timeframe of 60 minutes or less, or a whole number of hours. |
|
|
123
|
+
|
|
124
|
+
## Common issues
|
|
125
|
+
|
|
126
|
+
- **`Connect()` times out (`TimeoutException`)** → the host and port are wrong or unreachable. `ConnectTimeout`
|
|
127
|
+
bounds the wait. Brokers often publish several access servers — use one that is reachable. The SDK fails
|
|
128
|
+
over across the broker's cluster members automatically, but only **after** it has reached the initial host;
|
|
129
|
+
it cannot fail over away from a dead primary, so pick a live one (try the broker's listed alternatives if the
|
|
130
|
+
first does not answer).
|
|
131
|
+
- **`RateLimited` is a sliding window, not a hard block** → a key has a per-minute request budget, and every
|
|
132
|
+
`Connect()` (including each automatic reconnect) uses one request. The first calls in a fresh minute
|
|
133
|
+
succeed; once the budget is used, further calls get `RateLimited` until the window rolls.
|
|
134
|
+
- **History never returns** → use the bounded `DownloadOrderHistory(from, to, timeoutMs, ct)` overload, or
|
|
135
|
+
leave `DownloadOrderHistoryTimeoutMs` at its default so the call throws `TimeoutException` instead of
|
|
136
|
+
waiting.
|
|
137
|
+
- **`IsInvestor == true`** → you connected with a read-only (investor) password; trading is not permitted and
|
|
138
|
+
trading calls throw `InvestorModeException`. Check `IsTradeAllowed` first.
|
|
139
|
+
- **Equity equals balance and profit reads 0** → the open-position quotes are cold. Subscribe the symbols (or
|
|
140
|
+
call `GetQuote(symbol, timeout)`), then `UpdateProfits()`.
|
|
141
|
+
- **`CalculateLots` returns `Ok = false`, "not calibrated"** → call `CalibrateMoneyScale(from)` first. If that
|
|
142
|
+
returns `false`, `MoneyScaleEvidence` says why (typically: too few closed deals without swap or
|
|
143
|
+
commission).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Licensing & key management — wmt5api
|
|
2
|
+
|
|
3
|
+
Applies to wmt5api 3.1.0.
|
|
4
|
+
|
|
5
|
+
## How authorization works
|
|
6
|
+
|
|
7
|
+
Every `Connect()` makes a licence request to our hosted **licence service** over HTTPS, authenticated by your
|
|
8
|
+
**API key**. The service checks the key, its validity period, its rate limit and its allowed logins, and
|
|
9
|
+
answers so that the connection can proceed. Your DLL talks to your broker for trading data and to the licence
|
|
10
|
+
service only for this handshake. No key → no connection.
|
|
11
|
+
|
|
12
|
+
What the licence request carries:
|
|
13
|
+
|
|
14
|
+
- your **API key**, in the `X-Api-Key` request header;
|
|
15
|
+
- the name and version of the SDK (for example `wmt5api/3.1.0`);
|
|
16
|
+
- your broker login **number**, and the session handshake data your broker's server issued for this
|
|
17
|
+
connection.
|
|
18
|
+
|
|
19
|
+
It does **not** carry your broker password, and it carries no trading data. See [Security](SECURITY.md).
|
|
20
|
+
|
|
21
|
+
## Supplying the key
|
|
22
|
+
|
|
23
|
+
Provide the key as a **property** or an **environment variable**. The property wins; the environment variable
|
|
24
|
+
is the fallback. Leave the licence service URL at its default.
|
|
25
|
+
|
|
26
|
+
| What | Property | Environment variable |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| API key | `api.ApiKey = "wk_…"` | `WMTAPI_KEY` |
|
|
29
|
+
| Licence service URL (optional — leave at its default) | `api.TokenServiceUrl` | `WMTAPI_TOKEN_URL` |
|
|
30
|
+
|
|
31
|
+
```csharp
|
|
32
|
+
api = new MT5API(user, password, host, port);
|
|
33
|
+
api.ApiKey = Environment.GetEnvironmentVariable("WMTAPI_KEY"); // or set the property directly
|
|
34
|
+
api.Connect();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
A key is shown **once**, at issue time. Treat it like a password (see [Security](SECURITY.md)).
|
|
38
|
+
|
|
39
|
+
## Validity, expiry, renewal
|
|
40
|
+
|
|
41
|
+
- A licence is valid for your **subscription period**. On renewal it is extended automatically — your existing
|
|
42
|
+
key keeps working.
|
|
43
|
+
- If the subscription **lapses or is cancelled**, the key stops authorizing new connections: you get a
|
|
44
|
+
`LicenseException` with `Reason = Revoked` or `Expired`. Open connections are unaffected until they drop; a
|
|
45
|
+
reconnect (including an automatic one) needs a new licence request.
|
|
46
|
+
|
|
47
|
+
## Revoking / rotating
|
|
48
|
+
|
|
49
|
+
- Cancel a subscription in the dashboard → its keys are revoked on the next connect.
|
|
50
|
+
- To rotate a key, issue a new one and update your deployment; revoke the old one.
|
|
51
|
+
|
|
52
|
+
## Rate limits, allowed logins and the account limit
|
|
53
|
+
|
|
54
|
+
- Each key has a per-minute request rate. Every `Connect()` — including each automatic reconnect — makes one
|
|
55
|
+
licence request. Exceeding the rate → `Reason = RateLimited`; back off and retry.
|
|
56
|
+
- A key may be restricted to specific broker logins. Connecting with a login outside that list →
|
|
57
|
+
`Reason = LoginNotAllowed`.
|
|
58
|
+
- A plan has a maximum number of distinct broker logins. Connecting with a new login beyond that number →
|
|
59
|
+
`Reason = AccountLimitReached`; connect with an already-enrolled login, or upgrade the plan.
|
|
60
|
+
|
|
61
|
+
## When the service cannot be reached
|
|
62
|
+
|
|
63
|
+
A licence request that fails on the network (unreachable, timeout, malformed reply) is retried a few times
|
|
64
|
+
and then throws `TokenServiceException` — that is not a licence decision, so retry rather than prompting for
|
|
65
|
+
a new key. `TokenServiceTimeout` (default 10 000 ms) bounds each request.
|
|
66
|
+
|
|
67
|
+
See [Errors & troubleshooting](ERRORS.md) for the full failure matrix.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Security — wmt5api
|
|
2
|
+
|
|
3
|
+
Applies to wmt5api 3.1.0.
|
|
4
|
+
|
|
5
|
+
## Your API key is a secret
|
|
6
|
+
|
|
7
|
+
Treat your `wk_…` key like a password. Anyone with it can open licensed connections billed to your account.
|
|
8
|
+
|
|
9
|
+
- **Never commit it** to source control, log it, or embed it in client-side or shipped binaries.
|
|
10
|
+
- Prefer the **`WMTAPI_KEY` environment variable** (or a secret manager) over a hard-coded `ApiKey`.
|
|
11
|
+
- The SDK never writes your key to a log or to disk. It is sent only to the licence service, over HTTPS, in
|
|
12
|
+
the `X-Api-Key` request header.
|
|
13
|
+
- Rotate the key if you suspect exposure (issue a new one, update your deployment, revoke the old one).
|
|
14
|
+
|
|
15
|
+
## Broker credentials
|
|
16
|
+
|
|
17
|
+
- Use an **investor (read-only) password** for read-only integrations — it cannot place trades.
|
|
18
|
+
- Your broker **password** is used only for the handshake with your broker's server. It is never sent to the
|
|
19
|
+
licence service.
|
|
20
|
+
- Your broker login **number** is sent to the licence service (it is how the licence's allowed-logins list and
|
|
21
|
+
account limit are enforced), together with the SDK name and version and session handshake data issued by your
|
|
22
|
+
broker's server.
|
|
23
|
+
|
|
24
|
+
## Transport
|
|
25
|
+
|
|
26
|
+
- The licence request is made over **HTTPS**. Leave `TokenServiceUrl` at its default; do not point it at a host
|
|
27
|
+
you do not trust.
|
|
28
|
+
- The connection to your broker is the MT5 client protocol to the access server you configure (`host`,
|
|
29
|
+
`port`), optionally through the SOCKS or HTTPS proxy you set with `ProxyEnable`, `ProxyHost` and the related
|
|
30
|
+
fields.
|
|
31
|
+
|
|
32
|
+
## What leaves your machine
|
|
33
|
+
|
|
34
|
+
| To | What | When |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| Licence service | Your API key; the SDK name and version; your broker login number; non-secret session handshake data issued by your broker's server | Once per `Connect()` (and per automatic reconnect) |
|
|
37
|
+
| Your broker | Your login and password (used in the standard MT5 handshake), and your trading requests | During the session |
|
|
38
|
+
|
|
39
|
+
The licence service receives **no** broker password and **no** trading data — only the licence handshake.
|
|
40
|
+
|
|
41
|
+
## Data handling
|
|
42
|
+
|
|
43
|
+
- `Account` exposes your own account holder details (name and, if your broker returns them, address and
|
|
44
|
+
contact fields). This is **your** data from **your** broker; handle it per your privacy obligations if you
|
|
45
|
+
store it.
|
|
46
|
+
- Nothing in the SDK sends data anywhere beyond your broker and the licence handshake.
|
|
47
|
+
|
|
48
|
+
## Reporting
|
|
49
|
+
|
|
50
|
+
Security concerns: contact your account representative at mtapi.one.
|
|
Binary file
|
pmt5api/_risk.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Position sizing, margin and P/L arithmetic that the core computes for the account's own conventions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from ._base import _Base
|
|
9
|
+
from ._util import enum_value, finite, to_core_time, translated
|
|
10
|
+
from .errors import InvalidRequestError
|
|
11
|
+
from .models import Deal, LotSizing, _opt_float
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _RiskMixin(_Base):
|
|
15
|
+
@translated
|
|
16
|
+
def calibrate_money_scale(self, start: datetime, *, min_deals_per_symbol: Optional[int] = None,
|
|
17
|
+
max_self_scatter: Optional[float] = None, max_disagreement: Optional[float] = None,
|
|
18
|
+
max_snap_distance: Optional[float] = None) -> bool:
|
|
19
|
+
"""Measure what a price unit is worth *on this account* from its own closed deals since ``start``.
|
|
20
|
+
|
|
21
|
+
Read-only; places no order. Cent handling is a per-broker configuration, not a property of the currency, so
|
|
22
|
+
sizing is refused until this has succeeded. Returns ``False`` when the scale cannot be established (read
|
|
23
|
+
:attr:`money_scale_evidence`); more history usually fixes it.
|
|
24
|
+
"""
|
|
25
|
+
self._require_connected()
|
|
26
|
+
kwargs = {}
|
|
27
|
+
if min_deals_per_symbol is not None:
|
|
28
|
+
kwargs["minDealsPerSymbol"] = int(min_deals_per_symbol)
|
|
29
|
+
if max_self_scatter is not None:
|
|
30
|
+
kwargs["maxSelfScatter"] = float(max_self_scatter)
|
|
31
|
+
if max_disagreement is not None:
|
|
32
|
+
kwargs["maxDisagreement"] = float(max_disagreement)
|
|
33
|
+
if max_snap_distance is not None:
|
|
34
|
+
kwargs["maxSnapDistance"] = float(max_snap_distance)
|
|
35
|
+
return bool(self._api.CalibrateMoneyScale(to_core_time(self._server_naive(start)), **kwargs))
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def money_scale(self) -> Optional[float]:
|
|
39
|
+
"""The measured scale (``None`` until :meth:`calibrate_money_scale` has succeeded)."""
|
|
40
|
+
return _opt_float(self._api.MoneyScale)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def money_scale_evidence(self) -> str:
|
|
44
|
+
"""Why the scale is what it is, or why it could not be established."""
|
|
45
|
+
return str(self._api.MoneyScaleEvidence or "")
|
|
46
|
+
|
|
47
|
+
@translated
|
|
48
|
+
def lots_for_risk(self, symbol: str, entry_price: float, stop_price: float, risk_amount: float, *,
|
|
49
|
+
lot_step: Optional[float] = None, min_lot: Optional[float] = None,
|
|
50
|
+
max_ratio: Optional[float] = None, max_equity_share: Optional[float] = None,
|
|
51
|
+
max_lot: Optional[float] = None) -> LotSizing:
|
|
52
|
+
"""The volume that loses ``risk_amount`` (account currency) if ``stop_price`` is hit, with the evidence.
|
|
53
|
+
|
|
54
|
+
Requires a successful :meth:`calibrate_money_scale`; without it the answer is a refusal. The volume is
|
|
55
|
+
rounded **down**. Check ``ok``: when false, ``lots`` is 0 and ``reason`` says which check failed.
|
|
56
|
+
"""
|
|
57
|
+
self._require_connected()
|
|
58
|
+
kwargs = {}
|
|
59
|
+
for key, value in (("lotStep", lot_step), ("minLot", min_lot), ("maxRatio", max_ratio),
|
|
60
|
+
("maxEquityShare", max_equity_share), ("maxLot", max_lot)):
|
|
61
|
+
if value is not None:
|
|
62
|
+
kwargs[key] = float(value)
|
|
63
|
+
result = self._api.CalculateLots(symbol, float(entry_price), float(stop_price), float(risk_amount), **kwargs)
|
|
64
|
+
return LotSizing._from_core(result)
|
|
65
|
+
|
|
66
|
+
@translated
|
|
67
|
+
def required_margin(self, symbol: str, lots: float, side: str = "buy", price: float = 0.0) -> float:
|
|
68
|
+
"""The margin needed to open ``lots`` of ``symbol``, computed as the terminal does (``price`` 0 = live quote).
|
|
69
|
+
A reference figure, not a sizing input."""
|
|
70
|
+
self._require_connected()
|
|
71
|
+
kind = enum_value(self._core.DealType, "deal" + str(side), "side")
|
|
72
|
+
value = float(self._api.RequiredMargin(symbol, finite(lots, "lots", positive=True), kind,
|
|
73
|
+
finite(price, "price", non_negative=True)))
|
|
74
|
+
if math.isnan(value):
|
|
75
|
+
raise InvalidRequestError("the margin of %s cannot be computed (no quote or conversion rate yet)" % symbol)
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
@translated
|
|
79
|
+
def order_profit(self, symbol: str, open_price: float, close_price: float, lots: float, buy: bool = True) -> float:
|
|
80
|
+
"""The P/L, in account currency, of ``lots`` moving from ``open_price`` to ``close_price`` (cent accounts
|
|
81
|
+
included). For reporting a completed or hypothetical move; use :meth:`lots_for_risk` to size."""
|
|
82
|
+
self._require_connected()
|
|
83
|
+
value = float(self._api.CalculateOrderProfit(symbol, finite(open_price, "open_price"),
|
|
84
|
+
finite(close_price, "close_price"),
|
|
85
|
+
finite(lots, "lots", positive=True), bool(buy)))
|
|
86
|
+
if math.isnan(value):
|
|
87
|
+
raise InvalidRequestError("the profit of %s cannot be computed (no conversion rate yet)" % symbol)
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
@translated
|
|
91
|
+
def money_scale_from_deal(self, deal: Deal) -> Optional[float]:
|
|
92
|
+
"""Re-measure the scale from ONE closed deal — the broker's own accounting of a trade that happened. Compare
|
|
93
|
+
it with :attr:`money_scale`; a disagreement means every size derived from the calibration was wrong."""
|
|
94
|
+
self._require_connected()
|
|
95
|
+
core_deal = self._core.Deal()
|
|
96
|
+
core_deal.Ticket = int(deal.ticket)
|
|
97
|
+
core_deal.OrderTicket = int(deal.order_ticket)
|
|
98
|
+
core_deal.PositionTicket = int(deal.position_ticket)
|
|
99
|
+
core_deal.Symbol = deal.symbol
|
|
100
|
+
core_deal.Type = enum_value(self._core.DealType, deal.type, "deal type")
|
|
101
|
+
core_deal.Direction = enum_value(self._core.Direction, deal.direction, "direction")
|
|
102
|
+
if deal.time is not None:
|
|
103
|
+
core_deal.Time = to_core_time(deal.time)
|
|
104
|
+
core_deal.OpenPrice = float(deal.open_price)
|
|
105
|
+
core_deal.Price = float(deal.price)
|
|
106
|
+
core_deal.StopLoss = float(deal.stop_loss)
|
|
107
|
+
core_deal.TakeProfit = float(deal.take_profit)
|
|
108
|
+
core_deal.Lots = float(deal.lots)
|
|
109
|
+
core_deal.Profit = float(deal.profit)
|
|
110
|
+
core_deal.Commission = float(deal.commission)
|
|
111
|
+
core_deal.Swap = float(deal.swap)
|
|
112
|
+
core_deal.ContractSize = float(deal.contract_size)
|
|
113
|
+
core_deal.Digits = int(deal.digits)
|
|
114
|
+
core_deal.Comment = deal.comment
|
|
115
|
+
core_deal.ExpertId = int(deal.magic)
|
|
116
|
+
return _opt_float(self._api.MoneyScaleFromDeal(core_deal))
|
pmt5api/_runtime.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Locates the .NET runtime and the licensed core library and loads them once per process."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import glob
|
|
5
|
+
import importlib
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Iterator, Optional
|
|
12
|
+
|
|
13
|
+
from .errors import CoreNotFoundError, RuntimeNotFoundError
|
|
14
|
+
|
|
15
|
+
#: Environment variable that points at wmt5api.dll.
|
|
16
|
+
ENV_DLL = "WMTAPI_DLL"
|
|
17
|
+
|
|
18
|
+
_NAMESPACE = "wmt5api" # the .NET namespace of the core; NOT the name of this Python package
|
|
19
|
+
_PKG = Path(__file__).resolve().parent
|
|
20
|
+
_BUNDLED_RUNTIME = _PKG / "_dotnet" # present in the platform wheels: host/fxr + shared/Microsoft.NETCore.App
|
|
21
|
+
_lock = threading.Lock()
|
|
22
|
+
_core = None
|
|
23
|
+
_dll: Optional[Path] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def find_dll(explicit=None) -> Path:
|
|
27
|
+
"""The core library to load: the caller's path, else ``WMTAPI_DLL``, else the one bundled in the wheel.
|
|
28
|
+
|
|
29
|
+
A path the caller gave explicitly (argument or environment) is never silently replaced by another: if it does not
|
|
30
|
+
exist that is an error.
|
|
31
|
+
"""
|
|
32
|
+
requested = explicit or os.environ.get(ENV_DLL)
|
|
33
|
+
if requested:
|
|
34
|
+
path = Path(requested)
|
|
35
|
+
if path.is_file():
|
|
36
|
+
return path
|
|
37
|
+
raise CoreNotFoundError("wmt5api.dll was not found at %s (from %s)" %
|
|
38
|
+
(path, "dll_path" if explicit else ENV_DLL))
|
|
39
|
+
bundled = _PKG / "_native" / "wmt5api.dll"
|
|
40
|
+
if bundled.is_file():
|
|
41
|
+
return bundled
|
|
42
|
+
raise CoreNotFoundError(
|
|
43
|
+
"wmt5api.dll was not found. Pass dll_path=..., or set the " + ENV_DLL + " environment variable to the DLL "
|
|
44
|
+
"taken from your wmt5api package, or install a pmt5api wheel that bundles it (looked for " + str(bundled) + ")."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _dotnet_root_candidates() -> Iterator[Path]:
|
|
49
|
+
exe = shutil.which("dotnet")
|
|
50
|
+
if exe:
|
|
51
|
+
yield Path(exe).resolve().parent
|
|
52
|
+
home = Path.home()
|
|
53
|
+
yield home / ".dotnet"
|
|
54
|
+
for p in ("/usr/share/dotnet", "/usr/lib/dotnet", "/usr/local/share/dotnet", "/opt/dotnet"):
|
|
55
|
+
yield Path(p)
|
|
56
|
+
pf = os.environ.get("ProgramFiles")
|
|
57
|
+
if pf:
|
|
58
|
+
yield Path(pf) / "dotnet"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _icu_present() -> bool:
|
|
62
|
+
"""True when a system ICU library is installed (the .NET runtime needs one unless globalization is invariant)."""
|
|
63
|
+
patterns = ("/usr/lib*/libicuuc.so*", "/usr/lib/*-linux-gnu/libicuuc.so*", "/lib*/libicuuc.so*",
|
|
64
|
+
"/lib/*-linux-gnu/libicuuc.so*", "/usr/local/lib*/libicuuc.so*", "/opt/homebrew/lib/libicuuc*",
|
|
65
|
+
"/usr/lib/libicuuc*")
|
|
66
|
+
return any(glob.glob(p) for p in patterns)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _ensure_dotnet_root() -> None:
|
|
70
|
+
"""Point the runtime host at a .NET runtime: the caller's ``DOTNET_ROOT``, else the one bundled in this package,
|
|
71
|
+
else an installed one. Say so clearly when there is none."""
|
|
72
|
+
# Without an ICU library the runtime aborts the whole process (uncatchably) at start-up unless globalization is
|
|
73
|
+
# invariant. The core does not depend on culture-specific behaviour, so on a machine with no ICU ask for invariant
|
|
74
|
+
# mode — whichever runtime is used, and including a DOTNET_ROOT inherited from a parent process.
|
|
75
|
+
if sys.platform.startswith("linux") and not _icu_present():
|
|
76
|
+
os.environ.setdefault("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1")
|
|
77
|
+
if os.environ.get("DOTNET_ROOT"):
|
|
78
|
+
return
|
|
79
|
+
if (_BUNDLED_RUNTIME / "shared" / "Microsoft.NETCore.App").is_dir():
|
|
80
|
+
os.environ["DOTNET_ROOT"] = str(_BUNDLED_RUNTIME)
|
|
81
|
+
return
|
|
82
|
+
for root in _dotnet_root_candidates():
|
|
83
|
+
if (root / "shared" / "Microsoft.NETCore.App").is_dir():
|
|
84
|
+
os.environ["DOTNET_ROOT"] = str(root)
|
|
85
|
+
return
|
|
86
|
+
raise RuntimeNotFoundError(
|
|
87
|
+
"No .NET runtime was found. Install the .NET 8 (or later) runtime, or set DOTNET_ROOT to its install directory."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def runtime_root() -> Optional[Path]:
|
|
92
|
+
"""The .NET root in use (after ``load``), or None."""
|
|
93
|
+
root = os.environ.get("DOTNET_ROOT")
|
|
94
|
+
return Path(root) if root else None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _has_entry(path_entry: str, name: str) -> bool:
|
|
98
|
+
base = path_entry or os.getcwd()
|
|
99
|
+
try:
|
|
100
|
+
return os.path.isdir(os.path.join(base, name))
|
|
101
|
+
except OSError:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _import_namespace():
|
|
106
|
+
"""Import the core's .NET namespace even if a same-named folder is lying around on sys.path."""
|
|
107
|
+
mod = sys.modules.get(_NAMESPACE)
|
|
108
|
+
if mod is not None and hasattr(mod, "MT5API"):
|
|
109
|
+
return mod
|
|
110
|
+
sys.modules.pop(_NAMESPACE, None)
|
|
111
|
+
saved = list(sys.path)
|
|
112
|
+
try:
|
|
113
|
+
sys.path[:] = [p for p in sys.path if not _has_entry(p, _NAMESPACE)]
|
|
114
|
+
importlib.invalidate_caches()
|
|
115
|
+
mod = importlib.import_module(_NAMESPACE)
|
|
116
|
+
finally:
|
|
117
|
+
sys.path[:] = saved
|
|
118
|
+
if not hasattr(mod, "MT5API"):
|
|
119
|
+
raise CoreNotFoundError("the loaded library does not expose MT5API; is it the wmt5api package?")
|
|
120
|
+
return mod
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def load(dll_path=None):
|
|
124
|
+
"""Load the runtime and the core (idempotent, thread-safe). Returns the core's .NET namespace module."""
|
|
125
|
+
global _core, _dll
|
|
126
|
+
with _lock:
|
|
127
|
+
if _core is not None:
|
|
128
|
+
if dll_path is not None and _dll is not None and Path(dll_path).resolve() != _dll.resolve():
|
|
129
|
+
raise CoreNotFoundError(
|
|
130
|
+
"a different core is already loaded in this process (%s); one process can use one core, "
|
|
131
|
+
"so dll_path=%s was not loaded" % (_dll, dll_path))
|
|
132
|
+
return _core
|
|
133
|
+
dll = find_dll(dll_path)
|
|
134
|
+
_ensure_dotnet_root()
|
|
135
|
+
try:
|
|
136
|
+
from pythonnet import load as _load
|
|
137
|
+
_load("coreclr")
|
|
138
|
+
import clr # noqa: F401 (provided by pythonnet)
|
|
139
|
+
except Exception as e: # noqa: BLE001 — anything here means the runtime could not start
|
|
140
|
+
raise RuntimeNotFoundError("could not start the .NET runtime: %s" % e) from e
|
|
141
|
+
import clr as _clr
|
|
142
|
+
from System.Reflection import Assembly # available once the runtime is up
|
|
143
|
+
try:
|
|
144
|
+
Assembly.LoadFrom(str(dll)) # AddReference(<absolute path>) fails on pythonnet 3.0.x
|
|
145
|
+
except Exception as e: # noqa: BLE001
|
|
146
|
+
raise CoreNotFoundError("could not load %s: %s" % (dll, e)) from e
|
|
147
|
+
_clr.AddReference("wmt5api") # now by name: registers the namespace for `import`
|
|
148
|
+
_core = _import_namespace()
|
|
149
|
+
_dll = dll
|
|
150
|
+
return _core
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def loaded_dll() -> Optional[Path]:
|
|
154
|
+
return _dll
|