pdmt5 0.0.8__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/dataframe.py +3 -3
- pdmt5/mt5.py +18 -15
- pdmt5/trading.py +53 -9
- {pdmt5-0.0.8.dist-info → pdmt5-0.0.9.dist-info}/METADATA +1 -1
- pdmt5-0.0.9.dist-info/RECORD +9 -0
- pdmt5-0.0.8.dist-info/RECORD +0 -9
- {pdmt5-0.0.8.dist-info → pdmt5-0.0.9.dist-info}/WHEEL +0 -0
- {pdmt5-0.0.8.dist-info → pdmt5-0.0.9.dist-info}/licenses/LICENSE +0 -0
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
|
@@ -59,16 +59,20 @@ class Mt5Client(BaseModel):
|
|
|
59
59
|
@wraps(func)
|
|
60
60
|
def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
61
61
|
try:
|
|
62
|
-
|
|
62
|
+
response = func(self, *args, **kwargs)
|
|
63
63
|
except Exception as e:
|
|
64
|
-
error_message = f"
|
|
65
|
-
self.logger.exception(error_message)
|
|
64
|
+
error_message = f"MT5 {func.__name__} failed with error: {e}"
|
|
66
65
|
raise Mt5RuntimeError(error_message) from e
|
|
67
66
|
else:
|
|
68
|
-
|
|
67
|
+
self.logger.info(
|
|
68
|
+
"MT5 %s returned a response: %s",
|
|
69
|
+
func.__name__,
|
|
70
|
+
response,
|
|
71
|
+
)
|
|
72
|
+
return response
|
|
69
73
|
finally:
|
|
70
74
|
last_error_response = self.mt5.last_error()
|
|
71
|
-
message = f"
|
|
75
|
+
message = f"MT5 last status: {last_error_response}"
|
|
72
76
|
if last_error_response[0] != self.mt5.RES_S_OK:
|
|
73
77
|
self.logger.warning(message)
|
|
74
78
|
else:
|
|
@@ -119,7 +123,7 @@ class Mt5Client(BaseModel):
|
|
|
119
123
|
"""
|
|
120
124
|
if path is not None:
|
|
121
125
|
self.logger.info(
|
|
122
|
-
"Initializing
|
|
126
|
+
"Initializing MT5 connection with path: %s",
|
|
123
127
|
path,
|
|
124
128
|
)
|
|
125
129
|
self._is_initialized = self.mt5.initialize(
|
|
@@ -137,7 +141,7 @@ class Mt5Client(BaseModel):
|
|
|
137
141
|
},
|
|
138
142
|
)
|
|
139
143
|
else:
|
|
140
|
-
self.logger.info("Initializing
|
|
144
|
+
self.logger.info("Initializing MT5 connection.")
|
|
141
145
|
self._is_initialized = self.mt5.initialize()
|
|
142
146
|
return self._is_initialized
|
|
143
147
|
|
|
@@ -161,7 +165,7 @@ class Mt5Client(BaseModel):
|
|
|
161
165
|
True if successful, False otherwise.
|
|
162
166
|
"""
|
|
163
167
|
self._initialize_if_needed()
|
|
164
|
-
self.logger.info("Logging in to
|
|
168
|
+
self.logger.info("Logging in to MT5 account: %d", login)
|
|
165
169
|
return self.mt5.login(
|
|
166
170
|
login,
|
|
167
171
|
**{
|
|
@@ -178,7 +182,7 @@ class Mt5Client(BaseModel):
|
|
|
178
182
|
@_log_mt5_last_status_code
|
|
179
183
|
def shutdown(self) -> None:
|
|
180
184
|
"""Close the previously established connection to the MetaTrader 5 terminal."""
|
|
181
|
-
self.logger.info("Shutting down
|
|
185
|
+
self.logger.info("Shutting down MT5 connection.")
|
|
182
186
|
response = self.mt5.shutdown()
|
|
183
187
|
self._is_initialized = False
|
|
184
188
|
return response
|
|
@@ -191,16 +195,17 @@ class Mt5Client(BaseModel):
|
|
|
191
195
|
Tuple of (terminal_version, build, release_date).
|
|
192
196
|
"""
|
|
193
197
|
self._initialize_if_needed()
|
|
194
|
-
self.logger.info("Retrieving
|
|
198
|
+
self.logger.info("Retrieving MT5 version information.")
|
|
195
199
|
return self.mt5.version()
|
|
196
200
|
|
|
201
|
+
@_log_mt5_last_status_code
|
|
197
202
|
def last_error(self) -> tuple[int, str]:
|
|
198
203
|
"""Return data on the last error.
|
|
199
204
|
|
|
200
205
|
Returns:
|
|
201
206
|
Tuple of (error_code, error_description).
|
|
202
207
|
"""
|
|
203
|
-
self.logger.info("Retrieving last
|
|
208
|
+
self.logger.info("Retrieving last MT5 error")
|
|
204
209
|
return self.mt5.last_error()
|
|
205
210
|
|
|
206
211
|
@_log_mt5_last_status_code
|
|
@@ -985,10 +990,8 @@ class Mt5Client(BaseModel):
|
|
|
985
990
|
Mt5RuntimeError: With error details from MetaTrader5.
|
|
986
991
|
"""
|
|
987
992
|
if response is None:
|
|
988
|
-
last_error_response = self.mt5.last_error()
|
|
989
993
|
error_message = (
|
|
990
|
-
f"
|
|
991
|
-
f" last_error={
|
|
994
|
+
f"MT5 {operation} returned {response}:"
|
|
995
|
+
f" last_error={self.mt5.last_error()}"
|
|
992
996
|
) + (f" context={context}" if context else "")
|
|
993
|
-
self.logger.error(error_message)
|
|
994
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.8.dist-info/RECORD
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
pdmt5/__init__.py,sha256=QbSFrsi7_bgFzb-ma4DmmUjR90UvrqKMnRZq1wPRmoI,446
|
|
2
|
-
pdmt5/dataframe.py,sha256=808kwi_eUlpXvgvJzXxwDTvxp80z-1zh605ONyOIoU4,38369
|
|
3
|
-
pdmt5/mt5.py,sha256=NOcG4NrsgH7Jbs5keXdQgri2UNt9JKrxcf-FBxchEbE,32390
|
|
4
|
-
pdmt5/trading.py,sha256=8JagrLc9cjr9TUdkoRmyAmVUOV2SQElqsrtcwg5FqBs,5284
|
|
5
|
-
pdmt5/utils.py,sha256=Ll5Q3OE5h1A_sZ_qVEnOPGniFlT6_MmHfuu0zqeLdeU,3913
|
|
6
|
-
pdmt5-0.0.8.dist-info/METADATA,sha256=PbbFgHdYLTwcDnm3Uf8sswIUnShTl5xbw9E20bBqrrI,9029
|
|
7
|
-
pdmt5-0.0.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
-
pdmt5-0.0.8.dist-info/licenses/LICENSE,sha256=iABrdaUGOBWLYotFupB_PGe8arV5o7rVhn-_vK6P704,1073
|
|
9
|
-
pdmt5-0.0.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|