pdmt5 0.1.3__py3-none-any.whl → 0.1.5__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/trading.py
CHANGED
|
@@ -31,12 +31,11 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
31
31
|
description="Order filling mode: 'IOC' (Immediate or Cancel), "
|
|
32
32
|
"'FOK' (Fill or Kill), 'RETURN' (Return if not filled)",
|
|
33
33
|
)
|
|
34
|
-
dry_run: bool = Field(default=False, description="Enable dry run mode for testing.")
|
|
35
34
|
|
|
36
35
|
def close_open_positions(
|
|
37
36
|
self,
|
|
38
37
|
symbols: str | list[str] | tuple[str, ...] | None = None,
|
|
39
|
-
dry_run: bool
|
|
38
|
+
dry_run: bool = False,
|
|
40
39
|
**kwargs: Any, # noqa: ANN401
|
|
41
40
|
) -> dict[str, list[dict[str, Any]]]:
|
|
42
41
|
"""Close all open positions for specified symbols.
|
|
@@ -44,8 +43,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
44
43
|
Args:
|
|
45
44
|
symbols: Optional symbol or list of symbols to filter positions.
|
|
46
45
|
If None, all symbols will be considered.
|
|
47
|
-
dry_run:
|
|
48
|
-
`dry_run` attribute.
|
|
46
|
+
dry_run: If True, only check the order without sending it.
|
|
49
47
|
**kwargs: Additional keyword arguments for request parameters.
|
|
50
48
|
|
|
51
49
|
Returns:
|
|
@@ -67,15 +65,14 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
67
65
|
def _fetch_and_close_position(
|
|
68
66
|
self,
|
|
69
67
|
symbol: str | None = None,
|
|
70
|
-
dry_run: bool
|
|
68
|
+
dry_run: bool = False,
|
|
71
69
|
**kwargs: Any, # noqa: ANN401
|
|
72
70
|
) -> list[dict[str, Any]]:
|
|
73
71
|
"""Close all open positions for a specific symbol.
|
|
74
72
|
|
|
75
73
|
Args:
|
|
76
74
|
symbol: Optional symbol filter.
|
|
77
|
-
dry_run:
|
|
78
|
-
`dry_run` attribute.
|
|
75
|
+
dry_run: If True, only check the order without sending it.
|
|
79
76
|
**kwargs: Additional keyword arguments for request parameters.
|
|
80
77
|
|
|
81
78
|
Returns:
|
|
@@ -92,7 +89,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
92
89
|
f"ORDER_FILLING_{self.order_filling_mode}",
|
|
93
90
|
)
|
|
94
91
|
return [
|
|
95
|
-
self.
|
|
92
|
+
self._send_or_check_order(
|
|
96
93
|
request={
|
|
97
94
|
"action": self.mt5.TRADE_ACTION_DEAL,
|
|
98
95
|
"symbol": p["symbol"],
|
|
@@ -112,17 +109,16 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
112
109
|
for p in positions_dict
|
|
113
110
|
]
|
|
114
111
|
|
|
115
|
-
def
|
|
112
|
+
def _send_or_check_order(
|
|
116
113
|
self,
|
|
117
114
|
request: dict[str, Any],
|
|
118
|
-
dry_run: bool
|
|
115
|
+
dry_run: bool = False,
|
|
119
116
|
) -> dict[str, Any]:
|
|
120
117
|
"""Send or check an order request.
|
|
121
118
|
|
|
122
119
|
Args:
|
|
123
120
|
request: Order request dictionary.
|
|
124
|
-
dry_run:
|
|
125
|
-
`dry_run` attribute.
|
|
121
|
+
dry_run: If True, only check the order without sending it.
|
|
126
122
|
|
|
127
123
|
Returns:
|
|
128
124
|
Dictionary with operation result.
|
|
@@ -131,17 +127,15 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
131
127
|
Mt5TradingError: If the order operation fails.
|
|
132
128
|
"""
|
|
133
129
|
self.logger.debug("request: %s", request)
|
|
134
|
-
|
|
135
|
-
self.logger.debug("is_dry_run: %s", is_dry_run)
|
|
136
|
-
if is_dry_run:
|
|
130
|
+
if dry_run:
|
|
137
131
|
response = self.order_check_as_dict(request=request)
|
|
138
132
|
order_func = "order_check"
|
|
139
133
|
else:
|
|
140
134
|
response = self.order_send_as_dict(request=request)
|
|
141
135
|
order_func = "order_send"
|
|
142
136
|
retcode = response.get("retcode")
|
|
143
|
-
if ((not
|
|
144
|
-
|
|
137
|
+
if ((not dry_run) and retcode == self.mt5.TRADE_RETCODE_DONE) or (
|
|
138
|
+
dry_run and retcode == 0
|
|
145
139
|
):
|
|
146
140
|
self.logger.info("response: %s", response)
|
|
147
141
|
return response
|
|
@@ -159,6 +153,116 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
159
153
|
error_message = f"{order_func}() failed and aborted. <= `{comment}`"
|
|
160
154
|
raise Mt5TradingError(error_message)
|
|
161
155
|
|
|
156
|
+
def place_market_order(
|
|
157
|
+
self,
|
|
158
|
+
symbol: str,
|
|
159
|
+
volume: float,
|
|
160
|
+
order_side: Literal["BUY", "SELL"],
|
|
161
|
+
order_filling_mode: Literal["IOC", "FOK", "RETURN"] = "IOC",
|
|
162
|
+
order_time_mode: Literal["GTC", "DAY", "SPECIFIED", "SPECIFIED_DAY"] = "GTC",
|
|
163
|
+
dry_run: bool = False,
|
|
164
|
+
**kwargs: Any, # noqa: ANN401
|
|
165
|
+
) -> dict[str, Any]:
|
|
166
|
+
"""Send or check an order request to place a market order.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
symbol: Symbol for the order.
|
|
170
|
+
volume: Volume of the order.
|
|
171
|
+
order_side: Side of the order, either "BUY" or "SELL".
|
|
172
|
+
order_filling_mode: Order filling mode, either "IOC", "FOK", or "RETURN".
|
|
173
|
+
order_time_mode: Order time mode, either "GTC", "DAY", "SPECIFIED",
|
|
174
|
+
or "SPECIFIED_DAY".
|
|
175
|
+
dry_run: If True, only check the order without sending it.
|
|
176
|
+
**kwargs: Additional keyword arguments for request parameters.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Dictionary with operation result.
|
|
180
|
+
"""
|
|
181
|
+
return self._send_or_check_order(
|
|
182
|
+
request={
|
|
183
|
+
"action": self.mt5.TRADE_ACTION_DEAL,
|
|
184
|
+
"symbol": symbol,
|
|
185
|
+
"volume": volume,
|
|
186
|
+
"type": getattr(self.mt5, f"ORDER_TYPE_{order_side.upper()}"),
|
|
187
|
+
"type_filling": getattr(
|
|
188
|
+
self.mt5, f"ORDER_FILLING_{order_filling_mode.upper()}"
|
|
189
|
+
),
|
|
190
|
+
"type_time": getattr(self.mt5, f"ORDER_TIME_{order_time_mode.upper()}"),
|
|
191
|
+
**kwargs,
|
|
192
|
+
},
|
|
193
|
+
dry_run=dry_run,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def update_sltp_for_open_positions(
|
|
197
|
+
self,
|
|
198
|
+
symbol: str,
|
|
199
|
+
stop_loss: float | None = None,
|
|
200
|
+
take_profit: float | None = None,
|
|
201
|
+
tickets: list[int] | None = None,
|
|
202
|
+
dry_run: bool = False,
|
|
203
|
+
**kwargs: Any, # noqa: ANN401
|
|
204
|
+
) -> list[dict[str, Any]]:
|
|
205
|
+
"""Change Stop Loss and Take Profit for open positions.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
symbol: Symbol for the position.
|
|
209
|
+
stop_loss: New Stop Loss price. If None, it will not be changed.
|
|
210
|
+
take_profit: New Take Profit price. If None, it will not be changed.
|
|
211
|
+
tickets: List of position tickets to filter positions. If None, all open
|
|
212
|
+
positions for the symbol will be considered.
|
|
213
|
+
dry_run: If True, only check the order without sending it.
|
|
214
|
+
**kwargs: Additional keyword arguments for request parameters.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
List of dictionaries with operation results for each updated position.
|
|
218
|
+
"""
|
|
219
|
+
positions_df = self.positions_get_as_df(symbol=symbol)
|
|
220
|
+
if positions_df.empty:
|
|
221
|
+
self.logger.warning("No open positions found for symbol: %s", symbol)
|
|
222
|
+
return []
|
|
223
|
+
elif tickets:
|
|
224
|
+
filtered_positions_df = positions_df.pipe(
|
|
225
|
+
lambda d: d[d["ticket"].isin(tickets)]
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
filtered_positions_df = positions_df
|
|
229
|
+
if filtered_positions_df.empty:
|
|
230
|
+
self.logger.warning(
|
|
231
|
+
"No open positions found for symbol: %s with specified tickets: %s",
|
|
232
|
+
symbol,
|
|
233
|
+
tickets,
|
|
234
|
+
)
|
|
235
|
+
return []
|
|
236
|
+
else:
|
|
237
|
+
symbol_info = self.symbol_info_as_dict(symbol=symbol)
|
|
238
|
+
sl = round(stop_loss, symbol_info["digits"]) if stop_loss else None
|
|
239
|
+
tp = round(take_profit, symbol_info["digits"]) if take_profit else None
|
|
240
|
+
order_requests = [
|
|
241
|
+
{
|
|
242
|
+
"action": self.mt5.TRADE_ACTION_SLTP,
|
|
243
|
+
"symbol": p["symbol"],
|
|
244
|
+
"position": p["ticket"],
|
|
245
|
+
"sl": (sl or p["sl"]),
|
|
246
|
+
"tp": (tp or p["tp"]),
|
|
247
|
+
**kwargs,
|
|
248
|
+
}
|
|
249
|
+
for _, p in filtered_positions_df.iterrows()
|
|
250
|
+
if sl != p["sl"] or tp != p["tp"]
|
|
251
|
+
]
|
|
252
|
+
if order_requests:
|
|
253
|
+
return [
|
|
254
|
+
self._send_or_check_order(request=r, dry_run=dry_run)
|
|
255
|
+
for r in order_requests
|
|
256
|
+
]
|
|
257
|
+
else:
|
|
258
|
+
self.logger.info(
|
|
259
|
+
"No positions to update for symbol: %s with SL: %s and TP: %s",
|
|
260
|
+
symbol,
|
|
261
|
+
sl,
|
|
262
|
+
tp,
|
|
263
|
+
)
|
|
264
|
+
return []
|
|
265
|
+
|
|
162
266
|
def calculate_minimum_order_margins(self, symbol: str) -> dict[str, float]:
|
|
163
267
|
"""Calculate minimum order margins for a given symbol.
|
|
164
268
|
|
|
@@ -214,7 +318,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
214
318
|
* 2
|
|
215
319
|
)
|
|
216
320
|
|
|
217
|
-
def
|
|
321
|
+
def fetch_latest_rates_as_df(
|
|
218
322
|
self,
|
|
219
323
|
symbol: str,
|
|
220
324
|
granularity: str = "M1",
|
|
@@ -251,7 +355,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
251
355
|
index_keys=index_keys,
|
|
252
356
|
)
|
|
253
357
|
|
|
254
|
-
def
|
|
358
|
+
def fetch_latest_ticks_as_df(
|
|
255
359
|
self,
|
|
256
360
|
symbol: str,
|
|
257
361
|
seconds: int = 300,
|
|
@@ -275,3 +379,144 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
275
379
|
flags=self.mt5.COPY_TICKS_ALL,
|
|
276
380
|
index_keys=index_keys,
|
|
277
381
|
)
|
|
382
|
+
|
|
383
|
+
def collect_entry_deals_as_df(
|
|
384
|
+
self,
|
|
385
|
+
symbol: str,
|
|
386
|
+
history_seconds: int = 3600,
|
|
387
|
+
index_keys: str | None = "ticket",
|
|
388
|
+
) -> pd.DataFrame:
|
|
389
|
+
"""Collect entry deals as a DataFrame.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
symbol: Symbol to collect entry deals for.
|
|
393
|
+
history_seconds: Time range in seconds to fetch deals around the last tick.
|
|
394
|
+
index_keys: Optional index keys for the DataFrame.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
pd.DataFrame: Entry deals with time index.
|
|
398
|
+
"""
|
|
399
|
+
last_tick_time = self.symbol_info_tick_as_dict(symbol=symbol)["time"]
|
|
400
|
+
deals_df = self.history_deals_get_as_df(
|
|
401
|
+
date_from=(last_tick_time - timedelta(seconds=history_seconds)),
|
|
402
|
+
date_to=(last_tick_time + timedelta(seconds=history_seconds)),
|
|
403
|
+
symbol=symbol,
|
|
404
|
+
index_keys=index_keys,
|
|
405
|
+
)
|
|
406
|
+
if deals_df.empty:
|
|
407
|
+
return deals_df
|
|
408
|
+
else:
|
|
409
|
+
return deals_df.pipe(
|
|
410
|
+
lambda d: d[
|
|
411
|
+
d["entry"]
|
|
412
|
+
& d["type"].isin({self.mt5.DEAL_TYPE_BUY, self.mt5.DEAL_TYPE_SELL})
|
|
413
|
+
]
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def fetch_positions_with_metrics_as_df(
|
|
417
|
+
self,
|
|
418
|
+
symbol: str,
|
|
419
|
+
) -> pd.DataFrame:
|
|
420
|
+
"""Fetch open positions as a DataFrame with additional metrics.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
symbol: Symbol to fetch positions for.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
pd.DataFrame: DataFrame containing open positions with additional metrics.
|
|
427
|
+
"""
|
|
428
|
+
positions_df = self.positions_get_as_df(symbol=symbol)
|
|
429
|
+
if positions_df.empty:
|
|
430
|
+
return positions_df
|
|
431
|
+
else:
|
|
432
|
+
symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
|
|
433
|
+
ask_margin = self.order_calc_margin(
|
|
434
|
+
action=self.mt5.ORDER_TYPE_BUY,
|
|
435
|
+
symbol=symbol,
|
|
436
|
+
volume=1,
|
|
437
|
+
price=symbol_info_tick["ask"],
|
|
438
|
+
)
|
|
439
|
+
bid_margin = self.order_calc_margin(
|
|
440
|
+
action=self.mt5.ORDER_TYPE_SELL,
|
|
441
|
+
symbol=symbol,
|
|
442
|
+
volume=1,
|
|
443
|
+
price=symbol_info_tick["bid"],
|
|
444
|
+
)
|
|
445
|
+
return (
|
|
446
|
+
positions_df.assign(
|
|
447
|
+
elapsed_seconds=lambda d: (
|
|
448
|
+
symbol_info_tick["time"] - d["time"]
|
|
449
|
+
).dt.total_seconds(),
|
|
450
|
+
underlier_increase_ratio=lambda d: (
|
|
451
|
+
d["price_current"] / d["price_open"] - 1
|
|
452
|
+
),
|
|
453
|
+
buy=lambda d: (d["type"] == self.mt5.POSITION_TYPE_BUY),
|
|
454
|
+
sell=lambda d: (d["type"] == self.mt5.POSITION_TYPE_SELL),
|
|
455
|
+
)
|
|
456
|
+
.assign(
|
|
457
|
+
buy_i=lambda d: d["buy"].astype(int),
|
|
458
|
+
sell_i=lambda d: d["sell"].astype(int),
|
|
459
|
+
)
|
|
460
|
+
.assign(
|
|
461
|
+
sign=lambda d: (d["buy_i"] - d["sell_i"]),
|
|
462
|
+
margin=lambda d: (
|
|
463
|
+
(d["buy_i"] * ask_margin + d["sell_i"] * bid_margin)
|
|
464
|
+
* d["volume"]
|
|
465
|
+
),
|
|
466
|
+
)
|
|
467
|
+
.assign(
|
|
468
|
+
signed_volume=lambda d: (d["volume"] * d["sign"]),
|
|
469
|
+
signed_margin=lambda d: (d["margin"] * d["sign"]),
|
|
470
|
+
underlier_profit_ratio=lambda d: (
|
|
471
|
+
d["underlier_increase_ratio"] * d["sign"]
|
|
472
|
+
),
|
|
473
|
+
)
|
|
474
|
+
.drop(columns=["buy_i", "sell_i", "sign", "underlier_increase_ratio"])
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
def calculate_new_position_margin_ratio(
|
|
478
|
+
self,
|
|
479
|
+
symbol: str,
|
|
480
|
+
new_side: Literal["BUY", "SELL"] | None = None,
|
|
481
|
+
new_volume: float = 0,
|
|
482
|
+
) -> float:
|
|
483
|
+
"""Calculate the margin ratio for a new position.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
symbol: Symbol for which to calculate the margin ratio.
|
|
487
|
+
new_side: Side of the new position, either "BUY" or "SELL".
|
|
488
|
+
new_volume: Volume of the new position.
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
float: Margin ratio for the new position as a fraction of account equity.
|
|
492
|
+
"""
|
|
493
|
+
account_info = self.account_info_as_dict()
|
|
494
|
+
if not account_info["equity"]:
|
|
495
|
+
return 0.0
|
|
496
|
+
else:
|
|
497
|
+
positions_df = self.fetch_positions_with_metrics_as_df(symbol=symbol)
|
|
498
|
+
current_signed_margin = (
|
|
499
|
+
positions_df["signed_margin"].sum() if positions_df.size else 0
|
|
500
|
+
)
|
|
501
|
+
symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
|
|
502
|
+
if new_volume == 0:
|
|
503
|
+
new_signed_margin = 0
|
|
504
|
+
elif new_side == "BUY":
|
|
505
|
+
new_signed_margin = self.order_calc_margin(
|
|
506
|
+
action=self.mt5.ORDER_TYPE_BUY,
|
|
507
|
+
symbol=symbol,
|
|
508
|
+
volume=new_volume,
|
|
509
|
+
price=symbol_info_tick["ask"],
|
|
510
|
+
)
|
|
511
|
+
elif new_side == "SELL":
|
|
512
|
+
new_signed_margin = -self.order_calc_margin(
|
|
513
|
+
action=self.mt5.ORDER_TYPE_SELL,
|
|
514
|
+
symbol=symbol,
|
|
515
|
+
volume=new_volume,
|
|
516
|
+
price=symbol_info_tick["bid"],
|
|
517
|
+
)
|
|
518
|
+
else:
|
|
519
|
+
new_signed_margin = 0
|
|
520
|
+
return abs(
|
|
521
|
+
(new_signed_margin + current_signed_margin) / account_info["equity"]
|
|
522
|
+
)
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pdmt5
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: Pandas-based data handler for MetaTrader 5
|
|
5
|
+
Project-URL: Repository, https://github.com/dceoy/pdmt5.git
|
|
6
|
+
Author-email: dceoy <dceoy@users.noreply.github.com>
|
|
7
|
+
Maintainer-email: dceoy <dceoy@users.noreply.github.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: metatrader5>=5.0.4424; sys_platform == 'win32'
|
|
20
|
+
Requires-Dist: pandas>=2.2.2
|
|
21
|
+
Requires-Dist: pydantic>=2.9.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# pdmt5
|
|
25
|
+
|
|
26
|
+
Pandas-based data handler for MetaTrader 5
|
|
27
|
+
|
|
28
|
+
[](https://github.com/dceoy/pdmt5/actions/workflows/ci.yml)
|
|
29
|
+
[](https://www.python.org/downloads/)
|
|
30
|
+
[](https://opensource.org/licenses/MIT)
|
|
31
|
+
[](https://www.microsoft.com/windows)
|
|
32
|
+
|
|
33
|
+
## Overview
|
|
34
|
+
|
|
35
|
+
**pdmt5** is a Python package that provides a pandas-based interface for MetaTrader 5 (MT5), making it easier to work with financial market data in Python. It automatically converts MT5's native data structures into pandas DataFrames, enabling seamless integration with data science workflows.
|
|
36
|
+
|
|
37
|
+
### Key Features
|
|
38
|
+
|
|
39
|
+
- 📊 **Pandas Integration**: All data returned as pandas DataFrames for easy analysis
|
|
40
|
+
- 🔧 **Type Safety**: Full type hints with strict pyright checking and pydantic validation
|
|
41
|
+
- 🏦 **Comprehensive MT5 Coverage**: Account info, market data, tick data, orders, positions, and more
|
|
42
|
+
- 🚀 **Context Manager Support**: Clean initialization and cleanup with `with` statements
|
|
43
|
+
- 📈 **Time Series Ready**: OHLCV data with proper datetime indexing
|
|
44
|
+
- 🛡️ **Robust Error Handling**: Custom exceptions with detailed MT5 error information
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
- **Operating System**: Windows (required by MetaTrader5 API)
|
|
49
|
+
- **Python**: 3.11 or higher
|
|
50
|
+
- **MetaTrader 5**: Terminal must be installed
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
### From GitHub
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/dceoy/pdmt5.git
|
|
58
|
+
pip install -U --no-cache-dir ./pdmt5
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Using uv (recommended for development)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/dceoy/pdmt5.git
|
|
65
|
+
cd pdmt5
|
|
66
|
+
uv sync
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Quick Start
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import MetaTrader5 as mt5
|
|
73
|
+
from datetime import datetime
|
|
74
|
+
from pdmt5 import Mt5DataClient, Mt5Config
|
|
75
|
+
|
|
76
|
+
# Configure connection
|
|
77
|
+
config = Mt5Config(
|
|
78
|
+
login=12345678,
|
|
79
|
+
password="your_password",
|
|
80
|
+
server="YourBroker-Server",
|
|
81
|
+
timeout=60000
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Use as context manager
|
|
85
|
+
with Mt5DataClient(config=config) as client:
|
|
86
|
+
# Get account information as DataFrame
|
|
87
|
+
account_info = client.account_info_as_df()
|
|
88
|
+
print(account_info)
|
|
89
|
+
|
|
90
|
+
# Get OHLCV data as DataFrame
|
|
91
|
+
rates = client.copy_rates_from_as_df(
|
|
92
|
+
symbol="EURUSD",
|
|
93
|
+
timeframe=mt5.TIMEFRAME_H1,
|
|
94
|
+
date_from=datetime(2024, 1, 1),
|
|
95
|
+
count=100
|
|
96
|
+
)
|
|
97
|
+
print(rates.head())
|
|
98
|
+
|
|
99
|
+
# Get current positions as DataFrame
|
|
100
|
+
positions = client.positions_get_as_df()
|
|
101
|
+
print(positions)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Core Components
|
|
105
|
+
|
|
106
|
+
### Mt5Client
|
|
107
|
+
|
|
108
|
+
The base client wrapper for all MetaTrader5 operations with context manager support:
|
|
109
|
+
|
|
110
|
+
- **Connection Management**:
|
|
111
|
+
- `initialize()` - Establish connection with MT5 terminal (with optional path, login, password, server, timeout)
|
|
112
|
+
- `login()` - Connect to trading account with credentials
|
|
113
|
+
- `shutdown()` - Close MT5 terminal connection
|
|
114
|
+
- Context manager support (`with` statement) for automatic initialization/cleanup
|
|
115
|
+
- **Terminal Information**:
|
|
116
|
+
- `version()` - Get MT5 terminal version, build, and release date
|
|
117
|
+
- `last_error()` - Get last error code and description
|
|
118
|
+
- `account_info()` - Get current trading account information
|
|
119
|
+
- `terminal_info()` - Get terminal status and settings
|
|
120
|
+
- **Symbol Operations**:
|
|
121
|
+
- `symbols_total()` - Get total number of financial instruments
|
|
122
|
+
- `symbols_get()` - Get all symbols or filter by group
|
|
123
|
+
- `symbol_info()` - Get detailed data on specific symbol
|
|
124
|
+
- `symbol_info_tick()` - Get last tick for symbol
|
|
125
|
+
- `symbol_select()` - Show/hide symbol in MarketWatch
|
|
126
|
+
- **Market Depth**:
|
|
127
|
+
- `market_book_add()` - Subscribe to Market Depth events
|
|
128
|
+
- `market_book_get()` - Get current Market Depth data
|
|
129
|
+
- `market_book_release()` - Unsubscribe from Market Depth
|
|
130
|
+
- **Market Data**:
|
|
131
|
+
- `copy_rates_from()` - Get bars from specified date
|
|
132
|
+
- `copy_rates_from_pos()` - Get bars from specified position
|
|
133
|
+
- `copy_rates_range()` - Get bars for date range
|
|
134
|
+
- `copy_ticks_from()` - Get ticks from specified date
|
|
135
|
+
- `copy_ticks_range()` - Get ticks for date range
|
|
136
|
+
- **Order Operations**:
|
|
137
|
+
- `orders_total()` - Get number of active orders
|
|
138
|
+
- `orders_get()` - Get active orders with optional filters
|
|
139
|
+
- `order_calc_margin()` - Calculate required margin
|
|
140
|
+
- `order_calc_profit()` - Calculate potential profit
|
|
141
|
+
- `order_check()` - Check if order can be placed
|
|
142
|
+
- `order_send()` - Send order to trade server
|
|
143
|
+
- **Position Operations**:
|
|
144
|
+
- `positions_total()` - Get number of open positions
|
|
145
|
+
- `positions_get()` - Get open positions with optional filters
|
|
146
|
+
- **Trading History**:
|
|
147
|
+
- `history_orders_total()` - Get number of historical orders
|
|
148
|
+
- `history_orders_get()` - Get historical orders with filters
|
|
149
|
+
- `history_deals_total()` - Get number of historical deals
|
|
150
|
+
- `history_deals_get()` - Get historical deals with filters
|
|
151
|
+
|
|
152
|
+
### Mt5DataClient
|
|
153
|
+
|
|
154
|
+
Extends Mt5Client with pandas DataFrame and dictionary conversions:
|
|
155
|
+
|
|
156
|
+
- **Enhanced Connection**:
|
|
157
|
+
- `initialize_and_login_mt5()` - Combined initialization and login with retry logic
|
|
158
|
+
- Configurable retry attempts via `retry_count` parameter
|
|
159
|
+
- **DataFrame/Dictionary Conversions**: All methods have both `_as_df` and `_as_dict` variants:
|
|
160
|
+
- `version_as_dict/df()` - MT5 version information
|
|
161
|
+
- `last_error_as_dict/df()` - Last error details
|
|
162
|
+
- `account_info_as_dict/df()` - Account information
|
|
163
|
+
- `terminal_info_as_dict/df()` - Terminal information
|
|
164
|
+
- `symbols_get_as_dicts/df()` - Symbol list with optional group filter
|
|
165
|
+
- `symbol_info_as_dict/df()` - Single symbol information
|
|
166
|
+
- `symbol_info_tick_as_dict/df()` - Last tick data
|
|
167
|
+
- `market_book_get_as_dicts/df()` - Market depth data
|
|
168
|
+
- **OHLCV Data Methods**:
|
|
169
|
+
- `copy_rates_from_as_dicts/df()` - Historical bars from date
|
|
170
|
+
- `copy_rates_from_pos_as_dicts/df()` - Historical bars from position
|
|
171
|
+
- `copy_rates_range_as_dicts/df()` - Historical bars for date range
|
|
172
|
+
- **Tick Data Methods**:
|
|
173
|
+
- `copy_ticks_from_as_dicts/df()` - Historical ticks from date
|
|
174
|
+
- `copy_ticks_range_as_dicts/df()` - Historical ticks for date range
|
|
175
|
+
- **Trading Data Methods**:
|
|
176
|
+
- `orders_get_as_dicts/df()` - Active orders with filters
|
|
177
|
+
- `order_check_as_dict/df()` - Order validation results
|
|
178
|
+
- `order_send_as_dict/df()` - Order execution results
|
|
179
|
+
- `positions_get_as_dicts/df()` - Open positions with filters
|
|
180
|
+
- `history_orders_get_as_dicts/df()` - Historical orders with date/ticket/position filters
|
|
181
|
+
- `history_deals_get_as_dicts/df()` - Historical deals with date/ticket/position filters
|
|
182
|
+
- **Features**:
|
|
183
|
+
- Automatic time conversion to datetime objects
|
|
184
|
+
- Optional DataFrame indexing with `index_keys` parameter
|
|
185
|
+
- Input validation for dates, counts, and positions
|
|
186
|
+
- Pydantic-based configuration via `Mt5Config`
|
|
187
|
+
|
|
188
|
+
### Mt5TradingClient
|
|
189
|
+
|
|
190
|
+
Advanced trading operations client that extends Mt5DataClient:
|
|
191
|
+
|
|
192
|
+
- **Trading Configuration**:
|
|
193
|
+
- `order_filling_mode` - Order execution mode: "IOC" (default), "FOK", or "RETURN"
|
|
194
|
+
- `dry_run` - Test mode flag for simulating trades without execution
|
|
195
|
+
- **Position Management**:
|
|
196
|
+
- `close_open_positions()` - Close all positions for specified symbol(s)
|
|
197
|
+
- `place_market_order()` - Place market orders with configurable side, volume, and execution modes
|
|
198
|
+
- `update_open_position_sltp()` - Modify stop loss and take profit levels for open positions
|
|
199
|
+
- **Market Analysis**:
|
|
200
|
+
- `calculate_minimum_order_margins()` - Calculate minimum required margins for buy/sell orders
|
|
201
|
+
- `calculate_spread_ratio()` - Calculate normalized bid-ask spread ratio
|
|
202
|
+
- `calculate_new_position_margin_ratio()` - Calculate margin ratio for potential new positions
|
|
203
|
+
- **Simplified Data Access**:
|
|
204
|
+
- `fetch_latest_rates_as_df()` - Get recent OHLC data with timeframe strings (e.g., "M1", "H1", "D1")
|
|
205
|
+
- `fetch_latest_ticks_as_df()` - Get tick data for specified seconds around last tick
|
|
206
|
+
- `collect_entry_deals_as_df()` - Filter and collect entry deals (BUY/SELL) from history
|
|
207
|
+
- `fetch_positions_with_metrics_as_df()` - Get open positions with calculated metrics (elapsed time, margin, profit ratios)
|
|
208
|
+
- **Features**:
|
|
209
|
+
- Smart order routing with configurable filling modes
|
|
210
|
+
- Comprehensive error handling with `Mt5TradingError`
|
|
211
|
+
- Support for batch operations on multiple symbols
|
|
212
|
+
- Automatic position closing with proper order type reversal
|
|
213
|
+
- Dry run mode for strategy testing without real trades
|
|
214
|
+
|
|
215
|
+
### Configuration
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from pdmt5 import Mt5Config
|
|
219
|
+
|
|
220
|
+
config = Mt5Config(
|
|
221
|
+
login=12345678, # MT5 account number
|
|
222
|
+
password="password", # MT5 password
|
|
223
|
+
server="Broker-Server", # MT5 server name
|
|
224
|
+
timeout=60000 # Connection timeout in ms
|
|
225
|
+
)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Examples
|
|
229
|
+
|
|
230
|
+
### Getting Historical Data
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
import MetaTrader5 as mt5
|
|
234
|
+
from datetime import datetime
|
|
235
|
+
|
|
236
|
+
with Mt5DataClient(config=config) as client:
|
|
237
|
+
# Get last 1000 H1 bars for EURUSD as DataFrame
|
|
238
|
+
df = client.copy_rates_from_as_df(
|
|
239
|
+
symbol="EURUSD",
|
|
240
|
+
timeframe=mt5.TIMEFRAME_H1,
|
|
241
|
+
date_from=datetime.now(),
|
|
242
|
+
count=1000
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Data includes: time, open, high, low, close, tick_volume, spread, real_volume
|
|
246
|
+
print(df.columns)
|
|
247
|
+
print(df.describe())
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Working with Tick Data
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
from datetime import datetime, timedelta
|
|
254
|
+
|
|
255
|
+
with Mt5DataClient(config=config) as client:
|
|
256
|
+
# Get ticks for the last hour as DataFrame
|
|
257
|
+
ticks = client.copy_ticks_from_as_df(
|
|
258
|
+
symbol="EURUSD",
|
|
259
|
+
date_from=datetime.now() - timedelta(hours=1),
|
|
260
|
+
count=10000,
|
|
261
|
+
flags=mt5.COPY_TICKS_ALL
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Tick data includes: time, bid, ask, last, volume, flags
|
|
265
|
+
print(ticks.head())
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Analyzing Positions
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
with Mt5DataClient(config=config) as client:
|
|
272
|
+
# Get all open positions as DataFrame
|
|
273
|
+
positions = client.positions_get_as_df()
|
|
274
|
+
|
|
275
|
+
if not positions.empty:
|
|
276
|
+
# Calculate summary statistics
|
|
277
|
+
summary = positions.groupby('symbol').agg({
|
|
278
|
+
'volume': 'sum',
|
|
279
|
+
'profit': 'sum',
|
|
280
|
+
'price_open': 'mean'
|
|
281
|
+
})
|
|
282
|
+
print(summary)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Trading Operations
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
from pdmt5 import Mt5TradingClient
|
|
289
|
+
|
|
290
|
+
# Create trading client with specific order filling mode
|
|
291
|
+
with Mt5TradingClient(config=config, order_filling_mode="IOC") as trader:
|
|
292
|
+
# Place a market buy order
|
|
293
|
+
order_result = trader.place_market_order(
|
|
294
|
+
symbol="EURUSD",
|
|
295
|
+
volume=0.1,
|
|
296
|
+
order_side="BUY",
|
|
297
|
+
order_filling_mode="IOC", # Immediate or Cancel
|
|
298
|
+
order_time_mode="GTC" # Good Till Cancelled
|
|
299
|
+
)
|
|
300
|
+
print(f"Order placed: {order_result['retcode']}")
|
|
301
|
+
|
|
302
|
+
# Update stop loss and take profit for an open position
|
|
303
|
+
if positions := trader.positions_get_as_df(symbol="EURUSD"):
|
|
304
|
+
position_ticket = positions.iloc[0]['ticket']
|
|
305
|
+
update_result = trader.update_open_position_sltp(
|
|
306
|
+
symbol="EURUSD",
|
|
307
|
+
position_ticket=position_ticket,
|
|
308
|
+
sl=1.0950, # New stop loss
|
|
309
|
+
tp=1.1050 # New take profit
|
|
310
|
+
)
|
|
311
|
+
print(f"Position updated: {update_result['retcode']}")
|
|
312
|
+
|
|
313
|
+
# Calculate margin ratio for a new position
|
|
314
|
+
margin_ratio = trader.calculate_new_position_margin_ratio(
|
|
315
|
+
symbol="EURUSD",
|
|
316
|
+
new_side="SELL",
|
|
317
|
+
new_volume=0.2
|
|
318
|
+
)
|
|
319
|
+
print(f"New position margin ratio: {margin_ratio:.2%}")
|
|
320
|
+
|
|
321
|
+
# Close all EURUSD positions
|
|
322
|
+
results = trader.close_open_positions(symbols="EURUSD")
|
|
323
|
+
|
|
324
|
+
if results:
|
|
325
|
+
for symbol, close_results in results.items():
|
|
326
|
+
for result in close_results:
|
|
327
|
+
print(f"Closed position {result.get('position')} with result: {result['retcode']}")
|
|
328
|
+
|
|
329
|
+
# Using dry run mode for testing
|
|
330
|
+
trader_dry = Mt5TradingClient(config=config, dry_run=True)
|
|
331
|
+
with trader_dry:
|
|
332
|
+
# Test placing an order without actual execution
|
|
333
|
+
test_order = trader_dry.place_market_order(
|
|
334
|
+
symbol="GBPUSD",
|
|
335
|
+
volume=0.1,
|
|
336
|
+
order_side="SELL",
|
|
337
|
+
dry_run=True # Override instance setting
|
|
338
|
+
)
|
|
339
|
+
print(f"Test order validation: {test_order['retcode']}")
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Market Analysis with Mt5TradingClient
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
with Mt5TradingClient(config=config) as trader:
|
|
346
|
+
# Calculate spread ratio for EURUSD
|
|
347
|
+
spread_ratio = trader.calculate_spread_ratio("EURUSD")
|
|
348
|
+
print(f"EURUSD spread ratio: {spread_ratio:.5f}")
|
|
349
|
+
|
|
350
|
+
# Get minimum order margins
|
|
351
|
+
margins = trader.calculate_minimum_order_margins("EURUSD")
|
|
352
|
+
print(f"Minimum ask margin: {margins['ask']}")
|
|
353
|
+
print(f"Minimum bid margin: {margins['bid']}")
|
|
354
|
+
|
|
355
|
+
# Get recent OHLC data with custom timeframe
|
|
356
|
+
rates_df = trader.fetch_latest_rates_as_df(
|
|
357
|
+
symbol="EURUSD",
|
|
358
|
+
granularity="M15", # 15-minute bars
|
|
359
|
+
count=100
|
|
360
|
+
)
|
|
361
|
+
print(rates_df.tail())
|
|
362
|
+
|
|
363
|
+
# Get tick data for the last 60 seconds
|
|
364
|
+
ticks_df = trader.fetch_latest_ticks_as_df(
|
|
365
|
+
symbol="EURUSD",
|
|
366
|
+
seconds=60
|
|
367
|
+
)
|
|
368
|
+
print(f"Received {len(ticks_df)} ticks")
|
|
369
|
+
|
|
370
|
+
# Collect entry deals for the last hour
|
|
371
|
+
deals_df = trader.collect_entry_deals_as_df(
|
|
372
|
+
symbol="EURUSD",
|
|
373
|
+
history_seconds=3600
|
|
374
|
+
)
|
|
375
|
+
if not deals_df.empty:
|
|
376
|
+
print(f"Found {len(deals_df)} entry deals")
|
|
377
|
+
print(deals_df[['time', 'type', 'volume', 'price']].head())
|
|
378
|
+
|
|
379
|
+
# Get positions with calculated metrics
|
|
380
|
+
positions_df = trader.fetch_positions_with_metrics_as_df("EURUSD")
|
|
381
|
+
if not positions_df.empty:
|
|
382
|
+
print(f"Open positions with metrics:")
|
|
383
|
+
print(positions_df[['ticket', 'volume', 'profit', 'elapsed_seconds', 'underlier_profit_ratio']].head())
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Development
|
|
387
|
+
|
|
388
|
+
### Setup Development Environment
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
# Clone repository
|
|
392
|
+
git clone https://github.com/dceoy/pdmt5.git
|
|
393
|
+
cd pdmt5
|
|
394
|
+
|
|
395
|
+
# Install with uv
|
|
396
|
+
uv sync
|
|
397
|
+
|
|
398
|
+
# Run tests
|
|
399
|
+
uv run pytest test/ -v
|
|
400
|
+
|
|
401
|
+
# Run type checking
|
|
402
|
+
uv run pyright .
|
|
403
|
+
|
|
404
|
+
# Run linting
|
|
405
|
+
uv run ruff check --fix .
|
|
406
|
+
uv run ruff format .
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Code Quality
|
|
410
|
+
|
|
411
|
+
This project maintains high code quality standards:
|
|
412
|
+
|
|
413
|
+
- **Type Checking**: Strict mode with pyright
|
|
414
|
+
- **Linting**: Comprehensive ruff configuration with 40+ rule categories
|
|
415
|
+
- **Testing**: pytest with coverage tracking (minimum 90%)
|
|
416
|
+
- **Documentation**: Google-style docstrings
|
|
417
|
+
|
|
418
|
+
## Error Handling
|
|
419
|
+
|
|
420
|
+
The package provides detailed error information:
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
from pdmt5 import Mt5RuntimeError
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
with Mt5DataClient(config=config) as client:
|
|
427
|
+
data = client.copy_rates_from("INVALID", mt5.TIMEFRAME_H1, datetime.now(), 100)
|
|
428
|
+
except Mt5RuntimeError as e:
|
|
429
|
+
print(f"MT5 Error: {e}")
|
|
430
|
+
print(f"Error code: {e.error_code}")
|
|
431
|
+
print(f"Description: {e.description}")
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Limitations
|
|
435
|
+
|
|
436
|
+
- **Windows Only**: Due to MetaTrader5 API requirements
|
|
437
|
+
- **MT5 Terminal Required**: The MetaTrader 5 terminal must be installed
|
|
438
|
+
- **Single Thread**: MT5 API is not thread-safe
|
|
439
|
+
|
|
440
|
+
## Contributing
|
|
441
|
+
|
|
442
|
+
Contributions are welcome! Please:
|
|
443
|
+
|
|
444
|
+
1. Fork the repository
|
|
445
|
+
2. Create a feature branch
|
|
446
|
+
3. Ensure tests pass and coverage is maintained
|
|
447
|
+
4. Submit a pull request
|
|
448
|
+
|
|
449
|
+
See [CLAUDE.md](CLAUDE.md) for development guidelines.
|
|
450
|
+
|
|
451
|
+
## License
|
|
452
|
+
|
|
453
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
454
|
+
|
|
455
|
+
## Author
|
|
456
|
+
|
|
457
|
+
Daichi Narushima, Ph.D.
|
|
458
|
+
|
|
459
|
+
## Acknowledgments
|
|
460
|
+
|
|
461
|
+
- MetaTrader 5 for providing the Python API
|
|
462
|
+
- The pandas community for the excellent data manipulation tools
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
pdmt5/__init__.py,sha256=QbSFrsi7_bgFzb-ma4DmmUjR90UvrqKMnRZq1wPRmoI,446
|
|
2
2
|
pdmt5/dataframe.py,sha256=rUWtR23hrXBdBqzJhbOlIemNy73RrjSTZZJUhwoL6io,38084
|
|
3
3
|
pdmt5/mt5.py,sha256=KgxHapIrh5b4L0wIOAQIjfXNZafalihbFrh9fhYHmrI,32254
|
|
4
|
-
pdmt5/trading.py,sha256=
|
|
4
|
+
pdmt5/trading.py,sha256=TJjxq2PP2eIRM8pe4G0DzxLkLjOFR-0ZV0rpzDwl-TU,19488
|
|
5
5
|
pdmt5/utils.py,sha256=Ll5Q3OE5h1A_sZ_qVEnOPGniFlT6_MmHfuu0zqeLdeU,3913
|
|
6
|
-
pdmt5-0.1.
|
|
7
|
-
pdmt5-0.1.
|
|
8
|
-
pdmt5-0.1.
|
|
9
|
-
pdmt5-0.1.
|
|
6
|
+
pdmt5-0.1.5.dist-info/METADATA,sha256=YjTgWZTyhoLtzbLii57WYM5UajB0pix15sM7CzhjgwY,16085
|
|
7
|
+
pdmt5-0.1.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
pdmt5-0.1.5.dist-info/licenses/LICENSE,sha256=iABrdaUGOBWLYotFupB_PGe8arV5o7rVhn-_vK6P704,1073
|
|
9
|
+
pdmt5-0.1.5.dist-info/RECORD,,
|
pdmt5-0.1.3.dist-info/METADATA
DELETED
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pdmt5
|
|
3
|
-
Version: 0.1.3
|
|
4
|
-
Summary: Pandas-based data handler for MetaTrader 5
|
|
5
|
-
Project-URL: Repository, https://github.com/dceoy/pdmt5.git
|
|
6
|
-
Author-email: dceoy <dceoy@users.noreply.github.com>
|
|
7
|
-
Maintainer-email: dceoy <dceoy@users.noreply.github.com>
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Classifier: Development Status :: 4 - Beta
|
|
11
|
-
Classifier: Environment :: Console
|
|
12
|
-
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
-
Classifier: Operating System :: Microsoft :: Windows
|
|
15
|
-
Classifier: Programming Language :: Python
|
|
16
|
-
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
18
|
-
Requires-Python: >=3.11
|
|
19
|
-
Requires-Dist: metatrader5>=5.0.4424; sys_platform == 'win32'
|
|
20
|
-
Requires-Dist: pandas>=2.2.2
|
|
21
|
-
Requires-Dist: pydantic>=2.9.0
|
|
22
|
-
Description-Content-Type: text/markdown
|
|
23
|
-
|
|
24
|
-
# pdmt5
|
|
25
|
-
|
|
26
|
-
Pandas-based data handler for MetaTrader 5
|
|
27
|
-
|
|
28
|
-
[](https://github.com/dceoy/pdmt5/actions/workflows/ci.yml)
|
|
29
|
-
[](https://www.python.org/downloads/)
|
|
30
|
-
[](https://opensource.org/licenses/MIT)
|
|
31
|
-
[](https://www.microsoft.com/windows)
|
|
32
|
-
|
|
33
|
-
## Overview
|
|
34
|
-
|
|
35
|
-
**pdmt5** is a Python package that provides a pandas-based interface for MetaTrader 5 (MT5), making it easier to work with financial market data in Python. It automatically converts MT5's native data structures into pandas DataFrames, enabling seamless integration with data science workflows.
|
|
36
|
-
|
|
37
|
-
### Key Features
|
|
38
|
-
|
|
39
|
-
- 📊 **Pandas Integration**: All data returned as pandas DataFrames for easy analysis
|
|
40
|
-
- 🔧 **Type Safety**: Full type hints with strict pyright checking and pydantic validation
|
|
41
|
-
- 🏦 **Comprehensive MT5 Coverage**: Account info, market data, tick data, orders, positions, and more
|
|
42
|
-
- 🚀 **Context Manager Support**: Clean initialization and cleanup with `with` statements
|
|
43
|
-
- 📈 **Time Series Ready**: OHLCV data with proper datetime indexing
|
|
44
|
-
- 🛡️ **Robust Error Handling**: Custom exceptions with detailed MT5 error information
|
|
45
|
-
|
|
46
|
-
## Requirements
|
|
47
|
-
|
|
48
|
-
- **Operating System**: Windows (required by MetaTrader5 API)
|
|
49
|
-
- **Python**: 3.11 or higher
|
|
50
|
-
- **MetaTrader 5**: Terminal must be installed
|
|
51
|
-
|
|
52
|
-
## Installation
|
|
53
|
-
|
|
54
|
-
### From GitHub
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
git clone https://github.com/dceoy/pdmt5.git
|
|
58
|
-
pip install -U --no-cache-dir ./pdmt5
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Using uv (recommended for development)
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
git clone https://github.com/dceoy/pdmt5.git
|
|
65
|
-
cd pdmt5
|
|
66
|
-
uv sync
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Quick Start
|
|
70
|
-
|
|
71
|
-
```python
|
|
72
|
-
import MetaTrader5 as mt5
|
|
73
|
-
from datetime import datetime
|
|
74
|
-
from pdmt5 import Mt5DataClient, Mt5Config
|
|
75
|
-
|
|
76
|
-
# Configure connection
|
|
77
|
-
config = Mt5Config(
|
|
78
|
-
login=12345678,
|
|
79
|
-
password="your_password",
|
|
80
|
-
server="YourBroker-Server",
|
|
81
|
-
timeout=60000,
|
|
82
|
-
portable=False
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
# Use as context manager
|
|
86
|
-
with Mt5DataClient(config=config) as client:
|
|
87
|
-
# Get account information as DataFrame
|
|
88
|
-
account_info = client.get_account_info_as_df()
|
|
89
|
-
print(account_info)
|
|
90
|
-
|
|
91
|
-
# Get OHLCV data as DataFrame
|
|
92
|
-
rates = client.copy_rates_from_as_df(
|
|
93
|
-
symbol="EURUSD",
|
|
94
|
-
timeframe=mt5.TIMEFRAME_H1,
|
|
95
|
-
date_from=datetime(2024, 1, 1),
|
|
96
|
-
count=100
|
|
97
|
-
)
|
|
98
|
-
print(rates.head())
|
|
99
|
-
|
|
100
|
-
# Get current positions as DataFrame
|
|
101
|
-
positions = client.get_positions_as_df()
|
|
102
|
-
print(positions)
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## Core Components
|
|
106
|
-
|
|
107
|
-
### Mt5Client
|
|
108
|
-
|
|
109
|
-
The base client for MT5 operations with context manager support:
|
|
110
|
-
|
|
111
|
-
- **Connection Management**: `initialize()`, `login()`, `shutdown()`
|
|
112
|
-
- **Account & Terminal Info**: Access account details and terminal information
|
|
113
|
-
- **Symbol Operations**: Get symbol information and market data
|
|
114
|
-
- **Trading Operations**: Execute orders, manage positions and deals
|
|
115
|
-
- **History Access**: Retrieve historical orders and deals
|
|
116
|
-
|
|
117
|
-
### Mt5DataClient
|
|
118
|
-
|
|
119
|
-
Extends Mt5Client with pandas DataFrame conversions:
|
|
120
|
-
|
|
121
|
-
- **DataFrame Methods**: All data methods have `_as_df` variants returning DataFrames
|
|
122
|
-
- **Dictionary Methods**: All data methods have `_as_dict` variants returning dictionaries
|
|
123
|
-
- **Account Operations**: `get_account_info()`, `get_terminal_info()`
|
|
124
|
-
- **Market Data**: `copy_rates_*()` methods for OHLCV data
|
|
125
|
-
- **Tick Data**: `copy_ticks_*()` methods for tick-level data
|
|
126
|
-
- **Trading Info**: `get_orders()`, `get_positions()`, `get_deals()`
|
|
127
|
-
- **Symbol Info**: `get_symbols()`, `get_symbol_info()`
|
|
128
|
-
|
|
129
|
-
### Mt5TradingClient
|
|
130
|
-
|
|
131
|
-
Advanced trading operations interface that extends Mt5DataClient:
|
|
132
|
-
|
|
133
|
-
- **Position Management**: `close_open_positions()` - Close positions by symbol
|
|
134
|
-
- **Order Filling Modes**: IOC (Immediate or Cancel), FOK (Fill or Kill), or RETURN
|
|
135
|
-
- **Dry Run Mode**: Test trading logic without executing real trades
|
|
136
|
-
- **Full Trading Operations**: Includes all Mt5DataClient capabilities plus trading features
|
|
137
|
-
|
|
138
|
-
### Configuration
|
|
139
|
-
|
|
140
|
-
```python
|
|
141
|
-
from pdmt5 import Mt5Config
|
|
142
|
-
|
|
143
|
-
config = Mt5Config(
|
|
144
|
-
login=12345678, # MT5 account number
|
|
145
|
-
password="password", # MT5 password
|
|
146
|
-
server="Broker-Server", # MT5 server name
|
|
147
|
-
timeout=60000, # Connection timeout in ms
|
|
148
|
-
portable=False # Use portable mode
|
|
149
|
-
)
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
## Examples
|
|
153
|
-
|
|
154
|
-
### Getting Historical Data
|
|
155
|
-
|
|
156
|
-
```python
|
|
157
|
-
import MetaTrader5 as mt5
|
|
158
|
-
from datetime import datetime
|
|
159
|
-
|
|
160
|
-
with Mt5DataClient(config=config) as client:
|
|
161
|
-
# Get last 1000 H1 bars for EURUSD as DataFrame
|
|
162
|
-
df = client.copy_rates_from_as_df(
|
|
163
|
-
symbol="EURUSD",
|
|
164
|
-
timeframe=mt5.TIMEFRAME_H1,
|
|
165
|
-
date_from=datetime.now(),
|
|
166
|
-
count=1000
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
# Data includes: time, open, high, low, close, tick_volume, spread, real_volume
|
|
170
|
-
print(df.columns)
|
|
171
|
-
print(df.describe())
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### Working with Tick Data
|
|
175
|
-
|
|
176
|
-
```python
|
|
177
|
-
from datetime import datetime, timedelta
|
|
178
|
-
|
|
179
|
-
with Mt5DataClient(config=config) as client:
|
|
180
|
-
# Get ticks for the last hour as DataFrame
|
|
181
|
-
ticks = client.copy_ticks_from_as_df(
|
|
182
|
-
symbol="EURUSD",
|
|
183
|
-
date_from=datetime.now() - timedelta(hours=1),
|
|
184
|
-
count=10000,
|
|
185
|
-
flags=mt5.COPY_TICKS_ALL
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
# Tick data includes: time, bid, ask, last, volume, flags
|
|
189
|
-
print(ticks.head())
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### Analyzing Positions
|
|
193
|
-
|
|
194
|
-
```python
|
|
195
|
-
with Mt5DataClient(config=config) as client:
|
|
196
|
-
# Get all open positions as DataFrame
|
|
197
|
-
positions = client.get_positions_as_df()
|
|
198
|
-
|
|
199
|
-
if not positions.empty:
|
|
200
|
-
# Calculate summary statistics
|
|
201
|
-
summary = positions.groupby('symbol').agg({
|
|
202
|
-
'volume': 'sum',
|
|
203
|
-
'profit': 'sum',
|
|
204
|
-
'price_open': 'mean'
|
|
205
|
-
})
|
|
206
|
-
print(summary)
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
### Trading Operations
|
|
210
|
-
|
|
211
|
-
```python
|
|
212
|
-
from pdmt5 import Mt5TradingClient
|
|
213
|
-
|
|
214
|
-
# Create trading client with specific order filling mode
|
|
215
|
-
with Mt5TradingClient(config=config, order_filling_mode="IOC") as trader:
|
|
216
|
-
# Close all EURUSD positions
|
|
217
|
-
results = trader.close_open_positions(symbols="EURUSD")
|
|
218
|
-
|
|
219
|
-
if results:
|
|
220
|
-
for result in results:
|
|
221
|
-
print(f"Closed position {result['position']} with result: {result['retcode']}")
|
|
222
|
-
|
|
223
|
-
# Using dry run mode for testing
|
|
224
|
-
trader_dry = Mt5TradingClient(config=config, dry_run=True)
|
|
225
|
-
with trader_dry:
|
|
226
|
-
# Test closing positions without actual execution
|
|
227
|
-
test_results = trader_dry.close_open_positions(symbols=["EURUSD", "GBPUSD"])
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
## Development
|
|
231
|
-
|
|
232
|
-
### Setup Development Environment
|
|
233
|
-
|
|
234
|
-
```bash
|
|
235
|
-
# Clone repository
|
|
236
|
-
git clone https://github.com/dceoy/pdmt5.git
|
|
237
|
-
cd pdmt5
|
|
238
|
-
|
|
239
|
-
# Install with uv
|
|
240
|
-
uv sync
|
|
241
|
-
|
|
242
|
-
# Run tests
|
|
243
|
-
uv run pytest test/ -v
|
|
244
|
-
|
|
245
|
-
# Run type checking
|
|
246
|
-
uv run pyright .
|
|
247
|
-
|
|
248
|
-
# Run linting
|
|
249
|
-
uv run ruff check --fix .
|
|
250
|
-
uv run ruff format .
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
### Code Quality
|
|
254
|
-
|
|
255
|
-
This project maintains high code quality standards:
|
|
256
|
-
|
|
257
|
-
- **Type Checking**: Strict mode with pyright
|
|
258
|
-
- **Linting**: Comprehensive ruff configuration with 40+ rule categories
|
|
259
|
-
- **Testing**: pytest with coverage tracking (minimum 90%)
|
|
260
|
-
- **Documentation**: Google-style docstrings
|
|
261
|
-
|
|
262
|
-
## Error Handling
|
|
263
|
-
|
|
264
|
-
The package provides detailed error information:
|
|
265
|
-
|
|
266
|
-
```python
|
|
267
|
-
from pdmt5 import Mt5RuntimeError
|
|
268
|
-
|
|
269
|
-
try:
|
|
270
|
-
with Mt5DataClient(config=config) as client:
|
|
271
|
-
data = client.copy_rates_from("INVALID", mt5.TIMEFRAME_H1, datetime.now(), 100)
|
|
272
|
-
except Mt5RuntimeError as e:
|
|
273
|
-
print(f"MT5 Error: {e}")
|
|
274
|
-
print(f"Error code: {e.error_code}")
|
|
275
|
-
print(f"Description: {e.description}")
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
## Limitations
|
|
279
|
-
|
|
280
|
-
- **Windows Only**: Due to MetaTrader5 API requirements
|
|
281
|
-
- **MT5 Terminal Required**: The MetaTrader 5 terminal must be installed
|
|
282
|
-
- **Single Thread**: MT5 API is not thread-safe
|
|
283
|
-
|
|
284
|
-
## Contributing
|
|
285
|
-
|
|
286
|
-
Contributions are welcome! Please:
|
|
287
|
-
|
|
288
|
-
1. Fork the repository
|
|
289
|
-
2. Create a feature branch
|
|
290
|
-
3. Ensure tests pass and coverage is maintained
|
|
291
|
-
4. Submit a pull request
|
|
292
|
-
|
|
293
|
-
See [CLAUDE.md](CLAUDE.md) for development guidelines.
|
|
294
|
-
|
|
295
|
-
## License
|
|
296
|
-
|
|
297
|
-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
298
|
-
|
|
299
|
-
## Author
|
|
300
|
-
|
|
301
|
-
Daichi Narushima, Ph.D.
|
|
302
|
-
|
|
303
|
-
## Acknowledgments
|
|
304
|
-
|
|
305
|
-
- MetaTrader 5 for providing the Python API
|
|
306
|
-
- The pandas community for the excellent data manipulation tools
|
|
File without changes
|
|
File without changes
|