pdmt5 0.0.7__py3-none-any.whl → 0.0.9__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/__init__.py CHANGED
@@ -4,7 +4,7 @@ from importlib.metadata import version
4
4
 
5
5
  from .dataframe import Mt5Config, Mt5DataClient
6
6
  from .mt5 import Mt5Client, Mt5RuntimeError
7
- from .trading import Mt5TradingClient
7
+ from .trading import Mt5TradingClient, Mt5TradingError
8
8
 
9
9
  __version__ = version(__package__) if __package__ else None
10
10
 
@@ -14,4 +14,5 @@ __all__ = [
14
14
  "Mt5DataClient",
15
15
  "Mt5RuntimeError",
16
16
  "Mt5TradingClient",
17
+ "Mt5TradingError",
17
18
  ]
pdmt5/dataframe.py CHANGED
@@ -86,16 +86,16 @@ class Mt5DataClient(Mt5Client):
86
86
  for i in range(1 + max(0, self.retry_count)):
87
87
  if i:
88
88
  self.logger.warning(
89
- "Retrying MetaTrader5 initialization (%d/%d)...",
89
+ "Retrying MT5 initialization (%d/%d)...",
90
90
  i,
91
91
  self.retry_count,
92
92
  )
93
93
  time.sleep(i)
94
94
  if self.initialize(**initialize_kwargs): # type: ignore[reportArgumentType]
95
- self.logger.info("MetaTrader5 initialization successful.")
95
+ self.logger.info("MT5 initialization successful.")
96
96
  return
97
97
  error_message = (
98
- f"MetaTrader5 initialization failed after {self.retry_count} retries:"
98
+ f"MT5 initialization failed after {self.retry_count} retries:"
99
99
  f" {self.last_error()}"
100
100
  )
101
101
  raise Mt5RuntimeError(error_message)
pdmt5/mt5.py CHANGED
@@ -6,12 +6,14 @@ from __future__ import annotations
6
6
 
7
7
  import importlib
8
8
  import logging
9
+ from functools import wraps
9
10
  from types import ModuleType # noqa: TC003
10
11
  from typing import TYPE_CHECKING, Any, Self
11
12
 
12
13
  from pydantic import BaseModel, ConfigDict, Field
13
14
 
14
15
  if TYPE_CHECKING:
16
+ from collections.abc import Callable
15
17
  from datetime import datetime
16
18
  from types import TracebackType
17
19
 
@@ -43,6 +45,41 @@ class Mt5Client(BaseModel):
43
45
  )
44
46
  _is_initialized: bool = False
45
47
 
48
+ @staticmethod
49
+ def _log_mt5_last_status_code(func: Callable[..., Any]) -> Callable[..., Any]:
50
+ """Decorator to log MetaTrader5 last status code after method execution.
51
+
52
+ Args:
53
+ func: The method to decorate.
54
+
55
+ Returns:
56
+ The decorated method.
57
+ """
58
+
59
+ @wraps(func)
60
+ def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
61
+ try:
62
+ response = func(self, *args, **kwargs)
63
+ except Exception as e:
64
+ error_message = f"MT5 {func.__name__} failed with error: {e}"
65
+ raise Mt5RuntimeError(error_message) from e
66
+ else:
67
+ self.logger.info(
68
+ "MT5 %s returned a response: %s",
69
+ func.__name__,
70
+ response,
71
+ )
72
+ return response
73
+ finally:
74
+ last_error_response = self.mt5.last_error()
75
+ message = f"MT5 last status: {last_error_response}"
76
+ if last_error_response[0] != self.mt5.RES_S_OK:
77
+ self.logger.warning(message)
78
+ else:
79
+ self.logger.info(message)
80
+
81
+ return wrapper
82
+
46
83
  def __enter__(self) -> Self:
47
84
  """Context manager entry.
48
85
 
@@ -61,6 +98,7 @@ class Mt5Client(BaseModel):
61
98
  """Context manager exit."""
62
99
  self.shutdown()
63
100
 
101
+ @_log_mt5_last_status_code
64
102
  def initialize(
65
103
  self,
66
104
  path: str | None = None,
@@ -83,11 +121,9 @@ class Mt5Client(BaseModel):
83
121
  Returns:
84
122
  True if successful, False otherwise.
85
123
  """
86
- if self._is_initialized:
87
- self.logger.warning("Skipping initialization, already initialized.")
88
- elif path is not None:
124
+ if path is not None:
89
125
  self.logger.info(
90
- "Initializing MetaTrader5 connection with path: %s",
126
+ "Initializing MT5 connection with path: %s",
91
127
  path,
92
128
  )
93
129
  self._is_initialized = self.mt5.initialize(
@@ -105,10 +141,11 @@ class Mt5Client(BaseModel):
105
141
  },
106
142
  )
107
143
  else:
108
- self.logger.info("Initializing MetaTrader5 connection.")
144
+ self.logger.info("Initializing MT5 connection.")
109
145
  self._is_initialized = self.mt5.initialize()
110
146
  return self._is_initialized
111
147
 
148
+ @_log_mt5_last_status_code
112
149
  def login(
113
150
  self,
114
151
  login: int,
@@ -127,8 +164,8 @@ class Mt5Client(BaseModel):
127
164
  Returns:
128
165
  True if successful, False otherwise.
129
166
  """
130
- self._ensure_initialized()
131
- self.logger.info("Logging in to MetaTrader5 account: %d", login)
167
+ self._initialize_if_needed()
168
+ self.logger.info("Logging in to MT5 account: %d", login)
132
169
  return self.mt5.login(
133
170
  login,
134
171
  **{
@@ -142,73 +179,78 @@ class Mt5Client(BaseModel):
142
179
  },
143
180
  )
144
181
 
182
+ @_log_mt5_last_status_code
145
183
  def shutdown(self) -> None:
146
184
  """Close the previously established connection to the MetaTrader 5 terminal."""
147
- if self._is_initialized:
148
- self.logger.info("Shutting down MetaTrader5 connection.")
149
- self.mt5.shutdown()
150
- self._is_initialized = False
151
- else:
152
- self.logger.warning(
153
- "MetaTrader5 connection is not initialized, nothing to shut down"
154
- )
185
+ self.logger.info("Shutting down MT5 connection.")
186
+ response = self.mt5.shutdown()
187
+ self._is_initialized = False
188
+ return response
155
189
 
190
+ @_log_mt5_last_status_code
156
191
  def version(self) -> tuple[int, int, str]:
157
192
  """Return the MetaTrader 5 terminal version.
158
193
 
159
194
  Returns:
160
195
  Tuple of (terminal_version, build, release_date).
161
196
  """
162
- self._ensure_initialized()
163
- self.logger.info("Retrieving MetaTrader5 version information.")
197
+ self._initialize_if_needed()
198
+ self.logger.info("Retrieving MT5 version information.")
164
199
  return self.mt5.version()
165
200
 
201
+ @_log_mt5_last_status_code
166
202
  def last_error(self) -> tuple[int, str]:
167
203
  """Return data on the last error.
168
204
 
169
205
  Returns:
170
206
  Tuple of (error_code, error_description).
171
207
  """
172
- self.logger.info("Retrieving last MetaTrader5 error")
208
+ self.logger.info("Retrieving last MT5 error")
173
209
  return self.mt5.last_error()
174
210
 
211
+ @_log_mt5_last_status_code
175
212
  def account_info(self) -> Any:
176
213
  """Get info on the current trading account.
177
214
 
178
215
  Returns:
179
216
  AccountInfo structure or None.
180
217
  """
181
- self._ensure_initialized()
218
+ self._initialize_if_needed()
182
219
  self.logger.info("Retrieving account information.")
183
220
  response = self.mt5.account_info()
184
- self._validate_metatrader5_response(response=response, operation="account_info")
221
+ self._validate_mt5_response_is_not_none(
222
+ response=response, operation="account_info"
223
+ )
185
224
  return response
186
225
 
226
+ @_log_mt5_last_status_code
187
227
  def terminal_info(self) -> Any:
188
228
  """Get the connected MetaTrader 5 client terminal status and settings.
189
229
 
190
230
  Returns:
191
231
  TerminalInfo structure or None.
192
232
  """
193
- self._ensure_initialized()
233
+ self._initialize_if_needed()
194
234
  self.logger.info("Retrieving terminal information.")
195
235
  response = self.mt5.terminal_info()
196
- self._validate_metatrader5_response(
236
+ self._validate_mt5_response_is_not_none(
197
237
  response=response,
198
238
  operation="terminal_info",
199
239
  )
200
240
  return response
201
241
 
242
+ @_log_mt5_last_status_code
202
243
  def symbols_total(self) -> int:
203
244
  """Get the number of all financial instruments in the terminal.
204
245
 
205
246
  Returns:
206
247
  Total number of symbols.
207
248
  """
208
- self._ensure_initialized()
249
+ self._initialize_if_needed()
209
250
  self.logger.info("Retrieving total number of symbols.")
210
251
  return self.mt5.symbols_total()
211
252
 
253
+ @_log_mt5_last_status_code
212
254
  def symbols_get(self, group: str | None = None) -> tuple[Any, ...]:
213
255
  """Get all financial instruments from the terminal.
214
256
 
@@ -218,7 +260,7 @@ class Mt5Client(BaseModel):
218
260
  Returns:
219
261
  Tuple of symbol info structures or None.
220
262
  """
221
- self._ensure_initialized()
263
+ self._initialize_if_needed()
222
264
  if group is not None:
223
265
  self.logger.info("Retrieving symbols for group: %s", group)
224
266
  response = self.mt5.symbols_get(group=group)
@@ -227,13 +269,14 @@ class Mt5Client(BaseModel):
227
269
  self.logger.info("Retrieving all symbols.")
228
270
  response = self.mt5.symbols_get()
229
271
  context = None
230
- self._validate_metatrader5_response(
272
+ self._validate_mt5_response_is_not_none(
231
273
  response=response,
232
274
  operation="symbols_get",
233
275
  context=context,
234
276
  )
235
277
  return response
236
278
 
279
+ @_log_mt5_last_status_code
237
280
  def symbol_info(self, symbol: str) -> Any:
238
281
  """Get data on the specified financial instrument.
239
282
 
@@ -243,16 +286,17 @@ class Mt5Client(BaseModel):
243
286
  Returns:
244
287
  Symbol info structure or None.
245
288
  """
246
- self._ensure_initialized()
289
+ self._initialize_if_needed()
247
290
  self.logger.info("Retrieving information for symbol: %s", symbol)
248
291
  response = self.mt5.symbol_info(symbol)
249
- self._validate_metatrader5_response(
292
+ self._validate_mt5_response_is_not_none(
250
293
  response=response,
251
294
  operation="symbol_info",
252
295
  context=f"symbol={symbol}",
253
296
  )
254
297
  return response
255
298
 
299
+ @_log_mt5_last_status_code
256
300
  def symbol_info_tick(self, symbol: str) -> Any:
257
301
  """Get the last tick for the specified financial instrument.
258
302
 
@@ -262,16 +306,17 @@ class Mt5Client(BaseModel):
262
306
  Returns:
263
307
  Tick info structure or None.
264
308
  """
265
- self._ensure_initialized()
309
+ self._initialize_if_needed()
266
310
  self.logger.info("Retrieving last tick for symbol: %s", symbol)
267
311
  response = self.mt5.symbol_info_tick(symbol)
268
- self._validate_metatrader5_response(
312
+ self._validate_mt5_response_is_not_none(
269
313
  response=response,
270
314
  operation="symbol_info_tick",
271
315
  context=f"symbol={symbol}",
272
316
  )
273
317
  return response
274
318
 
319
+ @_log_mt5_last_status_code
275
320
  def symbol_select(self, symbol: str, enable: bool = True) -> bool:
276
321
  """Select a symbol in the MarketWatch window or remove a symbol from the window.
277
322
 
@@ -282,16 +327,17 @@ class Mt5Client(BaseModel):
282
327
  Returns:
283
328
  True if successful, False otherwise.
284
329
  """
285
- self._ensure_initialized()
330
+ self._initialize_if_needed()
286
331
  self.logger.info("Selecting symbol: %s, enable=%s", symbol, enable)
287
332
  response = self.mt5.symbol_select(symbol, enable)
288
- self._validate_metatrader5_response(
333
+ self._validate_mt5_response_is_not_none(
289
334
  response=response,
290
335
  operation="symbol_select",
291
336
  context=f"symbol={symbol}, enable={enable}",
292
337
  )
293
338
  return response
294
339
 
340
+ @_log_mt5_last_status_code
295
341
  def market_book_add(self, symbol: str) -> bool:
296
342
  """Subscribe the terminal to the Market Depth change events for a specified symbol.
297
343
 
@@ -301,16 +347,17 @@ class Mt5Client(BaseModel):
301
347
  Returns:
302
348
  True if successful, False otherwise.
303
349
  """ # noqa: E501
304
- self._ensure_initialized()
350
+ self._initialize_if_needed()
305
351
  self.logger.info("Adding market book for symbol: %s", symbol)
306
352
  response = self.mt5.market_book_add(symbol)
307
- self._validate_metatrader5_response(
353
+ self._validate_mt5_response_is_not_none(
308
354
  response=response,
309
355
  operation="market_book_add",
310
356
  context=f"symbol={symbol}",
311
357
  )
312
358
  return response
313
359
 
360
+ @_log_mt5_last_status_code
314
361
  def market_book_get(self, symbol: str) -> tuple[Any, ...]:
315
362
  """Return a tuple from BookInfo featuring Market Depth entries for the specified symbol.
316
363
 
@@ -320,16 +367,17 @@ class Mt5Client(BaseModel):
320
367
  Returns:
321
368
  Tuple of BookInfo structures or None.
322
369
  """ # noqa: E501
323
- self._ensure_initialized()
370
+ self._initialize_if_needed()
324
371
  self.logger.info("Retrieving market book for symbol: %s", symbol)
325
372
  response = self.mt5.market_book_get(symbol)
326
- self._validate_metatrader5_response(
373
+ self._validate_mt5_response_is_not_none(
327
374
  response=response,
328
375
  operation="market_book_get",
329
376
  context=f"symbol={symbol}",
330
377
  )
331
378
  return response
332
379
 
380
+ @_log_mt5_last_status_code
333
381
  def market_book_release(self, symbol: str) -> bool:
334
382
  """Cancels subscription of the terminal to the Market Depth change events for a specified symbol.
335
383
 
@@ -339,16 +387,17 @@ class Mt5Client(BaseModel):
339
387
  Returns:
340
388
  True if successful, False otherwise.
341
389
  """ # noqa: E501
342
- self._ensure_initialized()
390
+ self._initialize_if_needed()
343
391
  self.logger.info("Releasing market book for symbol: %s", symbol)
344
392
  response = self.mt5.market_book_release(symbol)
345
- self._validate_metatrader5_response(
393
+ self._validate_mt5_response_is_not_none(
346
394
  response=response,
347
395
  operation="market_book_release",
348
396
  context=f"symbol={symbol}",
349
397
  )
350
398
  return response
351
399
 
400
+ @_log_mt5_last_status_code
352
401
  def copy_rates_from(
353
402
  self,
354
403
  symbol: str,
@@ -367,7 +416,7 @@ class Mt5Client(BaseModel):
367
416
  Returns:
368
417
  Array of rates or None.
369
418
  """
370
- self._ensure_initialized()
419
+ self._initialize_if_needed()
371
420
  self.logger.info(
372
421
  "Copying rates from symbol: %s, timeframe: %d, date_from: %s, count: %d",
373
422
  symbol,
@@ -376,7 +425,7 @@ class Mt5Client(BaseModel):
376
425
  count,
377
426
  )
378
427
  response = self.mt5.copy_rates_from(symbol, timeframe, date_from, count)
379
- self._validate_metatrader5_response(
428
+ self._validate_mt5_response_is_not_none(
380
429
  response=response,
381
430
  operation="copy_rates_from",
382
431
  context=(
@@ -386,6 +435,7 @@ class Mt5Client(BaseModel):
386
435
  )
387
436
  return response
388
437
 
438
+ @_log_mt5_last_status_code
389
439
  def copy_rates_from_pos(
390
440
  self,
391
441
  symbol: str,
@@ -404,7 +454,7 @@ class Mt5Client(BaseModel):
404
454
  Returns:
405
455
  Array of rates or None.
406
456
  """
407
- self._ensure_initialized()
457
+ self._initialize_if_needed()
408
458
  self.logger.info(
409
459
  (
410
460
  "Copying rates from position:"
@@ -416,7 +466,7 @@ class Mt5Client(BaseModel):
416
466
  count,
417
467
  )
418
468
  response = self.mt5.copy_rates_from_pos(symbol, timeframe, start_pos, count)
419
- self._validate_metatrader5_response(
469
+ self._validate_mt5_response_is_not_none(
420
470
  response=response,
421
471
  operation="copy_rates_from_pos",
422
472
  context=(
@@ -426,6 +476,7 @@ class Mt5Client(BaseModel):
426
476
  )
427
477
  return response
428
478
 
479
+ @_log_mt5_last_status_code
429
480
  def copy_rates_range(
430
481
  self,
431
482
  symbol: str,
@@ -444,7 +495,7 @@ class Mt5Client(BaseModel):
444
495
  Returns:
445
496
  Array of rates or None.
446
497
  """
447
- self._ensure_initialized()
498
+ self._initialize_if_needed()
448
499
  self.logger.info(
449
500
  "Copying rates range: symbol=%s, timeframe=%d, date_from=%s, date_to=%s",
450
501
  symbol,
@@ -453,7 +504,7 @@ class Mt5Client(BaseModel):
453
504
  date_to,
454
505
  )
455
506
  response = self.mt5.copy_rates_range(symbol, timeframe, date_from, date_to)
456
- self._validate_metatrader5_response(
507
+ self._validate_mt5_response_is_not_none(
457
508
  response=response,
458
509
  operation="copy_rates_range",
459
510
  context=(
@@ -463,6 +514,7 @@ class Mt5Client(BaseModel):
463
514
  )
464
515
  return response
465
516
 
517
+ @_log_mt5_last_status_code
466
518
  def copy_ticks_from(
467
519
  self,
468
520
  symbol: str,
@@ -481,7 +533,7 @@ class Mt5Client(BaseModel):
481
533
  Returns:
482
534
  Array of ticks or None.
483
535
  """
484
- self._ensure_initialized()
536
+ self._initialize_if_needed()
485
537
  self.logger.info(
486
538
  "Copying ticks from symbol: %s, date_from: %s, count: %d, flags: %d",
487
539
  symbol,
@@ -490,7 +542,7 @@ class Mt5Client(BaseModel):
490
542
  flags,
491
543
  )
492
544
  response = self.mt5.copy_ticks_from(symbol, date_from, count, flags)
493
- self._validate_metatrader5_response(
545
+ self._validate_mt5_response_is_not_none(
494
546
  response=response,
495
547
  operation="copy_ticks_from",
496
548
  context=(
@@ -499,6 +551,7 @@ class Mt5Client(BaseModel):
499
551
  )
500
552
  return response
501
553
 
554
+ @_log_mt5_last_status_code
502
555
  def copy_ticks_range(
503
556
  self,
504
557
  symbol: str,
@@ -517,7 +570,7 @@ class Mt5Client(BaseModel):
517
570
  Returns:
518
571
  Array of ticks or None.
519
572
  """
520
- self._ensure_initialized()
573
+ self._initialize_if_needed()
521
574
  self.logger.info(
522
575
  "Copying ticks range: symbol=%s, date_from=%s, date_to=%s, flags=%d",
523
576
  symbol,
@@ -526,7 +579,7 @@ class Mt5Client(BaseModel):
526
579
  flags,
527
580
  )
528
581
  response = self.mt5.copy_ticks_range(symbol, date_from, date_to, flags)
529
- self._validate_metatrader5_response(
582
+ self._validate_mt5_response_is_not_none(
530
583
  response=response,
531
584
  operation="copy_ticks_range",
532
585
  context=(
@@ -536,16 +589,18 @@ class Mt5Client(BaseModel):
536
589
  )
537
590
  return response
538
591
 
592
+ @_log_mt5_last_status_code
539
593
  def orders_total(self) -> int:
540
594
  """Get the number of active orders.
541
595
 
542
596
  Returns:
543
597
  Number of active orders.
544
598
  """
545
- self._ensure_initialized()
599
+ self._initialize_if_needed()
546
600
  self.logger.info("Retrieving total number of active orders.")
547
601
  return self.mt5.orders_total()
548
602
 
603
+ @_log_mt5_last_status_code
549
604
  def orders_get(
550
605
  self,
551
606
  symbol: str | None = None,
@@ -562,7 +617,7 @@ class Mt5Client(BaseModel):
562
617
  Returns:
563
618
  Tuple of order info structures or None.
564
619
  """
565
- self._ensure_initialized()
620
+ self._initialize_if_needed()
566
621
  if ticket is not None:
567
622
  self.logger.info("Retrieving order with ticket: %d", ticket)
568
623
  response = self.mt5.orders_get(ticket=ticket)
@@ -579,13 +634,14 @@ class Mt5Client(BaseModel):
579
634
  self.logger.info("Retrieving all active orders.")
580
635
  response = self.mt5.orders_get()
581
636
  context = None
582
- self._validate_metatrader5_response(
637
+ self._validate_mt5_response_is_not_none(
583
638
  response=response,
584
639
  operation="orders_get",
585
640
  context=context,
586
641
  )
587
642
  return response
588
643
 
644
+ @_log_mt5_last_status_code
589
645
  def order_calc_margin(
590
646
  self,
591
647
  action: int,
@@ -604,7 +660,7 @@ class Mt5Client(BaseModel):
604
660
  Returns:
605
661
  Required margin amount or None.
606
662
  """ # noqa: E501
607
- self._ensure_initialized()
663
+ self._initialize_if_needed()
608
664
  self.logger.info(
609
665
  "Calculating margin: action=%d, symbol=%s, volume=%.2f, price=%.5f",
610
666
  action,
@@ -613,13 +669,14 @@ class Mt5Client(BaseModel):
613
669
  price,
614
670
  )
615
671
  response = self.mt5.order_calc_margin(action, symbol, volume, price)
616
- self._validate_metatrader5_response(
672
+ self._validate_mt5_response_is_not_none(
617
673
  response=response,
618
674
  operation="order_calc_margin",
619
675
  context=f"action={action}, symbol={symbol}, volume={volume}, price={price}",
620
676
  )
621
677
  return response
622
678
 
679
+ @_log_mt5_last_status_code
623
680
  def order_calc_profit(
624
681
  self,
625
682
  action: int,
@@ -640,7 +697,7 @@ class Mt5Client(BaseModel):
640
697
  Returns:
641
698
  Calculated profit or None.
642
699
  """
643
- self._ensure_initialized()
700
+ self._initialize_if_needed()
644
701
  self.logger.info(
645
702
  (
646
703
  "Calculating profit: action=%d, symbol=%s, volume=%.2f,"
@@ -655,7 +712,7 @@ class Mt5Client(BaseModel):
655
712
  response = self.mt5.order_calc_profit(
656
713
  action, symbol, volume, price_open, price_close
657
714
  )
658
- self._validate_metatrader5_response(
715
+ self._validate_mt5_response_is_not_none(
659
716
  response=response,
660
717
  operation="order_calc_profit",
661
718
  context=(
@@ -665,6 +722,7 @@ class Mt5Client(BaseModel):
665
722
  )
666
723
  return response
667
724
 
725
+ @_log_mt5_last_status_code
668
726
  def order_check(self, request: dict[str, Any]) -> Any:
669
727
  """Check funds sufficiency for performing a required trading operation.
670
728
 
@@ -674,16 +732,17 @@ class Mt5Client(BaseModel):
674
732
  Returns:
675
733
  OrderCheckResult structure or None.
676
734
  """
677
- self._ensure_initialized()
735
+ self._initialize_if_needed()
678
736
  self.logger.info("Checking order with request: %s", request)
679
737
  response = self.mt5.order_check(request)
680
- self._validate_metatrader5_response(
738
+ self._validate_mt5_response_is_not_none(
681
739
  response=response,
682
740
  operation="order_check",
683
741
  context=f"request={request}",
684
742
  )
685
743
  return response
686
744
 
745
+ @_log_mt5_last_status_code
687
746
  def order_send(self, request: dict[str, Any]) -> Any:
688
747
  """Send a request to perform a trading operation from the terminal to the trade server.
689
748
 
@@ -693,26 +752,28 @@ class Mt5Client(BaseModel):
693
752
  Returns:
694
753
  OrderSendResult structure or None.
695
754
  """ # noqa: E501
696
- self._ensure_initialized()
755
+ self._initialize_if_needed()
697
756
  self.logger.info("Sending order with request: %s", request)
698
757
  response = self.mt5.order_send(request)
699
- self._validate_metatrader5_response(
758
+ self._validate_mt5_response_is_not_none(
700
759
  response=response,
701
760
  operation="order_send",
702
761
  context=f"request={request}",
703
762
  )
704
763
  return response
705
764
 
765
+ @_log_mt5_last_status_code
706
766
  def positions_total(self) -> int:
707
767
  """Get the number of open positions.
708
768
 
709
769
  Returns:
710
770
  Number of open positions.
711
771
  """
712
- self._ensure_initialized()
772
+ self._initialize_if_needed()
713
773
  self.logger.info("Retrieving total number of open positions.")
714
774
  return self.mt5.positions_total()
715
775
 
776
+ @_log_mt5_last_status_code
716
777
  def positions_get(
717
778
  self,
718
779
  symbol: str | None = None,
@@ -729,7 +790,7 @@ class Mt5Client(BaseModel):
729
790
  Returns:
730
791
  Tuple of position info structures or None.
731
792
  """
732
- self._ensure_initialized()
793
+ self._initialize_if_needed()
733
794
  if ticket is not None:
734
795
  self.logger.info("Retrieving position with ticket: %d", ticket)
735
796
  response = self.mt5.positions_get(ticket=ticket)
@@ -746,13 +807,14 @@ class Mt5Client(BaseModel):
746
807
  self.logger.info("Retrieving all open positions.")
747
808
  response = self.mt5.positions_get()
748
809
  context = None
749
- self._validate_metatrader5_response(
810
+ self._validate_mt5_response_is_not_none(
750
811
  response=response,
751
812
  operation="positions_get",
752
813
  context=context,
753
814
  )
754
815
  return response
755
816
 
817
+ @_log_mt5_last_status_code
756
818
  def history_orders_total(
757
819
  self,
758
820
  date_from: datetime | int,
@@ -767,7 +829,7 @@ class Mt5Client(BaseModel):
767
829
  Returns:
768
830
  Number of historical orders.
769
831
  """
770
- self._ensure_initialized()
832
+ self._initialize_if_needed()
771
833
  self.logger.info(
772
834
  "Retrieving total number of historical orders from %s to %s",
773
835
  date_from,
@@ -775,6 +837,7 @@ class Mt5Client(BaseModel):
775
837
  )
776
838
  return self.mt5.history_orders_total(date_from, date_to)
777
839
 
840
+ @_log_mt5_last_status_code
778
841
  def history_orders_get(
779
842
  self,
780
843
  date_from: datetime | int | None = None,
@@ -795,7 +858,7 @@ class Mt5Client(BaseModel):
795
858
  Returns:
796
859
  Tuple of historical order info structures or None.
797
860
  """ # noqa: E501
798
- self._ensure_initialized()
861
+ self._initialize_if_needed()
799
862
  if ticket is not None:
800
863
  self.logger.info("Retrieving order with ticket: %d", ticket)
801
864
  response = self.mt5.history_orders_get(ticket=ticket)
@@ -821,13 +884,14 @@ class Mt5Client(BaseModel):
821
884
  )
822
885
  response = self.mt5.history_orders_get(date_from, date_to)
823
886
  context = f"date_from={date_from}, date_to={date_to}"
824
- self._validate_metatrader5_response(
887
+ self._validate_mt5_response_is_not_none(
825
888
  response=response,
826
889
  operation="history_orders_get",
827
890
  context=context,
828
891
  )
829
892
  return response
830
893
 
894
+ @_log_mt5_last_status_code
831
895
  def history_deals_total(
832
896
  self,
833
897
  date_from: datetime | int,
@@ -842,7 +906,7 @@ class Mt5Client(BaseModel):
842
906
  Returns:
843
907
  Number of historical deals.
844
908
  """
845
- self._ensure_initialized()
909
+ self._initialize_if_needed()
846
910
  self.logger.info(
847
911
  "Retrieving total number of historical deals from %s to %s",
848
912
  date_from,
@@ -850,6 +914,7 @@ class Mt5Client(BaseModel):
850
914
  )
851
915
  return self.mt5.history_deals_total(date_from, date_to)
852
916
 
917
+ @_log_mt5_last_status_code
853
918
  def history_deals_get(
854
919
  self,
855
920
  date_from: datetime | int | None = None,
@@ -870,7 +935,7 @@ class Mt5Client(BaseModel):
870
935
  Returns:
871
936
  Tuple of historical deal info structures or None.
872
937
  """ # noqa: E501
873
- self._ensure_initialized()
938
+ self._initialize_if_needed()
874
939
  if ticket is not None:
875
940
  self.logger.info("Retrieving deal with ticket: %d", ticket)
876
941
  response = self.mt5.history_deals_get(ticket=ticket)
@@ -896,20 +961,25 @@ class Mt5Client(BaseModel):
896
961
  )
897
962
  response = self.mt5.history_deals_get(date_from, date_to)
898
963
  context = f"date_from={date_from}, date_to={date_to}"
899
- self._validate_metatrader5_response(
964
+ self._validate_mt5_response_is_not_none(
900
965
  response=response,
901
966
  operation="history_deals_get",
902
967
  context=context,
903
968
  )
904
969
  return response
905
970
 
906
- def _validate_metatrader5_response(
971
+ def _initialize_if_needed(self) -> None:
972
+ """Ensure the MetaTrader5 client is initialized before performing operations."""
973
+ if not self._is_initialized:
974
+ self.initialize()
975
+
976
+ def _validate_mt5_response_is_not_none(
907
977
  self,
908
978
  response: Any,
909
979
  operation: str,
910
980
  context: str | None = None,
911
981
  ) -> None:
912
- """Validate the response from MetaTrader5 terminal functions.
982
+ """Validate that the MetaTrader5 response is not None.
913
983
 
914
984
  Args:
915
985
  response: The response object to validate.
@@ -920,20 +990,8 @@ class Mt5Client(BaseModel):
920
990
  Mt5RuntimeError: With error details from MetaTrader5.
921
991
  """
922
992
  if response is None:
923
- error_code, error_description = self.last_error()
924
993
  error_message = (
925
- f"{operation} failed: {error_code} - {error_description}"
926
- + (f" (context: {context})" if context else "")
927
- )
928
- self.logger.error(error_message)
929
- raise Mt5RuntimeError(error_message)
930
-
931
- def _ensure_initialized(self) -> None:
932
- """Ensure MetaTrader5 is initialized.
933
-
934
- Raises:
935
- Mt5RuntimeError: If MetaTrader5 is not initialized.
936
- """
937
- if not self._is_initialized:
938
- error_message = "MetaTrader5 not initialized. Call initialize() first."
994
+ f"MT5 {operation} returned {response}:"
995
+ f" last_error={self.mt5.last_error()}"
996
+ ) + (f" context={context}" if context else "")
939
997
  raise Mt5RuntimeError(error_message)
pdmt5/trading.py CHANGED
@@ -31,7 +31,7 @@ class Mt5TradingClient(Mt5DataClient):
31
31
 
32
32
  def close_open_positions(
33
33
  self,
34
- symbols: str | list[str] | tuple[str] | None = None,
34
+ symbols: str | list[str] | tuple[str, ...] | None = None,
35
35
  **kwargs: Any, # noqa: ANN401
36
36
  ) -> dict[str, list[dict[str, Any]]]:
37
37
  """Close all open positions for specified symbols.
@@ -51,6 +51,7 @@ class Mt5TradingClient(Mt5DataClient):
51
51
  symbol_list = symbols
52
52
  else:
53
53
  symbol_list = self.symbols_get()
54
+ self.logger.info("Fetching and closing positions for symbols: %s", symbol_list)
54
55
  return {
55
56
  s: self._fetch_and_close_position(symbol=s, **kwargs) for s in symbol_list
56
57
  }
@@ -99,11 +100,17 @@ class Mt5TradingClient(Mt5DataClient):
99
100
  for p in positions_dict
100
101
  ]
101
102
 
102
- def send_or_check_order(self, request: dict[str, Any]) -> dict[str, Any]:
103
+ def send_or_check_order(
104
+ self,
105
+ request: dict[str, Any],
106
+ dry_run: bool | None = None,
107
+ ) -> dict[str, Any]:
103
108
  """Send or check an order request.
104
109
 
105
110
  Args:
106
111
  request: Order request dictionary.
112
+ dry_run: Optional flag to enable dry run mode. If None, uses the instance's
113
+ `dry_run` attribute.
107
114
 
108
115
  Returns:
109
116
  Dictionary with operation result.
@@ -112,29 +119,66 @@ class Mt5TradingClient(Mt5DataClient):
112
119
  Mt5TradingError: If the order operation fails.
113
120
  """
114
121
  self.logger.debug("request: %s", request)
115
- if self.dry_run:
122
+ is_dry_run = dry_run if dry_run is not None else self.dry_run
123
+ self.logger.debug("is_dry_run: %s", is_dry_run)
124
+ if is_dry_run:
116
125
  response = self.order_check_as_dict(request=request)
117
126
  order_func = "order_check"
118
127
  else:
119
128
  response = self.order_send_as_dict(request=request)
120
129
  order_func = "order_send"
121
130
  retcode = response.get("retcode")
122
- if ((not self.dry_run) and retcode == self.mt5.TRADE_RETCODE_DONE) or (
123
- self.dry_run and retcode == 0
131
+ if ((not is_dry_run) and retcode == self.mt5.TRADE_RETCODE_DONE) or (
132
+ is_dry_run and retcode == 0
124
133
  ):
125
- self.logger.info("retcode: %s, response: %s", retcode, response)
134
+ self.logger.info("response: %s", response)
126
135
  return response
127
136
  elif retcode in {
128
137
  self.mt5.TRADE_RETCODE_TRADE_DISABLED,
129
138
  self.mt5.TRADE_RETCODE_MARKET_CLOSED,
130
139
  }:
131
- self.logger.info("retcode: %s, response: %s", retcode, response)
140
+ self.logger.info("response: %s", response)
132
141
  comment = response.get("comment", "Unknown error")
133
142
  self.logger.warning("%s() failed and skipped. <= `%s`", order_func, comment)
134
143
  return response
135
144
  else:
136
- self.logger.error("retcode: %s, response: %s", retcode, response)
145
+ self.logger.error("response: %s", response)
137
146
  comment = response.get("comment", "Unknown error")
138
147
  error_message = f"{order_func}() failed and aborted. <= `{comment}`"
139
- self.logger.error(error_message)
148
+ raise Mt5TradingError(error_message)
149
+
150
+ def calculate_minimum_order_margins(self, symbol: str) -> dict[str, float]:
151
+ """Calculate minimum order margins for a given symbol.
152
+
153
+ Args:
154
+ symbol: Symbol for which to calculate minimum order margins.
155
+
156
+ Returns:
157
+ Dictionary with margin information.
158
+
159
+ Raises:
160
+ Mt5TradingError: If margin calculation fails.
161
+ """
162
+ symbol_info = self.symbol_info_as_dict(symbol=symbol)
163
+ symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
164
+ min_ask_order_margin = self.mt5.order_calc_margin(
165
+ action=self.mt5.ORDER_TYPE_BUY,
166
+ symbol=symbol,
167
+ volume=symbol_info["volume_min"],
168
+ price=symbol_info_tick["ask"],
169
+ )
170
+ min_bid_order_margin = self.mt5.order_calc_margin(
171
+ action=self.mt5.ORDER_TYPE_SELL,
172
+ symbol=symbol,
173
+ volume=symbol_info["volume_min"],
174
+ price=symbol_info_tick["bid"],
175
+ )
176
+ min_order_margins = {"ask": min_ask_order_margin, "bid": min_bid_order_margin}
177
+ self.logger.info("Minimum order margins for %s: %s", symbol, min_order_margins)
178
+ if all(min_order_margins.values()):
179
+ return min_order_margins
180
+ else:
181
+ error_message = (
182
+ f"Failed to calculate minimum order margins for symbol: {symbol}."
183
+ )
140
184
  raise Mt5TradingError(error_message)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdmt5
3
- Version: 0.0.7
3
+ Version: 0.0.9
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>
@@ -0,0 +1,9 @@
1
+ pdmt5/__init__.py,sha256=QbSFrsi7_bgFzb-ma4DmmUjR90UvrqKMnRZq1wPRmoI,446
2
+ pdmt5/dataframe.py,sha256=MumdFp72ZN_394X_viMkovfyb1c8C-LxLFTdymLEMYM,38345
3
+ pdmt5/mt5.py,sha256=rAY7MQalobUWZtMfC_xyTFCDTuU3EkFAyY_geBl6cyg,32379
4
+ pdmt5/trading.py,sha256=NCrZZuPC1AoUQ_Rmv3_g39VbMxq7R1wASQa5oAMBKDQ,6995
5
+ pdmt5/utils.py,sha256=Ll5Q3OE5h1A_sZ_qVEnOPGniFlT6_MmHfuu0zqeLdeU,3913
6
+ pdmt5-0.0.9.dist-info/METADATA,sha256=Dpc0ApxD4EK_E1Bggyv2vexqIVr4JnfuCglERHHuy8s,9029
7
+ pdmt5-0.0.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ pdmt5-0.0.9.dist-info/licenses/LICENSE,sha256=iABrdaUGOBWLYotFupB_PGe8arV5o7rVhn-_vK6P704,1073
9
+ pdmt5-0.0.9.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- pdmt5/__init__.py,sha256=mJ1gMqZ_MKYZ-53N41MvkwNdPhfcpq15NczCz74fD7w,406
2
- pdmt5/dataframe.py,sha256=808kwi_eUlpXvgvJzXxwDTvxp80z-1zh605ONyOIoU4,38369
3
- pdmt5/mt5.py,sha256=vZMIz7h7KWDegX23KEIryby-WC5hJ4wN7pp-FMTdeZ8,30498
4
- pdmt5/trading.py,sha256=8JagrLc9cjr9TUdkoRmyAmVUOV2SQElqsrtcwg5FqBs,5284
5
- pdmt5/utils.py,sha256=Ll5Q3OE5h1A_sZ_qVEnOPGniFlT6_MmHfuu0zqeLdeU,3913
6
- pdmt5-0.0.7.dist-info/METADATA,sha256=w0kr0tob2G_h-u9DuVifML9Q5BIyVY5tHUD1YKyyLjk,9029
7
- pdmt5-0.0.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
- pdmt5-0.0.7.dist-info/licenses/LICENSE,sha256=iABrdaUGOBWLYotFupB_PGe8arV5o7rVhn-_vK6P704,1073
9
- pdmt5-0.0.7.dist-info/RECORD,,
File without changes