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/_market.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Symbols, quotes, subscriptions, bars and per-symbol values."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import functools
|
|
6
|
+
import math
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from datetime import date, datetime, timedelta, timezone
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
|
+
|
|
12
|
+
from ._base import _Base
|
|
13
|
+
from ._util import translated
|
|
14
|
+
from .bars import Bar, aggregate, timeframe_minutes
|
|
15
|
+
from .errors import InvalidRequestError, NoQuoteError, RequestTimeout, translate
|
|
16
|
+
from .models import (CommissionInfo, Quote, SymbolInfo, SymbolSessions, _opt_float, _symbols_from_core, _time)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _names(groups: Tuple[Any, ...]) -> List[str]:
|
|
20
|
+
return [s for group in groups for s in ([group] if isinstance(group, str) else group)]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _MarketMixin(_Base):
|
|
24
|
+
# ---- symbols -----------------------------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
@translated
|
|
27
|
+
def symbols(self) -> Dict[str, SymbolInfo]:
|
|
28
|
+
"""Every symbol the broker offers on this account, by name, with its trading rules."""
|
|
29
|
+
self._require_connected()
|
|
30
|
+
syms = self._api.Symbols
|
|
31
|
+
return _symbols_from_core(syms.Infos if syms is not None else None, syms)
|
|
32
|
+
|
|
33
|
+
@translated
|
|
34
|
+
def symbol(self, name: str) -> SymbolInfo:
|
|
35
|
+
"""One symbol's specification and trading rules."""
|
|
36
|
+
self._require_connected()
|
|
37
|
+
syms = self._api.Symbols
|
|
38
|
+
info = syms.GetInfo(name)
|
|
39
|
+
if info is None:
|
|
40
|
+
raise InvalidRequestError("unknown symbol %r" % name)
|
|
41
|
+
return SymbolInfo._from_core(name, info, syms.GetGroup(name))
|
|
42
|
+
|
|
43
|
+
@translated
|
|
44
|
+
def has_symbol(self, name: str) -> bool:
|
|
45
|
+
"""Whether the broker offers ``name`` exactly as spelled."""
|
|
46
|
+
self._require_connected()
|
|
47
|
+
return bool(self._api.Symbols.Exist(name))
|
|
48
|
+
|
|
49
|
+
@translated
|
|
50
|
+
def find_symbols(self, prefix: str) -> List[str]:
|
|
51
|
+
"""Every symbol whose name starts with ``prefix`` (brokers add suffixes such as ``m`` or ``.s``), exact
|
|
52
|
+
match first, then shortest name."""
|
|
53
|
+
self._require_connected()
|
|
54
|
+
names = [str(kv.Key) for kv in (self._api.Symbols.Infos or ())]
|
|
55
|
+
hits = sorted((n for n in names if n.startswith(prefix)), key=lambda n: (n != prefix, len(n), n))
|
|
56
|
+
return hits
|
|
57
|
+
|
|
58
|
+
@translated
|
|
59
|
+
def find_symbol(self, prefix: str) -> Optional[str]:
|
|
60
|
+
"""The symbol ``prefix`` names: the exact name if the broker has it, else the only symbol that starts with
|
|
61
|
+
it. ``None`` when there is none; an :class:`InvalidRequestError` listing the candidates when several do
|
|
62
|
+
(use :meth:`find_symbols` and choose)."""
|
|
63
|
+
hits = self.find_symbols(prefix)
|
|
64
|
+
if not hits:
|
|
65
|
+
return None
|
|
66
|
+
if hits[0] == prefix or len(hits) == 1:
|
|
67
|
+
return hits[0]
|
|
68
|
+
raise InvalidRequestError("%r is ambiguous: %s" % (prefix, ", ".join(hits[:8]) + (" ..." if len(hits) > 8 else "")))
|
|
69
|
+
|
|
70
|
+
@translated
|
|
71
|
+
def symbol_groups(self) -> List[str]:
|
|
72
|
+
"""The names of the broker's symbol groups."""
|
|
73
|
+
self._require_connected()
|
|
74
|
+
return [str(g) for g in (self._api.Symbols.GroupNames or ())]
|
|
75
|
+
|
|
76
|
+
@translated
|
|
77
|
+
def sessions(self, symbol: str) -> SymbolSessions:
|
|
78
|
+
"""When a symbol is quoted and traded, per weekday (0 = Sunday), in minutes from 00:00 server time."""
|
|
79
|
+
self._require_connected()
|
|
80
|
+
for kv in self._api.Symbols.Sessions:
|
|
81
|
+
if str(kv.Key) == symbol:
|
|
82
|
+
return SymbolSessions._from_core(symbol, kv.Value)
|
|
83
|
+
raise InvalidRequestError("no session data for %r" % symbol)
|
|
84
|
+
|
|
85
|
+
@translated
|
|
86
|
+
def is_quote_session(self, symbol: str) -> bool:
|
|
87
|
+
"""The symbol is inside one of its quote sessions right now (server clock)."""
|
|
88
|
+
self._require_connected()
|
|
89
|
+
return bool(self._api.IsQuoteSession(symbol))
|
|
90
|
+
|
|
91
|
+
@translated
|
|
92
|
+
def is_trade_session(self, symbol: str) -> bool:
|
|
93
|
+
"""The symbol is inside one of its trade sessions right now (server clock)."""
|
|
94
|
+
self._require_connected()
|
|
95
|
+
return bool(self._api.IsTradeSession(symbol))
|
|
96
|
+
|
|
97
|
+
@translated
|
|
98
|
+
def commission(self, symbol: str) -> Optional[CommissionInfo]:
|
|
99
|
+
"""The commission schedule that applies to a symbol, or ``None`` when the broker defines none."""
|
|
100
|
+
self._require_connected()
|
|
101
|
+
info = self._api.Symbols.GetComission(symbol)
|
|
102
|
+
return CommissionInfo._from_core(info) if info is not None else None
|
|
103
|
+
|
|
104
|
+
# ---- quotes and subscriptions ------------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
def _quote_ms(self, timeout: Optional[float]) -> int:
|
|
107
|
+
return int(timeout * 1000) if timeout is not None else int(self._api.GetQuoteTimeoutMs)
|
|
108
|
+
|
|
109
|
+
def _get_quote(self, symbol: str, ms: int) -> Any:
|
|
110
|
+
"""The core's quote, or :class:`NoQuoteError` when none arrives in time (nothing is sent by this)."""
|
|
111
|
+
try:
|
|
112
|
+
q = self._api.GetQuote(symbol, ms)
|
|
113
|
+
except Exception as exc: # noqa: BLE001
|
|
114
|
+
mapped = translate(exc)
|
|
115
|
+
if isinstance(mapped, RequestTimeout):
|
|
116
|
+
raise NoQuoteError("no quote for %s within %.1f s" % (symbol, ms / 1000.0)) from exc
|
|
117
|
+
raise
|
|
118
|
+
if q is None:
|
|
119
|
+
raise NoQuoteError("no quote for %s within %.1f s" % (symbol, ms / 1000.0))
|
|
120
|
+
return q
|
|
121
|
+
|
|
122
|
+
@translated
|
|
123
|
+
def quote(self, symbol: str, timeout: Optional[float] = None) -> Quote:
|
|
124
|
+
"""The current quote of one symbol, waiting up to ``timeout`` seconds (default: the client's ``quote_timeout``)
|
|
125
|
+
for the first tick; :class:`NoQuoteError` if none arrives."""
|
|
126
|
+
self._require_connected()
|
|
127
|
+
return Quote._from_core(self._get_quote(symbol, self._quote_ms(timeout)))
|
|
128
|
+
|
|
129
|
+
async def aquote(self, symbol: str, timeout: Optional[float] = None) -> Quote:
|
|
130
|
+
"""Awaitable :meth:`quote` (runs on a worker thread; never blocks the event loop)."""
|
|
131
|
+
return await asyncio.get_running_loop().run_in_executor(None, functools.partial(self.quote, symbol, timeout))
|
|
132
|
+
|
|
133
|
+
@translated
|
|
134
|
+
def subscribe(self, *symbols: str) -> None:
|
|
135
|
+
"""Start the live quote feed for the given symbols (names, or lists of names)."""
|
|
136
|
+
self._require_connected()
|
|
137
|
+
names = _names(symbols)
|
|
138
|
+
if names:
|
|
139
|
+
import System # noqa: PLC0415 — available once the runtime is loaded
|
|
140
|
+
self._api.Subscribe(System.Array[System.String](names))
|
|
141
|
+
|
|
142
|
+
@translated
|
|
143
|
+
def unsubscribe(self, *symbols: str) -> None:
|
|
144
|
+
"""Stop the live quote feed for the given symbols (names, or lists of names)."""
|
|
145
|
+
self._require_connected()
|
|
146
|
+
names = _names(symbols)
|
|
147
|
+
if names:
|
|
148
|
+
import System # noqa: PLC0415
|
|
149
|
+
self._api.Unsubscribe(System.Array[System.String](names))
|
|
150
|
+
|
|
151
|
+
@translated
|
|
152
|
+
def subscriptions(self) -> List[str]:
|
|
153
|
+
"""The symbols currently subscribed."""
|
|
154
|
+
self._require_connected()
|
|
155
|
+
return [str(s) for s in (self._api.Subscriptions() or ())]
|
|
156
|
+
|
|
157
|
+
@translated
|
|
158
|
+
def is_subscribed(self, symbol: str) -> bool:
|
|
159
|
+
"""Whether the live feed of ``symbol`` is on."""
|
|
160
|
+
self._require_connected()
|
|
161
|
+
return bool(self._api.IsSubscribed(symbol))
|
|
162
|
+
|
|
163
|
+
# ---- values per symbol -------------------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
@translated
|
|
166
|
+
def tick_value(self, symbol: str, side: str = "ask", timeout: Optional[float] = None) -> float:
|
|
167
|
+
"""The value, in account currency, of one full price unit per 1.0 lot (as the terminal computes it).
|
|
168
|
+
|
|
169
|
+
This is an *input* to position sizing, not a recipe: see :meth:`lots_for_risk`. ``side`` is ``"ask"`` (what a
|
|
170
|
+
long position is priced against) or ``"bid"``. Waits up to ``timeout`` seconds (default: ``quote_timeout``)
|
|
171
|
+
for a live quote. Raises :class:`InvalidRequestError` when the value cannot be computed (a symbol whose
|
|
172
|
+
profit currency has no conversion rate), never a NaN.
|
|
173
|
+
"""
|
|
174
|
+
self._require_connected()
|
|
175
|
+
side_key = str(side).lower()
|
|
176
|
+
if side_key not in ("ask", "bid"):
|
|
177
|
+
raise ValueError("side must be 'ask' or 'bid'")
|
|
178
|
+
ms = self._quote_ms(timeout)
|
|
179
|
+
if side_key == "ask":
|
|
180
|
+
value = float(self._api.GetTickValue(symbol, ms))
|
|
181
|
+
else:
|
|
182
|
+
value = float(self._api.GetBidTickValue(self._get_quote(symbol, ms)))
|
|
183
|
+
if math.isnan(value):
|
|
184
|
+
raise InvalidRequestError("the tick value of %s cannot be computed (no conversion rate yet)" % symbol)
|
|
185
|
+
return value
|
|
186
|
+
|
|
187
|
+
@translated
|
|
188
|
+
def contract_size(self, symbol: str) -> float:
|
|
189
|
+
"""The contract size of a symbol (units of the instrument per 1.0 lot)."""
|
|
190
|
+
self._require_connected()
|
|
191
|
+
return float(self._api.GetContractSize(symbol))
|
|
192
|
+
|
|
193
|
+
@translated
|
|
194
|
+
def ledger_value_per_lot(self, symbol: str) -> Optional[float]:
|
|
195
|
+
"""What the account's own closed deals say one price unit on one lot is worth for this symbol, or ``None``
|
|
196
|
+
when the account has not closed enough deals on it."""
|
|
197
|
+
self._require_connected()
|
|
198
|
+
return _opt_float(self._api.LedgerValuePerUnitLot(symbol))
|
|
199
|
+
|
|
200
|
+
# ---- bars --------------------------------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def _minute_block(self, symbol: str, day: date, timeout: float) -> List[Bar]:
|
|
203
|
+
"""One server block of one-minute bars ending at ``day`` (the server picks the block size). Only a response
|
|
204
|
+
that really is a block for that day is accepted (a late answer to an earlier request is ignored)."""
|
|
205
|
+
done = threading.Event()
|
|
206
|
+
got: List[Any] = []
|
|
207
|
+
lo = day - timedelta(days=60)
|
|
208
|
+
|
|
209
|
+
def handler(sender: Any, args: Any) -> None:
|
|
210
|
+
if str(args.Symbol) != symbol or done.is_set():
|
|
211
|
+
return
|
|
212
|
+
bars = list(args.Bars)
|
|
213
|
+
if not bars:
|
|
214
|
+
got[:] = bars
|
|
215
|
+
done.set()
|
|
216
|
+
return
|
|
217
|
+
last = _time(bars[-1].Time)
|
|
218
|
+
if last is not None and lo <= last.date() <= day:
|
|
219
|
+
got[:] = bars
|
|
220
|
+
done.set()
|
|
221
|
+
|
|
222
|
+
api = self._api
|
|
223
|
+
with self._bars_lock:
|
|
224
|
+
api.OnQuoteHistory += handler
|
|
225
|
+
try:
|
|
226
|
+
api.RequestQuoteHistoryMonth(symbol, day.year, day.month, day.day)
|
|
227
|
+
if not done.wait(timeout):
|
|
228
|
+
raise RequestTimeout("no bars for %s within %.0f s" % (symbol, timeout))
|
|
229
|
+
finally:
|
|
230
|
+
api.OnQuoteHistory -= handler
|
|
231
|
+
return [Bar._from_core(b) for b in got]
|
|
232
|
+
|
|
233
|
+
@translated
|
|
234
|
+
def bars(self, symbol: str, timeframe: Union[str, int] = "H1", *, count: Optional[int] = None,
|
|
235
|
+
start: Optional[datetime] = None, end: Optional[datetime] = None,
|
|
236
|
+
timeout: Optional[float] = None) -> List[Bar]:
|
|
237
|
+
"""OHLC bars, oldest first, in the broker's server time.
|
|
238
|
+
|
|
239
|
+
Give ``count`` for the latest N bars, or ``start`` (and optionally ``end``) for a window; with neither, the
|
|
240
|
+
latest 500. ``timeframe`` is a name — ``M1 M2 M3 M4 M5 M6 M10 M12 M15 M20 M30 H1 H2 H3 H4 H6 H8 H12 D1 W1 MN1`` —
|
|
241
|
+
or its length in minutes. Higher timeframes are built from the server's one-minute bars with the tick volume
|
|
242
|
+
summed; minutes after ``end`` are not used (no look-ahead); the last bar may still be forming.
|
|
243
|
+
``timeout`` (seconds, default: the client's ``bars_timeout``) bounds the whole call.
|
|
244
|
+
"""
|
|
245
|
+
self._require_connected()
|
|
246
|
+
minutes = timeframe_minutes(timeframe)
|
|
247
|
+
if count is not None and start is not None:
|
|
248
|
+
raise ValueError("give count or start, not both")
|
|
249
|
+
if count is not None and count <= 0:
|
|
250
|
+
raise ValueError("count must be positive")
|
|
251
|
+
if count is None and start is None:
|
|
252
|
+
count = 500
|
|
253
|
+
if not self._api.Symbols.Exist(symbol):
|
|
254
|
+
raise InvalidRequestError("unknown symbol %r" % symbol)
|
|
255
|
+
total = float(timeout) if timeout is not None else self._api.DownloadQuoteHistoryTimeout / 1000.0
|
|
256
|
+
deadline = time.monotonic() + total
|
|
257
|
+
server_now = self.server_time or datetime.now(timezone.utc).replace(tzinfo=None)
|
|
258
|
+
ref = end if end is not None else server_now + timedelta(days=1)
|
|
259
|
+
cursor = ref.date()
|
|
260
|
+
minute_bars: Dict[datetime, Bar] = {}
|
|
261
|
+
earliest: Optional[datetime] = None
|
|
262
|
+
for _ in range(400): # hard stop: a block is ~2 weeks of FX, so this is years of data
|
|
263
|
+
remaining = deadline - time.monotonic()
|
|
264
|
+
if remaining <= 0:
|
|
265
|
+
raise RequestTimeout("bars for %s not complete within %.0f s" % (symbol, total))
|
|
266
|
+
block = self._minute_block(symbol, cursor, remaining)
|
|
267
|
+
if not block:
|
|
268
|
+
break
|
|
269
|
+
for b in block:
|
|
270
|
+
if end is None or b.time <= end:
|
|
271
|
+
minute_bars.setdefault(b.time, b)
|
|
272
|
+
first = min(b.time for b in block)
|
|
273
|
+
if earliest is not None and first >= earliest:
|
|
274
|
+
break
|
|
275
|
+
earliest = first
|
|
276
|
+
if start is not None and first <= start:
|
|
277
|
+
break
|
|
278
|
+
if count is not None and len(aggregate(minute_bars.values(), minutes)) > count:
|
|
279
|
+
break # one spare: the oldest bucket may be cut by the block edge
|
|
280
|
+
cursor = first.date() # the same day: nothing before `first` on that day is skipped
|
|
281
|
+
series = aggregate(minute_bars.values(), minutes)
|
|
282
|
+
if start is not None:
|
|
283
|
+
series = [b for b in series if b.time >= start]
|
|
284
|
+
if count is not None:
|
|
285
|
+
series = series[-count:]
|
|
286
|
+
return series
|
|
287
|
+
|
|
288
|
+
@translated
|
|
289
|
+
def request_bars(self, symbol: str, day: Optional[date] = None) -> None:
|
|
290
|
+
"""Ask the server for one-minute bars without waiting; the answer arrives through :meth:`on_quote_history`.
|
|
291
|
+
With no ``day`` this is today's bars, otherwise the block ending at ``day``."""
|
|
292
|
+
self._require_connected()
|
|
293
|
+
if day is None:
|
|
294
|
+
self._api.RequestQuoteHistoryToday(symbol)
|
|
295
|
+
else:
|
|
296
|
+
self._api.RequestQuoteHistoryMonth(symbol, day.year, day.month, day.day)
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Changelog — wmt5api
|
|
2
|
+
|
|
3
|
+
All notable changes to the **wmt5api** SDK. This project follows [Semantic Versioning](https://semver.org).
|
|
4
|
+
Upgrading from 2.x? Read [MIGRATION.md](MIGRATION.md) first.
|
|
5
|
+
|
|
6
|
+
## [3.1.0] — 2026-10-12
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- **Closing a position against an opposite one (`closeByTicket`) left both positions in `GetOpenedOrders()`** on
|
|
10
|
+
brokers whose tickets exceed 2,147,483,647. The close-by ticket was read as a 32-bit number, the conversion threw
|
|
11
|
+
inside the update handler, and the session's list of open positions was never updated (the server had closed them).
|
|
12
|
+
Tickets are now read as 64-bit.
|
|
13
|
+
|
|
14
|
+
- **Connecting through a proxy without credentials threw `ArgumentNullException`.** `ProxyUser` and `ProxyPassword`
|
|
15
|
+
default to `null` and the proxy handlers refuse null; a missing user or password is now sent as empty.
|
|
16
|
+
|
|
17
|
+
- **The HTTPS (`CONNECT`) proxy request ended its header lines with a bare LF on Linux and macOS**, which strict
|
|
18
|
+
proxies do not accept; lines now end in CRLF on every platform.
|
|
19
|
+
|
|
20
|
+
- **`FillPolicy` other than `Any` was ignored** by `OrderSend`, `OrderClose`, `OrderCancel` and their asynchronous
|
|
21
|
+
forms: only `Any` was applied (as the symbol's default) and every other value went out as fill-or-kill. The value
|
|
22
|
+
you pass is now the value sent.
|
|
23
|
+
- **`Disconnect()` threw `NullReferenceException` when the client had never connected.**
|
|
24
|
+
- **`OnOrderHistory` delivered an empty `Deals` list** (only `Orders` were filled in; the deals were reachable only
|
|
25
|
+
through `DownloadOrderHistory`). The event now carries the deals too.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- `OrderUpdate.CloseByTicket` is now `long` (it was `int` and could not hold such tickets). Code that stored it in
|
|
29
|
+
an `int` needs a cast; nothing that worked before is affected.
|
|
30
|
+
- The licence notice, the package metadata and the support and key contact lines now name **mtapi.one** as the
|
|
31
|
+
licensor and the place to contact. The behaviour of the SDK is otherwise unchanged.
|
|
32
|
+
|
|
33
|
+
### Documentation
|
|
34
|
+
- Telling positions from pending orders in `GetOpenedOrders()`: the README and `sizing.cs` advised
|
|
35
|
+
`State == OrderState.Filled`. A freshly opened position can still read `Started` for a long time, so use the
|
|
36
|
+
order type instead — `OrderType.Buy` / `OrderType.Sell` are positions, the `*Limit` and `*Stop` types are pending.
|
|
37
|
+
|
|
38
|
+
## [3.0.0] — 2026-10-11
|
|
39
|
+
|
|
40
|
+
### Removed (breaking)
|
|
41
|
+
- **`Broker.Search` has been removed**, together with the broker-directory types it returned: `Broker`,
|
|
42
|
+
`Broker.Company`, `Broker.Companies`, `Broker.Result` and `BrokerSearchResultsAsArray` (with its nested types).
|
|
43
|
+
Use the access-server address your broker gave you, or read the addresses from a `servers.dat` file with
|
|
44
|
+
`MT5API.LoadServersDat`. Nothing else in the public API was removed or changed — see
|
|
45
|
+
[MIGRATION.md](MIGRATION.md).
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- `GetQuote`, `GetQuoteTask`: when a quote arrived with a zero bid or ask, the wait ignored its timeout and
|
|
49
|
+
could spin well past `msTimeout`. It now waits for a usable quote and honours the timeout
|
|
50
|
+
(`TimeoutException`, or `null` for `msTimeout = 0`).
|
|
51
|
+
- The licence request now finishes inside the connect budget (the retries and back-offs are bounded by
|
|
52
|
+
`ConnectTimeout`), so a slow or unreachable licence service surfaces as `TokenServiceException` instead of a
|
|
53
|
+
`TimeoutException` that reads as "the broker host is unreachable".
|
|
54
|
+
- With `AutoReconnect` on, the supervisor stops when the licence service refuses the connection for a reason
|
|
55
|
+
retrying cannot cure (an expired or revoked key, a login not allowed, an account limit). `Connect()` resumes it.
|
|
56
|
+
- `GetQuoteTask` now honours its timeout when no tick arrives (it could wait indefinitely).
|
|
57
|
+
- Connecting no longer replays the licence request on every cluster member when the licence service refuses
|
|
58
|
+
or cannot be reached (another broker host cannot cure that). Failover across the broker's servers is
|
|
59
|
+
unchanged for broker-side failures.
|
|
60
|
+
- Waiting for the first reply to a market or pending order no longer spins a CPU core.
|
|
61
|
+
- The message "Previuos request still running" is now "Previous request still running".
|
|
62
|
+
- A trade request that the server did not confirm within `ExecutionTimeout` now throws `TimeoutException`
|
|
63
|
+
(it was a plain `Exception("Trade timeout")`). `TimeoutException` derives from `Exception`, so code that
|
|
64
|
+
already caught `Exception` is unaffected.
|
|
65
|
+
- The obsolete warning on `EquityPoint.RealizedPL` no longer promises removal in 3.0.0; the field remains,
|
|
66
|
+
and may be removed in a future major release.
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
- Internal clean-up of the shipped library. No other public type, member or signature changed.
|
|
70
|
+
- The SDK now sends its name and version (for example `wmt5api/3.0.0`) to the licence service together with
|
|
71
|
+
each licence request, so the service can tell which release is calling.
|
|
72
|
+
- Documentation: the README, the guides and the API documentation (`wmt5api.xml`) were rewritten and
|
|
73
|
+
corrected; `MIGRATION.md` and `THIRD-PARTY-NOTICES.txt` are now part of the package.
|
|
74
|
+
|
|
75
|
+
## [2.6.1] — 2026-09-10
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
- Connecting to older MT5 servers (build below 4852) now works.
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
- `Bar.Open`, `Bar.High`, `Bar.Low` and `Bar.Close`: properties that return the same values as
|
|
82
|
+
`OpenPrice`, `HighPrice`, `LowPrice` and `ClosePrice`, so code can use the same bar member names as the MT4 SDK.
|
|
83
|
+
The existing fields are unchanged.
|
|
84
|
+
|
|
85
|
+
## [2.6.0] — 2026-08-28
|
|
86
|
+
|
|
87
|
+
The account-currency conversion is now read from the pairs your broker publishes instead of being assumed.
|
|
88
|
+
Everything below follows from that.
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
- **Cross-pair tick values were `NaN` on brokers that suffix their symbols.** The conversion-pair lookup
|
|
92
|
+
failed when only the suffixed name (for example `USDJPY.s`) exists. The pair is now looked up under the name
|
|
93
|
+
as built, with the bare name only as a fallback.
|
|
94
|
+
- **The account currency is no longer assumed.** Cent currencies (`USC`, `RUC`) were treated as if they were
|
|
95
|
+
USD and RUB; brokers publish their own conversion pair and the conventions differ, so the assumption could
|
|
96
|
+
be wrong by a factor of one hundred. One path now serves every account type.
|
|
97
|
+
- **Margin is answered in the account currency from the broker's own rate.** `RequiredMargin` needs no
|
|
98
|
+
calibration and no deal history on any account.
|
|
99
|
+
- **The first `GetTickValue` on a cold cross-currency symbol could return a value 100 times too large.** A tick
|
|
100
|
+
arriving while the value was being derived could apply the final conversion twice. Values are now built
|
|
101
|
+
privately and published only once complete.
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
- **`LotSizing.MinimumTradeCost`** (new): what the broker's smallest allowed trade costs at your stop. It is
|
|
105
|
+
filled in even when the sizing is refused, and answers "is my account big enough for this instrument?".
|
|
106
|
+
- **A symbol you have never traded is sized properly** instead of being pinned to the minimum volume: the
|
|
107
|
+
corroborating contract-specification route now takes its currency factor from the pairs the broker
|
|
108
|
+
publishes.
|
|
109
|
+
- **A refused size no longer leaves a volume behind.** `Lots` and `ExpectedLossAtStop` are 0 whenever `Ok` is
|
|
110
|
+
false.
|
|
111
|
+
- **`GetTickValue` works on a brand-new account.** The conversion comes from published prices, so there is
|
|
112
|
+
nothing to calibrate first. `CalibrateMoneyScale` remains the second opinion that `CalculateLots` requires.
|
|
113
|
+
- **`MoneyScaleEvidence` says whether a ruler had to cross a currency, and over how long a window.**
|
|
114
|
+
|
|
115
|
+
### Documentation
|
|
116
|
+
- The sizing and margin guidance was corrected: `AccountMargin` prices each open position at the conversion
|
|
117
|
+
rate stamped when it opened, `RequiredMargin` at today's rate, so a small difference between them is
|
|
118
|
+
expected.
|
|
119
|
+
|
|
120
|
+
## [2.5.0] — 2026-08-28
|
|
121
|
+
|
|
122
|
+
Floating P/L was wrong in sign and size on a cent account; position sizing assumed a broker convention that is
|
|
123
|
+
not universal; margin was answered in the wrong currency and could be negative; the connect path could raise
|
|
124
|
+
an exception type that was not documented.
|
|
125
|
+
|
|
126
|
+
### Added
|
|
127
|
+
- **Position sizing that measures the broker instead of assuming it:** `CalibrateMoneyScale` reads what a price
|
|
128
|
+
move was worth from your closed deals; `CalculateLots` sizes with it, corroborates the answer and returns no
|
|
129
|
+
volume when the checks disagree. New members: `LotSizing`, `MoneyScale`, `MoneyScaleEvidence`,
|
|
130
|
+
`MoneyScaleFromDeal`, `LedgerValuePerUnitLot`. Runnable example: `sizing.cs`.
|
|
131
|
+
- **Split account totals:** `AccountFloatingProfit`, `AccountSwap`, `AccountCommission` and `AccountFee`;
|
|
132
|
+
`AccountProfit` is exactly their sum. `Order.Fee` surfaces the fee charged on an order or deal.
|
|
133
|
+
- `EquityPoint.Incomplete`: true when a curve could not price every open position.
|
|
134
|
+
|
|
135
|
+
### Fixed
|
|
136
|
+
- **Floating P/L is converted into the account currency.** A cent account previously read the raw figure.
|
|
137
|
+
- **A price is no longer added to profit as a commission.** Some brokers send the opposite-side price in the
|
|
138
|
+
commission slot of an open position; it was summed into `AccountProfit`.
|
|
139
|
+
- **`Connect()` raises the documented exception types.** Several failures on the connect path used to raise a
|
|
140
|
+
bare `Exception`, so code that caught the documented types caught nothing.
|
|
141
|
+
- **`RequiredMargin` returns what one order costs on its own**, in the account currency, as a terminal shows it.
|
|
142
|
+
It could previously return the change in the account's total margin, which goes negative when an order
|
|
143
|
+
offsets an open position. It also waits for a price instead of returning `NaN` for a symbol not already held.
|
|
144
|
+
- `CalculateOrderProfit` documents its parameters: volume comes after both prices.
|
|
145
|
+
|
|
146
|
+
## [2.4.0] — 2026-08-22
|
|
147
|
+
|
|
148
|
+
### Added
|
|
149
|
+
- `GetAccountSummary()` and `GetAccountSummary(from, to)` returning `AccountSummary`: realized trading profit,
|
|
150
|
+
deposits and withdrawals over a range, plus the current balance and credit.
|
|
151
|
+
- `EquityPoint.BalanceChange`. `EquityPoint.RealizedPL` is deprecated: it held a balance difference, which
|
|
152
|
+
includes deposits and withdrawals, and is not a trading result. Both names return the same value.
|
|
153
|
+
- `InvestorModeException`: a typed exception for a trading call made on a read-only (investor) login.
|
|
154
|
+
|
|
155
|
+
## [2.3.2] — 2026-08-16
|
|
156
|
+
|
|
157
|
+
### Fixed
|
|
158
|
+
- `CalculateEquityHistory`: the floating P/L of historical points is priced from the bar at or before each
|
|
159
|
+
step. No public API change.
|
|
160
|
+
|
|
161
|
+
## [2.3.1] — 2026-08-15
|
|
162
|
+
|
|
163
|
+
### Added
|
|
164
|
+
- `CalculateEquityHistory(from)` and `CalculateEquityHistory(from, timeframe, excludeSameBars)`: reconstruct the
|
|
165
|
+
balance/equity curve and its drawdown. New types: `EquityPoint`, `EquityTimeframe`.
|
|
166
|
+
|
|
167
|
+
## [2.3.0] — 2026-08-14
|
|
168
|
+
|
|
169
|
+
### Added
|
|
170
|
+
- `MT5API.LoadServersDat` and `MT5API.SaveServersDat`: read and write a `servers.dat` file offline, as
|
|
171
|
+
`ServerRecord[]` (name, company and the (host, port) access points of each server).
|
|
172
|
+
|
|
173
|
+
### Fixed
|
|
174
|
+
- `host:port` parsing understands IPv6 addresses (bracketed `[addr]:port` and bare literals). A broker whose
|
|
175
|
+
access server is an IPv6 endpoint no longer crashes the address parse.
|
|
176
|
+
|
|
177
|
+
## [2.2.5] — 2026-08-14
|
|
178
|
+
|
|
179
|
+
Documentation-only release. The API documentation (`wmt5api.xml`) was reworded and thin summaries were filled
|
|
180
|
+
in. No public API change.
|
|
181
|
+
|
|
182
|
+
## [2.2.4] — 2026-08-14
|
|
183
|
+
|
|
184
|
+
### Added
|
|
185
|
+
- `AccountMarginLevel`: equity / margin × 100, the ratio the terminal shows; 0 when the account is flat.
|
|
186
|
+
|
|
187
|
+
### Documentation
|
|
188
|
+
- How to read account state correctly: equity, profit, free margin and margin level are computed from cached
|
|
189
|
+
quotes, so subscribe the open positions' symbols and call `UpdateProfits()` before reading them.
|
|
190
|
+
|
|
191
|
+
## [2.2.3] — 2026-08-13
|
|
192
|
+
|
|
193
|
+
### Fixed
|
|
194
|
+
- Cent accounts: `AccountMargin`, `AccountFreeMargin` and `RequiredMargin` were multiplied by 100 and no longer
|
|
195
|
+
are. No public API change. (The cent-account handling was reworked again in 2.6.0.)
|
|
196
|
+
|
|
197
|
+
## [2.2.2] — 2026-08-12
|
|
198
|
+
|
|
199
|
+
### Fixed
|
|
200
|
+
- Cent accounts: `GetTickValue` and `CalculateOrderProfit` were multiplied by 100 by 2.2.1 and no longer are;
|
|
201
|
+
floating P/L on cent accounts is no longer inflated. **Do not use 2.2.1 on a cent account.**
|
|
202
|
+
- `AccountMargin` and `AccountFreeMargin` refresh after an in-session trade and on quote ticks; previously they
|
|
203
|
+
updated only on connect.
|
|
204
|
+
|
|
205
|
+
## [2.2.1] — 2026-08-12
|
|
206
|
+
|
|
207
|
+
### Added
|
|
208
|
+
- `DownloadOrderHistoryTimeoutMs` (default 30000, 0 waits indefinitely). `DownloadOrderHistory(from, to)` now
|
|
209
|
+
throws `TimeoutException` after that time instead of waiting forever when a broker withholds history.
|
|
210
|
+
|
|
211
|
+
### Known issue
|
|
212
|
+
- This release changed how cent accounts are scaled, and the change was wrong; it is corrected in 2.2.2.
|
|
213
|
+
|
|
214
|
+
## [2.2.0] — 2026-07-24
|
|
215
|
+
|
|
216
|
+
### Added
|
|
217
|
+
- `Logger.OnMsg` and `Logger.OnMsgEx` (with `Logger.MsgType`): the library's diagnostic log stream can be
|
|
218
|
+
subscribed to.
|
|
219
|
+
- `MT5API.OnSymbolUpdate` and `SymbolUpdate`: a per-symbol signal raised when one symbol's data is (re)received,
|
|
220
|
+
alongside the bulk `OnSymbolsUpdate`.
|
|
221
|
+
|
|
222
|
+
## [2.1.2] — 2026-07-16
|
|
223
|
+
|
|
224
|
+
Documentation-only release: the README and `Sample.cs` document `AutoReconnect` ("Staying connected"). No
|
|
225
|
+
code or public API change.
|
|
226
|
+
|
|
227
|
+
## [2.1.1] — 2026-07-15
|
|
228
|
+
|
|
229
|
+
### Fixed
|
|
230
|
+
- `TokenServiceUrl` defaults to the hosted licence service, so setting only `ApiKey` is enough to connect.
|
|
231
|
+
Previously `Connect()` threw `TokenServiceException` unless the URL was also set.
|
|
232
|
+
|
|
233
|
+
## [2.1.0] — 2026-06-23
|
|
234
|
+
|
|
235
|
+
### Added
|
|
236
|
+
- `MT5API.ComputeTickValueFromDeal(closedProfit, openPrice, closePrice, lots)`: derives the per-price-unit
|
|
237
|
+
value from a closed deal, as a check on `GetTickValue`.
|
|
238
|
+
|
|
239
|
+
## [2.0.0] — 2026-06-20
|
|
240
|
+
|
|
241
|
+
### Changed (breaking behaviour)
|
|
242
|
+
- `GetTickValue`, `GetAskTickValue`, `GetBidTickValue`, `CalculateOrderProfit` and `RequiredMargin` return
|
|
243
|
+
`double.NaN` when the quote they need has not resolved, instead of `0` or an exception. No signature changed.
|
|
244
|
+
If you tested the result with `== 0`, use `double.IsNaN(value)`; a `value > 0` guard already handles `NaN`.
|
|
245
|
+
|
|
246
|
+
## [1.1.0] — 2026-06-17
|
|
247
|
+
|
|
248
|
+
### Added
|
|
249
|
+
- `LicenseReason.AccountLimitReached`: the licence already has its maximum number of distinct broker logins and
|
|
250
|
+
this is a new one.
|
|
251
|
+
|
|
252
|
+
## [1.0.2] — 2026-06-15
|
|
253
|
+
|
|
254
|
+
### Fixed
|
|
255
|
+
- An unknown symbol on the quote and bar calls (`GetQuote`, `Subscribe`, `RequestQuoteHistoryToday`,
|
|
256
|
+
`RequestQuoteHistoryMonth`, `DownloadQuoteHistoryToday`, `DownloadQuoteHistoryMonth`) throws
|
|
257
|
+
`ArgumentException` instead of a bare `Exception`, matching `Symbols.GetInfo` and `Symbols.GetGroup`.
|
|
258
|
+
|
|
259
|
+
### Documentation
|
|
260
|
+
- `Symbols.GetInfo(...).TickValue` and `TickSize` are raw server fields and are frequently 0; the guides point
|
|
261
|
+
to `GetTickValue(symbol)` instead.
|
|
262
|
+
|
|
263
|
+
## [1.0.1] — 2026-06-15
|
|
264
|
+
|
|
265
|
+
### Fixed
|
|
266
|
+
- `Connect()` throws the documented typed exceptions directly (`LicenseException`, `TokenServiceException`,
|
|
267
|
+
`ServerException`, `ConnectException`, `TimeoutException`) instead of wrapping them in an
|
|
268
|
+
`AggregateException`.
|
|
269
|
+
- `ConnectException` is public, so it can be caught by type.
|
|
270
|
+
|
|
271
|
+
## [1.0.0] — 2026-06-15
|
|
272
|
+
|
|
273
|
+
First commercial release.
|
|
274
|
+
|
|
275
|
+
### Added
|
|
276
|
+
- Connect, account, positions, deal history, live quotes, quote-history bars and symbol specifications.
|
|
277
|
+
- Synchronous, asynchronous and awaitable trading: `OrderSend`, `OrderModify`, `OrderClose`, `OrderCancel`;
|
|
278
|
+
`CalculateOrderProfit` and `RequiredMargin`.
|
|
279
|
+
- Events: `OnQuote`, `OnOrderUpdate`, `OnOrderProgress`, `OnOrderHistory`, `OnQuoteHistory`,
|
|
280
|
+
`OnSymbolsUpdate`, `OnConnectProgress`.
|
|
281
|
+
- Licensing through the hosted licence service, gated by your API key (`ApiKey` / `WMTAPI_KEY`), with typed
|
|
282
|
+
`LicenseException` and `TokenServiceException`.
|
|
283
|
+
- Full IntelliSense documentation (`wmt5api.xml`) and the `Sample.cs` cookbook. Target framework:
|
|
284
|
+
`netstandard2.0`.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
THIRD-PARTY NOTICES — wmt5api
|
|
2
|
+
==============================
|
|
3
|
+
|
|
4
|
+
wmt5api references the .NET platform (netstandard 2.0) and compiles in the following third-party components.
|
|
5
|
+
Their notices are reproduced as their licences require.
|
|
6
|
+
|
|
7
|
+
It also contains an implementation of the LZO1X-compatible compression format, which it uses for the
|
|
8
|
+
compressed records and requests of the trading protocol.
|
|
9
|
+
|
|
10
|
+
------------------------------------------------------------------------------------------------------
|
|
11
|
+
1. ProxySocket (SOCKS4/SOCKS5 proxy support)
|
|
12
|
+
------------------------------------------------------------------------------------------------------
|
|
13
|
+
Copyright (c) 2002, The KPD-Team
|
|
14
|
+
All rights reserved.
|
|
15
|
+
http://www.mentalis.org/
|
|
16
|
+
|
|
17
|
+
Redistribution and use in source and binary forms, with or without
|
|
18
|
+
modification, are permitted provided that the following conditions
|
|
19
|
+
are met:
|
|
20
|
+
|
|
21
|
+
- Redistributions of source code must retain the above copyright
|
|
22
|
+
notice, this list of conditions and the following disclaimer.
|
|
23
|
+
|
|
24
|
+
- Neither the name of the KPD-Team, nor the names of its contributors
|
|
25
|
+
may be used to endorse or promote products derived from this
|
|
26
|
+
software without specific prior written permission.
|
|
27
|
+
|
|
28
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
29
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
30
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
31
|
+
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
|
32
|
+
THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
|
33
|
+
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
34
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
35
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
36
|
+
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
37
|
+
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
38
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
|
39
|
+
OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
40
|
+
|
|
41
|
+
------------------------------------------------------------------------------------------------------
|
|
42
|
+
2. SHA-256 implementation
|
|
43
|
+
------------------------------------------------------------------------------------------------------
|
|
44
|
+
Copyright (c) 2010 Yuri K. Schlesner
|
|
45
|
+
|
|
46
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
47
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
48
|
+
in the Software without restriction, including without limitation the rights
|
|
49
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
50
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
51
|
+
furnished to do so, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
The above copyright notice and this permission notice shall be included in
|
|
54
|
+
all copies or substantial portions of the Software.
|
|
55
|
+
|
|
56
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
57
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
58
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
59
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
60
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
61
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
62
|
+
THE SOFTWARE.
|
|
63
|
+
|
|
64
|
+
------------------------------------------------------------------------------------------------------
|
|
65
|
+
3. MD5 (derived from the RSA Data Security, Inc. MD5 Message-Digest Algorithm, RFC 1321)
|
|
66
|
+
------------------------------------------------------------------------------------------------------
|
|
67
|
+
Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
|
|
68
|
+
rights reserved.
|
|
69
|
+
|
|
70
|
+
License to copy and use this software is granted provided that it
|
|
71
|
+
is identified as the "RSA Data Security, Inc. MD5 Message-Digest
|
|
72
|
+
Algorithm" in all material mentioning or referencing this software
|
|
73
|
+
or this function.
|
|
74
|
+
|
|
75
|
+
License is also granted to make and use derivative works provided
|
|
76
|
+
that such works are identified as "derived from the RSA Data
|
|
77
|
+
Security, Inc. MD5 Message-Digest Algorithm" in all material
|
|
78
|
+
mentioning or referencing the derived work.
|
|
79
|
+
|
|
80
|
+
RSA Data Security, Inc. makes no representations concerning either
|
|
81
|
+
the merchantability of this software or the suitability of this
|
|
82
|
+
software for any particular purpose. It is provided "as is"
|
|
83
|
+
without express or implied warranty of any kind.
|
|
84
|
+
|
|
85
|
+
These notices must be retained in any copies of any part of this
|
|
86
|
+
documentation and/or software.
|
|
87
|
+
|
|
88
|
+
------------------------------------------------------------------------------------------------------
|
|
89
|
+
4. AsyncAutoResetEvent
|
|
90
|
+
------------------------------------------------------------------------------------------------------
|
|
91
|
+
A small asynchronous auto-reset event helper that follows the pattern published by Stephen Toub
|
|
92
|
+
(Microsoft) in the .NET parallel-programming blog.
|