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/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)))
|