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 ((not dry_run) and retcode == self.mt5.TRADE_RETCODE_DONE) or (
142
- dry_run and retcode == 0
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 retcode in {
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", "Unknown error")
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
- return {"volume": symbol_info["volume_min"], "margin": margin}
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
- "No margin available for symbol: %s with order side: %s",
302
- symbol,
375
+ "Calculated minimum order margin to %s %s: %s",
303
376
  order_side,
377
+ symbol,
378
+ result,
304
379
  )
305
- return {"volume": symbol_info["volume_min"], "margin": 0.0}
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
- return (
403
+ result = (
329
404
  floor(margin / min_order_margin_dict["margin"])
330
405
  * min_order_margin_dict["volume"]
331
406
  )
332
407
  else:
333
- return 0.0
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
- return (
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
- return self.copy_rates_from_pos_as_df(
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
- return self.copy_ticks_range_as_df(
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
- return deals_df
537
+ result = deals_df
441
538
  else:
442
- return deals_df.pipe(
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
- return positions_df
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
- return (
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
- return 0.0
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
- return abs(
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.1.8
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
- ### From GitHub
56
+ ### Using pip
57
57
 
58
58
  ```bash
59
- git clone https://github.com/dceoy/pdmt5.git
60
- pip install -U --no-cache-dir ./pdmt5
59
+ pip install -U pdmt5 MetaTrader5
61
60
  ```
62
61
 
63
- ### Using uv (recommended for development)
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=GWznfYp2ndUwJqGBunxIJ9xwiQ-9oOwuUV8UaMcny1w,20655
4
+ pdmt5/trading.py,sha256=TprWMtocw_eP5u4fVA6yflVk7Rd0-GL0kymM18YuiR4,25070
5
5
  pdmt5/utils.py,sha256=Ll5Q3OE5h1A_sZ_qVEnOPGniFlT6_MmHfuu0zqeLdeU,3913
6
- pdmt5-0.1.8.dist-info/METADATA,sha256=6wf-gKgkEt16UKITUBimrAsicdwtbN8z6mB0_Q0HgwE,16176
7
- pdmt5-0.1.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
- pdmt5-0.1.8.dist-info/licenses/LICENSE,sha256=iABrdaUGOBWLYotFupB_PGe8arV5o7rVhn-_vK6P704,1073
9
- pdmt5-0.1.8.dist-info/RECORD,,
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