pdmt5 0.2.1__py3-none-any.whl → 0.2.3__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/dataframe.py +77 -51
- pdmt5/mt5.py +12 -3
- pdmt5/trading.py +7 -6
- pdmt5/utils.py +29 -15
- {pdmt5-0.2.1.dist-info → pdmt5-0.2.3.dist-info}/METADATA +2 -2
- pdmt5-0.2.3.dist-info/RECORD +9 -0
- {pdmt5-0.2.1.dist-info → pdmt5-0.2.3.dist-info}/WHEEL +1 -1
- pdmt5-0.2.1.dist-info/RECORD +0 -9
- {pdmt5-0.2.1.dist-info → pdmt5-0.2.3.dist-info}/licenses/LICENSE +0 -0
pdmt5/dataframe.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import time
|
|
6
6
|
from datetime import datetime # noqa: TC003
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any, cast
|
|
8
8
|
|
|
9
9
|
import pandas as pd
|
|
10
10
|
from pydantic import BaseModel, ConfigDict, Field
|
|
@@ -73,12 +73,10 @@ class Mt5DataClient(Mt5Client):
|
|
|
73
73
|
Mt5RuntimeError: If initialization fails after retries.
|
|
74
74
|
"""
|
|
75
75
|
path = path or self.config.path
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"timeout": timeout or self.config.timeout,
|
|
81
|
-
}
|
|
76
|
+
login_value = login if login is not None else self.config.login
|
|
77
|
+
password_value = password if password is not None else self.config.password
|
|
78
|
+
server_value = server if server is not None else self.config.server
|
|
79
|
+
timeout_value = timeout if timeout is not None else self.config.timeout
|
|
82
80
|
for i in range(1 + max(0, self.retry_count)):
|
|
83
81
|
if i:
|
|
84
82
|
self.logger.warning(
|
|
@@ -87,8 +85,20 @@ class Mt5DataClient(Mt5Client):
|
|
|
87
85
|
self.retry_count,
|
|
88
86
|
)
|
|
89
87
|
time.sleep(i)
|
|
90
|
-
if self.initialize(
|
|
91
|
-
|
|
88
|
+
if self.initialize(
|
|
89
|
+
path=path,
|
|
90
|
+
login=login_value,
|
|
91
|
+
password=password_value,
|
|
92
|
+
server=server_value,
|
|
93
|
+
timeout=timeout_value,
|
|
94
|
+
) and (
|
|
95
|
+
login_value is None
|
|
96
|
+
or self.login(
|
|
97
|
+
login=login_value,
|
|
98
|
+
password=password_value,
|
|
99
|
+
server=server_value,
|
|
100
|
+
timeout=timeout_value,
|
|
101
|
+
)
|
|
92
102
|
):
|
|
93
103
|
return
|
|
94
104
|
error_message = (
|
|
@@ -359,14 +369,17 @@ class Mt5DataClient(Mt5Client):
|
|
|
359
369
|
Returns:
|
|
360
370
|
List of dictionaries with OHLCV data.
|
|
361
371
|
"""
|
|
362
|
-
return
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
372
|
+
return cast(
|
|
373
|
+
"list[dict[str, Any]]",
|
|
374
|
+
self.copy_rates_from_as_df(
|
|
375
|
+
symbol=symbol,
|
|
376
|
+
timeframe=timeframe,
|
|
377
|
+
date_from=date_from,
|
|
378
|
+
count=count,
|
|
379
|
+
skip_to_datetime=skip_to_datetime,
|
|
380
|
+
index_keys=None,
|
|
381
|
+
).to_dict(orient="records"),
|
|
382
|
+
)
|
|
370
383
|
|
|
371
384
|
@set_index_if_possible(index_parameters="index_keys")
|
|
372
385
|
@detect_and_convert_time_to_datetime(skip_toggle="skip_to_datetime")
|
|
@@ -422,14 +435,17 @@ class Mt5DataClient(Mt5Client):
|
|
|
422
435
|
Returns:
|
|
423
436
|
List of dictionaries with OHLCV data.
|
|
424
437
|
"""
|
|
425
|
-
return
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
438
|
+
return cast(
|
|
439
|
+
"list[dict[str, Any]]",
|
|
440
|
+
self.copy_rates_from_pos_as_df(
|
|
441
|
+
symbol=symbol,
|
|
442
|
+
timeframe=timeframe,
|
|
443
|
+
start_pos=start_pos,
|
|
444
|
+
count=count,
|
|
445
|
+
skip_to_datetime=skip_to_datetime,
|
|
446
|
+
index_keys=None,
|
|
447
|
+
).to_dict(orient="records"),
|
|
448
|
+
)
|
|
433
449
|
|
|
434
450
|
@set_index_if_possible(index_parameters="index_keys")
|
|
435
451
|
@detect_and_convert_time_to_datetime(skip_toggle="skip_to_datetime")
|
|
@@ -486,14 +502,17 @@ class Mt5DataClient(Mt5Client):
|
|
|
486
502
|
Returns:
|
|
487
503
|
List of dictionaries with OHLCV data.
|
|
488
504
|
"""
|
|
489
|
-
return
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
505
|
+
return cast(
|
|
506
|
+
"list[dict[str, Any]]",
|
|
507
|
+
self.copy_rates_range_as_df(
|
|
508
|
+
symbol=symbol,
|
|
509
|
+
timeframe=timeframe,
|
|
510
|
+
date_from=date_from,
|
|
511
|
+
date_to=date_to,
|
|
512
|
+
skip_to_datetime=skip_to_datetime,
|
|
513
|
+
index_keys=None,
|
|
514
|
+
).to_dict(orient="records"),
|
|
515
|
+
)
|
|
497
516
|
|
|
498
517
|
@set_index_if_possible(index_parameters="index_keys")
|
|
499
518
|
@detect_and_convert_time_to_datetime(skip_toggle="skip_to_datetime")
|
|
@@ -549,14 +568,17 @@ class Mt5DataClient(Mt5Client):
|
|
|
549
568
|
Returns:
|
|
550
569
|
List of dictionaries with tick data.
|
|
551
570
|
"""
|
|
552
|
-
return
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
571
|
+
return cast(
|
|
572
|
+
"list[dict[str, Any]]",
|
|
573
|
+
self.copy_ticks_from_as_df(
|
|
574
|
+
symbol=symbol,
|
|
575
|
+
date_from=date_from,
|
|
576
|
+
count=count,
|
|
577
|
+
flags=flags,
|
|
578
|
+
skip_to_datetime=skip_to_datetime,
|
|
579
|
+
index_keys=None,
|
|
580
|
+
).to_dict(orient="records"),
|
|
581
|
+
)
|
|
560
582
|
|
|
561
583
|
@set_index_if_possible(index_parameters="index_keys")
|
|
562
584
|
@detect_and_convert_time_to_datetime(skip_toggle="skip_to_datetime")
|
|
@@ -612,14 +634,17 @@ class Mt5DataClient(Mt5Client):
|
|
|
612
634
|
Returns:
|
|
613
635
|
List of dictionaries with tick data.
|
|
614
636
|
"""
|
|
615
|
-
return
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
637
|
+
return cast(
|
|
638
|
+
"list[dict[str, Any]]",
|
|
639
|
+
self.copy_ticks_range_as_df(
|
|
640
|
+
symbol=symbol,
|
|
641
|
+
date_from=date_from,
|
|
642
|
+
date_to=date_to,
|
|
643
|
+
flags=flags,
|
|
644
|
+
skip_to_datetime=skip_to_datetime,
|
|
645
|
+
index_keys=None,
|
|
646
|
+
).to_dict(orient="records"),
|
|
647
|
+
)
|
|
623
648
|
|
|
624
649
|
@set_index_if_possible(index_parameters="index_keys")
|
|
625
650
|
@detect_and_convert_time_to_datetime(skip_toggle="skip_to_datetime")
|
|
@@ -1110,10 +1135,11 @@ class Mt5DataClient(Mt5Client):
|
|
|
1110
1135
|
Returns:
|
|
1111
1136
|
Flattened dictionary.
|
|
1112
1137
|
"""
|
|
1113
|
-
items = []
|
|
1138
|
+
items: list[tuple[str, Any]] = []
|
|
1114
1139
|
for k, v in dictionary.items():
|
|
1115
1140
|
if isinstance(v, dict):
|
|
1116
|
-
|
|
1141
|
+
nested = cast("dict[str, Any]", v)
|
|
1142
|
+
items.extend((f"{k}{sep}{sk}", sv) for sk, sv in nested.items())
|
|
1117
1143
|
else:
|
|
1118
1144
|
items.append((k, v))
|
|
1119
1145
|
return dict(items)
|
pdmt5/mt5.py
CHANGED
|
@@ -8,7 +8,7 @@ import importlib
|
|
|
8
8
|
import logging
|
|
9
9
|
from functools import wraps
|
|
10
10
|
from types import ModuleType # noqa: TC003
|
|
11
|
-
from typing import TYPE_CHECKING, Any, Self
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, Self, TypeVar
|
|
12
12
|
|
|
13
13
|
from pydantic import BaseModel, ConfigDict, Field
|
|
14
14
|
|
|
@@ -17,6 +17,9 @@ if TYPE_CHECKING:
|
|
|
17
17
|
from datetime import datetime
|
|
18
18
|
from types import TracebackType
|
|
19
19
|
|
|
20
|
+
P = ParamSpec("P")
|
|
21
|
+
R = TypeVar("R")
|
|
22
|
+
|
|
20
23
|
|
|
21
24
|
class Mt5RuntimeError(RuntimeError):
|
|
22
25
|
"""MetaTrader5 specific runtime error.
|
|
@@ -46,7 +49,9 @@ class Mt5Client(BaseModel):
|
|
|
46
49
|
_is_initialized: bool = False
|
|
47
50
|
|
|
48
51
|
@staticmethod
|
|
49
|
-
def _log_mt5_last_status_code(
|
|
52
|
+
def _log_mt5_last_status_code(
|
|
53
|
+
func: Callable[Concatenate[Mt5Client, P], R],
|
|
54
|
+
) -> Callable[Concatenate[Mt5Client, P], R]:
|
|
50
55
|
"""Decorator to log MetaTrader5 last status code after method execution.
|
|
51
56
|
|
|
52
57
|
Args:
|
|
@@ -57,7 +62,11 @@ class Mt5Client(BaseModel):
|
|
|
57
62
|
"""
|
|
58
63
|
|
|
59
64
|
@wraps(func)
|
|
60
|
-
def wrapper(
|
|
65
|
+
def wrapper(
|
|
66
|
+
self: Mt5Client,
|
|
67
|
+
*args: P.args,
|
|
68
|
+
**kwargs: P.kwargs,
|
|
69
|
+
) -> R:
|
|
61
70
|
try:
|
|
62
71
|
response = func(self, *args, **kwargs)
|
|
63
72
|
except Exception as e:
|
pdmt5/trading.py
CHANGED
|
@@ -589,30 +589,31 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
589
589
|
price=symbol_info_tick["bid"],
|
|
590
590
|
)
|
|
591
591
|
result = (
|
|
592
|
-
positions_df
|
|
592
|
+
positions_df
|
|
593
|
+
.assign(
|
|
593
594
|
elapsed_seconds=lambda d: (
|
|
594
595
|
symbol_info_tick["time"] - d["time"]
|
|
595
596
|
).dt.total_seconds(),
|
|
596
597
|
underlier_increase_ratio=lambda d: (
|
|
597
598
|
d["price_current"] / d["price_open"] - 1
|
|
598
599
|
),
|
|
599
|
-
buy=lambda d:
|
|
600
|
-
sell=lambda d:
|
|
600
|
+
buy=lambda d: d["type"] == self.mt5.POSITION_TYPE_BUY,
|
|
601
|
+
sell=lambda d: d["type"] == self.mt5.POSITION_TYPE_SELL,
|
|
601
602
|
)
|
|
602
603
|
.assign(
|
|
603
604
|
buy_i=lambda d: d["buy"].astype(int),
|
|
604
605
|
sell_i=lambda d: d["sell"].astype(int),
|
|
605
606
|
)
|
|
606
607
|
.assign(
|
|
607
|
-
sign=lambda d:
|
|
608
|
+
sign=lambda d: d["buy_i"] - d["sell_i"],
|
|
608
609
|
margin=lambda d: (
|
|
609
610
|
(d["buy_i"] * ask_margin + d["sell_i"] * bid_margin)
|
|
610
611
|
* d["volume"]
|
|
611
612
|
),
|
|
612
613
|
)
|
|
613
614
|
.assign(
|
|
614
|
-
signed_volume=lambda d:
|
|
615
|
-
signed_margin=lambda d:
|
|
615
|
+
signed_volume=lambda d: d["volume"] * d["sign"],
|
|
616
|
+
signed_margin=lambda d: d["margin"] * d["sign"],
|
|
616
617
|
underlier_profit_ratio=lambda d: (
|
|
617
618
|
d["underlier_increase_ratio"] * d["sign"]
|
|
618
619
|
),
|
pdmt5/utils.py
CHANGED
|
@@ -3,17 +3,20 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from functools import wraps
|
|
6
|
-
from typing import TYPE_CHECKING, Any
|
|
6
|
+
from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast
|
|
7
7
|
|
|
8
8
|
import pandas as pd
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
from collections.abc import Callable
|
|
12
12
|
|
|
13
|
+
P = ParamSpec("P")
|
|
14
|
+
R = TypeVar("R")
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
def detect_and_convert_time_to_datetime(
|
|
15
18
|
skip_toggle: str | None = None,
|
|
16
|
-
) -> Callable[
|
|
19
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
17
20
|
"""Decorator to convert time values/columns to datetime based on result type.
|
|
18
21
|
|
|
19
22
|
Automatically detects result type and applies appropriate time conversion:
|
|
@@ -28,25 +31,32 @@ def detect_and_convert_time_to_datetime(
|
|
|
28
31
|
Decorator function.
|
|
29
32
|
"""
|
|
30
33
|
|
|
31
|
-
def decorator(func: Callable[
|
|
34
|
+
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
32
35
|
@wraps(func)
|
|
33
|
-
def wrapper(*args:
|
|
36
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
34
37
|
result = func(*args, **kwargs)
|
|
35
38
|
if skip_toggle and kwargs.get(skip_toggle):
|
|
36
39
|
return result
|
|
37
40
|
elif isinstance(result, dict):
|
|
38
|
-
return
|
|
41
|
+
return cast(
|
|
42
|
+
"R",
|
|
43
|
+
_convert_time_values_in_dict(
|
|
44
|
+
dictionary=cast("dict[str, Any]", result)
|
|
45
|
+
),
|
|
46
|
+
)
|
|
39
47
|
elif isinstance(result, list):
|
|
40
48
|
return [
|
|
41
49
|
(
|
|
42
|
-
_convert_time_values_in_dict(
|
|
50
|
+
_convert_time_values_in_dict(
|
|
51
|
+
dictionary=cast("dict[str, Any]", d)
|
|
52
|
+
)
|
|
43
53
|
if isinstance(d, dict)
|
|
44
54
|
else d
|
|
45
55
|
)
|
|
46
|
-
for d in result
|
|
47
|
-
]
|
|
56
|
+
for d in cast("list[Any]", result)
|
|
57
|
+
] # type: ignore[return-value]
|
|
48
58
|
elif isinstance(result, pd.DataFrame):
|
|
49
|
-
return _convert_time_columns_in_df(result)
|
|
59
|
+
return cast("R", _convert_time_columns_in_df(result))
|
|
50
60
|
else:
|
|
51
61
|
return result
|
|
52
62
|
|
|
@@ -87,13 +97,15 @@ def _convert_time_columns_in_df(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
87
97
|
new_df = df.copy()
|
|
88
98
|
for c in new_df.columns:
|
|
89
99
|
if c.startswith("time_") and c.endswith("_msc"):
|
|
90
|
-
new_df[c] = pd.to_datetime(new_df[c], unit="ms")
|
|
100
|
+
new_df[c] = pd.to_datetime(new_df[c], unit="ms").astype("datetime64[ns]")
|
|
91
101
|
elif c == "time" or c.startswith("time_"):
|
|
92
|
-
new_df[c] = pd.to_datetime(new_df[c], unit="s")
|
|
102
|
+
new_df[c] = pd.to_datetime(new_df[c], unit="s").astype("datetime64[ns]")
|
|
93
103
|
return new_df
|
|
94
104
|
|
|
95
105
|
|
|
96
|
-
def set_index_if_possible(
|
|
106
|
+
def set_index_if_possible(
|
|
107
|
+
index_parameters: str | None = None,
|
|
108
|
+
) -> Callable[[Callable[P, pd.DataFrame]], Callable[P, pd.DataFrame]]:
|
|
97
109
|
"""Decorator to set index on DataFrame results if not empty.
|
|
98
110
|
|
|
99
111
|
Args:
|
|
@@ -103,11 +115,13 @@ def set_index_if_possible(index_parameters: str | None = None) -> Callable[...,
|
|
|
103
115
|
Decorator function.
|
|
104
116
|
"""
|
|
105
117
|
|
|
106
|
-
def decorator(
|
|
118
|
+
def decorator(
|
|
119
|
+
func: Callable[P, pd.DataFrame],
|
|
120
|
+
) -> Callable[P, pd.DataFrame]:
|
|
107
121
|
@wraps(func)
|
|
108
|
-
def wrapper(*args:
|
|
122
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> pd.DataFrame:
|
|
109
123
|
result = func(*args, **kwargs)
|
|
110
|
-
if not isinstance(result, pd.DataFrame):
|
|
124
|
+
if not isinstance(result, pd.DataFrame): # type: ignore[reportUnnecessaryIsInstance]
|
|
111
125
|
error_message = (
|
|
112
126
|
f"Function {func.__name__} returned non-DataFrame result: "
|
|
113
127
|
f"{type(result).__name__}. Expected DataFrame."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pdmt5
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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>
|
|
@@ -339,7 +339,7 @@ with Mt5TradingClient(config=config) as trader:
|
|
|
339
339
|
sell_margin = trader.calculate_minimum_order_margin("EURUSD", "SELL")
|
|
340
340
|
print(f"Minimum BUY margin: {buy_margin['margin']} (volume: {buy_margin['volume']})")
|
|
341
341
|
print(f"Minimum SELL margin: {sell_margin['margin']} (volume: {sell_margin['volume']})")
|
|
342
|
-
|
|
342
|
+
|
|
343
343
|
# Calculate volume by margin
|
|
344
344
|
available_margin = 1000.0
|
|
345
345
|
max_buy_volume = trader.calculate_volume_by_margin("EURUSD", available_margin, "BUY")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pdmt5/__init__.py,sha256=QbSFrsi7_bgFzb-ma4DmmUjR90UvrqKMnRZq1wPRmoI,446
|
|
2
|
+
pdmt5/dataframe.py,sha256=ju0q8z_hsZeuDb4eWj1SFIzmp5fQDX6UfB57bX_kqsg,38975
|
|
3
|
+
pdmt5/mt5.py,sha256=n_VpAC7CTTO0DcPVQ1sUhp2RfQVRsps8PuhEGY4f1Y0,32438
|
|
4
|
+
pdmt5/trading.py,sha256=OFBkONLTrut9aWPvi0-JJMVdoaZBFEsVIC8ZrErqfY8,25576
|
|
5
|
+
pdmt5/utils.py,sha256=8hSokAi8yJSmvOSMEAdgOKBazUpD_S7ui5chvc0RYp0,4438
|
|
6
|
+
pdmt5-0.2.3.dist-info/METADATA,sha256=GPRkjCqVSEuW22FFrPcX-i3bTnD78oLMGbrPQtTL5E0,16096
|
|
7
|
+
pdmt5-0.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
8
|
+
pdmt5-0.2.3.dist-info/licenses/LICENSE,sha256=iABrdaUGOBWLYotFupB_PGe8arV5o7rVhn-_vK6P704,1073
|
|
9
|
+
pdmt5-0.2.3.dist-info/RECORD,,
|
pdmt5-0.2.1.dist-info/RECORD
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
pdmt5/__init__.py,sha256=QbSFrsi7_bgFzb-ma4DmmUjR90UvrqKMnRZq1wPRmoI,446
|
|
2
|
-
pdmt5/dataframe.py,sha256=rUWtR23hrXBdBqzJhbOlIemNy73RrjSTZZJUhwoL6io,38084
|
|
3
|
-
pdmt5/mt5.py,sha256=KgxHapIrh5b4L0wIOAQIjfXNZafalihbFrh9fhYHmrI,32254
|
|
4
|
-
pdmt5/trading.py,sha256=Qd4RhZprDcWTzT3JmKl8XGVq8i9hExNdPSJbCRdUx-s,25569
|
|
5
|
-
pdmt5/utils.py,sha256=Ll5Q3OE5h1A_sZ_qVEnOPGniFlT6_MmHfuu0zqeLdeU,3913
|
|
6
|
-
pdmt5-0.2.1.dist-info/METADATA,sha256=OjDjumI_5kGHyEjpIg-xgZyGJSULRUJM_LQnv2IeJ-4,16100
|
|
7
|
-
pdmt5-0.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
-
pdmt5-0.2.1.dist-info/licenses/LICENSE,sha256=iABrdaUGOBWLYotFupB_PGe8arV5o7rVhn-_vK6P704,1073
|
|
9
|
-
pdmt5-0.2.1.dist-info/RECORD,,
|
|
File without changes
|