pdmt5 0.1.4__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
 
@@ -369,3 +473,50 @@ class Mt5TradingClient(Mt5DataClient):
369
473
  )
370
474
  .drop(columns=["buy_i", "sell_i", "sign", "underlier_increase_ratio"])
371
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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdmt5
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Pandas-based data handler for MetaTrader 5
5
5
  Project-URL: Repository, https://github.com/dceoy/pdmt5.git
6
6
  Author-email: dceoy <dceoy@users.noreply.github.com>
@@ -29,7 +29,6 @@ Pandas-based data handler for MetaTrader 5
29
29
  [![Python Version](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
30
30
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
31
  [![Platform](https://img.shields.io/badge/platform-Windows-blue.svg)](https://www.microsoft.com/windows)
32
- [![Version](https://img.shields.io/badge/version-0.1.4-green.svg)](https://github.com/dceoy/pdmt5)
33
32
 
34
33
  ## Overview
35
34
 
@@ -195,10 +194,12 @@ Advanced trading operations client that extends Mt5DataClient:
195
194
  - `dry_run` - Test mode flag for simulating trades without execution
196
195
  - **Position Management**:
197
196
  - `close_open_positions()` - Close all positions for specified symbol(s)
198
- - `send_or_check_order()` - Execute or validate orders based on dry_run mode
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
199
  - **Market Analysis**:
200
200
  - `calculate_minimum_order_margins()` - Calculate minimum required margins for buy/sell orders
201
201
  - `calculate_spread_ratio()` - Calculate normalized bid-ask spread ratio
202
+ - `calculate_new_position_margin_ratio()` - Calculate margin ratio for potential new positions
202
203
  - **Simplified Data Access**:
203
204
  - `fetch_latest_rates_as_df()` - Get recent OHLC data with timeframe strings (e.g., "M1", "H1", "D1")
204
205
  - `fetch_latest_ticks_as_df()` - Get tick data for specified seconds around last tick
@@ -288,18 +289,54 @@ from pdmt5 import Mt5TradingClient
288
289
 
289
290
  # Create trading client with specific order filling mode
290
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
+
291
321
  # Close all EURUSD positions
292
322
  results = trader.close_open_positions(symbols="EURUSD")
293
323
 
294
324
  if results:
295
- for result in results:
296
- print(f"Closed position {result['position']} with result: {result['retcode']}")
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']}")
297
328
 
298
329
  # Using dry run mode for testing
299
330
  trader_dry = Mt5TradingClient(config=config, dry_run=True)
300
331
  with trader_dry:
301
- # Test closing positions without actual execution
302
- test_results = trader_dry.close_open_positions(symbols=["EURUSD", "GBPUSD"])
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']}")
303
340
  ```
304
341
 
305
342
  ### Market Analysis with Mt5TradingClient
@@ -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=qZFORyBlSQNp_DQPeuEIqFHPAnVrCJK_6GhFRc5vDR8,13683
4
+ pdmt5/trading.py,sha256=TJjxq2PP2eIRM8pe4G0DzxLkLjOFR-0ZV0rpzDwl-TU,19488
5
5
  pdmt5/utils.py,sha256=Ll5Q3OE5h1A_sZ_qVEnOPGniFlT6_MmHfuu0zqeLdeU,3913
6
- pdmt5-0.1.4.dist-info/METADATA,sha256=XFIW5O6a7bePMxnDVc7MG6mnkXwNTY_53_mVb_BJxnY,14678
7
- pdmt5-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
- pdmt5-0.1.4.dist-info/licenses/LICENSE,sha256=iABrdaUGOBWLYotFupB_PGe8arV5o7rVhn-_vK6P704,1073
9
- pdmt5-0.1.4.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,,
File without changes