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
pmt5api/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""pmt5api — Python access to the wmt5api MetaTrader 5 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, aggregate
|
|
9
|
+
from .client import Client, Proxy
|
|
10
|
+
from .diagnostics import enable_core_logging
|
|
11
|
+
from .errors import (BusyError, ConnectError, NoQuoteError, ConnectionLostError, CoreNotFoundError, InvalidRequestError, InvestorModeError,
|
|
12
|
+
LicenseError, NotConnectedError, ProxyError, RequestTimeout, RuntimeNotFoundError,
|
|
13
|
+
ServerError, TokenServiceError, WmtError)
|
|
14
|
+
from .frames import to_frame
|
|
15
|
+
from .models import (AccountInfo, AccountSummary, BaseInfo, CommissionInfo, CommissionTier, ConnectEvent, Deal,
|
|
16
|
+
DemoAccount, DemoAccountRequest, EquityPoint, History, LotSizing, Order, OrderProgress,
|
|
17
|
+
OrderUpdate, Quote, ServerRecord, SymbolInfo, SymbolSessions, TradingSession)
|
|
18
|
+
from .servers import load_servers_dat, request_demo_account, save_servers_dat, tick_value_from_deal
|
|
19
|
+
|
|
20
|
+
__version__ = "1.0.0"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Client", "Proxy",
|
|
24
|
+
"Quote", "SymbolInfo", "SymbolSessions", "TradingSession", "CommissionInfo", "CommissionTier", "BaseInfo",
|
|
25
|
+
"AccountInfo", "AccountSummary", "EquityPoint", "LotSizing", "Order", "Deal", "History",
|
|
26
|
+
"OrderUpdate", "OrderProgress", "ConnectEvent", "QuoteHistoryEvent",
|
|
27
|
+
"ServerRecord", "DemoAccountRequest", "DemoAccount",
|
|
28
|
+
"Bar", "TIMEFRAMES", "aggregate", "to_frame",
|
|
29
|
+
"load_servers_dat", "save_servers_dat", "request_demo_account", "tick_value_from_deal", "enable_core_logging",
|
|
30
|
+
"WmtError", "LicenseError", "TokenServiceError", "ConnectError", "ConnectionLostError", "NotConnectedError",
|
|
31
|
+
"ServerError", "InvestorModeError", "ProxyError", "RequestTimeout", "NoQuoteError", "InvalidRequestError", "BusyError",
|
|
32
|
+
"CoreNotFoundError",
|
|
33
|
+
"RuntimeNotFoundError", "COVERAGE", "TYPES", "__version__",
|
|
34
|
+
]
|
pmt5api/__main__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""``python -m pmt5api`` — what is installed, and whether it works on this machine.
|
|
2
|
+
|
|
3
|
+
python -m pmt5api offline: versions, the core, the .NET runtime, this platform
|
|
4
|
+
python -m pmt5api --connect also connect read-only using PMT5_LOGIN, PMT5_PASSWORD, PMT5_HOST[, PMT5_PORT]
|
|
5
|
+
and WMTAPI_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("WMTAPI_KEY", "")
|
|
23
|
+
if not key:
|
|
24
|
+
return "not set (WMTAPI_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 pmt5api", 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("pmt5api %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["PMT5_LOGIN"]), os.environ["PMT5_PASSWORD"],
|
|
60
|
+
os.environ["PMT5_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("PMT5_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, mode %s, trading %s"
|
|
69
|
+
% (live.company, a.currency, a.leverage, len(live.symbols()), live.account_method,
|
|
70
|
+
"allowed" if live.can_trade else "not allowed (investor or disabled)"))
|
|
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())
|
pmt5api/_account.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""The account: balances, positions, orders, history, and the curves derived from them."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from ._base import _Base
|
|
8
|
+
from ._util import enum_value, to_core_time, translated
|
|
9
|
+
from .errors import NotConnectedError
|
|
10
|
+
from .models import AccountInfo, AccountSummary, EquityPoint, History, Order
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _AccountMixin(_Base):
|
|
14
|
+
@translated
|
|
15
|
+
def account(self, refresh: bool = True) -> AccountInfo:
|
|
16
|
+
"""Account details. With ``refresh`` (the default) the equity, profit and margin figures are brought up to date
|
|
17
|
+
first: every open position's symbol is quoted once and the profits are recalculated."""
|
|
18
|
+
self._require_connected()
|
|
19
|
+
api = self._api
|
|
20
|
+
if refresh:
|
|
21
|
+
symbols = {o.Symbol for o in api.GetOpenedOrders() if o.Symbol}
|
|
22
|
+
for s in symbols:
|
|
23
|
+
if not api.Connected: # never let a refresh trigger a connect of its own
|
|
24
|
+
raise NotConnectedError("the connection was lost while refreshing the account")
|
|
25
|
+
try:
|
|
26
|
+
api.GetQuote(s, 3000)
|
|
27
|
+
except Exception: # noqa: BLE001 — a quiet symbol must not block the account snapshot
|
|
28
|
+
pass
|
|
29
|
+
if not api.Connected:
|
|
30
|
+
raise NotConnectedError("the connection was lost while refreshing the account")
|
|
31
|
+
api.UpdateProfits()
|
|
32
|
+
a = api.Account
|
|
33
|
+
return AccountInfo(
|
|
34
|
+
login=int(a.Login), name=str(a.UserName or ""), currency=str(api.AccountCurrency or ""),
|
|
35
|
+
leverage=int(a.Leverage), balance=float(a.Balance), credit=float(a.Credit),
|
|
36
|
+
equity=float(api.AccountEquity), profit=float(api.AccountProfit), margin=float(api.AccountMargin),
|
|
37
|
+
free_margin=float(api.AccountFreeMargin), margin_level=float(api.AccountMarginLevel),
|
|
38
|
+
floating_pl=float(api.AccountFloatingProfit), swap=float(api.AccountSwap),
|
|
39
|
+
commission=float(api.AccountCommission), fee=float(api.AccountFee), blocked=float(a.Blocked),
|
|
40
|
+
company=str(api.AccountCompanyName or ""), method=str(api.AccountMethod), account_type=str(a.Type or ""),
|
|
41
|
+
trade_flags=int(a.TradeFlags), country=str(a.Country or ""), city=str(a.City or ""),
|
|
42
|
+
state=str(a.State or ""), zip_code=str(a.ZipCode or ""), address=str(a.UserAddress or ""),
|
|
43
|
+
phone=str(a.Phone or ""), email=str(a.Email or ""),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@translated
|
|
47
|
+
def update_profits(self) -> None:
|
|
48
|
+
"""Recompute floating P/L and used margin from the current quotes. Warm the open positions' quotes first (or
|
|
49
|
+
use ``account(refresh=True)``, which does both)."""
|
|
50
|
+
self._require_connected()
|
|
51
|
+
self._api.UpdateProfits()
|
|
52
|
+
|
|
53
|
+
# ---- orders and positions ----------------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
@translated
|
|
56
|
+
def orders(self) -> List[Order]:
|
|
57
|
+
"""Everything currently working: open positions and pending orders."""
|
|
58
|
+
self._require_connected()
|
|
59
|
+
return [Order._from_core(o) for o in self._api.GetOpenedOrders()]
|
|
60
|
+
|
|
61
|
+
def positions(self) -> List[Order]:
|
|
62
|
+
"""Open positions only (pending orders excluded).
|
|
63
|
+
|
|
64
|
+
A position is an order of type ``Buy`` or ``Sell``; pending orders are the ``*Limit`` and ``*Stop`` types. The
|
|
65
|
+
order's ``state`` is not used: the core can leave a freshly opened position in ``Started`` for a long time.
|
|
66
|
+
"""
|
|
67
|
+
return [o for o in self.orders() if o.order_type in ("Buy", "Sell")]
|
|
68
|
+
|
|
69
|
+
@translated
|
|
70
|
+
def closed_orders(self) -> List[Order]:
|
|
71
|
+
"""The orders this session has seen close (a rolling cache of about ten). For anything older use
|
|
72
|
+
:meth:`history`."""
|
|
73
|
+
self._require_connected()
|
|
74
|
+
return [Order._from_core(o) for o in self._api.ClosedOrders()]
|
|
75
|
+
|
|
76
|
+
# ---- history -----------------------------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
@translated
|
|
79
|
+
def history(self, start: Optional[datetime] = None, end: Optional[datetime] = None,
|
|
80
|
+
timeout: Optional[float] = None) -> History:
|
|
81
|
+
"""Orders and deals between ``start`` and ``end`` (default: the last 30 days up to now). Blocks until the
|
|
82
|
+
server has sent everything, up to ``timeout`` seconds (default: the client's ``history_timeout``).
|
|
83
|
+
|
|
84
|
+
Naive datetimes are used exactly as given, in the broker's server time; timezone-aware ones are converted to
|
|
85
|
+
it. The default window is computed on the server's clock.
|
|
86
|
+
"""
|
|
87
|
+
self._require_connected()
|
|
88
|
+
now = self.server_time or datetime.now(timezone.utc).replace(tzinfo=None)
|
|
89
|
+
end = self._server_naive(end)
|
|
90
|
+
start = self._server_naive(start)
|
|
91
|
+
if end is None:
|
|
92
|
+
end = now + timedelta(days=1) # nothing the server has booked is later than this
|
|
93
|
+
if start is None:
|
|
94
|
+
start = now - timedelta(days=30)
|
|
95
|
+
ms = int(timeout * 1000) if timeout is not None else int(self._api.DownloadOrderHistoryTimeoutMs)
|
|
96
|
+
return History._from_core(self._api.DownloadOrderHistory(to_core_time(start), to_core_time(end), ms))
|
|
97
|
+
|
|
98
|
+
@translated
|
|
99
|
+
def deal_history(self, year: int, month: int) -> History:
|
|
100
|
+
"""One calendar month of history (a convenience over :meth:`history`)."""
|
|
101
|
+
self._require_connected()
|
|
102
|
+
return History._from_core(self._api.RequestDealHistory(int(year), int(month)))
|
|
103
|
+
|
|
104
|
+
@translated
|
|
105
|
+
def request_history(self, start: datetime, end: datetime) -> None:
|
|
106
|
+
"""Ask for history without waiting; it arrives through :meth:`on_history`. The server's answer is not trimmed
|
|
107
|
+
to ``start`` / ``end`` (it can hold more than you asked for, in pages): filter by time. The blocking
|
|
108
|
+
:meth:`history` does trim."""
|
|
109
|
+
self._require_connected()
|
|
110
|
+
self._api.RequestOrderHistory(to_core_time(self._server_naive(start)), to_core_time(self._server_naive(end)))
|
|
111
|
+
|
|
112
|
+
@translated
|
|
113
|
+
def request_history_month(self, year: int, month: int) -> None:
|
|
114
|
+
"""Ask for one calendar month of history without waiting; it arrives through :meth:`on_history`."""
|
|
115
|
+
self._require_connected()
|
|
116
|
+
self._api.RequestOrderHistory(int(year), int(month))
|
|
117
|
+
|
|
118
|
+
# ---- realized figures and curves ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
@translated
|
|
121
|
+
def summary(self, start: Optional[datetime] = None, end: Optional[datetime] = None) -> AccountSummary:
|
|
122
|
+
"""Realized trading result, deposits and withdrawals, from the deal ledger (all time by default).
|
|
123
|
+
|
|
124
|
+
Downloads the deal history, so it blocks; call it off your UI thread (or use ``asyncio.to_thread``).
|
|
125
|
+
"""
|
|
126
|
+
self._require_connected()
|
|
127
|
+
if start is None and end is None:
|
|
128
|
+
return AccountSummary._from_core(self._api.GetAccountSummary())
|
|
129
|
+
if start is None or end is None:
|
|
130
|
+
raise ValueError("give both start and end, or neither")
|
|
131
|
+
return AccountSummary._from_core(self._api.GetAccountSummary(to_core_time(self._server_naive(start)),
|
|
132
|
+
to_core_time(self._server_naive(end))))
|
|
133
|
+
|
|
134
|
+
@translated
|
|
135
|
+
def equity_history(self, start: datetime, timeframe: str = "D1", exclude_same_bars: bool = True) -> List[EquityPoint]:
|
|
136
|
+
"""The balance and equity curve since ``start`` (``timeframe`` is ``H1``, ``H4`` or ``D1``).
|
|
137
|
+
|
|
138
|
+
``balance`` is exact; the historical ``equity`` is a bar-priced reconstruction and a point with
|
|
139
|
+
``incomplete`` set has understated floating P/L. With ``exclude_same_bars`` (the default, as in the C# library)
|
|
140
|
+
consecutive points with an unchanged balance and equity are dropped. Downloads deals and per-symbol bars, so it
|
|
141
|
+
blocks.
|
|
142
|
+
"""
|
|
143
|
+
self._require_connected()
|
|
144
|
+
tf = enum_value(self._core.EquityTimeframe, timeframe, "equity timeframe")
|
|
145
|
+
points = self._api.CalculateEquityHistory(to_core_time(self._server_naive(start)), tf, bool(exclude_same_bars))
|
|
146
|
+
return [EquityPoint._from_core(p) for p in (points or ())]
|
pmt5api/_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
|
pmt5api/_coverage.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Where every public member and type of the C# SDK went.
|
|
2
|
+
|
|
3
|
+
``COVERAGE`` maps each public member name of ``MT5API`` to the Python name that provides it:
|
|
4
|
+
|
|
5
|
+
* ``"Client.<attr>"`` a method, property or constructor argument of :class:`pmt5api.Client`
|
|
6
|
+
* ``"Client(<argument>)"`` an argument of the constructor
|
|
7
|
+
* ``"pmt5api.<name>"`` a module-level function
|
|
8
|
+
* ``"not applicable: …"`` deliberately has no Python counterpart, with the reason
|
|
9
|
+
|
|
10
|
+
``TYPES`` does the same for the public types. The integration test ``test_coverage`` reflects the DLL it is pointed at
|
|
11
|
+
and fails when a public member or type has no entry here, or when an entry names a Python attribute that does not
|
|
12
|
+
exist. A new SDK release therefore cannot add API without a decision being recorded.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Dict
|
|
17
|
+
|
|
18
|
+
COVERAGE: Dict[str, str] = {
|
|
19
|
+
# ---- lifecycle and session ----
|
|
20
|
+
"Connect": "Client.connect",
|
|
21
|
+
"Disconnect": "Client.disconnect",
|
|
22
|
+
"Connected": "Client.connected",
|
|
23
|
+
"ConnectTime": "Client.connect_time",
|
|
24
|
+
"LastQuoteTime": "Client.last_quote_time",
|
|
25
|
+
"LastServerMessageTime": "Client.last_server_message_time",
|
|
26
|
+
"ServerTime": "Client.server_time",
|
|
27
|
+
"ServerTimeZoneInMinutes": "Client.server_utc_offset",
|
|
28
|
+
"ServerTradeBuild": "Client.trade_build",
|
|
29
|
+
"ServerSymBuild": "Client.symbol_build",
|
|
30
|
+
"AccountCompanyName": "Client.company",
|
|
31
|
+
"AccountMethod": "Client.account_method",
|
|
32
|
+
"IsInvestor": "Client.is_investor",
|
|
33
|
+
"IsTradeAllowed": "Client.can_trade",
|
|
34
|
+
"IsTradeDisableOnServer": "Client.trading_disabled_by_server",
|
|
35
|
+
"IsNotConfirmedAccount": "Client.is_confirmed",
|
|
36
|
+
"User": "Client.login",
|
|
37
|
+
"Password": "Client(password)",
|
|
38
|
+
"Host": "Client.host",
|
|
39
|
+
"Server": "Client.host",
|
|
40
|
+
"Port": "Client.port",
|
|
41
|
+
"Id": "not applicable: a free-form tag for the caller's own use; keep it in your own code",
|
|
42
|
+
"ProcessServerMessagesInThread": "not applicable: documented by the core as reserved and unused",
|
|
43
|
+
"AuthTagsSeen": "not applicable: protocol diagnostics for the ops sentinel, of no use to an application",
|
|
44
|
+
"AuthUnknownTags": "not applicable: protocol diagnostics for the ops sentinel, of no use to an application",
|
|
45
|
+
"ChangePassword": "Client.change_password",
|
|
46
|
+
# ---- configuration ----
|
|
47
|
+
"ApiKey": "Client(api_key)",
|
|
48
|
+
"TokenServiceUrl": "Client(token_url)",
|
|
49
|
+
"TokenServiceTimeout": "Client(token_timeout)",
|
|
50
|
+
"ConnectTimeout": "Client(connect_timeout)",
|
|
51
|
+
"ClusterMemberConnectTimeout": "Client(cluster_member_timeout)",
|
|
52
|
+
"ExecutionTimeout": "Client(execution_timeout)",
|
|
53
|
+
"GetQuoteTimeoutMs": "Client(quote_timeout)",
|
|
54
|
+
"DownloadOrderHistoryTimeoutMs": "Client(history_timeout)",
|
|
55
|
+
"DownloadQuoteHistoryTimeout": "Client(bars_timeout)",
|
|
56
|
+
"PingTime": "Client(ping_interval)",
|
|
57
|
+
"NoServerMessagesTimeout": "Client(no_message_timeout)",
|
|
58
|
+
"ProcessEventTimeoutMs": "not applicable: the core accepts the value but never applies it (events run on the thread pool); "
|
|
59
|
+
"pmt5api delivers events through one queued dispatcher instead — see Client.events_dropped",
|
|
60
|
+
"AutoReconnect": "Client(auto_reconnect)",
|
|
61
|
+
"ReconnectBackoffMs": "Client(reconnect_backoff)",
|
|
62
|
+
"ReconnectMaxBackoffMs": "Client(reconnect_max_backoff)",
|
|
63
|
+
"ReconnectMaxAttempts": "Client(reconnect_max_attempts)",
|
|
64
|
+
"ProxyEnable": "Client(proxy)",
|
|
65
|
+
"ProxyType": "Client(proxy)",
|
|
66
|
+
"ProxyHost": "Client(proxy)",
|
|
67
|
+
"ProxyPort": "Client(proxy)",
|
|
68
|
+
"ProxyUser": "Client(proxy)",
|
|
69
|
+
"ProxyPassword": "Client(proxy)",
|
|
70
|
+
"PfxFile": "Client(certificate)",
|
|
71
|
+
"PfxFilePassword": "Client(certificate_password)",
|
|
72
|
+
"CommonKey": "Client(common_key)",
|
|
73
|
+
"PlacedType": "Client(placed_type)",
|
|
74
|
+
"EnableAllOrderUpdates": "Client(all_order_updates)",
|
|
75
|
+
# ---- symbols and market data ----
|
|
76
|
+
"Symbols": "Client.symbols",
|
|
77
|
+
"Subscribe": "Client.subscribe",
|
|
78
|
+
"Unsubscribe": "Client.unsubscribe",
|
|
79
|
+
"Subscriptions": "Client.subscriptions",
|
|
80
|
+
"IsSubscribed": "Client.is_subscribed",
|
|
81
|
+
"IsQuoteSession": "Client.is_quote_session",
|
|
82
|
+
"IsTradeSession": "Client.is_trade_session",
|
|
83
|
+
"GetQuote": "Client.quote",
|
|
84
|
+
"GetQuoteTask": "Client.aquote",
|
|
85
|
+
"GetTickValue": "Client.tick_value",
|
|
86
|
+
"GetAskTickValue": "Client.tick_value",
|
|
87
|
+
"GetBidTickValue": "Client.tick_value",
|
|
88
|
+
"GetContractSize": "Client.contract_size",
|
|
89
|
+
"LedgerValuePerUnitLot": "Client.ledger_value_per_lot",
|
|
90
|
+
"DownloadQuoteHistoryToday": "Client.bars",
|
|
91
|
+
"DownloadQuoteHistoryMonth": "Client.bars",
|
|
92
|
+
"RequestQuoteHistoryToday": "Client.request_bars",
|
|
93
|
+
"RequestQuoteHistoryMonth": "Client.request_bars",
|
|
94
|
+
"ConvertToTimeframe": "pmt5api.aggregate",
|
|
95
|
+
# ---- account ----
|
|
96
|
+
"Account": "Client.account",
|
|
97
|
+
"AccountCurrency": "Client.account",
|
|
98
|
+
"AccountEquity": "Client.account",
|
|
99
|
+
"AccountProfit": "Client.account",
|
|
100
|
+
"AccountFloatingProfit": "Client.account",
|
|
101
|
+
"AccountSwap": "Client.account",
|
|
102
|
+
"AccountCommission": "Client.account",
|
|
103
|
+
"AccountFee": "Client.account",
|
|
104
|
+
"AccountMargin": "Client.account",
|
|
105
|
+
"AccountFreeMargin": "Client.account",
|
|
106
|
+
"AccountMarginLevel": "Client.account",
|
|
107
|
+
"UpdateProfits": "Client.update_profits",
|
|
108
|
+
"GetOpenedOrders": "Client.orders",
|
|
109
|
+
"ClosedOrders": "Client.closed_orders",
|
|
110
|
+
"DownloadOrderHistory": "Client.history",
|
|
111
|
+
"RequestDealHistory": "Client.deal_history",
|
|
112
|
+
"RequestOrderHistory": "Client.request_history",
|
|
113
|
+
"GetAccountSummary": "Client.summary",
|
|
114
|
+
"CalculateEquityHistory": "Client.equity_history",
|
|
115
|
+
# ---- sizing and risk ----
|
|
116
|
+
"CalibrateMoneyScale": "Client.calibrate_money_scale",
|
|
117
|
+
"MoneyScale": "Client.money_scale",
|
|
118
|
+
"MoneyScaleEvidence": "Client.money_scale_evidence",
|
|
119
|
+
"MoneyScaleFromDeal": "Client.money_scale_from_deal",
|
|
120
|
+
"CalculateLots": "Client.lots_for_risk",
|
|
121
|
+
"RequiredMargin": "Client.required_margin",
|
|
122
|
+
"CalculateOrderProfit": "Client.order_profit",
|
|
123
|
+
"ComputeTickValueFromDeal": "pmt5api.tick_value_from_deal",
|
|
124
|
+
# ---- trading ----
|
|
125
|
+
"OrderSend": "Client.buy",
|
|
126
|
+
"OrderModify": "Client.modify",
|
|
127
|
+
"OrderClose": "Client.close",
|
|
128
|
+
"OrderCancel": "Client.cancel",
|
|
129
|
+
"OrderSendAsync": "Client.buy_async",
|
|
130
|
+
"OrderModifyAsync": "Client.modify_async",
|
|
131
|
+
"OrderCloseAsync": "Client.close_async",
|
|
132
|
+
"OrderCancelAsync": "Client.cancel_async",
|
|
133
|
+
"OrderSendAsyncTask": "Client.abuy",
|
|
134
|
+
"OrderModifyAsyncTask": "Client.amodify",
|
|
135
|
+
"OrderCloseAsyncTask": "Client.aclose",
|
|
136
|
+
"OrderCancelAsyncTask": "Client.acancel",
|
|
137
|
+
"GetRequestId": "Client.new_request_id",
|
|
138
|
+
# ---- events ----
|
|
139
|
+
"OnQuote": "Client.on_quote",
|
|
140
|
+
"OnOrderUpdate": "Client.on_order_update",
|
|
141
|
+
"OnOrderProgress": "Client.on_order_progress",
|
|
142
|
+
"OnConnectProgress": "Client.on_connect_progress",
|
|
143
|
+
"OnOrderHistory": "Client.on_history",
|
|
144
|
+
"OnQuoteHistory": "Client.on_quote_history",
|
|
145
|
+
"OnSymbolsUpdate": "Client.on_symbols_update",
|
|
146
|
+
"OnSymbolUpdate": "Client.on_symbol_update",
|
|
147
|
+
# ---- offline and administration ----
|
|
148
|
+
"LoadServersDat": "pmt5api.load_servers_dat",
|
|
149
|
+
"SaveServersDat": "pmt5api.save_servers_dat",
|
|
150
|
+
"RequestDemoAccount": "pmt5api.request_demo_account",
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
TYPES: Dict[str, str] = {
|
|
154
|
+
"MT5API": "pmt5api.Client",
|
|
155
|
+
"Account": "pmt5api.AccountInfo",
|
|
156
|
+
"AccountAnswer": "pmt5api.DemoAccount",
|
|
157
|
+
"AccountRequest": "pmt5api.DemoAccountRequest",
|
|
158
|
+
"AccountSummary": "pmt5api.AccountSummary",
|
|
159
|
+
"Bar": "pmt5api.Bar",
|
|
160
|
+
"Comission": "pmt5api.CommissionTier",
|
|
161
|
+
"ComissionInfo": "pmt5api.CommissionInfo",
|
|
162
|
+
"ConnectEventArgs": "pmt5api.ConnectEvent",
|
|
163
|
+
"ConnectException": "pmt5api.ConnectError",
|
|
164
|
+
"ConnectionException": "pmt5api.ConnectionLostError",
|
|
165
|
+
"Deal": "pmt5api.Deal",
|
|
166
|
+
"Diag": "pmt5api.enable_core_logging",
|
|
167
|
+
"EquityPoint": "pmt5api.EquityPoint",
|
|
168
|
+
"Expiration": "Client.place_order(expiration)",
|
|
169
|
+
"FromBufReader": "not applicable: a protocol buffer reader; it carries no application meaning",
|
|
170
|
+
"InvestorModeException": "pmt5api.InvestorModeError",
|
|
171
|
+
"LicenseException": "pmt5api.LicenseError",
|
|
172
|
+
"Logger": "pmt5api.enable_core_logging",
|
|
173
|
+
"Logger+OnMsgHandler": "pmt5api.enable_core_logging",
|
|
174
|
+
"Logger+OnMsgHandlerEx": "pmt5api.enable_core_logging",
|
|
175
|
+
"LotSizing": "pmt5api.LotSizing",
|
|
176
|
+
"OnConnectProgress": "Client.on_connect_progress",
|
|
177
|
+
"OnOrderHistory": "Client.on_history",
|
|
178
|
+
"OnOrderProgress": "Client.on_order_progress",
|
|
179
|
+
"OnOrderUpdate": "Client.on_order_update",
|
|
180
|
+
"OnQuote": "Client.on_quote",
|
|
181
|
+
"OnQuoteHistory": "Client.on_quote_history",
|
|
182
|
+
"OnSymbolUpdate": "Client.on_symbol_update",
|
|
183
|
+
"OnSymbolsUpdate": "Client.on_symbols_update",
|
|
184
|
+
"Order": "pmt5api.Order",
|
|
185
|
+
"OrderHistoryEventArgs": "pmt5api.History",
|
|
186
|
+
"OrderProgress": "pmt5api.OrderProgress",
|
|
187
|
+
"OrderProgressEventArgs": "not applicable: superseded by OrderProgress, which carries the request, the result and the update",
|
|
188
|
+
"OrderUpdate": "pmt5api.OrderUpdate",
|
|
189
|
+
"ProxyException": "pmt5api.ProxyError",
|
|
190
|
+
"ProxySocket": "not applicable: the transport the core uses for a proxy; configure it with Client(proxy=Proxy(...))",
|
|
191
|
+
"Quote": "pmt5api.Quote",
|
|
192
|
+
"QuoteHistoryEventArgs": "pmt5api.QuoteHistoryEvent",
|
|
193
|
+
"ServerException": "pmt5api.ServerError",
|
|
194
|
+
"ServerRecord": "pmt5api.ServerRecord",
|
|
195
|
+
"Session": "pmt5api.TradingSession",
|
|
196
|
+
"SymBaseInfo": "pmt5api.BaseInfo",
|
|
197
|
+
"SymGroup": "pmt5api.SymbolInfo",
|
|
198
|
+
"SymbolInfo": "pmt5api.SymbolInfo",
|
|
199
|
+
"SymbolSessions": "pmt5api.SymbolSessions",
|
|
200
|
+
"SymbolUpdate": "Client.on_symbol_update",
|
|
201
|
+
"Symbols": "Client.symbols",
|
|
202
|
+
"TokenServiceException": "pmt5api.TokenServiceError",
|
|
203
|
+
"TradeRequest": "pmt5api.OrderProgress",
|
|
204
|
+
"TradeResult": "pmt5api.OrderProgress",
|
|
205
|
+
"TransactionInfo": "pmt5api.OrderUpdate",
|
|
206
|
+
# enums are exposed as their member names (strings)
|
|
207
|
+
"AccMethod": "enum: str", "CalculationMode": "enum: str", "CommissionPeriod": "enum: str",
|
|
208
|
+
"CommissionType": "enum: str", "ConnectProgress": "enum: str", "DealType": "enum: str", "Direction": "enum: str",
|
|
209
|
+
"EquityTimeframe": "enum: str", "ExecutionType": "enum: str", "ExpirationType": "enum: str",
|
|
210
|
+
"FillPolicy": "enum: str", "GTCMode": "enum: str", "InstantCommissionType": "enum: str",
|
|
211
|
+
"LicenseReason": "enum: str", "Logger+MsgType": "enum: str", "MarginMode": "enum: str", "Msg": "enum: str",
|
|
212
|
+
"OrderState": "enum: str", "OrderType": "enum: str", "PlacedType": "enum: str", "ProgressType": "enum: str",
|
|
213
|
+
"ProxyTypes": "enum: str", "SwapType": "enum: str", "TradeMode": "enum: str", "TradeType": "enum: str",
|
|
214
|
+
"UpdateType": "enum: str", "V3DaysSwap": "enum: str", "vCommissionUnitMode": "enum: str",
|
|
215
|
+
"vCommissionUnitType": "enum: str",
|
|
216
|
+
}
|
pmt5api/_events.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Events: typed callbacks, and sync / async iterators over them.
|
|
2
|
+
|
|
3
|
+
Callbacks are delivered by the per-client dispatcher in :mod:`._streams`: each event is snapshotted on the core's
|
|
4
|
+
thread and called back one at a time, in arrival order, on one thread; ``Client.events_dropped`` counts events lost
|
|
5
|
+
to a callback that fell too far behind.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, AsyncIterator, Callable, Iterator, Optional
|
|
11
|
+
|
|
12
|
+
from ._base import _Base
|
|
13
|
+
from ._streams import _AsyncQueueIterator, _Dispatcher, _QueueIterator
|
|
14
|
+
from .bars import QuoteHistoryEvent
|
|
15
|
+
from .errors import translate
|
|
16
|
+
from .models import ConnectEvent, History, OrderProgress, OrderUpdate, Quote
|
|
17
|
+
|
|
18
|
+
log = logging.getLogger("pmt5api")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _EventsMixin(_Base):
|
|
22
|
+
def _add_handler(self, event: str, handler: Any) -> None:
|
|
23
|
+
ev = getattr(self._api, event)
|
|
24
|
+
ev += handler
|
|
25
|
+
setattr(self._api, event, ev)
|
|
26
|
+
|
|
27
|
+
def _remove_handler(self, event: str, handler: Any) -> None:
|
|
28
|
+
ev = getattr(self._api, event)
|
|
29
|
+
ev -= handler
|
|
30
|
+
setattr(self._api, event, ev)
|
|
31
|
+
|
|
32
|
+
def _dispatcher(self) -> _Dispatcher:
|
|
33
|
+
with self._lock:
|
|
34
|
+
d = getattr(self, "_dispatch", None)
|
|
35
|
+
if d is None:
|
|
36
|
+
d = self._dispatch = _Dispatcher()
|
|
37
|
+
return d
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def events_dropped(self) -> int:
|
|
41
|
+
"""How many events were dropped because callbacks fell too far behind (see the module notes)."""
|
|
42
|
+
d = getattr(self, "_dispatch", None)
|
|
43
|
+
return d.dropped if d is not None else 0
|
|
44
|
+
|
|
45
|
+
def _watch(self, event: str, convert: Optional[Callable[[Any], Any]],
|
|
46
|
+
callback: Callable[..., None]) -> Callable[[], None]:
|
|
47
|
+
"""Subscribe ``callback`` to a core event; returns the function that removes it."""
|
|
48
|
+
dispatcher = self._dispatcher()
|
|
49
|
+
active = {"on": True}
|
|
50
|
+
|
|
51
|
+
def deliver(value: Any) -> None:
|
|
52
|
+
if not active["on"]:
|
|
53
|
+
return
|
|
54
|
+
if convert is None:
|
|
55
|
+
callback()
|
|
56
|
+
else:
|
|
57
|
+
callback(value)
|
|
58
|
+
|
|
59
|
+
def handler(*raw: Any) -> None:
|
|
60
|
+
try:
|
|
61
|
+
value = convert(raw[1]) if convert is not None else None # snapshot now, on the core's thread
|
|
62
|
+
except Exception: # noqa: BLE001
|
|
63
|
+
log.exception("%s: could not convert the event", event)
|
|
64
|
+
return
|
|
65
|
+
dispatcher.submit(deliver, value, event)
|
|
66
|
+
|
|
67
|
+
with self._lock:
|
|
68
|
+
self._add_handler(event, handler)
|
|
69
|
+
self._handlers.append(handler)
|
|
70
|
+
|
|
71
|
+
def remove() -> None:
|
|
72
|
+
active["on"] = False
|
|
73
|
+
with self._lock:
|
|
74
|
+
if handler in self._handlers:
|
|
75
|
+
self._handlers.remove(handler)
|
|
76
|
+
self._remove_handler(event, handler)
|
|
77
|
+
return remove
|
|
78
|
+
|
|
79
|
+
# ---- callbacks ---------------------------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def on_quote(self, callback: Callable[[Quote], None]) -> Callable[[], None]:
|
|
82
|
+
"""Call ``callback(quote)`` for every tick of every subscribed symbol. Returns a function that removes it."""
|
|
83
|
+
return self._watch("OnQuote", Quote._from_core, callback)
|
|
84
|
+
|
|
85
|
+
def on_order_update(self, callback: Callable[[OrderUpdate], None]) -> Callable[[], None]:
|
|
86
|
+
"""Call ``callback(update)`` when the server pushes a change to an order or position (opened, filled,
|
|
87
|
+
modified, closed, stopped out ...). Set ``all_order_updates=True`` on the client to also receive the
|
|
88
|
+
intermediate Started / Filled / Cancelling states."""
|
|
89
|
+
return self._watch("OnOrderUpdate", OrderUpdate._from_core, callback)
|
|
90
|
+
|
|
91
|
+
def on_order_progress(self, callback: Callable[[OrderProgress], None]) -> Callable[[], None]:
|
|
92
|
+
"""Call ``callback(progress)`` as an asynchronous trade request advances (see :meth:`buy_async`)."""
|
|
93
|
+
return self._watch("OnOrderProgress", OrderProgress._from_core, callback)
|
|
94
|
+
|
|
95
|
+
def on_connect_progress(self, callback: Callable[[ConnectEvent], None]) -> Callable[[], None]:
|
|
96
|
+
"""Call ``callback(event)`` for each connect step and for a drop. This is how to tell a permanent failure from
|
|
97
|
+
a transient one while ``auto_reconnect`` is on."""
|
|
98
|
+
def convert(args: Any) -> ConnectEvent:
|
|
99
|
+
err = args.Exception
|
|
100
|
+
mapped = translate(err) if err is not None else None
|
|
101
|
+
return ConnectEvent(stage=str(args.Progress), error=mapped if mapped is not None else err)
|
|
102
|
+
return self._watch("OnConnectProgress", convert, callback)
|
|
103
|
+
|
|
104
|
+
def on_history(self, callback: Callable[[History], None]) -> Callable[[], None]:
|
|
105
|
+
"""Call ``callback(history)`` when history requested with :meth:`request_history` arrives. The server may
|
|
106
|
+
deliver it in pages (``history.more`` says another page follows) and does not trim it to the range you asked
|
|
107
|
+
for: filter by time."""
|
|
108
|
+
return self._watch("OnOrderHistory", History._from_core, callback)
|
|
109
|
+
|
|
110
|
+
def on_quote_history(self, callback: Callable[[QuoteHistoryEvent], None]) -> Callable[[], None]:
|
|
111
|
+
"""Call ``callback(event)`` when bars requested with :meth:`request_bars` arrive."""
|
|
112
|
+
return self._watch("OnQuoteHistory", QuoteHistoryEvent._from_core, callback)
|
|
113
|
+
|
|
114
|
+
def on_symbols_update(self, callback: Callable[[], None]) -> Callable[[], None]:
|
|
115
|
+
"""Call ``callback()`` when the server pushes a change to the symbol list; read :meth:`symbols` again."""
|
|
116
|
+
return self._watch("OnSymbolsUpdate", None, callback)
|
|
117
|
+
|
|
118
|
+
def on_symbol_update(self, callback: Callable[[], None]) -> Callable[[], None]:
|
|
119
|
+
"""Call ``callback()`` when one symbol's data is (re)received; read :meth:`symbols` again."""
|
|
120
|
+
return self._watch("OnSymbolUpdate", None, callback)
|
|
121
|
+
|
|
122
|
+
# ---- iterators ---------------------------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
def quotes(self, *symbols: str, timeout: Optional[float] = None, maxsize: int = 10000) -> Iterator[Quote]:
|
|
125
|
+
"""Iterate live quotes of ``symbols`` (subscribing them first; with none given, every subscribed symbol).
|
|
126
|
+
Ends when ``timeout`` seconds pass without a tick. The feed is attached when this is called, not at the first
|
|
127
|
+
``next()``. If the consumer falls behind by more than ``maxsize`` quotes the oldest are dropped."""
|
|
128
|
+
wanted = {s for g in symbols for s in ([g] if isinstance(g, str) else g)}
|
|
129
|
+
pred = (lambda q: q.symbol in wanted) if wanted else None
|
|
130
|
+
return _QueueIterator(self.on_quote, timeout, maxsize, pred,
|
|
131
|
+
(lambda: self.subscribe(*sorted(wanted))) if wanted else None) # type: ignore[return-value]
|
|
132
|
+
|
|
133
|
+
def aquotes(self, *symbols: str, maxsize: int = 10000) -> AsyncIterator[Quote]:
|
|
134
|
+
"""Async version of :meth:`quotes` (no timeout; ``aclose()`` or cancel the task to stop). Call it inside a
|
|
135
|
+
running event loop."""
|
|
136
|
+
wanted = {s for g in symbols for s in ([g] if isinstance(g, str) else g)}
|
|
137
|
+
pred = (lambda q: q.symbol in wanted) if wanted else None
|
|
138
|
+
return _AsyncQueueIterator(self.on_quote, maxsize, pred,
|
|
139
|
+
(lambda: self.subscribe(*sorted(wanted))) if wanted else None) # type: ignore[return-value]
|
|
140
|
+
|
|
141
|
+
def order_updates(self, *, timeout: Optional[float] = None, maxsize: int = 1000) -> Iterator[OrderUpdate]:
|
|
142
|
+
"""Iterate order updates (attached when called). Ends when ``timeout`` seconds pass without one."""
|
|
143
|
+
return _QueueIterator(self.on_order_update, timeout, maxsize) # type: ignore[return-value]
|
|
144
|
+
|
|
145
|
+
def aorder_updates(self, *, maxsize: int = 1000) -> AsyncIterator[OrderUpdate]:
|
|
146
|
+
"""Async version of :meth:`order_updates`."""
|
|
147
|
+
return _AsyncQueueIterator(self.on_order_update, maxsize) # type: ignore[return-value]
|