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 +2 -1
- pdmt5/dataframe.py +3 -3
- pdmt5/mt5.py +142 -84
- pdmt5/trading.py +53 -9
- {pdmt5-0.0.7.dist-info → pdmt5-0.0.9.dist-info}/METADATA +1 -1
- pdmt5-0.0.9.dist-info/RECORD +9 -0
- pdmt5-0.0.7.dist-info/RECORD +0 -9
- {pdmt5-0.0.7.dist-info → pdmt5-0.0.9.dist-info}/WHEEL +0 -0
- {pdmt5-0.0.7.dist-info → pdmt5-0.0.9.dist-info}/licenses/LICENSE +0 -0
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
|
|
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("
|
|
95
|
+
self.logger.info("MT5 initialization successful.")
|
|
96
96
|
return
|
|
97
97
|
error_message = (
|
|
98
|
-
f"
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
131
|
-
self.logger.info("Logging in to
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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.
|
|
163
|
-
self.logger.info("Retrieving
|
|
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
|
|
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.
|
|
218
|
+
self._initialize_if_needed()
|
|
182
219
|
self.logger.info("Retrieving account information.")
|
|
183
220
|
response = self.mt5.account_info()
|
|
184
|
-
self.
|
|
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.
|
|
233
|
+
self._initialize_if_needed()
|
|
194
234
|
self.logger.info("Retrieving terminal information.")
|
|
195
235
|
response = self.mt5.terminal_info()
|
|
196
|
-
self.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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}
|
|
926
|
-
|
|
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(
|
|
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
|
|
123
|
-
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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)
|
|
@@ -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,,
|
pdmt5-0.0.7.dist-info/RECORD
DELETED
|
@@ -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
|
|
File without changes
|