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 | None = None,
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: Optional flag to enable dry run mode. If None, uses the instance's
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 | None = None,
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: Optional flag to enable dry run mode. If None, uses the instance's
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.send_or_check_order(
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 send_or_check_order(
112
+ def _send_or_check_order(
116
113
  self,
117
114
  request: dict[str, Any],
118
- dry_run: bool | None = None,
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: Optional flag to enable dry run mode. If None, uses the instance's
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
- is_dry_run = dry_run if dry_run is not None else self.dry_run
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 is_dry_run) and retcode == self.mt5.TRADE_RETCODE_DONE) or (
144
- is_dry_run and retcode == 0
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 copy_latest_rates_as_df(
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 copy_latest_ticks_as_df(
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
+ [![CI/CD](https://github.com/dceoy/pdmt5/actions/workflows/ci.yml/badge.svg)](https://github.com/dceoy/pdmt5/actions/workflows/ci.yml)
29
+ [![Python Version](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
+ [![Platform](https://img.shields.io/badge/platform-Windows-blue.svg)](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=FsLC_X_bwBnnQ9l54wPA4OlwD2HlO1RnPvOCnMopNNE,10113
4
+ pdmt5/trading.py,sha256=TJjxq2PP2eIRM8pe4G0DzxLkLjOFR-0ZV0rpzDwl-TU,19488
5
5
  pdmt5/utils.py,sha256=Ll5Q3OE5h1A_sZ_qVEnOPGniFlT6_MmHfuu0zqeLdeU,3913
6
- pdmt5-0.1.3.dist-info/METADATA,sha256=UpjES_3YPgMHCpmX4da8ox0OlqJ36nE5B-cYNKvv2A8,9029
7
- pdmt5-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
- pdmt5-0.1.3.dist-info/licenses/LICENSE,sha256=iABrdaUGOBWLYotFupB_PGe8arV5o7rVhn-_vK6P704,1073
9
- pdmt5-0.1.3.dist-info/RECORD,,
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,,
@@ -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
- [![CI/CD](https://github.com/dceoy/pdmt5/actions/workflows/ci.yml/badge.svg)](https://github.com/dceoy/pdmt5/actions/workflows/ci.yml)
29
- [![Python Version](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
30
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
- [![Platform](https://img.shields.io/badge/platform-Windows-blue.svg)](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