pmt4api 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.
- pmt4api/__init__.py +32 -0
- pmt4api/__main__.py +78 -0
- pmt4api/_account.py +253 -0
- pmt4api/_base.py +24 -0
- pmt4api/_coverage.py +186 -0
- pmt4api/_events.py +189 -0
- pmt4api/_market.py +263 -0
- pmt4api/_native/CHANGELOG.md +362 -0
- pmt4api/_native/THIRD-PARTY-NOTICES.txt +7 -0
- pmt4api/_native/docs/ERRORS.md +136 -0
- pmt4api/_native/docs/LICENSING.md +55 -0
- pmt4api/_native/docs/SECURITY.md +38 -0
- pmt4api/_native/wmt4api.dll +0 -0
- pmt4api/_risk.py +66 -0
- pmt4api/_runtime.py +154 -0
- pmt4api/_session.py +171 -0
- pmt4api/_streams.py +238 -0
- pmt4api/_trading.py +384 -0
- pmt4api/_util.py +78 -0
- pmt4api/bars.py +62 -0
- pmt4api/client.py +143 -0
- pmt4api/diagnostics.py +38 -0
- pmt4api/docs/API.md +702 -0
- pmt4api/docs/FROM-CSHARP.md +151 -0
- pmt4api/errors.py +233 -0
- pmt4api/frames.py +21 -0
- pmt4api/models.py +337 -0
- pmt4api/py.typed +0 -0
- pmt4api/servers.py +40 -0
- pmt4api-1.0.0.dist-info/METADATA +389 -0
- pmt4api-1.0.0.dist-info/RECORD +33 -0
- pmt4api-1.0.0.dist-info/WHEEL +4 -0
- pmt4api-1.0.0.dist-info/licenses/LICENSE.txt +29 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# From C# to Python
|
|
2
|
+
|
|
3
|
+
Every public member of the C# `MT4API` and `OrderClient` classes and where it went. Generated from `pmt4api.COVERAGE`, which an integration test checks against the DLL: a new C# member cannot ship without an entry here.
|
|
4
|
+
|
|
5
|
+
| C# member | Python | Note |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `Account` | `Client.account` | |
|
|
8
|
+
| `AccountBalance` | `Client.balance` | |
|
|
9
|
+
| `AccountCredit` | `Client.credit` | |
|
|
10
|
+
| `AccountEquity` | `Client.equity` | |
|
|
11
|
+
| `AccountFreeMargin` | `Client.free_margin` | |
|
|
12
|
+
| `AccountLeverage` | `Client.leverage` | |
|
|
13
|
+
| `AccountMargin` | `Client.margin` | |
|
|
14
|
+
| `AccountMarginLevel` | `Client.margin_level` | |
|
|
15
|
+
| `AccountName` | `Client.account` | |
|
|
16
|
+
| `AccountProfit` | `Client.profit` | |
|
|
17
|
+
| `AccountType` | `Client.account` | |
|
|
18
|
+
| `ApiKey` | `Client(api_key)` | |
|
|
19
|
+
| `AutoReconnect` | `Client(auto_reconnect)` | |
|
|
20
|
+
| `CalculateEquityHistory` | `Client.equity_history` | |
|
|
21
|
+
| `CalculateLots` | `Client.lots_for_risk` | |
|
|
22
|
+
| `CalculateOrderProfit` | `Client.order_profit` | |
|
|
23
|
+
| `Connect` | `Client.connect` | |
|
|
24
|
+
| `Connected` | `Client.connected` | |
|
|
25
|
+
| `ConnectTimeout` | `Client(connect_timeout)` | |
|
|
26
|
+
| `Disconnect` | `Client.disconnect` | |
|
|
27
|
+
| `DownloadOrderHistory` | `Client.history` | `timeout=` and a `threading.Event` for `cancel=` replace the timeout and cancellation token arguments; `ahistory` cancels with the awaiting task |
|
|
28
|
+
| `DownloadOrderHistoryTimeoutMs` | `Client(history_timeout)` | |
|
|
29
|
+
| `DownloadQuoteHistory` | `Client.bars` | `count=` bars ending at `end`, or everything since `start=`; pages past the wire's 32767-bar limit; the server's own depth limit applies |
|
|
30
|
+
| `DownloadQuoteHistoryTimeout` | `Client(bars_timeout)` | |
|
|
31
|
+
| `Encoding` | `Client(comment_encoding)` | |
|
|
32
|
+
| `GetAccountSummary` | `Client.summary` | |
|
|
33
|
+
| `GetAskTickValue` | `Client.tick_value` | |
|
|
34
|
+
| `GetBidTickValue` | `Client.tick_value` | |
|
|
35
|
+
| `GetOpenedOrder` | `Client.order` | |
|
|
36
|
+
| `GetOpenedOrders` | `Client.orders` | |
|
|
37
|
+
| `GetQuote` | `Client.quote` | |
|
|
38
|
+
| `GetQuoteTimeoutMs` | `Client(quote_timeout)` | |
|
|
39
|
+
| `GetSymbolNames` | `Client.symbols` | |
|
|
40
|
+
| `GetSymbolSpec` | `Client.symbol` | also `SymbolDigits` and `SymbolExecution`: `SymbolInfo.digits` and `.execution` |
|
|
41
|
+
| `GetTickValue` | `Client.tick_value` | `side='ask'`; `side='bid'` is `GetBidTickValue`. Raises instead of returning NaN |
|
|
42
|
+
| `Groups` | `Client.symbol_groups` | |
|
|
43
|
+
| `Host` | `Client.host` | |
|
|
44
|
+
| `IsInvestor` | `Client.is_investor` | |
|
|
45
|
+
| `IsSubscribed` | `Client.is_subscribed` | |
|
|
46
|
+
| `LastSrvRecord` | not applicable: the raw wire record the server list was parsed from; read Client.server_list | |
|
|
47
|
+
| `LiveEquity` | `Client.live_equity` | |
|
|
48
|
+
| `LiveEquityCached` | `Client.equity` | |
|
|
49
|
+
| `LiveMarginCached` | `Client.margin` | |
|
|
50
|
+
| `LoadServersIni` | `pmt4api.load_servers_ini` | |
|
|
51
|
+
| `NoServerMessagesTimeout` | `Client(no_message_timeout)` | |
|
|
52
|
+
| `OnConnect` | `Client.on_connect` | |
|
|
53
|
+
| `OnDisconnect` | `Client.on_disconnect` | |
|
|
54
|
+
| `OnOrderProgress` | `Client.on_order_progress` | |
|
|
55
|
+
| `OnOrderUpdate` | `Client.on_order_update` | |
|
|
56
|
+
| `OnQuote` | `Client.on_quote` | |
|
|
57
|
+
| `OnQuoteHistory` | `Client.on_quote_history` | |
|
|
58
|
+
| `OnSymbolsUpdate` | `Client.on_symbols_update` | |
|
|
59
|
+
| `OrderCancel` | `Client.cancel` | |
|
|
60
|
+
| `OrderCancelAsync` | `Client.cancel_async` | |
|
|
61
|
+
| `OrderCancelAsyncTask` | `Client.acancel` | |
|
|
62
|
+
| `OrderClient` | not applicable: its members (timeout, placement source, progress event, async trading) are reached through Client(trade_timeout), Client(placed_type), Client.on_order_progress and the *_async methods; Client.raw gives the core object | |
|
|
63
|
+
| `OrderClose` | `Client.close` | partial close with `lots=`; the price defaults to the current bid / ask |
|
|
64
|
+
| `OrderCloseAsync` | `Client.close_async` | |
|
|
65
|
+
| `OrderCloseAsyncTask` | `Client.aclose` | |
|
|
66
|
+
| `OrderCloseBy` | `Client.close_by` | |
|
|
67
|
+
| `OrderCloseByAsync` | `Client.close_by_async` | |
|
|
68
|
+
| `OrderCloseByAsyncTask` | `Client.aclose_by` | |
|
|
69
|
+
| `OrderModify` | `Client.modify` | the symbol, type, volume and the values you did not change are taken from the open order, including a pending order's expiration |
|
|
70
|
+
| `OrderModifyAsync` | `Client.modify_async` | |
|
|
71
|
+
| `OrderModifyAsyncTask` | `Client.amodify` | |
|
|
72
|
+
| `OrderMultipleCloseBy` | `Client.close_all_by` | |
|
|
73
|
+
| `OrderMultipleCloseByAsync` | `Client.close_all_by_async` | |
|
|
74
|
+
| `OrderSend` | `Client.buy` | `buy`, `sell` and `place_order` cover the operations; `magic` other than 0 is refused (the core does not transmit it); the volume must be a multiple of 0.01 |
|
|
75
|
+
| `OrderSendAsync` | `Client.buy_async` | `buy_async`, `sell_async`, `place_order_async`; the integer returned is the request id that `on_order_progress` reports |
|
|
76
|
+
| `OrderSendAsyncTask` | `Client.abuy` | |
|
|
77
|
+
| `PlacedType` | `Client(placed_type)` | |
|
|
78
|
+
| `Port` | `Client.port` | |
|
|
79
|
+
| `Reconnect` | `Client.reconnect` | returns False when every server failed; with `auto_reconnect=True` the core does it by itself |
|
|
80
|
+
| `ReconnectBackoffMs` | `Client(reconnect_backoff)` | |
|
|
81
|
+
| `ReconnectMaxAttempts` | `Client(reconnect_max_attempts)` | |
|
|
82
|
+
| `RequiredMargin` | `Client.required_margin` | |
|
|
83
|
+
| `SaveServersIni` | `pmt4api.save_servers_ini` | |
|
|
84
|
+
| `Servers` | `Client.servers` | give `servers=[(host, port), ...]` to the constructor or call `add_server` |
|
|
85
|
+
| `ServerTime` | `Client.server_time` | |
|
|
86
|
+
| `ServerTradeBuild` | `Client.trade_build` | |
|
|
87
|
+
| `SrvList` | `Client.server_list` | |
|
|
88
|
+
| `Subscribe` | `Client.subscribe` | |
|
|
89
|
+
| `SymbolDigits` | `Client.symbol` | |
|
|
90
|
+
| `SymbolExecution` | `Client.symbol` | |
|
|
91
|
+
| `TokenServiceTimeout` | `Client(token_timeout)` | |
|
|
92
|
+
| `TokenServiceUrl` | `Client(token_url)` | |
|
|
93
|
+
| `TradeTimeout` | `Client(trade_timeout)` | |
|
|
94
|
+
| `Unsubscribe` | `Client.unsubscribe` | |
|
|
95
|
+
| `UpdateMargin` | `Client.update_margin` | |
|
|
96
|
+
| `UpdateProfits` | `Client.update_profits` | |
|
|
97
|
+
| `User` | `Client.login` | |
|
|
98
|
+
|
|
99
|
+
## Types
|
|
100
|
+
|
|
101
|
+
| C# type | Python |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `AccountInfo` | `pmt4api.AccountInfo` |
|
|
104
|
+
| `AccountSummary` | `pmt4api.AccountSummary` |
|
|
105
|
+
| `AccountType` | enum: str |
|
|
106
|
+
| `Bar` | `pmt4api.Bar` |
|
|
107
|
+
| `ConnectEventArgs` | `pmt4api.ConnectEvent` |
|
|
108
|
+
| `ConnectEventHandler` | not applicable: a .NET delegate type; subscribe with Client.on_connect |
|
|
109
|
+
| `ConnectException` | `pmt4api.ConnectError` |
|
|
110
|
+
| `Diag` | `pmt4api.enable_core_logging` |
|
|
111
|
+
| `DisconnectEventArgs` | `pmt4api.ConnectEvent` |
|
|
112
|
+
| `DisconnectEventHandler` | not applicable: a .NET delegate type; subscribe with Client.on_disconnect |
|
|
113
|
+
| `EquityPoint` | `pmt4api.EquityPoint` |
|
|
114
|
+
| `EquityTimeframe` | enum: str |
|
|
115
|
+
| `ExecutionType` | enum: str |
|
|
116
|
+
| `InvestorModeException` | `pmt4api.InvestorModeError` |
|
|
117
|
+
| `LicenseException` | `pmt4api.LicenseError` |
|
|
118
|
+
| `LicenseReason` | enum: str |
|
|
119
|
+
| `Logger` | `pmt4api.enable_core_logging` |
|
|
120
|
+
| `Logger+MsgType` | enum: str |
|
|
121
|
+
| `Logger+OnMsgHandler` | not applicable: a .NET delegate type; enable_core_logging forwards to Python logging |
|
|
122
|
+
| `Logger+OnMsgHandlerEx` | not applicable: a .NET delegate type; enable_core_logging forwards to Python logging |
|
|
123
|
+
| `LoginRejectedException` | `pmt4api.LoginRejectedError` |
|
|
124
|
+
| `LotSizing` | `pmt4api.LotSizing` |
|
|
125
|
+
| `MT4API` | `pmt4api.Client` |
|
|
126
|
+
| `Mt4Server` | `pmt4api.ServerRecord` |
|
|
127
|
+
| `Order` | `pmt4api.Order` |
|
|
128
|
+
| `OrderClient` | `pmt4api.Client` |
|
|
129
|
+
| `OrderClientAsyncTaskExtensions` | `Client.abuy` |
|
|
130
|
+
| `OrderProgress` | `pmt4api.OrderProgress` |
|
|
131
|
+
| `OrderProgressEventHandler` | not applicable: a .NET delegate type; subscribe with Client.on_order_progress |
|
|
132
|
+
| `OrderType` | enum: str |
|
|
133
|
+
| `OrderUpdate` | `pmt4api.OrderUpdate` |
|
|
134
|
+
| `OrderUpdateEventHandler` | not applicable: a .NET delegate type; subscribe with Client.on_order_update |
|
|
135
|
+
| `PlacedType` | enum: str |
|
|
136
|
+
| `ProgressType` | enum: str |
|
|
137
|
+
| `Quote` | `pmt4api.Quote` |
|
|
138
|
+
| `QuoteEventHandler` | not applicable: a .NET delegate type; subscribe with Client.on_quote |
|
|
139
|
+
| `QuoteHistoryEventArgs` | `pmt4api.QuoteHistoryEvent` |
|
|
140
|
+
| `QuoteHistoryEventHandler` | not applicable: a .NET delegate type; subscribe with Client.on_quote_history |
|
|
141
|
+
| `RequoteException` | `pmt4api.RequoteError` |
|
|
142
|
+
| `ServerException` | `pmt4api.ServerError` |
|
|
143
|
+
| `SymbolSpec` | `pmt4api.SymbolInfo` |
|
|
144
|
+
| `SymbolsUpdateEventHandler` | not applicable: a .NET delegate type; subscribe with Client.on_symbols_update |
|
|
145
|
+
| `SymGroup` | `pmt4api.SymbolGroup` |
|
|
146
|
+
| `Timeframe` | enum: str |
|
|
147
|
+
| `TokenServiceException` | `pmt4api.TokenServiceError` |
|
|
148
|
+
| `TradeTimeoutException` | `pmt4api.RequestTimeout` |
|
|
149
|
+
| `UpdateType` | enum: str |
|
|
150
|
+
|
|
151
|
+
Enumerations are exposed as their member names (strings): `order.order_type == "Buy"`.
|
pmt4api/errors.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Exceptions raised by pmt4api.
|
|
2
|
+
|
|
3
|
+
Every error the licensed core raises is translated into one of these, so callers catch a small, stable hierarchy and
|
|
4
|
+
never depend on .NET exception types. The original exception is kept as ``__cause__``.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WmtError(Exception):
|
|
12
|
+
"""Base class of every pmt4api error."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CoreNotFoundError(WmtError):
|
|
16
|
+
"""The licensed core library (wmt4api.dll) could not be located."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RuntimeNotFoundError(WmtError):
|
|
20
|
+
"""The .NET runtime needed to host the core could not be started."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LicenseError(WmtError):
|
|
24
|
+
"""The licence service refused the request.
|
|
25
|
+
|
|
26
|
+
``reason`` is one of: NoKey, Invalid, Expired, Revoked, RateLimited, LoginNotAllowed, AccountLimitReached, Unknown.
|
|
27
|
+
``status`` is the service's HTTP status and ``detail`` its own message, for support.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str, reason: str = "Unknown", status: Optional[int] = None,
|
|
31
|
+
detail: Optional[str] = None):
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.reason = reason
|
|
34
|
+
self.status = status
|
|
35
|
+
self.detail = detail
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TokenServiceError(WmtError):
|
|
39
|
+
"""The licence service could not be reached or answered with something unusable (not a licence decision)."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ConnectError(WmtError):
|
|
43
|
+
"""No access server could be reached (or none answered within ``connect_timeout``)."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class NotConnectedError(WmtError):
|
|
47
|
+
"""The call needs a connection and the client is not connected."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ConnectionLostError(WmtError):
|
|
51
|
+
"""An established connection was lost, possibly with a request in flight: whether it was applied is unknown;
|
|
52
|
+
reconnect and look at :meth:`Client.orders` before sending again."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ProtocolError(WmtError):
|
|
56
|
+
"""The server sent something the core could not decode."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
#: The MT4 trade-return codes, by number (the names follow the MetaQuotes error list; the number is authoritative).
|
|
60
|
+
TRADE_CODES: Dict[int, str] = {
|
|
61
|
+
0: "OK", 1: "NO_RESULT", 2: "COMMON_ERROR", 3: "INVALID_TRADE_PARAMETERS", 4: "SERVER_BUSY", 5: "OLD_VERSION",
|
|
62
|
+
6: "NO_CONNECTION", 7: "NOT_ENOUGH_RIGHTS", 8: "TOO_FREQUENT_REQUESTS", 9: "MALFUNCTIONAL_TRADE",
|
|
63
|
+
64: "ACCOUNT_DISABLED", 65: "INVALID_ACCOUNT", 128: "TRADE_TIMEOUT", 129: "INVALID_PRICE", 130: "INVALID_STOPS",
|
|
64
|
+
131: "INVALID_TRADE_VOLUME", 132: "MARKET_CLOSED", 133: "TRADE_DISABLED", 134: "NOT_ENOUGH_MONEY",
|
|
65
|
+
135: "PRICE_CHANGED", 136: "OFF_QUOTES", 137: "BROKER_BUSY", 138: "REQUOTE", 139: "ORDER_LOCKED",
|
|
66
|
+
140: "LONG_POSITIONS_ONLY_ALLOWED", 141: "TOO_MANY_REQUESTS", 145: "MODIFICATION_DENIED",
|
|
67
|
+
146: "TRADE_CONTEXT_BUSY", 147: "EXPIRATION_DENIED", 148: "TOO_MANY_ORDERS", 149: "HEDGE_PROHIBITED",
|
|
68
|
+
150: "PROHIBITED_BY_FIFO",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def code_name(value: Optional[int]) -> Optional[str]:
|
|
73
|
+
"""The name of an MT4 return code (``None`` for ``None``, ``"CODE_<n>"`` for a number not in the table)."""
|
|
74
|
+
if value is None:
|
|
75
|
+
return None
|
|
76
|
+
return TRADE_CODES.get(int(value), "CODE_%d" % int(value))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ServerError(WmtError):
|
|
80
|
+
"""The broker's server refused the request (a rejected login or a rejected order).
|
|
81
|
+
|
|
82
|
+
``code_value`` is the MT4 return code and ``code`` its name (``"REQUOTE"``, ``"NOT_ENOUGH_MONEY"``,
|
|
83
|
+
``"INVALID_STOPS"``, ...); both are ``None`` when the refusal carried no code.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, message: str, code: Optional[str] = None, code_value: Optional[int] = None):
|
|
87
|
+
super().__init__(message)
|
|
88
|
+
self.code = code
|
|
89
|
+
self.code_value = code_value
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class LoginRejectedError(ServerError):
|
|
93
|
+
"""The broker rejected the login: a wrong account number or password (typically code 65), a disabled or expired
|
|
94
|
+
login, or an authorisation refusal. Fix the credentials; this is not an SDK fault and is not retried across
|
|
95
|
+
servers."""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class RequoteError(ServerError):
|
|
99
|
+
"""An instant-execution price moved before the fill. ``bid`` / ``ask`` are the prices the server now offers:
|
|
100
|
+
decide again and resend (nothing was filled)."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, message: str, bid: float, ask: float, code_value: int = 138):
|
|
103
|
+
super().__init__(message, code_name(code_value), code_value)
|
|
104
|
+
self.bid = bid
|
|
105
|
+
self.ask = ask
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class InvestorModeError(ServerError):
|
|
109
|
+
"""The account was opened with an investor (read-only) password and the request needs trading rights. Nothing
|
|
110
|
+
was sent."""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class RequestTimeout(WmtError, TimeoutError):
|
|
114
|
+
"""The server did not answer within the requested time.
|
|
115
|
+
|
|
116
|
+
For a trade call this means the request WAS sent and was not confirmed in time: the order may have been accepted;
|
|
117
|
+
look at ``orders()`` before sending again. (When nothing was sent, :class:`NoQuoteError` is raised instead.)
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class NoQuoteError(WmtError, TimeoutError):
|
|
122
|
+
"""No quote was available in time to read or to trade against. Nothing was sent."""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class BusyError(WmtError):
|
|
126
|
+
"""The core allows one request of this kind at a time and one is still running; wait for it and retry."""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class RequestCancelled(WmtError):
|
|
130
|
+
"""The wait was cancelled by the caller (the ``cancel`` event of a download was set)."""
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class InvalidRequestError(WmtError, ValueError):
|
|
134
|
+
"""The request was rejected before it was sent (for example, a symbol the server does not know)."""
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
_BY_NAME = {
|
|
138
|
+
"LicenseException": "license",
|
|
139
|
+
"TokenServiceException": "token",
|
|
140
|
+
"ConnectException": "connect",
|
|
141
|
+
"SocketException": "connect",
|
|
142
|
+
"LoginRejectedException": "login",
|
|
143
|
+
"RequoteException": "requote",
|
|
144
|
+
"InvestorModeException": "investor",
|
|
145
|
+
"ServerException": "server",
|
|
146
|
+
"TradeTimeoutException": "timeout",
|
|
147
|
+
"TimeoutException": "timeout",
|
|
148
|
+
"OperationCanceledException": "cancelled",
|
|
149
|
+
"ArgumentException": "invalid",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# The core raises a few conditions as a bare System.Exception whose message is the only thing that identifies them.
|
|
153
|
+
_BARE_MESSAGES = (
|
|
154
|
+
("MT4 connection closed by server", ConnectionLostError),
|
|
155
|
+
("MT4 server closed the session", ConnectionLostError),
|
|
156
|
+
("MT4 LZO decompress failed", ProtocolError),
|
|
157
|
+
("MT4 pump: unknown opcode", ProtocolError),
|
|
158
|
+
("missing '", TokenServiceError),
|
|
159
|
+
("malformed '", TokenServiceError),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Wrappers the runtime puts around the exception that matters (a failed task, a reflected call).
|
|
163
|
+
_WRAPPERS = ("AggregateException", "TargetInvocationException")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _status(text: str) -> Optional[int]:
|
|
167
|
+
"""The number in "... rejected (status N)" of the core's bare connect-phase refusals."""
|
|
168
|
+
if "(status " not in text:
|
|
169
|
+
return None
|
|
170
|
+
try:
|
|
171
|
+
return int(text.rsplit("(status ", 1)[1].split(")", 1)[0])
|
|
172
|
+
except ValueError:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def translate(exc: BaseException) -> Optional[WmtError]:
|
|
177
|
+
"""Return the pmt4api error that corresponds to a core exception, or ``None`` if it is not one of ours.
|
|
178
|
+
|
|
179
|
+
Matching is by class name along the exception's inheritance chain (most derived first), so it works without importing
|
|
180
|
+
the core's types and keeps working if a core exception gains a new base class.
|
|
181
|
+
"""
|
|
182
|
+
if type(exc).__name__ in _WRAPPERS:
|
|
183
|
+
inner = getattr(exc, "InnerException", None)
|
|
184
|
+
if inner is not None:
|
|
185
|
+
return translate(inner)
|
|
186
|
+
for klass in type(exc).__mro__:
|
|
187
|
+
kind = _BY_NAME.get(klass.__name__)
|
|
188
|
+
if kind is None:
|
|
189
|
+
continue
|
|
190
|
+
message = str(getattr(exc, "Message", None) or exc)
|
|
191
|
+
if kind == "license":
|
|
192
|
+
reason = getattr(exc, "Reason", None)
|
|
193
|
+
status = getattr(exc, "HttpStatus", None)
|
|
194
|
+
detail = getattr(exc, "ServerError", None)
|
|
195
|
+
return LicenseError(message, str(reason) if reason is not None else "Unknown",
|
|
196
|
+
int(status) if status else None, str(detail) if detail else None)
|
|
197
|
+
if kind == "token":
|
|
198
|
+
return TokenServiceError(message)
|
|
199
|
+
if kind == "connect":
|
|
200
|
+
if "connection lost" in message:
|
|
201
|
+
return ConnectionLostError(message)
|
|
202
|
+
return ConnectError(message)
|
|
203
|
+
if kind in ("login", "server", "requote", "investor"):
|
|
204
|
+
code = getattr(exc, "Code", None)
|
|
205
|
+
try:
|
|
206
|
+
value = int(code) if code is not None else None
|
|
207
|
+
except (TypeError, ValueError):
|
|
208
|
+
value = None
|
|
209
|
+
if kind == "requote":
|
|
210
|
+
return RequoteError(message, float(exc.Bid), float(exc.Ask), value if value is not None else 138)
|
|
211
|
+
cls = {"login": LoginRejectedError, "server": ServerError, "investor": InvestorModeError}[kind]
|
|
212
|
+
return cls(message, code_name(value), value)
|
|
213
|
+
if kind == "timeout":
|
|
214
|
+
return RequestTimeout(message)
|
|
215
|
+
if kind == "cancelled":
|
|
216
|
+
return RequestCancelled(message)
|
|
217
|
+
if kind == "invalid":
|
|
218
|
+
return InvalidRequestError(message)
|
|
219
|
+
name = type(exc).__name__
|
|
220
|
+
module = getattr(type(exc), "__module__", "")
|
|
221
|
+
if name == "Exception" and module == "System":
|
|
222
|
+
text = str(getattr(exc, "Message", None) or exc)
|
|
223
|
+
for prefix, cls in _BARE_MESSAGES:
|
|
224
|
+
if text.startswith(prefix):
|
|
225
|
+
return cls(text)
|
|
226
|
+
if text.startswith("MT4 command ") or text.startswith("MT4 cmd-"):
|
|
227
|
+
value = _status(text)
|
|
228
|
+
return ServerError(text, code_name(value), value)
|
|
229
|
+
if name == "InvalidOperationException" and module == "System":
|
|
230
|
+
text = str(getattr(exc, "Message", None) or exc)
|
|
231
|
+
if "already in flight" in text:
|
|
232
|
+
return BusyError(text)
|
|
233
|
+
return None
|
pmt4api/frames.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Optional pandas helpers (``pip install pandas``)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import dataclasses
|
|
5
|
+
from typing import Any, Iterable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def to_frame(items: Iterable[Any]):
|
|
9
|
+
"""A ``pandas.DataFrame`` from a list of the package's value objects (``Bar``, ``Order``, ``Quote``, ...).
|
|
10
|
+
|
|
11
|
+
A list of bars is indexed by its ``time``; anything else keeps a plain integer index.
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
import pandas as pd
|
|
15
|
+
except ImportError as exc: # pragma: no cover - depends on the environment
|
|
16
|
+
raise ImportError("to_frame() needs pandas: pip install pandas") from exc
|
|
17
|
+
rows = [dataclasses.asdict(x) for x in items]
|
|
18
|
+
frame = pd.DataFrame(rows)
|
|
19
|
+
if "time" in frame.columns and {"open", "high", "low", "close"} <= set(frame.columns):
|
|
20
|
+
frame = frame.set_index("time")
|
|
21
|
+
return frame
|