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
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
|
+
}
|