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/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