pdmt5 0.1.8__py3-none-any.whl → 0.2.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.
pdmt5/trading.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from datetime import timedelta
|
|
6
|
+
from functools import cached_property
|
|
6
7
|
from math import floor
|
|
7
8
|
from typing import TYPE_CHECKING, Any, Literal
|
|
8
9
|
|
|
@@ -28,6 +29,67 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
28
29
|
|
|
29
30
|
model_config = ConfigDict(frozen=True)
|
|
30
31
|
|
|
32
|
+
@cached_property
|
|
33
|
+
def mt5_successful_trade_retcodes(self) -> set[int]:
|
|
34
|
+
"""Set of successful trade return codes.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Set of successful trade return codes.
|
|
38
|
+
"""
|
|
39
|
+
return {
|
|
40
|
+
self.mt5.TRADE_RETCODE_PLACED, # 10008
|
|
41
|
+
self.mt5.TRADE_RETCODE_DONE, # 10009
|
|
42
|
+
self.mt5.TRADE_RETCODE_DONE_PARTIAL, # 10010
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@cached_property
|
|
46
|
+
def mt5_failed_trade_retcodes(self) -> set[int]:
|
|
47
|
+
"""Set of failed trade return codes.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Set of failed trade return codes.
|
|
51
|
+
"""
|
|
52
|
+
return {
|
|
53
|
+
self.mt5.TRADE_RETCODE_REQUOTE, # 10004
|
|
54
|
+
self.mt5.TRADE_RETCODE_REJECT, # 10006
|
|
55
|
+
self.mt5.TRADE_RETCODE_CANCEL, # 10007
|
|
56
|
+
self.mt5.TRADE_RETCODE_ERROR, # 10011
|
|
57
|
+
self.mt5.TRADE_RETCODE_TIMEOUT, # 10012
|
|
58
|
+
self.mt5.TRADE_RETCODE_INVALID, # 10013
|
|
59
|
+
self.mt5.TRADE_RETCODE_INVALID_VOLUME, # 10014
|
|
60
|
+
self.mt5.TRADE_RETCODE_INVALID_PRICE, # 10015
|
|
61
|
+
self.mt5.TRADE_RETCODE_INVALID_STOPS, # 10016
|
|
62
|
+
self.mt5.TRADE_RETCODE_TRADE_DISABLED, # 10017
|
|
63
|
+
self.mt5.TRADE_RETCODE_MARKET_CLOSED, # 10018
|
|
64
|
+
self.mt5.TRADE_RETCODE_NO_MONEY, # 10019
|
|
65
|
+
self.mt5.TRADE_RETCODE_PRICE_CHANGED, # 10020
|
|
66
|
+
self.mt5.TRADE_RETCODE_PRICE_OFF, # 10021
|
|
67
|
+
self.mt5.TRADE_RETCODE_INVALID_EXPIRATION, # 10022
|
|
68
|
+
self.mt5.TRADE_RETCODE_ORDER_CHANGED, # 10023
|
|
69
|
+
self.mt5.TRADE_RETCODE_TOO_MANY_REQUESTS, # 10024
|
|
70
|
+
self.mt5.TRADE_RETCODE_NO_CHANGES, # 10025
|
|
71
|
+
self.mt5.TRADE_RETCODE_SERVER_DISABLES_AT, # 10026
|
|
72
|
+
self.mt5.TRADE_RETCODE_CLIENT_DISABLES_AT, # 10027
|
|
73
|
+
self.mt5.TRADE_RETCODE_LOCKED, # 10028
|
|
74
|
+
self.mt5.TRADE_RETCODE_FROZEN, # 10029
|
|
75
|
+
self.mt5.TRADE_RETCODE_INVALID_FILL, # 10030
|
|
76
|
+
self.mt5.TRADE_RETCODE_CONNECTION, # 10031
|
|
77
|
+
self.mt5.TRADE_RETCODE_ONLY_REAL, # 10032
|
|
78
|
+
self.mt5.TRADE_RETCODE_LIMIT_ORDERS, # 10033
|
|
79
|
+
self.mt5.TRADE_RETCODE_LIMIT_VOLUME, # 10034
|
|
80
|
+
self.mt5.TRADE_RETCODE_INVALID_ORDER, # 10035
|
|
81
|
+
self.mt5.TRADE_RETCODE_POSITION_CLOSED, # 10036
|
|
82
|
+
self.mt5.TRADE_RETCODE_INVALID_CLOSE_VOLUME, # 10038
|
|
83
|
+
self.mt5.TRADE_RETCODE_CLOSE_ORDER_EXIST, # 10039
|
|
84
|
+
self.mt5.TRADE_RETCODE_LIMIT_POSITIONS, # 10040
|
|
85
|
+
self.mt5.TRADE_RETCODE_REJECT_CANCEL, # 10041
|
|
86
|
+
self.mt5.TRADE_RETCODE_LONG_ONLY, # 10042
|
|
87
|
+
self.mt5.TRADE_RETCODE_SHORT_ONLY, # 10043
|
|
88
|
+
self.mt5.TRADE_RETCODE_CLOSE_ONLY, # 10044
|
|
89
|
+
self.mt5.TRADE_RETCODE_FIFO_CLOSE, # 10045
|
|
90
|
+
self.mt5.TRADE_RETCODE_HEDGE_PROHIBITED, # 10046
|
|
91
|
+
}
|
|
92
|
+
|
|
31
93
|
def close_open_positions(
|
|
32
94
|
self,
|
|
33
95
|
symbols: str | list[str] | tuple[str, ...] | None = None,
|
|
@@ -116,12 +178,14 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
116
178
|
def _send_or_check_order(
|
|
117
179
|
self,
|
|
118
180
|
request: dict[str, Any],
|
|
181
|
+
raise_on_error: bool = False,
|
|
119
182
|
dry_run: bool = False,
|
|
120
183
|
) -> dict[str, Any]:
|
|
121
184
|
"""Send or check an order request.
|
|
122
185
|
|
|
123
186
|
Args:
|
|
124
187
|
request: Order request dictionary.
|
|
188
|
+
raise_on_error: If True, raise an error on operation failure.
|
|
125
189
|
dry_run: If True, only check the order without sending it.
|
|
126
190
|
|
|
127
191
|
Returns:
|
|
@@ -138,25 +202,21 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
138
202
|
response = self.order_send_as_dict(request=request)
|
|
139
203
|
order_func = "order_send"
|
|
140
204
|
retcode = response.get("retcode")
|
|
141
|
-
if (
|
|
142
|
-
dry_run and retcode
|
|
205
|
+
if (dry_run and retcode == 0) or (
|
|
206
|
+
not dry_run and retcode in self.mt5_successful_trade_retcodes
|
|
143
207
|
):
|
|
144
208
|
self.logger.info("response: %s", response)
|
|
145
209
|
return response
|
|
146
|
-
elif
|
|
147
|
-
self.mt5.TRADE_RETCODE_TRADE_DISABLED,
|
|
148
|
-
self.mt5.TRADE_RETCODE_MARKET_CLOSED,
|
|
149
|
-
self.mt5.TRADE_RETCODE_NO_CHANGES,
|
|
150
|
-
}:
|
|
151
|
-
self.logger.info("response: %s", response)
|
|
152
|
-
comment = response.get("comment", "Unknown error")
|
|
153
|
-
self.logger.warning("%s() failed and skipped. <= `%s`", order_func, comment)
|
|
154
|
-
return response
|
|
155
|
-
else:
|
|
210
|
+
elif raise_on_error:
|
|
156
211
|
self.logger.error("response: %s", response)
|
|
157
|
-
comment = response.get("comment"
|
|
212
|
+
comment = response.get("comment")
|
|
158
213
|
error_message = f"{order_func}() failed and aborted. <= `{comment}`"
|
|
159
214
|
raise Mt5TradingError(error_message)
|
|
215
|
+
else:
|
|
216
|
+
self.logger.warning("response: %s", response)
|
|
217
|
+
comment = response.get("comment")
|
|
218
|
+
self.logger.warning("%s() failed and skipped. <= `%s`", order_func, comment)
|
|
219
|
+
return response
|
|
160
220
|
|
|
161
221
|
def place_market_order(
|
|
162
222
|
self,
|
|
@@ -183,6 +243,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
183
243
|
Returns:
|
|
184
244
|
Dictionary with operation result.
|
|
185
245
|
"""
|
|
246
|
+
self.logger.info("Placing market order: %s %s %s", order_side, volume, symbol)
|
|
186
247
|
return self._send_or_check_order(
|
|
187
248
|
request={
|
|
188
249
|
"action": self.mt5.TRADE_ACTION_DEAL,
|
|
@@ -255,6 +316,13 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
255
316
|
if sl != p["sl"] or tp != p["tp"]
|
|
256
317
|
]
|
|
257
318
|
if order_requests:
|
|
319
|
+
self.logger.info(
|
|
320
|
+
"Updating SL/TP for %d positions for %s: %s/%s",
|
|
321
|
+
len(order_requests),
|
|
322
|
+
symbol,
|
|
323
|
+
sl,
|
|
324
|
+
tp,
|
|
325
|
+
)
|
|
258
326
|
return [
|
|
259
327
|
self._send_or_check_order(request=r, dry_run=dry_run)
|
|
260
328
|
for r in order_requests
|
|
@@ -294,15 +362,22 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
294
362
|
else symbol_info_tick["ask"]
|
|
295
363
|
),
|
|
296
364
|
)
|
|
365
|
+
result = {"volume": symbol_info["volume_min"], "margin": margin}
|
|
297
366
|
if margin:
|
|
298
|
-
|
|
367
|
+
self.logger.info(
|
|
368
|
+
"Calculated minimum %s order margin for %s: %s",
|
|
369
|
+
order_side,
|
|
370
|
+
symbol,
|
|
371
|
+
result,
|
|
372
|
+
)
|
|
299
373
|
else:
|
|
300
374
|
self.logger.warning(
|
|
301
|
-
"
|
|
302
|
-
symbol,
|
|
375
|
+
"Calculated minimum order margin to %s %s: %s",
|
|
303
376
|
order_side,
|
|
377
|
+
symbol,
|
|
378
|
+
result,
|
|
304
379
|
)
|
|
305
|
-
|
|
380
|
+
return result
|
|
306
381
|
|
|
307
382
|
def calculate_volume_by_margin(
|
|
308
383
|
self,
|
|
@@ -325,12 +400,19 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
325
400
|
order_side=order_side,
|
|
326
401
|
)
|
|
327
402
|
if min_order_margin_dict["margin"]:
|
|
328
|
-
|
|
403
|
+
result = (
|
|
329
404
|
floor(margin / min_order_margin_dict["margin"])
|
|
330
405
|
* min_order_margin_dict["volume"]
|
|
331
406
|
)
|
|
332
407
|
else:
|
|
333
|
-
|
|
408
|
+
result = 0.0
|
|
409
|
+
self.logger.info(
|
|
410
|
+
"Calculated volume by margin to %s %s: %s",
|
|
411
|
+
order_side,
|
|
412
|
+
symbol,
|
|
413
|
+
result,
|
|
414
|
+
)
|
|
415
|
+
return result
|
|
334
416
|
|
|
335
417
|
def calculate_spread_ratio(
|
|
336
418
|
self,
|
|
@@ -345,11 +427,13 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
345
427
|
Spread ratio as a float.
|
|
346
428
|
"""
|
|
347
429
|
symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
|
|
348
|
-
|
|
430
|
+
result = (
|
|
349
431
|
(symbol_info_tick["ask"] - symbol_info_tick["bid"])
|
|
350
432
|
/ (symbol_info_tick["ask"] + symbol_info_tick["bid"])
|
|
351
433
|
* 2
|
|
352
434
|
)
|
|
435
|
+
self.logger.info("Calculated spread ratio for %s: %s", symbol, result)
|
|
436
|
+
return result
|
|
353
437
|
|
|
354
438
|
def fetch_latest_rates_as_df(
|
|
355
439
|
self,
|
|
@@ -380,13 +464,20 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
380
464
|
)
|
|
381
465
|
raise Mt5TradingError(error_message) from e
|
|
382
466
|
else:
|
|
383
|
-
|
|
467
|
+
result = self.copy_rates_from_pos_as_df(
|
|
384
468
|
symbol=symbol,
|
|
385
469
|
timeframe=timeframe,
|
|
386
470
|
start_pos=0,
|
|
387
471
|
count=count,
|
|
388
472
|
index_keys=index_keys,
|
|
389
473
|
)
|
|
474
|
+
self.logger.info(
|
|
475
|
+
"Fetched latest %s rates for %s: %d rows",
|
|
476
|
+
granularity,
|
|
477
|
+
symbol,
|
|
478
|
+
result.shape[0],
|
|
479
|
+
)
|
|
480
|
+
return result
|
|
390
481
|
|
|
391
482
|
def fetch_latest_ticks_as_df(
|
|
392
483
|
self,
|
|
@@ -405,13 +496,19 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
405
496
|
pd.DataFrame: Tick data with time index.
|
|
406
497
|
"""
|
|
407
498
|
last_tick_time = self.symbol_info_tick_as_dict(symbol=symbol)["time"]
|
|
408
|
-
|
|
499
|
+
result = self.copy_ticks_range_as_df(
|
|
409
500
|
symbol=symbol,
|
|
410
501
|
date_from=(last_tick_time - timedelta(seconds=seconds)),
|
|
411
502
|
date_to=(last_tick_time + timedelta(seconds=seconds)),
|
|
412
503
|
flags=self.mt5.COPY_TICKS_ALL,
|
|
413
504
|
index_keys=index_keys,
|
|
414
505
|
)
|
|
506
|
+
self.logger.info(
|
|
507
|
+
"Fetched latest ticks for %s: %d rows",
|
|
508
|
+
symbol,
|
|
509
|
+
result.shape[0],
|
|
510
|
+
)
|
|
511
|
+
return result
|
|
415
512
|
|
|
416
513
|
def collect_entry_deals_as_df(
|
|
417
514
|
self,
|
|
@@ -437,14 +534,20 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
437
534
|
index_keys=index_keys,
|
|
438
535
|
)
|
|
439
536
|
if deals_df.empty:
|
|
440
|
-
|
|
537
|
+
result = deals_df
|
|
441
538
|
else:
|
|
442
|
-
|
|
539
|
+
result = deals_df.pipe(
|
|
443
540
|
lambda d: d[
|
|
444
541
|
d["entry"]
|
|
445
542
|
& d["type"].isin({self.mt5.DEAL_TYPE_BUY, self.mt5.DEAL_TYPE_SELL})
|
|
446
543
|
]
|
|
447
544
|
)
|
|
545
|
+
self.logger.info(
|
|
546
|
+
"Collected entry deals for %s: %d rows",
|
|
547
|
+
symbol,
|
|
548
|
+
result.shape[0],
|
|
549
|
+
)
|
|
550
|
+
return result
|
|
448
551
|
|
|
449
552
|
def fetch_positions_with_metrics_as_df(
|
|
450
553
|
self,
|
|
@@ -460,7 +563,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
460
563
|
"""
|
|
461
564
|
positions_df = self.positions_get_as_df(symbol=symbol)
|
|
462
565
|
if positions_df.empty:
|
|
463
|
-
|
|
566
|
+
result = positions_df
|
|
464
567
|
else:
|
|
465
568
|
symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
|
|
466
569
|
ask_margin = self.order_calc_margin(
|
|
@@ -475,7 +578,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
475
578
|
volume=1,
|
|
476
579
|
price=symbol_info_tick["bid"],
|
|
477
580
|
)
|
|
478
|
-
|
|
581
|
+
result = (
|
|
479
582
|
positions_df.assign(
|
|
480
583
|
elapsed_seconds=lambda d: (
|
|
481
584
|
symbol_info_tick["time"] - d["time"]
|
|
@@ -506,6 +609,12 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
506
609
|
)
|
|
507
610
|
.drop(columns=["buy_i", "sell_i", "sign", "underlier_increase_ratio"])
|
|
508
611
|
)
|
|
612
|
+
self.logger.info(
|
|
613
|
+
"Fetched positions with metrics for %s: %d rows",
|
|
614
|
+
symbol,
|
|
615
|
+
result.shape[0],
|
|
616
|
+
)
|
|
617
|
+
return result
|
|
509
618
|
|
|
510
619
|
def calculate_new_position_margin_ratio(
|
|
511
620
|
self,
|
|
@@ -525,7 +634,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
525
634
|
"""
|
|
526
635
|
account_info = self.account_info_as_dict()
|
|
527
636
|
if not account_info["equity"]:
|
|
528
|
-
|
|
637
|
+
result = 0.0
|
|
529
638
|
else:
|
|
530
639
|
positions_df = self.fetch_positions_with_metrics_as_df(symbol=symbol)
|
|
531
640
|
current_signed_margin = (
|
|
@@ -550,6 +659,12 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
550
659
|
)
|
|
551
660
|
else:
|
|
552
661
|
new_signed_margin = 0
|
|
553
|
-
|
|
662
|
+
result = abs(
|
|
554
663
|
(new_signed_margin + current_signed_margin) / account_info["equity"]
|
|
555
664
|
)
|
|
665
|
+
self.logger.info(
|
|
666
|
+
"Calculated new position margin ratio for %s: %s",
|
|
667
|
+
symbol,
|
|
668
|
+
result,
|
|
669
|
+
)
|
|
670
|
+
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pdmt5
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Pandas-based data handler for MetaTrader 5
|
|
5
5
|
Project-URL: Repository, https://github.com/dceoy/pdmt5.git
|
|
6
6
|
Author-email: dceoy <dceoy@users.noreply.github.com>
|
|
@@ -53,14 +53,13 @@ Pandas-based data handler for MetaTrader 5
|
|
|
53
53
|
|
|
54
54
|
## Installation
|
|
55
55
|
|
|
56
|
-
###
|
|
56
|
+
### Using pip
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
|
-
|
|
60
|
-
pip install -U --no-cache-dir ./pdmt5
|
|
59
|
+
pip install -U pdmt5 MetaTrader5
|
|
61
60
|
```
|
|
62
61
|
|
|
63
|
-
### Using uv
|
|
62
|
+
### Using uv
|
|
64
63
|
|
|
65
64
|
```bash
|
|
66
65
|
git clone https://github.com/dceoy/pdmt5.git
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
pdmt5/__init__.py,sha256=QbSFrsi7_bgFzb-ma4DmmUjR90UvrqKMnRZq1wPRmoI,446
|
|
2
2
|
pdmt5/dataframe.py,sha256=rUWtR23hrXBdBqzJhbOlIemNy73RrjSTZZJUhwoL6io,38084
|
|
3
3
|
pdmt5/mt5.py,sha256=KgxHapIrh5b4L0wIOAQIjfXNZafalihbFrh9fhYHmrI,32254
|
|
4
|
-
pdmt5/trading.py,sha256=
|
|
4
|
+
pdmt5/trading.py,sha256=TprWMtocw_eP5u4fVA6yflVk7Rd0-GL0kymM18YuiR4,25070
|
|
5
5
|
pdmt5/utils.py,sha256=Ll5Q3OE5h1A_sZ_qVEnOPGniFlT6_MmHfuu0zqeLdeU,3913
|
|
6
|
-
pdmt5-0.
|
|
7
|
-
pdmt5-0.
|
|
8
|
-
pdmt5-0.
|
|
9
|
-
pdmt5-0.
|
|
6
|
+
pdmt5-0.2.0.dist-info/METADATA,sha256=DmVhjOtTOivrig_YhvbVzqvunGHNx9lZAG1Y6XLzAGI,16094
|
|
7
|
+
pdmt5-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
pdmt5-0.2.0.dist-info/licenses/LICENSE,sha256=iABrdaUGOBWLYotFupB_PGe8arV5o7rVhn-_vK6P704,1073
|
|
9
|
+
pdmt5-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|