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/client.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""The Python client: a typed, translated layer over every public capability of the licensed wmt5api core.
|
|
2
|
+
|
|
3
|
+
The class is assembled from topic mixins (session, market data, account, risk, trading, events). Everything the C#
|
|
4
|
+
SDK offers is reachable here by a Pythonic name; ``pmt5api.COVERAGE`` lists, member by member, where each C# member
|
|
5
|
+
went, and a test fails when the core gains a member that has no entry.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import threading
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, List, Optional
|
|
12
|
+
|
|
13
|
+
from . import _runtime
|
|
14
|
+
from ._account import _AccountMixin
|
|
15
|
+
from ._events import _EventsMixin
|
|
16
|
+
from ._market import _MarketMixin
|
|
17
|
+
from ._risk import _RiskMixin
|
|
18
|
+
from ._session import _SessionMixin
|
|
19
|
+
from ._trading import _TradingMixin
|
|
20
|
+
from ._util import enum_value, placed_type as _placed, seconds_to_ms
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class Proxy:
|
|
25
|
+
"""A proxy the core connects through. ``kind`` is ``"https"``, ``"socks4"`` or ``"socks5"``."""
|
|
26
|
+
kind: str
|
|
27
|
+
host: str
|
|
28
|
+
port: int
|
|
29
|
+
user: Optional[str] = None
|
|
30
|
+
password: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Client(_SessionMixin, _MarketMixin, _AccountMixin, _RiskMixin, _TradingMixin, _EventsMixin):
|
|
34
|
+
"""A connection to one MetaTrader 5 account through the licensed core.
|
|
35
|
+
|
|
36
|
+
>>> with Client(login, password, host, port, api_key="wk_...") as c:
|
|
37
|
+
... print(c.quote("EURUSD"))
|
|
38
|
+
|
|
39
|
+
Credentials and library: ``api_key`` falls back to the ``WMTAPI_KEY`` environment variable and ``token_url`` to
|
|
40
|
+
``WMTAPI_TOKEN_URL`` (the core reads both itself); ``dll_path`` falls back to ``WMTAPI_DLL``, then to the core
|
|
41
|
+
bundled in the wheel.
|
|
42
|
+
|
|
43
|
+
Timeouts are in seconds: ``connect_timeout`` (one connect attempt on one server), ``cluster_member_timeout`` (the
|
|
44
|
+
per-server limit while failing over across the broker's cluster), ``token_timeout`` (each licence request), ``execution_timeout`` (a trade confirmation),
|
|
45
|
+
``quote_timeout`` (waiting for a quote), ``history_timeout`` / ``bars_timeout`` (downloads), ``ping_interval``
|
|
46
|
+
(keepalive), ``no_message_timeout`` (silence after which ``connected`` turns false). Every timeout must be
|
|
47
|
+
positive; the quote, history and bars timeouts are the defaults of :meth:`quote`, :meth:`history` and :meth:`bars`.
|
|
48
|
+
|
|
49
|
+
Reconnecting: ``auto_reconnect=True`` turns on the core's watchdog (exponential backoff with jitter, failover to the
|
|
50
|
+
broker's cluster members, re-subscription); tune it with ``reconnect_backoff`` / ``reconnect_max_backoff`` (seconds)
|
|
51
|
+
and ``reconnect_max_attempts`` (0 = until reconnected or ``disconnect()``). Do not poll ``connected`` and call
|
|
52
|
+
``connect()`` in a loop.
|
|
53
|
+
|
|
54
|
+
``proxy`` routes the connection through a :class:`Proxy`. ``certificate`` / ``certificate_password`` are for brokers
|
|
55
|
+
that require a client certificate (the bytes of a ``.pfx`` file); ``common_key`` (15 bytes) is for servers that
|
|
56
|
+
require one. ``placed_type`` records how orders are placed (``"manual"``
|
|
57
|
+
or ``"expert"``; a per-call ``placed_type="manual"`` equals the default and so takes the client's value). ``all_order_updates=True`` also delivers the intermediate order states to
|
|
58
|
+
:meth:`on_order_update`.
|
|
59
|
+
|
|
60
|
+
Callbacks run on the core's worker threads, not on the thread that created the client.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, login: int, password: str, host: str, port: int = 443, *,
|
|
64
|
+
api_key: Optional[str] = None, token_url: Optional[str] = None, dll_path: Optional[str] = None,
|
|
65
|
+
connect_timeout: Optional[float] = None, cluster_member_timeout: Optional[float] = None,
|
|
66
|
+
token_timeout: Optional[float] = None, execution_timeout: Optional[float] = None,
|
|
67
|
+
quote_timeout: Optional[float] = None, history_timeout: Optional[float] = None,
|
|
68
|
+
bars_timeout: Optional[float] = None, ping_interval: Optional[float] = None,
|
|
69
|
+
no_message_timeout: Optional[float] = None,
|
|
70
|
+
auto_reconnect: bool = False, reconnect_backoff: Optional[float] = None,
|
|
71
|
+
reconnect_max_backoff: Optional[float] = None, reconnect_max_attempts: Optional[int] = None,
|
|
72
|
+
proxy: Optional[Proxy] = None, certificate: Optional[bytes] = None,
|
|
73
|
+
certificate_password: Optional[str] = None, common_key: Optional[bytes] = None,
|
|
74
|
+
placed_type: Optional[str] = None, all_order_updates: bool = False):
|
|
75
|
+
core = _runtime.load(dll_path)
|
|
76
|
+
self._core = core
|
|
77
|
+
self._login = int(login)
|
|
78
|
+
self._host = host
|
|
79
|
+
self._port = int(port)
|
|
80
|
+
self._api = core.MT5API(self._login, password, host, self._port)
|
|
81
|
+
self._lock = threading.RLock()
|
|
82
|
+
self._bars_lock = threading.Lock()
|
|
83
|
+
self._handlers: List[Any] = []
|
|
84
|
+
api = self._api
|
|
85
|
+
if api_key:
|
|
86
|
+
api.ApiKey = api_key
|
|
87
|
+
if token_url:
|
|
88
|
+
api.TokenServiceUrl = token_url
|
|
89
|
+
for attr, value in (("ConnectTimeout", connect_timeout), ("ClusterMemberConnectTimeout", cluster_member_timeout),
|
|
90
|
+
("TokenServiceTimeout", token_timeout), ("ExecutionTimeout", execution_timeout),
|
|
91
|
+
("GetQuoteTimeoutMs", quote_timeout), ("DownloadOrderHistoryTimeoutMs", history_timeout),
|
|
92
|
+
("DownloadQuoteHistoryTimeout", bars_timeout), ("PingTime", ping_interval),
|
|
93
|
+
("NoServerMessagesTimeout", no_message_timeout),
|
|
94
|
+
("ReconnectBackoffMs", reconnect_backoff), ("ReconnectMaxBackoffMs", reconnect_max_backoff)):
|
|
95
|
+
ms = seconds_to_ms(value, attr)
|
|
96
|
+
if ms is not None:
|
|
97
|
+
setattr(api, attr, ms)
|
|
98
|
+
if reconnect_max_attempts is not None:
|
|
99
|
+
if int(reconnect_max_attempts) < 0:
|
|
100
|
+
raise ValueError("reconnect_max_attempts must be 0 (until reconnected) or a positive number")
|
|
101
|
+
api.ReconnectMaxAttempts = int(reconnect_max_attempts)
|
|
102
|
+
if auto_reconnect:
|
|
103
|
+
api.AutoReconnect = True
|
|
104
|
+
if all_order_updates:
|
|
105
|
+
api.EnableAllOrderUpdates = True
|
|
106
|
+
if placed_type:
|
|
107
|
+
api.PlacedType = _placed(core.PlacedType, placed_type)
|
|
108
|
+
if proxy is not None:
|
|
109
|
+
api.ProxyEnable = True
|
|
110
|
+
api.ProxyType = enum_value(core.ProxyTypes, proxy.kind, "proxy kind")
|
|
111
|
+
api.ProxyHost = proxy.host
|
|
112
|
+
api.ProxyPort = int(proxy.port)
|
|
113
|
+
# the core's proxy handlers refuse null: "no credentials" is the empty string
|
|
114
|
+
api.ProxyUser = proxy.user or ""
|
|
115
|
+
api.ProxyPassword = proxy.password or ""
|
|
116
|
+
if certificate is not None:
|
|
117
|
+
import System # noqa: PLC0415
|
|
118
|
+
api.PfxFile = System.Array[System.Byte](bytes(certificate))
|
|
119
|
+
if certificate_password is not None:
|
|
120
|
+
api.PfxFilePassword = certificate_password
|
|
121
|
+
if common_key is not None:
|
|
122
|
+
if len(bytes(common_key)) != 15:
|
|
123
|
+
raise ValueError("common_key must be 15 bytes")
|
|
124
|
+
import System # noqa: PLC0415
|
|
125
|
+
api.CommonKey = System.Array[System.Byte](bytes(common_key))
|
|
126
|
+
|
|
127
|
+
def __repr__(self) -> str:
|
|
128
|
+
return "<pmt5api.Client login=%d host=%s:%d connected=%s>" % (
|
|
129
|
+
self._login, self._host, self._port, self.connected)
|
pmt5api/diagnostics.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Route the core's own diagnostics into Python's ``logging``."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from . import _runtime
|
|
8
|
+
|
|
9
|
+
_LEVELS = {"Trace": logging.DEBUG, "Debug": logging.DEBUG, "Info": logging.INFO, "Warn": logging.WARNING,
|
|
10
|
+
"Error": logging.ERROR, "Exception": logging.ERROR}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def enable_core_logging(logger_name: str = "pmt5api.core") -> Callable[[], None]:
|
|
14
|
+
"""Forward the core's log messages (``Logger.OnMsgEx``) and internal diagnostics (``Diag.Sink``) to the Python
|
|
15
|
+
logger ``logger_name``. Returns a function that switches it off again. Nothing is forwarded unless this is called,
|
|
16
|
+
and the core is silent by default."""
|
|
17
|
+
core = _runtime.load()
|
|
18
|
+
import System # noqa: PLC0415
|
|
19
|
+
logger = logging.getLogger(logger_name)
|
|
20
|
+
|
|
21
|
+
def on_msg(sender: Any, msg: Any, kind: Any, ex: Any = None) -> None:
|
|
22
|
+
level = _LEVELS.get(str(kind), logging.INFO)
|
|
23
|
+
if logger.isEnabledFor(level):
|
|
24
|
+
logger.log(level, "%s%s", msg, (" | %s" % ex.Message) if ex is not None else "")
|
|
25
|
+
|
|
26
|
+
def on_diag(message: Any) -> None:
|
|
27
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
28
|
+
logger.debug("%s", message)
|
|
29
|
+
|
|
30
|
+
core.Logger.OnMsgEx += on_msg
|
|
31
|
+
sink = System.Action[System.String](on_diag)
|
|
32
|
+
previous = core.Diag.Sink
|
|
33
|
+
core.Diag.Sink = sink
|
|
34
|
+
|
|
35
|
+
def disable() -> None:
|
|
36
|
+
core.Logger.OnMsgEx -= on_msg
|
|
37
|
+
core.Diag.Sink = previous
|
|
38
|
+
return disable
|