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 ADDED
@@ -0,0 +1,32 @@
1
+ """pmt4api — Python access to the wmt4api MetaTrader 4 client library.
2
+
3
+ A typed, translated layer over every public capability of the licensed C# core (loaded in-process): the protocol,
4
+ licensing and trading logic stay in the core; this package adds a Pythonic surface — plain data types, a small error
5
+ hierarchy, context managers, sync and async streams, bars, sizing and order entry.
6
+ """
7
+ from ._coverage import COVERAGE, TYPES
8
+ from .bars import Bar, QuoteHistoryEvent, TIMEFRAMES
9
+ from .client import Client
10
+ from .diagnostics import enable_core_logging
11
+ from .errors import (BusyError, ConnectError, ConnectionLostError, CoreNotFoundError, InvalidRequestError,
12
+ InvestorModeError, LicenseError, LoginRejectedError, NoQuoteError, NotConnectedError,
13
+ ProtocolError, RequestCancelled, RequestTimeout, RequoteError, RuntimeNotFoundError,
14
+ ServerError, TokenServiceError, TRADE_CODES, WmtError)
15
+ from .frames import to_frame
16
+ from .models import (AccountInfo, AccountSummary, ConnectEvent, EquityPoint, LotSizing, Order, OrderProgress,
17
+ OrderUpdate, Quote, ServerRecord, SymbolGroup, SymbolInfo)
18
+ from .servers import load_servers_ini, save_servers_ini
19
+
20
+ __version__ = "1.0.0"
21
+
22
+ __all__ = [
23
+ "Client",
24
+ "Quote", "SymbolInfo", "SymbolGroup", "AccountInfo", "AccountSummary", "EquityPoint", "LotSizing", "Order",
25
+ "OrderUpdate", "OrderProgress", "ConnectEvent", "QuoteHistoryEvent", "ServerRecord",
26
+ "Bar", "TIMEFRAMES", "to_frame",
27
+ "load_servers_ini", "save_servers_ini", "enable_core_logging",
28
+ "WmtError", "LicenseError", "TokenServiceError", "ConnectError", "ConnectionLostError", "NotConnectedError",
29
+ "ServerError", "LoginRejectedError", "RequoteError", "InvestorModeError", "RequestTimeout", "NoQuoteError",
30
+ "RequestCancelled", "InvalidRequestError", "BusyError", "ProtocolError", "CoreNotFoundError",
31
+ "RuntimeNotFoundError", "TRADE_CODES", "COVERAGE", "TYPES", "__version__",
32
+ ]
pmt4api/__main__.py ADDED
@@ -0,0 +1,78 @@
1
+ """``python -m pmt4api`` — what is installed, and whether it works on this machine.
2
+
3
+ python -m pmt4api offline: versions, the core, the .NET runtime, this platform
4
+ python -m pmt4api --connect also connect read-only using PMT4_LOGIN, PMT4_PASSWORD, PMT4_HOST[, PMT4_PORT]
5
+ and WMT4API_KEY, and print the account's currency, leverage and symbol count
6
+
7
+ The output contains no secrets; it is what to send when asking for help.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import os
13
+ import platform
14
+ import sys
15
+ from typing import List, Optional
16
+
17
+ from . import __version__, _runtime
18
+ from .errors import WmtError
19
+
20
+
21
+ def _key_hint() -> str:
22
+ key = os.environ.get("WMT4API_KEY", "")
23
+ if not key:
24
+ return "not set (WMT4API_KEY)"
25
+ return "set (%s…, %d characters)" % (key[:3], len(key))
26
+
27
+
28
+ def main(argv: Optional[List[str]] = None) -> int:
29
+ ap = argparse.ArgumentParser(prog="python -m pmt4api", description=__doc__.split("\n\n")[0])
30
+ ap.add_argument("--connect", action="store_true", help="also connect read-only and read the account")
31
+ args = ap.parse_args(argv)
32
+ ok = True
33
+
34
+ print("pmt4api %s" % __version__)
35
+ print("python %s on %s" % (sys.version.split()[0], platform.platform()))
36
+ try:
37
+ _runtime.load()
38
+ except WmtError as e:
39
+ print("core NOT LOADED: %s: %s" % (type(e).__name__, e))
40
+ return 1
41
+ print("core %s" % _runtime.loaded_dll())
42
+ root = _runtime.runtime_root()
43
+ bundled = root is not None and "_dotnet" in str(root)
44
+ print("runtime %s%s" % (root, " (bundled in the wheel)" if bundled else ""))
45
+ print("icu %s; invariant globalization %s" % (
46
+ "present" if _runtime._icu_present() else "absent",
47
+ os.environ.get("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "off")))
48
+ print("key %s" % _key_hint())
49
+ docs = _runtime.loaded_dll().parent / "docs" if _runtime.loaded_dll() is not None else None
50
+ if docs is not None and docs.is_dir():
51
+ print("docs %s (LICENSING.md, SECURITY.md, ERRORS.md)" % docs)
52
+
53
+ from .client import Client
54
+ c = Client(1, "x", "127.0.0.1", 1)
55
+ print("offline construct client: ok (core version %s)" % c.core_version)
56
+
57
+ if args.connect:
58
+ try:
59
+ login, password, host = (int(os.environ["PMT4_LOGIN"]), os.environ["PMT4_PASSWORD"],
60
+ os.environ["PMT4_HOST"])
61
+ except KeyError as e:
62
+ print("connect SKIPPED: environment variable %s is not set" % e)
63
+ return 0
64
+ port = int(os.environ.get("PMT4_PORT", "443"))
65
+ try:
66
+ with Client(login, password, host, port, connect_timeout=45) as live:
67
+ a = live.account()
68
+ print("connect ok: %s, %s, leverage 1:%d, %d symbols, %s account, trading %s"
69
+ % (live.company, a.currency, a.leverage, len(live.symbols()), a.account_type,
70
+ "allowed" if live.can_trade else "not allowed (investor password)"))
71
+ except WmtError as e:
72
+ ok = False
73
+ print("connect FAILED: %s: %s" % (type(e).__name__, e))
74
+ return 0 if ok else 1
75
+
76
+
77
+ if __name__ == "__main__":
78
+ sys.exit(main())
pmt4api/_account.py ADDED
@@ -0,0 +1,253 @@
1
+ """The account: balances, positions, orders, history, and the curves derived from them."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import functools
6
+ import threading
7
+ from datetime import datetime, timedelta, timezone
8
+ from typing import Any, List, Optional
9
+
10
+ from ._base import _Base
11
+ from ._util import enum_value, to_core_time, translated
12
+ from .errors import NotConnectedError, RequestCancelled
13
+ from .models import AccountInfo, AccountSummary, EquityPoint, Order, _time
14
+
15
+
16
+ class _Cancellation:
17
+ """Turns a :class:`threading.Event` into the core's ``CancellationToken`` for the duration of one call."""
18
+
19
+ def __init__(self, event: Optional[threading.Event]) -> None:
20
+ self._event = event
21
+ self._done = threading.Event()
22
+ self._cts: Any = None
23
+ self.token: Any = None
24
+
25
+ def __enter__(self) -> "_Cancellation":
26
+ if self._event is None:
27
+ return self
28
+ from System.Threading import CancellationTokenSource # noqa: PLC0415 — available once the runtime is up
29
+ self._cts = CancellationTokenSource()
30
+ self.token = self._cts.Token
31
+ if self._event.is_set():
32
+ self._cts.Cancel()
33
+ return self
34
+
35
+ def watch() -> None:
36
+ while not self._done.is_set():
37
+ if self._event.wait(0.05): # type: ignore[union-attr]
38
+ try:
39
+ self._cts.Cancel()
40
+ except Exception: # noqa: BLE001 — disposed: the call has already ended
41
+ pass
42
+ return
43
+
44
+ threading.Thread(target=watch, name="pmt4api-cancel", daemon=True).start()
45
+ return self
46
+
47
+ def __exit__(self, *exc: Any) -> None:
48
+ self._done.set()
49
+
50
+
51
+ class _AccountMixin(_Base):
52
+ # ---- the account -------------------------------------------------------------------------------------------
53
+
54
+ @translated
55
+ def account(self, refresh: bool = True) -> AccountInfo:
56
+ """Account details. With ``refresh`` (the default) every open position's symbol is quoted once first, so
57
+ ``equity``, ``profit``, ``margin``, ``free_margin`` and ``margin_level`` are priced from current ticks. They are
58
+ derived on the client (the server does not push them), as the terminal does; see :attr:`equity` for the cheap
59
+ read of one figure."""
60
+ self._require_connected()
61
+ api = self._api
62
+ if refresh:
63
+ api.UpdateProfits()
64
+ if not api.Connected: # never let a refresh hide a lost connection
65
+ raise NotConnectedError("the connection was lost while refreshing the account")
66
+ a = api.Account
67
+ oldest = None
68
+ for sym in {str(o.Symbol) for o in api.GetOpenedOrders() if str(o.Type) in ("Buy", "Sell")}:
69
+ q = api.GetQuote(sym, 1) # the price the figures use; its time says how old it is
70
+ t = _time(q.Time) if q is not None else None
71
+ if t is not None and (oldest is None or t < oldest):
72
+ oldest = t
73
+ return AccountInfo(
74
+ login=int(api.User), name=str(a.UserName or ""), company=str(a.Company or ""), group=str(a.Group or ""),
75
+ currency=str(a.Currency or ""), leverage=int(api.AccountLeverage), balance=float(api.AccountBalance),
76
+ credit=float(api.AccountCredit), equity=float(api.AccountEquity), profit=float(api.AccountProfit),
77
+ margin=float(api.AccountMargin), free_margin=float(api.AccountFreeMargin),
78
+ margin_level=float(api.AccountMarginLevel), account_type=str(api.AccountType),
79
+ is_investor=bool(api.IsInvestor), priced_from=oldest)
80
+
81
+ @property
82
+ def balance(self) -> float:
83
+ """The account balance (closed-trade funds, floating P/L excluded); a cheap read."""
84
+ return float(self._api.AccountBalance)
85
+
86
+ @property
87
+ def credit(self) -> float:
88
+ """The credit the broker granted; a cheap read."""
89
+ return float(self._api.AccountCredit)
90
+
91
+ @property
92
+ def equity(self) -> float:
93
+ """Balance + floating P/L + credit, priced from the quotes already cached (it does not subscribe or wait: a
94
+ position whose symbol has not ticked yet counts 0 floating). Use :meth:`live_equity` to wait for fresh
95
+ quotes."""
96
+ return float(self._api.AccountEquity)
97
+
98
+ @property
99
+ def profit(self) -> float:
100
+ """The floating P/L of the open positions (equity − balance − credit), from cached quotes."""
101
+ return float(self._api.AccountProfit)
102
+
103
+ @property
104
+ def margin(self) -> float:
105
+ """The margin the open positions hold, derived on the client like the terminal (Forex by leverage, CFD by
106
+ the symbol's margin divider; a Futures position counts 0 until its formula is verified). May wait for a
107
+ conversion quote."""
108
+ return float(self._api.AccountMargin)
109
+
110
+ @property
111
+ def free_margin(self) -> float:
112
+ """Equity − margin."""
113
+ return float(self._api.AccountFreeMargin)
114
+
115
+ @property
116
+ def margin_level(self) -> float:
117
+ """Equity / margin × 100, as the terminal shows it; 0 when no margin is used."""
118
+ return float(self._api.AccountMarginLevel)
119
+
120
+ @property
121
+ def leverage(self) -> int:
122
+ """The account leverage (the N in 1:N)."""
123
+ return int(self._api.AccountLeverage)
124
+
125
+ @translated
126
+ def live_equity(self) -> float:
127
+ """Equity priced from FRESH quotes: it subscribes each open position's symbol and waits (up to the client's
128
+ ``quote_timeout`` per symbol) for a tick. Blocks; the cached read is :attr:`equity`."""
129
+ self._require_connected()
130
+ return float(self._api.LiveEquity())
131
+
132
+ @translated
133
+ def update_profits(self, symbol: Optional[str] = None) -> None:
134
+ """Warm the quotes the equity figures are priced from: every open position's symbol, or just ``symbol``.
135
+ Blocks up to ``quote_timeout`` per symbol."""
136
+ self._require_connected()
137
+ if symbol is None:
138
+ self._api.UpdateProfits()
139
+ else:
140
+ self._spec(symbol)
141
+ self._api.UpdateProfits(symbol)
142
+
143
+ @translated
144
+ def update_margin(self) -> None:
145
+ """Warm the quotes the margin figure is priced from (every open position's symbol)."""
146
+ self._require_connected()
147
+ self._api.UpdateMargin()
148
+
149
+ # ---- orders and positions ----------------------------------------------------------------------------------
150
+
151
+ @translated
152
+ def orders(self) -> List[Order]:
153
+ """Everything currently working: open positions and pending orders. Each position's ``profit`` is re-priced
154
+ from the live quote on every read."""
155
+ self._require_connected()
156
+ return [Order._from_core(o) for o in self._api.GetOpenedOrders()]
157
+
158
+ def positions(self) -> List[Order]:
159
+ """Open market positions only (``Buy`` / ``Sell``)."""
160
+ return [o for o in self.orders() if o.is_market]
161
+
162
+ def pending_orders(self) -> List[Order]:
163
+ """Pending orders only (limits and stops)."""
164
+ return [o for o in self.orders() if o.is_pending]
165
+
166
+ @translated
167
+ def order(self, ticket: int) -> Optional[Order]:
168
+ """One open order or position by ticket, or ``None`` when it is not open."""
169
+ self._require_connected()
170
+ o = self._api.GetOpenedOrder(int(ticket))
171
+ return Order._from_core(o) if o is not None else None
172
+
173
+ # ---- history -----------------------------------------------------------------------------------------------
174
+
175
+ @translated
176
+ def history(self, start: Optional[datetime] = None, end: Optional[datetime] = None,
177
+ timeout: Optional[float] = None, cancel: Optional[threading.Event] = None) -> List[Order]:
178
+ """Closed orders and balance / credit entries between ``start`` and ``end`` (default: the last 30 days up to
179
+ now), sorted by close time. Blocks until the server has sent everything, up to ``timeout`` seconds (default:
180
+ the client's ``history_timeout``); set the ``cancel`` event to abandon the wait early
181
+ (:class:`RequestCancelled`).
182
+
183
+ Naive datetimes are used exactly as given, in the broker's server time; timezone-aware ones are converted to
184
+ it. The default window is computed on the server's clock.
185
+
186
+ Each call opens its own short-lived broker connection and costs one licence request, which the licence
187
+ service rate-limits per minute: fetch once and reuse the result rather than polling.
188
+ """
189
+ self._require_connected()
190
+ now = self.server_time or datetime.now(timezone.utc).replace(tzinfo=None)
191
+ end = self._server_naive(end)
192
+ start = self._server_naive(start)
193
+ if end is None:
194
+ end = now + timedelta(days=1) # nothing the server has booked is later than this
195
+ if start is None:
196
+ start = now - timedelta(days=30)
197
+ ms = int(timeout * 1000) if timeout is not None else int(self._api.DownloadOrderHistoryTimeoutMs)
198
+ if ms <= 0:
199
+ raise ValueError("timeout must be positive")
200
+ if cancel is not None and cancel.is_set():
201
+ raise RequestCancelled("cancelled before the request was made") # nothing was sent: no licence request spent
202
+ with _Cancellation(cancel) as c:
203
+ if c.token is not None:
204
+ rows = self._api.DownloadOrderHistory(to_core_time(start), to_core_time(end), ms, c.token)
205
+ else:
206
+ rows = self._api.DownloadOrderHistory(to_core_time(start), to_core_time(end), ms)
207
+ return [Order._from_core(o) for o in (rows or ())]
208
+
209
+ async def ahistory(self, start: Optional[datetime] = None, end: Optional[datetime] = None,
210
+ timeout: Optional[float] = None) -> List[Order]:
211
+ """Awaitable :meth:`history`. Cancelling the awaiting task abandons the download (the broker connection it
212
+ opened is closed), so ``asyncio.wait_for(c.ahistory(...), 10)`` does not leave a request running."""
213
+ cancel = threading.Event()
214
+ loop = asyncio.get_running_loop()
215
+ job = loop.run_in_executor(None, functools.partial(self.history, start, end, timeout, cancel))
216
+ job.add_done_callback(lambda f: f.cancelled() or f.exception()) # the outcome is ours; never "unretrieved"
217
+ try:
218
+ return await asyncio.shield(job)
219
+ except asyncio.CancelledError:
220
+ cancel.set()
221
+ raise
222
+
223
+ # ---- realized figures and curves ---------------------------------------------------------------------------
224
+
225
+ @translated
226
+ def summary(self, start: Optional[datetime] = None, end: Optional[datetime] = None) -> AccountSummary:
227
+ """Realized trading result, deposits and withdrawals, from the closed-order ledger (all time by default).
228
+
229
+ Downloads the history, so it blocks and costs one licence request; call it off your UI thread (or use
230
+ ``asyncio.to_thread``).
231
+ """
232
+ self._require_connected()
233
+ if start is None and end is None:
234
+ return AccountSummary._from_core(self._api.GetAccountSummary())
235
+ if start is None or end is None:
236
+ raise ValueError("give both start and end, or neither")
237
+ return AccountSummary._from_core(self._api.GetAccountSummary(to_core_time(self._server_naive(start)),
238
+ to_core_time(self._server_naive(end))))
239
+
240
+ @translated
241
+ def equity_history(self, start: datetime, timeframe: str = "D1", exclude_same_bars: bool = True) -> List[EquityPoint]:
242
+ """The balance and equity curve since ``start`` (``timeframe`` is ``H1``, ``H4`` or ``D1``).
243
+
244
+ ``balance`` is exact; the historical ``equity`` is a reconstruction priced from the symbols' bars. With
245
+ ``exclude_same_bars`` (the default, as in the C# library) consecutive points with an unchanged balance and
246
+ equity are dropped. Downloads the history and per-symbol bars, so it blocks and costs a licence request.
247
+ """
248
+ self._require_connected()
249
+ tf = enum_value(self._core.EquityTimeframe, timeframe, "equity timeframe")
250
+ with self._bars_lock: # it downloads bars through the same single-flight request as bars()
251
+ points = self._api.CalculateEquityHistory(to_core_time(self._server_naive(start)), tf,
252
+ bool(exclude_same_bars))
253
+ return [EquityPoint._from_core(p) for p in (points or ())]
pmt4api/_base.py ADDED
@@ -0,0 +1,24 @@
1
+ """What every client mixin may rely on: the core object, the core's namespace, and the connection guard."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime, timedelta, timezone
5
+ from typing import Any, Optional
6
+
7
+ from .errors import NotConnectedError
8
+
9
+
10
+ class _Base:
11
+ _api: Any # the core's client object (MT5API / MT4API)
12
+ _core: Any # the core's namespace (types and enums)
13
+
14
+ def _require_connected(self) -> None:
15
+ if not self._api.Connected:
16
+ raise NotConnectedError("not connected; call connect() first")
17
+
18
+ def _server_naive(self, value: Optional[datetime]) -> Optional[datetime]:
19
+ """A datetime as the naive server time the core works in. A naive value is taken to be server time already
20
+ (as documented); a timezone-aware one is converted: to UTC, then by the server's offset from UTC."""
21
+ if value is None or value.tzinfo is None:
22
+ return value
23
+ offset = getattr(self, "server_utc_offset", None) or timedelta(0)
24
+ return value.astimezone(timezone.utc).replace(tzinfo=None) + offset
pmt4api/_coverage.py ADDED
@@ -0,0 +1,186 @@
1
+ """Where every public member and type of the C# SDK went.
2
+
3
+ ``COVERAGE`` maps each public member name of ``MT4API`` (and of ``OrderClient``, which holds the trade timeout, the
4
+ default placement source, the progress event and the async trade extensions) to the Python name that provides it:
5
+
6
+ * ``"Client.<attr>"`` a method, property or constructor argument of :class:`pmt4api.Client`
7
+ * ``"Client(<argument>)"`` an argument of the constructor
8
+ * ``"pmt4api.<name>"`` a module-level function
9
+ * ``"not applicable: …"`` deliberately has no Python counterpart, with the reason
10
+
11
+ ``TYPES`` does the same for the public types. The integration test ``test_coverage`` reflects the DLL it is pointed at
12
+ and fails when a public member or type has no entry here, or when an entry names a Python attribute that does not
13
+ exist. A new SDK release therefore cannot add API without a decision being recorded. ``NOTES`` adds a sentence where
14
+ the mapping is not one to one.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from typing import Dict
19
+
20
+ COVERAGE: Dict[str, str] = {
21
+ # ---- lifecycle and session ----
22
+ "Connect": "Client.connect",
23
+ "Disconnect": "Client.disconnect",
24
+ "Reconnect": "Client.reconnect",
25
+ "Connected": "Client.connected",
26
+ "User": "Client.login",
27
+ "Host": "Client.host",
28
+ "Port": "Client.port",
29
+ "IsInvestor": "Client.is_investor",
30
+ "ServerTradeBuild": "Client.trade_build",
31
+ "ServerTime": "Client.server_time",
32
+ "Servers": "Client.servers",
33
+ "SrvList": "Client.server_list",
34
+ "LastSrvRecord": "not applicable: the raw wire record the server list was parsed from; read Client.server_list",
35
+ "OrderClient": "not applicable: its members (timeout, placement source, progress event, async trading) are "
36
+ "reached through Client(trade_timeout), Client(placed_type), Client.on_order_progress and the "
37
+ "*_async methods; Client.raw gives the core object",
38
+ # ---- configuration ----
39
+ "ApiKey": "Client(api_key)",
40
+ "TokenServiceUrl": "Client(token_url)",
41
+ "TokenServiceTimeout": "Client(token_timeout)",
42
+ "ConnectTimeout": "Client(connect_timeout)",
43
+ "GetQuoteTimeoutMs": "Client(quote_timeout)",
44
+ "DownloadOrderHistoryTimeoutMs": "Client(history_timeout)",
45
+ "DownloadQuoteHistoryTimeout": "Client(bars_timeout)",
46
+ "NoServerMessagesTimeout": "Client(no_message_timeout)",
47
+ "TradeTimeout": "Client(trade_timeout)",
48
+ "AutoReconnect": "Client(auto_reconnect)",
49
+ "ReconnectBackoffMs": "Client(reconnect_backoff)",
50
+ "ReconnectMaxAttempts": "Client(reconnect_max_attempts)",
51
+ "Encoding": "Client(comment_encoding)",
52
+ "PlacedType": "Client(placed_type)",
53
+ # ---- symbols and market data ----
54
+ "GetSymbolNames": "Client.symbols",
55
+ "GetSymbolSpec": "Client.symbol",
56
+ "SymbolDigits": "Client.symbol",
57
+ "SymbolExecution": "Client.symbol",
58
+ "Groups": "Client.symbol_groups",
59
+ "Subscribe": "Client.subscribe",
60
+ "Unsubscribe": "Client.unsubscribe",
61
+ "IsSubscribed": "Client.is_subscribed",
62
+ "GetQuote": "Client.quote",
63
+ "GetTickValue": "Client.tick_value",
64
+ "GetAskTickValue": "Client.tick_value",
65
+ "GetBidTickValue": "Client.tick_value",
66
+ "DownloadQuoteHistory": "Client.bars",
67
+ # ---- account ----
68
+ "Account": "Client.account",
69
+ "AccountName": "Client.account",
70
+ "AccountType": "Client.account",
71
+ "AccountBalance": "Client.balance",
72
+ "AccountCredit": "Client.credit",
73
+ "AccountEquity": "Client.equity",
74
+ "AccountProfit": "Client.profit",
75
+ "AccountMargin": "Client.margin",
76
+ "AccountFreeMargin": "Client.free_margin",
77
+ "AccountMarginLevel": "Client.margin_level",
78
+ "AccountLeverage": "Client.leverage",
79
+ "LiveEquity": "Client.live_equity",
80
+ "LiveEquityCached": "Client.equity",
81
+ "LiveMarginCached": "Client.margin",
82
+ "UpdateProfits": "Client.update_profits",
83
+ "UpdateMargin": "Client.update_margin",
84
+ "GetOpenedOrders": "Client.orders",
85
+ "GetOpenedOrder": "Client.order",
86
+ "DownloadOrderHistory": "Client.history",
87
+ "GetAccountSummary": "Client.summary",
88
+ "CalculateEquityHistory": "Client.equity_history",
89
+ # ---- sizing and risk ----
90
+ "CalculateLots": "Client.lots_for_risk",
91
+ "RequiredMargin": "Client.required_margin",
92
+ "CalculateOrderProfit": "Client.order_profit",
93
+ # ---- trading ----
94
+ "OrderSend": "Client.buy",
95
+ "OrderModify": "Client.modify",
96
+ "OrderClose": "Client.close",
97
+ "OrderCancel": "Client.cancel",
98
+ "OrderCloseBy": "Client.close_by",
99
+ "OrderMultipleCloseBy": "Client.close_all_by",
100
+ "OrderSendAsync": "Client.buy_async",
101
+ "OrderModifyAsync": "Client.modify_async",
102
+ "OrderCloseAsync": "Client.close_async",
103
+ "OrderCancelAsync": "Client.cancel_async",
104
+ "OrderCloseByAsync": "Client.close_by_async",
105
+ "OrderMultipleCloseByAsync": "Client.close_all_by_async",
106
+ "OrderSendAsyncTask": "Client.abuy",
107
+ "OrderModifyAsyncTask": "Client.amodify",
108
+ "OrderCloseAsyncTask": "Client.aclose",
109
+ "OrderCancelAsyncTask": "Client.acancel",
110
+ "OrderCloseByAsyncTask": "Client.aclose_by",
111
+ # ---- events ----
112
+ "OnQuote": "Client.on_quote",
113
+ "OnOrderUpdate": "Client.on_order_update",
114
+ "OnOrderProgress": "Client.on_order_progress",
115
+ "OnConnect": "Client.on_connect",
116
+ "OnDisconnect": "Client.on_disconnect",
117
+ "OnQuoteHistory": "Client.on_quote_history",
118
+ "OnSymbolsUpdate": "Client.on_symbols_update",
119
+ # ---- offline ----
120
+ "LoadServersIni": "pmt4api.load_servers_ini",
121
+ "SaveServersIni": "pmt4api.save_servers_ini",
122
+ }
123
+
124
+ _DELEGATE = "not applicable: a .NET delegate type; subscribe with %s"
125
+
126
+ TYPES: Dict[str, str] = {
127
+ "MT4API": "pmt4api.Client",
128
+ "OrderClient": "pmt4api.Client",
129
+ "OrderClientAsyncTaskExtensions": "Client.abuy",
130
+ "AccountInfo": "pmt4api.AccountInfo",
131
+ "AccountSummary": "pmt4api.AccountSummary",
132
+ "Bar": "pmt4api.Bar",
133
+ "EquityPoint": "pmt4api.EquityPoint",
134
+ "LotSizing": "pmt4api.LotSizing",
135
+ "Mt4Server": "pmt4api.ServerRecord",
136
+ "Order": "pmt4api.Order",
137
+ "OrderProgress": "pmt4api.OrderProgress",
138
+ "OrderUpdate": "pmt4api.OrderUpdate",
139
+ "Quote": "pmt4api.Quote",
140
+ "QuoteHistoryEventArgs": "pmt4api.QuoteHistoryEvent",
141
+ "SymGroup": "pmt4api.SymbolGroup",
142
+ "SymbolSpec": "pmt4api.SymbolInfo",
143
+ "ConnectEventArgs": "pmt4api.ConnectEvent",
144
+ "DisconnectEventArgs": "pmt4api.ConnectEvent",
145
+ "ConnectEventHandler": _DELEGATE % "Client.on_connect",
146
+ "DisconnectEventHandler": _DELEGATE % "Client.on_disconnect",
147
+ "OrderUpdateEventHandler": _DELEGATE % "Client.on_order_update",
148
+ "OrderProgressEventHandler": _DELEGATE % "Client.on_order_progress",
149
+ "QuoteEventHandler": _DELEGATE % "Client.on_quote",
150
+ "QuoteHistoryEventHandler": _DELEGATE % "Client.on_quote_history",
151
+ "SymbolsUpdateEventHandler": _DELEGATE % "Client.on_symbols_update",
152
+ "Logger": "pmt4api.enable_core_logging",
153
+ "Logger+OnMsgHandler": "not applicable: a .NET delegate type; enable_core_logging forwards to Python logging",
154
+ "Logger+OnMsgHandlerEx": "not applicable: a .NET delegate type; enable_core_logging forwards to Python logging",
155
+ "Diag": "pmt4api.enable_core_logging",
156
+ "ConnectException": "pmt4api.ConnectError",
157
+ "InvestorModeException": "pmt4api.InvestorModeError",
158
+ "LicenseException": "pmt4api.LicenseError",
159
+ "LoginRejectedException": "pmt4api.LoginRejectedError",
160
+ "RequoteException": "pmt4api.RequoteError",
161
+ "ServerException": "pmt4api.ServerError",
162
+ "TokenServiceException": "pmt4api.TokenServiceError",
163
+ "TradeTimeoutException": "pmt4api.RequestTimeout",
164
+ # enums are exposed as their member names (strings)
165
+ "AccountType": "enum: str", "EquityTimeframe": "enum: str", "ExecutionType": "enum: str",
166
+ "LicenseReason": "enum: str", "Logger+MsgType": "enum: str", "OrderType": "enum: str", "PlacedType": "enum: str",
167
+ "ProgressType": "enum: str", "Timeframe": "enum: str", "UpdateType": "enum: str",
168
+ }
169
+
170
+ NOTES: Dict[str, str] = {
171
+ "OrderSend": "`buy`, `sell` and `place_order` cover the operations; `magic` other than 0 is refused (the core "
172
+ "does not transmit it); the volume must be a multiple of 0.01",
173
+ "OrderSendAsync": "`buy_async`, `sell_async`, `place_order_async`; the integer returned is the request id that "
174
+ "`on_order_progress` reports",
175
+ "OrderModify": "the symbol, type, volume and the values you did not change are taken from the open order, "
176
+ "including a pending order's expiration",
177
+ "OrderClose": "partial close with `lots=`; the price defaults to the current bid / ask",
178
+ "GetSymbolSpec": "also `SymbolDigits` and `SymbolExecution`: `SymbolInfo.digits` and `.execution`",
179
+ "GetTickValue": "`side='ask'`; `side='bid'` is `GetBidTickValue`. Raises instead of returning NaN",
180
+ "DownloadQuoteHistory": "`count=` bars ending at `end`, or everything since `start=`; pages past the wire's "
181
+ "32767-bar limit; the server's own depth limit applies",
182
+ "DownloadOrderHistory": "`timeout=` and a `threading.Event` for `cancel=` replace the timeout and cancellation "
183
+ "token arguments; `ahistory` cancels with the awaiting task",
184
+ "Servers": "give `servers=[(host, port), ...]` to the constructor or call `add_server`",
185
+ "Reconnect": "returns False when every server failed; with `auto_reconnect=True` the core does it by itself",
186
+ }