pdmt5 0.1.8__tar.gz → 0.2.0__tar.gz

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.
Files changed (33) hide show
  1. {pdmt5-0.1.8 → pdmt5-0.2.0}/.github/workflows/ci.yml +4 -13
  2. pdmt5-0.2.0/.github/workflows/pr-agent.yml +34 -0
  3. {pdmt5-0.1.8 → pdmt5-0.2.0}/PKG-INFO +4 -5
  4. {pdmt5-0.1.8 → pdmt5-0.2.0}/README.md +3 -4
  5. {pdmt5-0.1.8 → pdmt5-0.2.0}/pdmt5/trading.py +143 -28
  6. {pdmt5-0.1.8 → pdmt5-0.2.0}/pyproject.toml +1 -1
  7. {pdmt5-0.1.8 → pdmt5-0.2.0}/tests/test_trading.py +125 -9
  8. {pdmt5-0.1.8 → pdmt5-0.2.0}/uv.lock +2 -2
  9. {pdmt5-0.1.8 → pdmt5-0.2.0}/.claude/settings.json +0 -0
  10. {pdmt5-0.1.8 → pdmt5-0.2.0}/.github/FUNDING.yml +0 -0
  11. {pdmt5-0.1.8 → pdmt5-0.2.0}/.github/copilot-instructions.md +0 -0
  12. {pdmt5-0.1.8 → pdmt5-0.2.0}/.github/dependabot.yml +0 -0
  13. {pdmt5-0.1.8 → pdmt5-0.2.0}/.github/workflows/claude.yml +0 -0
  14. {pdmt5-0.1.8 → pdmt5-0.2.0}/.gitignore +0 -0
  15. {pdmt5-0.1.8 → pdmt5-0.2.0}/CLAUDE.md +0 -0
  16. {pdmt5-0.1.8 → pdmt5-0.2.0}/LICENSE +0 -0
  17. {pdmt5-0.1.8 → pdmt5-0.2.0}/docs/api/dataframe.md +0 -0
  18. {pdmt5-0.1.8 → pdmt5-0.2.0}/docs/api/index.md +0 -0
  19. {pdmt5-0.1.8 → pdmt5-0.2.0}/docs/api/mt5.md +0 -0
  20. {pdmt5-0.1.8 → pdmt5-0.2.0}/docs/api/trading.md +0 -0
  21. {pdmt5-0.1.8 → pdmt5-0.2.0}/docs/api/utils.md +0 -0
  22. {pdmt5-0.1.8 → pdmt5-0.2.0}/docs/index.md +0 -0
  23. {pdmt5-0.1.8 → pdmt5-0.2.0}/mkdocs.yml +0 -0
  24. {pdmt5-0.1.8 → pdmt5-0.2.0}/pdmt5/__init__.py +0 -0
  25. {pdmt5-0.1.8 → pdmt5-0.2.0}/pdmt5/dataframe.py +0 -0
  26. {pdmt5-0.1.8 → pdmt5-0.2.0}/pdmt5/mt5.py +0 -0
  27. {pdmt5-0.1.8 → pdmt5-0.2.0}/pdmt5/utils.py +0 -0
  28. {pdmt5-0.1.8 → pdmt5-0.2.0}/renovate.json +0 -0
  29. {pdmt5-0.1.8 → pdmt5-0.2.0}/tests/__init__.py +0 -0
  30. {pdmt5-0.1.8 → pdmt5-0.2.0}/tests/test_dataframe.py +0 -0
  31. {pdmt5-0.1.8 → pdmt5-0.2.0}/tests/test_init.py +0 -0
  32. {pdmt5-0.1.8 → pdmt5-0.2.0}/tests/test_mt5.py +0 -0
  33. {pdmt5-0.1.8 → pdmt5-0.2.0}/tests/test_utils.py +0 -0
@@ -30,25 +30,16 @@ jobs:
30
30
  uses: dceoy/gh-actions-for-devops/.github/workflows/python-package-lint-and-scan.yml@main
31
31
  with:
32
32
  package-path: .
33
- python-version: 3.x
34
33
  runs-on: windows-latest
35
34
  python-test:
36
35
  if: >
37
36
  github.event_name == 'push'
38
37
  || github.event_name == 'pull_request'
39
38
  || (github.event_name == 'workflow_dispatch' && inputs.workflow == 'lint-and-test')
40
- runs-on: windows-latest
41
- steps:
42
- - name: Checkout repository
43
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
44
- - name: Set up uv
45
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
46
- - name: Install the package
47
- run: >
48
- uv sync
49
- - name: Run unit tests with pytest
50
- run: >
51
- uv run pytest
39
+ uses: dceoy/gh-actions-for-devops/.github/workflows/python-package-test.yml@main
40
+ with:
41
+ package-path: .
42
+ runs-on: windows-latest
52
43
  dependabot-auto-merge:
53
44
  if: >
54
45
  github.event_name == 'pull_request' && github.actor == 'dependabot[bot]'
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: PR-agent
3
+ on:
4
+ pull_request:
5
+ types:
6
+ - opened
7
+ - reopened
8
+ - ready_for_review
9
+ issue_comment:
10
+ types:
11
+ - created
12
+ - edited
13
+ - deleted
14
+ jobs:
15
+ pr-agent:
16
+ if: >
17
+ github.event.sender.type != 'Bot'
18
+ && (
19
+ github.event_name == 'pull_request'
20
+ || github.event_name == 'issue_comment'
21
+ )
22
+ uses: dceoy/gh-actions-for-devops/.github/workflows/pr-agent.yml@main
23
+ permissions:
24
+ contents: write
25
+ pull-requests: write
26
+ issues: write
27
+ id-token: write
28
+ with:
29
+ auto-describe: true
30
+ auto-review: true
31
+ auto-improve: true
32
+ secrets:
33
+ GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
34
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -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
@@ -30,14 +30,13 @@ Pandas-based data handler for MetaTrader 5
30
30
 
31
31
  ## Installation
32
32
 
33
- ### From GitHub
33
+ ### Using pip
34
34
 
35
35
  ```bash
36
- git clone https://github.com/dceoy/pdmt5.git
37
- pip install -U --no-cache-dir ./pdmt5
36
+ pip install -U pdmt5 MetaTrader5
38
37
  ```
39
38
 
40
- ### Using uv (recommended for development)
39
+ ### Using uv
41
40
 
42
41
  ```bash
43
42
  git clone https://github.com/dceoy/pdmt5.git
@@ -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
  [project]
2
2
  name = "pdmt5"
3
- version = "0.1.8"
3
+ version = "0.2.0"
4
4
  description = "Pandas-based data handler for MetaTrader 5"
5
5
  authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
6
6
  maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
@@ -20,7 +20,7 @@ Mt5TradingClient.model_rebuild()
20
20
 
21
21
 
22
22
  @pytest.fixture(autouse=True)
23
- def mock_mt5_import(
23
+ def mock_mt5_import( # noqa: PLR0915
24
24
  request: pytest.FixtureRequest,
25
25
  mocker: MockerFixture,
26
26
  ) -> Generator[ModuleType | None, None, None]:
@@ -69,10 +69,50 @@ def mock_mt5_import(
69
69
  mock_mt5.ORDER_FILLING_FOK = 2
70
70
  mock_mt5.ORDER_FILLING_RETURN = 3
71
71
  mock_mt5.ORDER_TIME_GTC = 0
72
+
73
+ # Trade return codes
74
+ mock_mt5.TRADE_RETCODE_REQUOTE = 10004
75
+ mock_mt5.TRADE_RETCODE_REJECT = 10006
76
+ mock_mt5.TRADE_RETCODE_CANCEL = 10007
77
+ mock_mt5.TRADE_RETCODE_PLACED = 10008
72
78
  mock_mt5.TRADE_RETCODE_DONE = 10009
79
+ mock_mt5.TRADE_RETCODE_DONE_PARTIAL = 10010
80
+ mock_mt5.TRADE_RETCODE_ERROR = 10011
81
+ mock_mt5.TRADE_RETCODE_TIMEOUT = 10012
82
+ mock_mt5.TRADE_RETCODE_INVALID = 10013
83
+ mock_mt5.TRADE_RETCODE_INVALID_VOLUME = 10014
84
+ mock_mt5.TRADE_RETCODE_INVALID_PRICE = 10015
85
+ mock_mt5.TRADE_RETCODE_INVALID_STOPS = 10016
73
86
  mock_mt5.TRADE_RETCODE_TRADE_DISABLED = 10017
74
87
  mock_mt5.TRADE_RETCODE_MARKET_CLOSED = 10018
88
+ mock_mt5.TRADE_RETCODE_NO_MONEY = 10019
89
+ mock_mt5.TRADE_RETCODE_PRICE_CHANGED = 10020
90
+ mock_mt5.TRADE_RETCODE_PRICE_OFF = 10021
91
+ mock_mt5.TRADE_RETCODE_INVALID_EXPIRATION = 10022
92
+ mock_mt5.TRADE_RETCODE_ORDER_CHANGED = 10023
93
+ mock_mt5.TRADE_RETCODE_TOO_MANY_REQUESTS = 10024
75
94
  mock_mt5.TRADE_RETCODE_NO_CHANGES = 10025
95
+ mock_mt5.TRADE_RETCODE_SERVER_DISABLES_AT = 10026
96
+ mock_mt5.TRADE_RETCODE_CLIENT_DISABLES_AT = 10027
97
+ mock_mt5.TRADE_RETCODE_LOCKED = 10028
98
+ mock_mt5.TRADE_RETCODE_FROZEN = 10029
99
+ mock_mt5.TRADE_RETCODE_INVALID_FILL = 10030
100
+ mock_mt5.TRADE_RETCODE_CONNECTION = 10031
101
+ mock_mt5.TRADE_RETCODE_ONLY_REAL = 10032
102
+ mock_mt5.TRADE_RETCODE_LIMIT_ORDERS = 10033
103
+ mock_mt5.TRADE_RETCODE_LIMIT_VOLUME = 10034
104
+ mock_mt5.TRADE_RETCODE_INVALID_ORDER = 10035
105
+ mock_mt5.TRADE_RETCODE_POSITION_CLOSED = 10036
106
+ mock_mt5.TRADE_RETCODE_INVALID_CLOSE_VOLUME = 10038
107
+ mock_mt5.TRADE_RETCODE_CLOSE_ORDER_EXIST = 10039
108
+ mock_mt5.TRADE_RETCODE_LIMIT_POSITIONS = 10040
109
+ mock_mt5.TRADE_RETCODE_REJECT_CANCEL = 10041
110
+ mock_mt5.TRADE_RETCODE_LONG_ONLY = 10042
111
+ mock_mt5.TRADE_RETCODE_SHORT_ONLY = 10043
112
+ mock_mt5.TRADE_RETCODE_CLOSE_ONLY = 10044
113
+ mock_mt5.TRADE_RETCODE_FIFO_CLOSE = 10045
114
+ mock_mt5.TRADE_RETCODE_HEDGE_PROHIBITED = 10046
115
+
76
116
  mock_mt5.RES_S_OK = 1
77
117
  mock_mt5.DEAL_TYPE_BUY = 0
78
118
  mock_mt5.DEAL_TYPE_SELL = 1
@@ -612,15 +652,15 @@ class TestMt5TradingClient:
612
652
  "type": 1,
613
653
  }
614
654
 
615
- # Mock failure response
616
- mock_mt5_import.order_send.return_value.retcode = 10004
655
+ # Mock failure response with error retcode
656
+ mock_mt5_import.order_send.return_value.retcode = 10006
617
657
  mock_mt5_import.order_send.return_value._asdict.return_value = {
618
- "retcode": 10004,
658
+ "retcode": 10006,
619
659
  "comment": "Invalid request",
620
660
  }
621
661
 
622
662
  with pytest.raises(Mt5TradingError, match=r"order_send\(\) failed and aborted"):
623
- client._send_or_check_order(request)
663
+ client._send_or_check_order(request, raise_on_error=True)
624
664
 
625
665
  def test_send_or_check_order_dry_run_failure(
626
666
  self,
@@ -638,17 +678,17 @@ class TestMt5TradingClient:
638
678
  "type": 1,
639
679
  }
640
680
 
641
- # Mock failure response
642
- mock_mt5_import.order_check.return_value.retcode = 10004
681
+ # Mock failure response with non-zero retcode for dry run
682
+ mock_mt5_import.order_check.return_value.retcode = 10013
643
683
  mock_mt5_import.order_check.return_value._asdict.return_value = {
644
- "retcode": 10004,
684
+ "retcode": 10013,
645
685
  "comment": "Invalid request",
646
686
  }
647
687
 
648
688
  with pytest.raises(
649
689
  Mt5TradingError, match=r"order_check\(\) failed and aborted"
650
690
  ):
651
- client._send_or_check_order(request, dry_run=True)
691
+ client._send_or_check_order(request, raise_on_error=True, dry_run=True)
652
692
 
653
693
  def test_send_or_check_order_dry_run_override(
654
694
  self,
@@ -1896,3 +1936,79 @@ class TestMt5TradingClient:
1896
1936
  assert isinstance(result, list)
1897
1937
  assert len(result) == 2
1898
1938
  assert all(r["retcode"] == 10009 for r in result)
1939
+
1940
+ def test_mt5_successful_trade_retcodes_property(
1941
+ self, mock_mt5_import: ModuleType
1942
+ ) -> None:
1943
+ """Test mt5_successful_trade_retcodes property returns correct set of codes."""
1944
+ client = Mt5TradingClient(mt5=mock_mt5_import)
1945
+
1946
+ # Get the property value
1947
+ retcodes = client.mt5_successful_trade_retcodes
1948
+
1949
+ # Verify it's a set
1950
+ assert isinstance(retcodes, set)
1951
+
1952
+ # Verify the expected codes are present
1953
+ assert retcodes == {
1954
+ mock_mt5_import.TRADE_RETCODE_PLACED, # 10008
1955
+ mock_mt5_import.TRADE_RETCODE_DONE, # 10009
1956
+ mock_mt5_import.TRADE_RETCODE_DONE_PARTIAL, # 10010
1957
+ }
1958
+
1959
+ def test_mt5_failed_trade_retcodes_property(
1960
+ self, mock_mt5_import: ModuleType
1961
+ ) -> None:
1962
+ """Test mt5_failed_trade_retcodes property returns correct set of codes."""
1963
+ client = Mt5TradingClient(mt5=mock_mt5_import)
1964
+
1965
+ # Get the property value
1966
+ retcodes = client.mt5_failed_trade_retcodes
1967
+
1968
+ # Verify it's a set
1969
+ assert isinstance(retcodes, set)
1970
+
1971
+ # Verify it contains the expected codes
1972
+ expected_codes = {
1973
+ mock_mt5_import.TRADE_RETCODE_REQUOTE, # 10004
1974
+ mock_mt5_import.TRADE_RETCODE_REJECT, # 10006
1975
+ mock_mt5_import.TRADE_RETCODE_CANCEL, # 10007
1976
+ mock_mt5_import.TRADE_RETCODE_ERROR, # 10011
1977
+ mock_mt5_import.TRADE_RETCODE_TIMEOUT, # 10012
1978
+ mock_mt5_import.TRADE_RETCODE_INVALID, # 10013
1979
+ mock_mt5_import.TRADE_RETCODE_INVALID_VOLUME, # 10014
1980
+ mock_mt5_import.TRADE_RETCODE_INVALID_PRICE, # 10015
1981
+ mock_mt5_import.TRADE_RETCODE_INVALID_STOPS, # 10016
1982
+ mock_mt5_import.TRADE_RETCODE_TRADE_DISABLED, # 10017
1983
+ mock_mt5_import.TRADE_RETCODE_MARKET_CLOSED, # 10018
1984
+ mock_mt5_import.TRADE_RETCODE_NO_MONEY, # 10019
1985
+ mock_mt5_import.TRADE_RETCODE_PRICE_CHANGED, # 10020
1986
+ mock_mt5_import.TRADE_RETCODE_PRICE_OFF, # 10021
1987
+ mock_mt5_import.TRADE_RETCODE_INVALID_EXPIRATION, # 10022
1988
+ mock_mt5_import.TRADE_RETCODE_ORDER_CHANGED, # 10023
1989
+ mock_mt5_import.TRADE_RETCODE_TOO_MANY_REQUESTS, # 10024
1990
+ mock_mt5_import.TRADE_RETCODE_NO_CHANGES, # 10025
1991
+ mock_mt5_import.TRADE_RETCODE_SERVER_DISABLES_AT, # 10026
1992
+ mock_mt5_import.TRADE_RETCODE_CLIENT_DISABLES_AT, # 10027
1993
+ mock_mt5_import.TRADE_RETCODE_LOCKED, # 10028
1994
+ mock_mt5_import.TRADE_RETCODE_FROZEN, # 10029
1995
+ mock_mt5_import.TRADE_RETCODE_INVALID_FILL, # 10030
1996
+ mock_mt5_import.TRADE_RETCODE_CONNECTION, # 10031
1997
+ mock_mt5_import.TRADE_RETCODE_ONLY_REAL, # 10032
1998
+ mock_mt5_import.TRADE_RETCODE_LIMIT_ORDERS, # 10033
1999
+ mock_mt5_import.TRADE_RETCODE_LIMIT_VOLUME, # 10034
2000
+ mock_mt5_import.TRADE_RETCODE_INVALID_ORDER, # 10035
2001
+ mock_mt5_import.TRADE_RETCODE_POSITION_CLOSED, # 10036
2002
+ mock_mt5_import.TRADE_RETCODE_INVALID_CLOSE_VOLUME, # 10038
2003
+ mock_mt5_import.TRADE_RETCODE_CLOSE_ORDER_EXIST, # 10039
2004
+ mock_mt5_import.TRADE_RETCODE_LIMIT_POSITIONS, # 10040
2005
+ mock_mt5_import.TRADE_RETCODE_REJECT_CANCEL, # 10041
2006
+ mock_mt5_import.TRADE_RETCODE_LONG_ONLY, # 10042
2007
+ mock_mt5_import.TRADE_RETCODE_SHORT_ONLY, # 10043
2008
+ mock_mt5_import.TRADE_RETCODE_CLOSE_ONLY, # 10044
2009
+ mock_mt5_import.TRADE_RETCODE_FIFO_CLOSE, # 10045
2010
+ mock_mt5_import.TRADE_RETCODE_HEDGE_PROHIBITED, # 10046
2011
+ }
2012
+
2013
+ # Verify all expected codes are present
2014
+ assert retcodes == expected_codes
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 2
2
+ revision = 3
3
3
  requires-python = ">=3.11"
4
4
  resolution-markers = [
5
5
  "python_full_version >= '3.12'",
@@ -613,7 +613,7 @@ wheels = [
613
613
 
614
614
  [[package]]
615
615
  name = "pdmt5"
616
- version = "0.1.8"
616
+ version = "0.2.0"
617
617
  source = { editable = "." }
618
618
  dependencies = [
619
619
  { name = "metatrader5", marker = "sys_platform == 'win32'" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes