pmt4api 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pmt4api/__init__.py +32 -0
- pmt4api/__main__.py +78 -0
- pmt4api/_account.py +253 -0
- pmt4api/_base.py +24 -0
- pmt4api/_coverage.py +186 -0
- pmt4api/_events.py +189 -0
- pmt4api/_market.py +263 -0
- pmt4api/_native/CHANGELOG.md +362 -0
- pmt4api/_native/THIRD-PARTY-NOTICES.txt +7 -0
- pmt4api/_native/docs/ERRORS.md +136 -0
- pmt4api/_native/docs/LICENSING.md +55 -0
- pmt4api/_native/docs/SECURITY.md +38 -0
- pmt4api/_native/wmt4api.dll +0 -0
- pmt4api/_risk.py +66 -0
- pmt4api/_runtime.py +154 -0
- pmt4api/_session.py +171 -0
- pmt4api/_streams.py +238 -0
- pmt4api/_trading.py +384 -0
- pmt4api/_util.py +78 -0
- pmt4api/bars.py +62 -0
- pmt4api/client.py +143 -0
- pmt4api/diagnostics.py +38 -0
- pmt4api/docs/API.md +702 -0
- pmt4api/docs/FROM-CSHARP.md +151 -0
- pmt4api/errors.py +233 -0
- pmt4api/frames.py +21 -0
- pmt4api/models.py +337 -0
- pmt4api/py.typed +0 -0
- pmt4api/servers.py +40 -0
- pmt4api-1.0.0.dist-info/METADATA +389 -0
- pmt4api-1.0.0.dist-info/RECORD +33 -0
- pmt4api-1.0.0.dist-info/WHEEL +4 -0
- pmt4api-1.0.0.dist-info/licenses/LICENSE.txt +29 -0
pmt4api/models.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""Plain Python value types returned by :class:`pmt4api.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
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Any, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
_EPOCH = datetime(1970, 1, 1)
|
|
18
|
+
|
|
19
|
+
_PROFIT_MODES = {0: "Forex", 1: "CFD", 2: "Futures"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _time(value: Any) -> Optional[datetime]:
|
|
23
|
+
"""A .NET ``DateTime`` (or ``datetime``) as a naive ``datetime``; the unset values (year 1, the Unix epoch) as ``None``."""
|
|
24
|
+
if value is None:
|
|
25
|
+
return None
|
|
26
|
+
if isinstance(value, datetime):
|
|
27
|
+
out = value.replace(tzinfo=None)
|
|
28
|
+
else:
|
|
29
|
+
year = int(value.Year)
|
|
30
|
+
if year <= 1:
|
|
31
|
+
return None
|
|
32
|
+
out = datetime(year, int(value.Month), int(value.Day), int(value.Hour), int(value.Minute),
|
|
33
|
+
int(value.Second), int(value.Millisecond) * 1000)
|
|
34
|
+
return None if out <= _EPOCH else out
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _name(value: Any) -> str:
|
|
38
|
+
"""An enum value as its member name."""
|
|
39
|
+
return "" if value is None else str(value)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _opt_float(value: Any) -> Optional[float]:
|
|
43
|
+
"""A double where NaN means "not known"."""
|
|
44
|
+
f = float(value)
|
|
45
|
+
return None if math.isnan(f) else f
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _unset_if_zero(value: Any) -> Optional[float]:
|
|
49
|
+
f = float(value)
|
|
50
|
+
return None if f == 0.0 or math.isnan(f) else f
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---- market data ------------------------------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class Quote:
|
|
57
|
+
"""A price. ``time`` is the server time of the tick (use it to judge how old the price is); ``high`` and ``low``
|
|
58
|
+
are the session's range once the server has sent it, and ``None`` before that."""
|
|
59
|
+
symbol: str
|
|
60
|
+
bid: float
|
|
61
|
+
ask: float
|
|
62
|
+
time: Optional[datetime] = None
|
|
63
|
+
high: Optional[float] = None
|
|
64
|
+
low: Optional[float] = None
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def spread(self) -> float:
|
|
68
|
+
return self.ask - self.bid
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def _from_core(cls, q: Any) -> "Quote":
|
|
72
|
+
return cls(symbol=str(q.Symbol), bid=float(q.Bid), ask=float(q.Ask), time=_time(q.Time),
|
|
73
|
+
high=_unset_if_zero(q.High), low=_unset_if_zero(q.Low))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class SymbolInfo:
|
|
78
|
+
"""A symbol's specification and trading rules, as the server publishes them.
|
|
79
|
+
|
|
80
|
+
``contract_size`` and ``digits`` are reliable. ``currency`` is the currency the server lists for the symbol: the
|
|
81
|
+
profit currency of a CFD or futures symbol, the *base* currency of a Forex one; :attr:`profit_currency` gives the
|
|
82
|
+
currency profit is realised in for both. ``raw_tick_value`` and the two ``raw_*_tick_value`` fields are the
|
|
83
|
+
server's own and are often 0: never size a position from them (use :meth:`Client.tick_value` and
|
|
84
|
+
:meth:`Client.lots_for_risk`). ``min_lots`` / ``max_lots`` / ``lots_step`` are 0 when the account's group published
|
|
85
|
+
none.
|
|
86
|
+
"""
|
|
87
|
+
name: str
|
|
88
|
+
digits: int
|
|
89
|
+
point: float
|
|
90
|
+
currency: str
|
|
91
|
+
profit_mode: str # Forex, CFD or Futures
|
|
92
|
+
contract_size: float
|
|
93
|
+
margin_divider: float
|
|
94
|
+
tick_size: float
|
|
95
|
+
execution: str # Request, Instant, Market or Exchange
|
|
96
|
+
security_index: int # the account group's security (symbol class) the volume rules are keyed on
|
|
97
|
+
min_lots: float
|
|
98
|
+
max_lots: float
|
|
99
|
+
lots_step: float
|
|
100
|
+
code: int # the feed code the server uses for this symbol
|
|
101
|
+
raw_tick_value: float = 0.0
|
|
102
|
+
raw_bid_tick_value: float = 0.0
|
|
103
|
+
raw_ask_tick_value: float = 0.0
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def profit_currency(self) -> str:
|
|
107
|
+
"""The currency profit is realised in: the quote currency of a Forex symbol (the last three letters of its
|
|
108
|
+
six-letter name, the way the core prices it), the listed currency otherwise."""
|
|
109
|
+
if self.profit_mode == "Forex" and len(self.name) >= 6 and self.name[:6].isalpha():
|
|
110
|
+
return self.name[3:6].upper()
|
|
111
|
+
return self.currency
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def _from_core(cls, s: Any) -> "SymbolInfo":
|
|
115
|
+
digits = int(s.Digits)
|
|
116
|
+
mode = int(s.ProfitMode)
|
|
117
|
+
return cls(name=str(s.Name), digits=digits, point=10.0 ** -digits, currency=str(s.Currency or ""),
|
|
118
|
+
profit_mode=_PROFIT_MODES.get(mode, "Mode%d" % mode), contract_size=float(s.ContractSize),
|
|
119
|
+
margin_divider=float(s.MarginDivider), tick_size=float(s.TickSize), execution=_name(s.ExecutionType),
|
|
120
|
+
security_index=int(s.SecurityIndex), min_lots=float(s.MinLot), max_lots=float(s.MaxLot),
|
|
121
|
+
lots_step=float(s.LotStep), code=int(s.Code), raw_tick_value=float(s.TickValue),
|
|
122
|
+
raw_bid_tick_value=float(s.BidTickValue), raw_ask_tick_value=float(s.AskTickValue))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True)
|
|
126
|
+
class SymbolGroup:
|
|
127
|
+
"""One of the broker's configured symbol groups (its name and description)."""
|
|
128
|
+
name: str
|
|
129
|
+
description: str
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def _from_core(cls, g: Any) -> "SymbolGroup":
|
|
133
|
+
return cls(name=str(g.Name or ""), description=str(g.Description or ""))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---- the account ------------------------------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True)
|
|
139
|
+
class AccountInfo:
|
|
140
|
+
"""The account at the moment it was read. ``equity``, ``profit``, ``margin``, ``free_margin`` and ``margin_level``
|
|
141
|
+
are derived on the client from the live quotes (the server does not push them), exactly as the terminal does.
|
|
142
|
+
``priced_from`` is the server time of the oldest tick those figures were priced from (``None`` with no open
|
|
143
|
+
position): minutes old while the market is open means the feed is not delivering — do not trust the equity."""
|
|
144
|
+
login: int
|
|
145
|
+
name: str
|
|
146
|
+
company: str
|
|
147
|
+
group: str
|
|
148
|
+
currency: str
|
|
149
|
+
leverage: int
|
|
150
|
+
balance: float
|
|
151
|
+
credit: float
|
|
152
|
+
equity: float
|
|
153
|
+
profit: float
|
|
154
|
+
margin: float
|
|
155
|
+
free_margin: float
|
|
156
|
+
margin_level: float
|
|
157
|
+
account_type: str # Real, Demo or Contest (a demo server can stamp Real: it is the server's class)
|
|
158
|
+
is_investor: bool = False
|
|
159
|
+
priced_from: Optional[datetime] = None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass(frozen=True)
|
|
163
|
+
class AccountSummary:
|
|
164
|
+
"""Realized totals over a history range (from the closed-order ledger, not the floating P/L of open positions)."""
|
|
165
|
+
balance: float
|
|
166
|
+
credit: float
|
|
167
|
+
realized_profit: float # closed trades: profit + commission + swap
|
|
168
|
+
deposit: float
|
|
169
|
+
withdrawal: float # negative or zero
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def _from_core(cls, s: Any) -> "AccountSummary":
|
|
173
|
+
return cls(balance=float(s.Balance), credit=float(s.Credit), realized_profit=float(s.RealizedProfit),
|
|
174
|
+
deposit=float(s.Deposit), withdrawal=float(s.Withdrawal))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(frozen=True)
|
|
178
|
+
class EquityPoint:
|
|
179
|
+
"""One sample of the balance and equity curve. ``balance_change`` is the change of balance since the first point
|
|
180
|
+
(deposits and withdrawals included, so it is not a trading result: use :class:`AccountSummary`)."""
|
|
181
|
+
time: Optional[datetime]
|
|
182
|
+
balance: float
|
|
183
|
+
equity: float
|
|
184
|
+
balance_change: float
|
|
185
|
+
unrealized_pl: float
|
|
186
|
+
balance_drawdown_raw: float
|
|
187
|
+
balance_drawdown_relative: float
|
|
188
|
+
equity_drawdown_raw: float
|
|
189
|
+
equity_drawdown_relative: float
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def _from_core(cls, p: Any) -> "EquityPoint":
|
|
193
|
+
return cls(time=_time(p.Time), balance=float(p.Balance), equity=float(p.Equity),
|
|
194
|
+
balance_change=float(p.BalanceChange), unrealized_pl=float(p.UnrealizedPL),
|
|
195
|
+
balance_drawdown_raw=float(p.BalanceDrawdownRaw),
|
|
196
|
+
balance_drawdown_relative=float(p.BalanceDrawdownRelative),
|
|
197
|
+
equity_drawdown_raw=float(p.EquityDrawdownRaw),
|
|
198
|
+
equity_drawdown_relative=float(p.EquityDrawdownRelative))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass(frozen=True)
|
|
202
|
+
class LotSizing:
|
|
203
|
+
"""The answer of :meth:`Client.lots_for_risk`. If ``ok`` is false, ``lots`` is 0 and ``reason`` says why: do not
|
|
204
|
+
trade it. ``warnings`` can accompany a true ``ok`` (most often that the broker's minimum volume risks more than
|
|
205
|
+
was asked)."""
|
|
206
|
+
ok: bool
|
|
207
|
+
lots: float
|
|
208
|
+
expected_loss_at_stop: float
|
|
209
|
+
risk_requested: float
|
|
210
|
+
minimum_trade_cost: float
|
|
211
|
+
ratio: float
|
|
212
|
+
reason: str
|
|
213
|
+
warnings: Tuple[str, ...]
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def _from_core(cls, r: Any) -> "LotSizing":
|
|
217
|
+
return cls(ok=bool(r.Ok), lots=float(r.Lots), expected_loss_at_stop=float(r.ExpectedLossAtStop),
|
|
218
|
+
risk_requested=float(r.RiskRequested), minimum_trade_cost=float(r.MinimumTradeCost),
|
|
219
|
+
ratio=float(r.Ratio), reason=str(r.Reason or ""),
|
|
220
|
+
warnings=tuple(str(w) for w in (r.Warnings or ())))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ---- orders ------------------------------------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
_MARKET = ("Buy", "Sell")
|
|
226
|
+
_PENDING = ("BuyLimit", "SellLimit", "BuyStop", "SellStop")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataclass(frozen=True)
|
|
230
|
+
class Order:
|
|
231
|
+
"""One MT4 order: an open position, a pending order, a closed order of the history, or a balance / credit entry.
|
|
232
|
+
|
|
233
|
+
MT4 keeps one record for all of them. ``order_type`` is ``Buy`` or ``Sell`` for a market position (open, or closed
|
|
234
|
+
when ``close_time`` is set), ``BuyLimit`` / ``SellLimit`` / ``BuyStop`` / ``SellStop`` for a pending order, and
|
|
235
|
+
``Balance`` / ``Credit`` for a ledger entry (the amount is in ``profit``, ``symbol`` is empty).
|
|
236
|
+
``magic`` is read back from the server; the core cannot set it on an order (see :meth:`Client.buy`).
|
|
237
|
+
"""
|
|
238
|
+
ticket: int
|
|
239
|
+
symbol: str
|
|
240
|
+
order_type: str
|
|
241
|
+
lots: float
|
|
242
|
+
open_time: Optional[datetime]
|
|
243
|
+
open_price: float
|
|
244
|
+
stop_loss: float
|
|
245
|
+
take_profit: float
|
|
246
|
+
close_time: Optional[datetime]
|
|
247
|
+
close_price: float
|
|
248
|
+
expiration: Optional[datetime]
|
|
249
|
+
magic: int
|
|
250
|
+
swap: float
|
|
251
|
+
commission: float
|
|
252
|
+
comment: str
|
|
253
|
+
profit: float
|
|
254
|
+
rate_open: float = 0.0
|
|
255
|
+
rate_close: float = 0.0
|
|
256
|
+
rate_margin: float = 0.0
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def is_market(self) -> bool:
|
|
260
|
+
"""A market position (``Buy`` or ``Sell``)."""
|
|
261
|
+
return self.order_type in _MARKET
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def is_pending(self) -> bool:
|
|
265
|
+
"""A pending order (a limit or a stop)."""
|
|
266
|
+
return self.order_type in _PENDING
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def is_capital_flow(self) -> bool:
|
|
270
|
+
"""A balance or credit entry (deposit, withdrawal, bonus), not a trade."""
|
|
271
|
+
return self.order_type in ("Balance", "Credit")
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def is_buy(self) -> bool:
|
|
275
|
+
"""The order is on the buy side (market or pending)."""
|
|
276
|
+
return self.order_type.startswith("Buy")
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def _from_core(cls, o: Any) -> "Order":
|
|
280
|
+
return cls(ticket=int(o.Ticket), symbol=str(o.Symbol or ""), order_type=_name(o.Type), lots=float(o.Lots),
|
|
281
|
+
open_time=_time(o.OpenTime), open_price=float(o.OpenPrice), stop_loss=float(o.StopLoss),
|
|
282
|
+
take_profit=float(o.TakeProfit), close_time=_time(o.CloseTime), close_price=float(o.ClosePrice),
|
|
283
|
+
expiration=_time(o.ExpirationTime), magic=int(o.ExpertId), swap=float(o.Swap),
|
|
284
|
+
commission=float(o.Commission), comment=str(o.Comment or ""), profit=float(o.Profit),
|
|
285
|
+
rate_open=float(o.RateOpen), rate_close=float(o.RateClose), rate_margin=float(o.RateMargin))
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@dataclass(frozen=True)
|
|
289
|
+
class OrderUpdate:
|
|
290
|
+
"""A change the server pushed: ``action`` is one of ``PositionOpen``, ``PositionClose``, ``PositionModify``,
|
|
291
|
+
``PendingOpen``, ``PendingClose``, ``PendingModify``, ``PendingFill``, ``Balance``, ``Credit`` (a pending order that
|
|
292
|
+
fills arrives as ``PositionOpen``)."""
|
|
293
|
+
action: str
|
|
294
|
+
order: Order
|
|
295
|
+
|
|
296
|
+
@classmethod
|
|
297
|
+
def _from_core(cls, u: Any) -> "OrderUpdate":
|
|
298
|
+
return cls(action=_name(u.Action), order=Order._from_core(u.Order))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@dataclass(frozen=True)
|
|
302
|
+
class OrderProgress:
|
|
303
|
+
"""Progress of a trade request, as the request id (the integer :meth:`Client.buy_async` and friends return)
|
|
304
|
+
advances through the server: ``stage`` is ``InProcess``, ``Accepted``, ``Opened``, ``Closed``, ``Modified``,
|
|
305
|
+
``PendingDeleted``, ``ClosedBy``, ``MultipleClosedBy``, ``Price`` or ``Exception`` (``Rejected`` and ``Timeout`` are
|
|
306
|
+
defined by the core but never raised by it). ``order`` is set where the server returned one, ``error`` for an
|
|
307
|
+
``Exception`` stage (a pmt4api error such as :class:`ServerError`)."""
|
|
308
|
+
request_id: int
|
|
309
|
+
stage: str
|
|
310
|
+
order: Optional[Order] = None
|
|
311
|
+
error: Optional[BaseException] = None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@dataclass(frozen=True)
|
|
315
|
+
class ConnectEvent:
|
|
316
|
+
"""A session started (``connected`` true: the first connect and every reconnect) or ended (``connected`` false;
|
|
317
|
+
``error`` is the reason for a drop and ``None`` when you closed the client yourself)."""
|
|
318
|
+
connected: bool
|
|
319
|
+
error: Optional[BaseException] = None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ---- offline files -----------------------------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
@dataclass(frozen=True)
|
|
325
|
+
class ServerRecord:
|
|
326
|
+
"""A broker of a ``servers.ini`` file: its name, company, demo flag, main host and (host, port) access servers."""
|
|
327
|
+
name: str
|
|
328
|
+
company: str
|
|
329
|
+
is_demo: bool
|
|
330
|
+
main_host: str
|
|
331
|
+
access: Tuple[Tuple[str, int], ...]
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
def _from_core(cls, r: Any) -> "ServerRecord":
|
|
335
|
+
return cls(name=str(r.Name or ""), company=str(r.Company or ""), is_demo=bool(r.IsDemo),
|
|
336
|
+
main_host=str(r.MainHost or ""),
|
|
337
|
+
access=tuple((str(a.Item1), int(a.Item2)) for a in (r.Access or ())))
|
pmt4api/py.typed
ADDED
|
File without changes
|
pmt4api/servers.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Offline helpers 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 ServerRecord
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@translated
|
|
13
|
+
def load_servers_ini(source: Union[str, "os.PathLike[str]", bytes]) -> List[ServerRecord]:
|
|
14
|
+
"""The brokers in a terminal's ``servers.ini``: each with its name, company, demo flag, main host and
|
|
15
|
+
``(host, port)`` access servers. 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.MT4API.LoadServersIni(System.Array[System.Byte](bytes(source)))
|
|
20
|
+
else:
|
|
21
|
+
records = core.MT4API.LoadServersIni(os.fspath(source))
|
|
22
|
+
return [ServerRecord._from_core(r) for r in (records or ())]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@translated
|
|
26
|
+
def save_servers_ini(records: Iterable[ServerRecord]) -> bytes:
|
|
27
|
+
"""The bytes of a ``servers.ini`` file holding ``records`` (the inverse of :func:`load_servers_ini`)."""
|
|
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.Mt4Server()
|
|
34
|
+
r.Name = rec.name
|
|
35
|
+
r.Company = rec.company
|
|
36
|
+
r.IsDemo = bool(rec.is_demo)
|
|
37
|
+
r.MainHost = rec.main_host
|
|
38
|
+
r.Access = System.Array[Access]([Access(host, int(port)) for host, port in rec.access])
|
|
39
|
+
built.append(r)
|
|
40
|
+
return bytes(core.MT4API.SaveServersIni(System.Array[core.Mt4Server](built)))
|