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