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/models.py ADDED
@@ -0,0 +1,522 @@
1
+ """Plain Python value types returned by :class:`pmt5api.Client`.
2
+
3
+ They are immutable snapshots built from the core's objects, so they can be stored, pickled, compared and handed to other
4
+ threads freely. Times are naive ``datetime`` values in the broker's server time, exactly as the server reports them
5
+ (``None`` when the server leaves the field unset).
6
+
7
+ Fields are read strictly: if the core ever drops or renames one, conversion raises ``AttributeError`` instead of
8
+ returning a quietly wrong value (the binding tests in ``tests/integration`` pin every field used here).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import math
13
+ from dataclasses import dataclass, field
14
+ from datetime import datetime
15
+ from typing import Any, Dict, Optional, Tuple
16
+
17
+ _EPOCH = datetime(1970, 1, 1)
18
+
19
+
20
+ def _time(value: Any) -> Optional[datetime]:
21
+ """A .NET ``DateTime`` (or ``datetime``) as a naive ``datetime``; the unset values (year 1, the Unix epoch) as ``None``."""
22
+ if value is None:
23
+ return None
24
+ if isinstance(value, datetime):
25
+ out = value.replace(tzinfo=None)
26
+ else:
27
+ year = int(value.Year)
28
+ if year <= 1:
29
+ return None
30
+ out = datetime(year, int(value.Month), int(value.Day), int(value.Hour), int(value.Minute),
31
+ int(value.Second), int(value.Millisecond) * 1000)
32
+ return None if out <= _EPOCH else out
33
+
34
+
35
+ def _name(value: Any) -> str:
36
+ """An enum value as its member name."""
37
+ return "" if value is None else str(value)
38
+
39
+
40
+ def _opt_float(value: Any) -> Optional[float]:
41
+ """A double where NaN means "not known"."""
42
+ f = float(value)
43
+ return None if math.isnan(f) else f
44
+
45
+
46
+ # ---- market data ------------------------------------------------------------------------------------------------
47
+
48
+ @dataclass(frozen=True)
49
+ class Quote:
50
+ symbol: str
51
+ bid: float
52
+ ask: float
53
+ time: Optional[datetime]
54
+ last: float
55
+ volume: int
56
+
57
+ @property
58
+ def spread(self) -> float:
59
+ return self.ask - self.bid
60
+
61
+ @classmethod
62
+ def _from_core(cls, q: Any) -> "Quote":
63
+ return cls(symbol=str(q.Symbol), bid=float(q.Bid), ask=float(q.Ask), time=_time(q.Time),
64
+ last=float(q.Last), volume=int(q.Volume))
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class SymbolInfo:
69
+ """A symbol's specification and trading rules.
70
+
71
+ ``raw_tick_value`` / ``raw_tick_size`` are the server's own fields and brokers often report them as 0: never size a
72
+ position from them (use :meth:`Client.tick_value` and :meth:`Client.lots_for_risk`).
73
+ """
74
+ name: str
75
+ description: str
76
+ digits: int
77
+ point: float
78
+ spread: int
79
+ contract_size: float
80
+ profit_currency: str
81
+ margin_currency: str
82
+ isin: str = ""
83
+ custom: bool = False
84
+ depth_of_market: int = 0
85
+ calc_mode: str = ""
86
+ gtc_mode: str = ""
87
+ settlement_price: float = 0.0
88
+ lower_limit: float = 0.0
89
+ upper_limit: float = 0.0
90
+ face_value: float = 0.0
91
+ accrued_interest: float = 0.0
92
+ raw_tick_value: float = 0.0
93
+ raw_tick_size: float = 0.0
94
+ # trading rules of the symbol's group
95
+ group: str = ""
96
+ trade_mode: str = ""
97
+ execution: str = ""
98
+ min_lots: float = 0.0
99
+ max_lots: float = 0.0
100
+ lots_step: float = 0.0
101
+ min_stop_distance: int = 0 # minimum stop-loss distance, points
102
+ min_take_distance: int = 0 # minimum take-profit distance, points
103
+ swap_type: str = ""
104
+ swap_long: float = 0.0
105
+ swap_short: float = 0.0
106
+ triple_swap_day: str = ""
107
+ initial_margin: float = 0.0
108
+ maintenance_margin: float = 0.0
109
+
110
+ @classmethod
111
+ def _from_core(cls, name: str, s: Any, group: Any = None) -> "SymbolInfo":
112
+ extra: Dict[str, Any] = {}
113
+ if group is not None:
114
+ extra = dict(group=str(group.GroupName or ""), trade_mode=_name(group.TradeMode),
115
+ execution=_name(group.TradeType), min_lots=float(group.MinLots), max_lots=float(group.MaxLots),
116
+ lots_step=float(group.LotsStep), min_stop_distance=int(group.SL),
117
+ min_take_distance=int(group.TP), swap_type=_name(group.SwapType),
118
+ swap_long=float(group.SwapLong), swap_short=float(group.SwapShort),
119
+ triple_swap_day=_name(group.ThreeDaysSwap), initial_margin=float(group.InitialMargin),
120
+ maintenance_margin=float(group.MaintenanceMargin))
121
+ return cls(name=name or str(s.Currency), description=str(s.Description or ""), digits=int(s.Digits),
122
+ point=float(s.Points), spread=int(s.Spread), contract_size=float(s.ContractSize),
123
+ profit_currency=str(s.ProfitCurrency or ""), margin_currency=str(s.MarginCurrency or ""),
124
+ isin=str(s.ISIN or ""), custom=bool(int(s.Custom)), depth_of_market=int(s.DepthOfMarket),
125
+ calc_mode=_name(s.CalcMode), gtc_mode=_name(s.GTCMode), settlement_price=float(s.SettlementPrice),
126
+ lower_limit=float(s.LowerLimit), upper_limit=float(s.UpperLimit), face_value=float(s.FaceValue),
127
+ accrued_interest=float(s.AccruedInterest), raw_tick_value=float(s.TickValue),
128
+ raw_tick_size=float(s.TickSize), **extra)
129
+
130
+
131
+ @dataclass(frozen=True)
132
+ class TradingSession:
133
+ """One trading or quoting window of a weekday, in minutes from 00:00 server time (``end`` may be 1440)."""
134
+ start: int
135
+ end: int
136
+
137
+
138
+ @dataclass(frozen=True)
139
+ class SymbolSessions:
140
+ """Per weekday (index 0 = Sunday … 6 = Saturday), the windows in which a symbol is quoted and traded."""
141
+ symbol: str
142
+ quotes: Tuple[Tuple[TradingSession, ...], ...]
143
+ trades: Tuple[Tuple[TradingSession, ...], ...]
144
+
145
+ @classmethod
146
+ def _from_core(cls, symbol: str, s: Any) -> "SymbolSessions":
147
+ def week(days: Any) -> Tuple[Tuple[TradingSession, ...], ...]:
148
+ return tuple(tuple(TradingSession(int(x.StartTime), int(x.EndTime)) for x in (day or ())) for day in days)
149
+ return cls(symbol=symbol, quotes=week(s.Quotes), trades=week(s.Trades))
150
+
151
+
152
+ @dataclass(frozen=True)
153
+ class CommissionTier:
154
+ unit_type: str
155
+ unit_mode: str
156
+ unit_value: float
157
+ min_value: float
158
+ max_value: float
159
+ min_unit: float
160
+ max_unit: float
161
+ currency_unit: str
162
+
163
+
164
+ @dataclass(frozen=True)
165
+ class CommissionInfo:
166
+ group_name: str
167
+ type: str
168
+ period: str
169
+ money_currency: str
170
+ instant_type: str
171
+ tiers: Tuple[CommissionTier, ...]
172
+
173
+ @classmethod
174
+ def _from_core(cls, c: Any) -> "CommissionInfo":
175
+ tiers = tuple(CommissionTier(unit_type=_name(t.UnitType), unit_mode=_name(t.UnitMode),
176
+ unit_value=float(t.UnitValue), min_value=float(t.MinValue),
177
+ max_value=float(t.MaxValue), min_unit=float(t.MinUnit),
178
+ max_unit=float(t.MaxUnit), currency_unit=str(t.CurrencyUnit or ""))
179
+ for t in (c.Comissions or ()))
180
+ return cls(group_name=str(c.GroupName or ""), type=_name(c.Type), period=_name(c.Period),
181
+ money_currency=str(c.MoneyCurrency or ""), instant_type=_name(c.InstantType), tiers=tiers)
182
+
183
+
184
+ # ---- account ----------------------------------------------------------------------------------------------------
185
+
186
+ @dataclass(frozen=True)
187
+ class AccountInfo:
188
+ login: int
189
+ name: str
190
+ currency: str
191
+ leverage: int
192
+ balance: float
193
+ credit: float
194
+ equity: float
195
+ profit: float # floating P/L of open positions including swap, commission and fee (the core's AccountProfit)
196
+ margin: float
197
+ free_margin: float
198
+ margin_level: float
199
+ floating_pl: float = 0.0 # the price-driven part of ``profit`` alone
200
+ swap: float = 0.0
201
+ commission: float = 0.0
202
+ fee: float = 0.0
203
+ blocked: float = 0.0
204
+ company: str = ""
205
+ method: str = "" # "Netting" or "Hedging"
206
+ account_type: str = ""
207
+ trade_flags: int = 0
208
+ country: str = ""
209
+ city: str = ""
210
+ state: str = ""
211
+ zip_code: str = ""
212
+ address: str = ""
213
+ phone: str = ""
214
+ email: str = ""
215
+
216
+
217
+ @dataclass(frozen=True)
218
+ class BaseInfo:
219
+ """What the server says about itself: the broker, the account currency and its digits, the account mode."""
220
+ company: str
221
+ currency: str
222
+ digits: int
223
+ account_method: str # "Netting" or "Hedging"
224
+ margin_mode: str
225
+
226
+ @classmethod
227
+ def _from_core(cls, b: Any) -> "BaseInfo":
228
+ return cls(company=str(b.CompanyName or ""), currency=str(b.Currency or ""), digits=int(b.Digits),
229
+ account_method=_name(b.AccMethod), margin_mode=_name(b.MarginMode))
230
+
231
+
232
+ @dataclass(frozen=True)
233
+ class AccountSummary:
234
+ """Realized figures from the deal ledger: trading result apart from deposits and withdrawals."""
235
+ balance: float
236
+ credit: float
237
+ realized_profit: float
238
+ deposit: float
239
+ withdrawal: float
240
+
241
+ @classmethod
242
+ def _from_core(cls, s: Any) -> "AccountSummary":
243
+ return cls(balance=float(s.Balance), credit=float(s.Credit), realized_profit=float(s.RealizedProfit),
244
+ deposit=float(s.Deposit), withdrawal=float(s.Withdrawal))
245
+
246
+
247
+ @dataclass(frozen=True)
248
+ class EquityPoint:
249
+ """One point of the balance/equity curve. ``balance`` and ``balance_change`` are exact (the deal ledger); the
250
+ historical ``equity`` and ``unrealized_pl`` are a bar-priced reconstruction. ``incomplete`` is set when a symbol's
251
+ bars did not arrive in time, so the floating part of that point is understated."""
252
+ time: Optional[datetime]
253
+ balance: float
254
+ equity: float
255
+ balance_drawdown_raw: float
256
+ balance_drawdown_relative: float
257
+ equity_drawdown_raw: float
258
+ equity_drawdown_relative: float
259
+ realized_pl: float
260
+ unrealized_pl: float
261
+ incomplete: bool
262
+ balance_change: float
263
+
264
+ @classmethod
265
+ def _from_core(cls, p: Any) -> "EquityPoint":
266
+ return cls(time=_time(p.Time), balance=float(p.Balance), equity=float(p.Equity),
267
+ balance_drawdown_raw=float(p.BalanceDrawdownRaw),
268
+ balance_drawdown_relative=float(p.BalanceDrawdownRelative),
269
+ equity_drawdown_raw=float(p.EquityDrawdownRaw),
270
+ equity_drawdown_relative=float(p.EquityDrawdownRelative), realized_pl=float(p.RealizedPL),
271
+ unrealized_pl=float(p.UnrealizedPL), incomplete=bool(p.Incomplete),
272
+ balance_change=float(p.BalanceChange))
273
+
274
+
275
+ @dataclass(frozen=True)
276
+ class LotSizing:
277
+ """The answer of :meth:`Client.lots_for_risk`. If ``ok`` is false, ``lots`` is 0 and ``reason`` says why:
278
+ do not trade it. ``warnings`` can accompany a true ``ok`` (for example, nothing corroborated the scale)."""
279
+ ok: bool
280
+ lots: float
281
+ expected_loss_at_stop: float
282
+ risk_requested: float
283
+ minimum_trade_cost: float
284
+ ratio: float
285
+ reason: str
286
+ warnings: Tuple[str, ...]
287
+
288
+ @classmethod
289
+ def _from_core(cls, r: Any) -> "LotSizing":
290
+ return cls(ok=bool(r.Ok), lots=float(r.Lots), expected_loss_at_stop=float(r.ExpectedLossAtStop),
291
+ risk_requested=float(r.RiskRequested), minimum_trade_cost=float(r.MinimumTradeCost),
292
+ ratio=float(r.Ratio), reason=str(r.Reason or ""),
293
+ warnings=tuple(str(w) for w in (r.Warnings or ())))
294
+
295
+
296
+ # ---- orders, deals, history --------------------------------------------------------------------------------------
297
+
298
+ @dataclass(frozen=True)
299
+ class Order:
300
+ """An open position, a pending order, or a historical order."""
301
+ ticket: int
302
+ symbol: str
303
+ order_type: str
304
+ deal_type: str
305
+ state: str
306
+ lots: float
307
+ digits: int
308
+ open_price: float
309
+ open_time: Optional[datetime]
310
+ close_price: float
311
+ close_time: Optional[datetime]
312
+ stop_loss: float
313
+ take_profit: float
314
+ profit: float
315
+ swap: float
316
+ commission: float
317
+ fee: float
318
+ comment: str
319
+ magic: int # the expert / magic number the order was sent with (0 if none)
320
+ close_lots: float = 0.0
321
+ close_comment: str = ""
322
+ contract_size: float = 0.0
323
+ placed_type: str = ""
324
+ request_id: int = 0
325
+ stop_limit_price: float = 0.0
326
+ profit_rate: float = 0.0
327
+ expiration_type: str = ""
328
+ expiration_time: Optional[datetime] = None
329
+ fill_policy: str = ""
330
+
331
+ @classmethod
332
+ def _from_core(cls, o: Any) -> "Order":
333
+ return cls(ticket=int(o.Ticket), symbol=str(o.Symbol or ""), order_type=_name(o.OrderType),
334
+ deal_type=_name(o.DealType), state=_name(o.State), lots=float(o.Lots), digits=int(o.Digits),
335
+ open_price=float(o.OpenPrice), open_time=_time(o.OpenTime), close_price=float(o.ClosePrice),
336
+ close_time=_time(o.CloseTime), stop_loss=float(o.StopLoss), take_profit=float(o.TakeProfit),
337
+ profit=float(o.Profit), swap=float(o.Swap), commission=float(o.Commission), fee=float(o.Fee),
338
+ comment=str(o.Comment or ""), magic=int(o.ExpertId), close_lots=float(o.CloseLots),
339
+ close_comment=str(o.CloseComment or ""), contract_size=float(o.ContractSize),
340
+ placed_type=_name(o.PlacedType), request_id=int(o.RequestId),
341
+ stop_limit_price=float(o.StopLimitPrice), profit_rate=float(o.ProfitRate),
342
+ expiration_type=_name(o.ExpirationType), expiration_time=_time(o.ExpirationTime),
343
+ fill_policy=_name(o.FillPolicy))
344
+
345
+
346
+ @dataclass(frozen=True)
347
+ class Deal:
348
+ """One executed deal from the account's ledger (a trade fill, or a balance operation such as a deposit)."""
349
+ ticket: int
350
+ order_ticket: int
351
+ position_ticket: int
352
+ symbol: str
353
+ type: str
354
+ direction: str
355
+ time: Optional[datetime]
356
+ price: float
357
+ open_price: float
358
+ stop_loss: float
359
+ take_profit: float
360
+ lots: float
361
+ profit: float
362
+ commission: float
363
+ swap: float
364
+ contract_size: float
365
+ digits: int
366
+ comment: str
367
+ magic: int # the expert / magic number of the order that produced the deal
368
+
369
+ @classmethod
370
+ def _from_core(cls, d: Any) -> "Deal":
371
+ return cls(ticket=int(d.Ticket), order_ticket=int(d.OrderTicket), position_ticket=int(d.PositionTicket),
372
+ symbol=str(d.Symbol or ""), type=_name(d.Type), direction=_name(d.Direction), time=_time(d.Time),
373
+ price=float(d.Price), open_price=float(d.OpenPrice), stop_loss=float(d.StopLoss),
374
+ take_profit=float(d.TakeProfit), lots=float(d.Lots), profit=float(d.Profit),
375
+ commission=float(d.Commission), swap=float(d.Swap), contract_size=float(d.ContractSize),
376
+ digits=int(d.Digits), comment=str(d.Comment or ""), magic=int(d.ExpertId))
377
+
378
+
379
+ @dataclass(frozen=True)
380
+ class History:
381
+ """Result of a history download: the orders and the deals in the requested window."""
382
+ orders: Tuple[Order, ...] = field(default_factory=tuple)
383
+ deals: Tuple[Deal, ...] = field(default_factory=tuple)
384
+ action: int = 0 # the server's action code; 14 means more pages follow
385
+
386
+ @property
387
+ def more(self) -> bool:
388
+ """Another page of this history follows (only seen on the event-driven path)."""
389
+ return self.action == 14
390
+
391
+ @classmethod
392
+ def _from_core(cls, h: Any) -> "History":
393
+ return cls(orders=tuple(Order._from_core(o) for o in (h.Orders or ())),
394
+ deals=tuple(Deal._from_core(d) for d in (h.Deals or ())), action=int(h.Action))
395
+
396
+
397
+ # ---- events -----------------------------------------------------------------------------------------------------
398
+
399
+ @dataclass(frozen=True)
400
+ class OrderUpdate:
401
+ """A change the server pushed about an order or a position. ``type`` is one of Started, Filled, PendingOpen,
402
+ PendingModify, PendingClose, MarketOpen, MarketModify, MarketClose, PartialClose, MarketCloseBy, OnStopLoss,
403
+ OnTakeProfit, OnStopOut, Balance, Expired, Rejected, Cancelling, Unknown.
404
+
405
+ ``ticket`` is the ticket of ``order`` (0 when the update carries no order, such as a balance operation);
406
+ ``transaction_id`` is the server's identifier of the transaction itself.
407
+ """
408
+ type: str
409
+ order: Optional[Order]
410
+ ticket: int
411
+ transaction_id: int
412
+ close_by_ticket: int
413
+
414
+ @classmethod
415
+ def _from_core(cls, u: Any) -> "OrderUpdate":
416
+ order = Order._from_core(u.Order) if u.Order is not None else None
417
+ return cls(type=_name(u.Type), order=order, ticket=order.ticket if order is not None else 0,
418
+ transaction_id=int(u.Trans.TicketNumber) if u.Trans is not None else 0,
419
+ close_by_ticket=int(u.CloseByTicket))
420
+
421
+
422
+ @dataclass(frozen=True)
423
+ class OrderProgress:
424
+ """Progress of an asynchronous trade request (the request id you got back from ``*_async``)."""
425
+ request_id: int
426
+ status: str # the server's answer, e.g. Done, Requote, Invalid ...
427
+ ticket: int
428
+ position_id: int
429
+ volume: float # lots
430
+ price: float
431
+ bid: float
432
+ ask: float
433
+ comment: str
434
+ order_type: str
435
+ symbol: str
436
+ fill_policy: str # the fill policy the request carried
437
+
438
+ @classmethod
439
+ def _from_core(cls, p: Any) -> "OrderProgress":
440
+ req, res = p.TradeRequest, p.TradeResult
441
+ symbol = ""
442
+ if p.OrderUpdate is not None:
443
+ symbol = str(getattr(p.OrderUpdate, "Currency", "") or "")
444
+ elif req is not None:
445
+ symbol = str(req.Currency or "")
446
+ return cls(request_id=int(req.RequestId) if req is not None else 0,
447
+ status=_name(res.Status) if res is not None else "",
448
+ ticket=int(res.TicketNumber) if res is not None else 0,
449
+ position_id=int(res.PositionId) if res is not None else 0,
450
+ volume=float(res.Volume) / 100000000.0 if res is not None else 0.0,
451
+ price=float(res.OpenPrice) if res is not None else 0.0,
452
+ bid=float(res.Bid) if res is not None else 0.0, ask=float(res.Ask) if res is not None else 0.0,
453
+ comment=str(res.Comment or "") if res is not None else "",
454
+ order_type=_name(req.OrderType) if req is not None else "", symbol=symbol,
455
+ fill_policy=_name(req.FillPolicy) if req is not None else "")
456
+
457
+
458
+ @dataclass(frozen=True)
459
+ class ConnectEvent:
460
+ """A step of the connect sequence: SendLogin, SendAccountPassword, AcceptAuthorized, RequestTradeInfo, Connected,
461
+ Exception, Disconnect. ``error`` is the translated exception for ``Exception`` steps."""
462
+ stage: str
463
+ error: Optional[BaseException] = None
464
+
465
+
466
+ # ---- offline files and account admin ------------------------------------------------------------------------------
467
+
468
+ @dataclass(frozen=True)
469
+ class ServerRecord:
470
+ """A trade server of a ``servers.dat`` file: its name, company and (host, port) access points."""
471
+ name: str
472
+ company: str
473
+ access: Tuple[Tuple[str, int], ...]
474
+
475
+ @classmethod
476
+ def _from_core(cls, r: Any) -> "ServerRecord":
477
+ return cls(name=str(r.Name or ""), company=str(r.Company or ""),
478
+ access=tuple((str(a.Item1), int(a.Item2)) for a in (r.Access or ())))
479
+
480
+
481
+ @dataclass(frozen=True)
482
+ class DemoAccountRequest:
483
+ """What :func:`pmt5api.request_demo_account` asks a broker's server for."""
484
+ user_name: str
485
+ email: str
486
+ deposit: float = 10000.0
487
+ leverage: int = 100
488
+ account_type: str = ""
489
+ country: str = ""
490
+ city: str = ""
491
+ state: str = ""
492
+ zip_code: str = ""
493
+ address: str = ""
494
+ phone: str = ""
495
+ company_name: str = ""
496
+ language_id: int = 0
497
+
498
+
499
+ @dataclass(frozen=True)
500
+ class DemoAccount:
501
+ login: int
502
+ password: str
503
+ investor_password: str
504
+
505
+
506
+ def _symbols_from_core(infos: Any, groups: Any = None) -> Dict[str, SymbolInfo]:
507
+ """``infos`` is the core's name -> SymbolInfo dictionary; ``groups`` is the core's ``Symbols`` object, used to
508
+ attach each symbol's group rules."""
509
+ out: Dict[str, SymbolInfo] = {}
510
+ for kv in infos or ():
511
+ if kv.Value is None:
512
+ continue
513
+ name = str(kv.Key) if kv.Key is not None else ""
514
+ group = None
515
+ if groups is not None:
516
+ try:
517
+ group = groups.GetGroup(name)
518
+ except Exception: # noqa: BLE001 — a symbol without a group still has its own record
519
+ group = None
520
+ info = SymbolInfo._from_core(name, kv.Value, group)
521
+ out[info.name] = info
522
+ return out
pmt5api/py.typed ADDED
File without changes
pmt5api/servers.py ADDED
@@ -0,0 +1,73 @@
1
+ """Offline helpers and account administration that do not need a connected client."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from typing import Iterable, List, Union
6
+
7
+ from . import _runtime
8
+ from ._util import translated
9
+ from .models import DemoAccount, DemoAccountRequest, ServerRecord
10
+
11
+
12
+ @translated
13
+ def load_servers_dat(source: Union[str, "os.PathLike[str]", bytes]) -> List[ServerRecord]:
14
+ """The trade servers in a terminal's ``servers.dat``: each with its name, company and (host, port) access points.
15
+ Offline: reads only the file (a path, or the file's bytes)."""
16
+ core = _runtime.load()
17
+ if isinstance(source, (bytes, bytearray)):
18
+ import System # noqa: PLC0415
19
+ records = core.MT5API.LoadServersDat(System.Array[System.Byte](bytes(source)))
20
+ else:
21
+ records = core.MT5API.LoadServersDat(os.fspath(source))
22
+ return [ServerRecord._from_core(r) for r in (records or ())]
23
+
24
+
25
+ @translated
26
+ def save_servers_dat(records: Iterable[ServerRecord]) -> bytes:
27
+ """The bytes of a ``servers.dat`` file holding ``records`` (the inverse of :func:`load_servers_dat`)."""
28
+ core = _runtime.load()
29
+ import System # noqa: PLC0415
30
+ Access = System.ValueTuple[System.String, System.Int32]
31
+ built = []
32
+ for rec in records:
33
+ r = core.ServerRecord()
34
+ r.Name = rec.name
35
+ r.Company = rec.company
36
+ r.Access = System.Array[Access]([Access(host, int(port)) for host, port in rec.access])
37
+ built.append(r)
38
+ return bytes(core.MT5API.SaveServersDat(System.Array[core.ServerRecord](built)))
39
+
40
+
41
+ @translated
42
+ def request_demo_account(request: DemoAccountRequest, host: str, port: int = 443) -> DemoAccount:
43
+ """Ask a broker's access server for a new demo account and return its login and passwords.
44
+
45
+ This creates a real account at the broker: use it deliberately.
46
+ """
47
+ core = _runtime.load()
48
+ req = core.AccountRequest()
49
+ req.UserName = request.user_name
50
+ req.Email = request.email
51
+ req.Deposit = float(request.deposit)
52
+ req.Leverage = int(request.leverage)
53
+ req.AccType = request.account_type
54
+ req.Country = request.country
55
+ req.City = request.city
56
+ req.State = request.state
57
+ req.ZipCode = request.zip_code
58
+ req.Address = request.address
59
+ req.Phone = request.phone
60
+ req.CompanyName = request.company_name
61
+ req.LanguageId = int(request.language_id)
62
+ answer = core.MT5API.RequestDemoAccount(req, host, int(port))
63
+ return DemoAccount(login=int(answer.Login), password=str(answer.Password or ""),
64
+ investor_password=str(answer.Investor or ""))
65
+
66
+
67
+ @translated
68
+ def tick_value_from_deal(closed_profit: float, open_price: float, close_price: float, lots: float) -> float:
69
+ """The value of one price unit per lot implied by a closed deal's realized P/L — the empirical check of a
70
+ tick value."""
71
+ core = _runtime.load()
72
+ return float(core.MT5API.ComputeTickValueFromDeal(float(closed_profit), float(open_price), float(close_price),
73
+ float(lots)))