pdmt5 0.1.5__py3-none-any.whl → 0.1.7__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
@@ -3,9 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from datetime import timedelta
6
+ from math import floor
6
7
  from typing import TYPE_CHECKING, Any, Literal
7
8
 
8
- from pydantic import ConfigDict, Field
9
+ from pydantic import ConfigDict
9
10
 
10
11
  from .dataframe import Mt5DataClient
11
12
  from .mt5 import Mt5RuntimeError
@@ -26,15 +27,11 @@ class Mt5TradingClient(Mt5DataClient):
26
27
  """
27
28
 
28
29
  model_config = ConfigDict(frozen=True)
29
- order_filling_mode: Literal["IOC", "FOK", "RETURN"] = Field(
30
- default="IOC",
31
- description="Order filling mode: 'IOC' (Immediate or Cancel), "
32
- "'FOK' (Fill or Kill), 'RETURN' (Return if not filled)",
33
- )
34
30
 
35
31
  def close_open_positions(
36
32
  self,
37
33
  symbols: str | list[str] | tuple[str, ...] | None = None,
34
+ order_filling_mode: Literal["IOC", "FOK", "RETURN"] = "IOC",
38
35
  dry_run: bool = False,
39
36
  **kwargs: Any, # noqa: ANN401
40
37
  ) -> dict[str, list[dict[str, Any]]]:
@@ -43,6 +40,7 @@ class Mt5TradingClient(Mt5DataClient):
43
40
  Args:
44
41
  symbols: Optional symbol or list of symbols to filter positions.
45
42
  If None, all symbols will be considered.
43
+ order_filling_mode: Order filling mode, either "IOC", "FOK", or "RETURN".
46
44
  dry_run: If True, only check the order without sending it.
47
45
  **kwargs: Additional keyword arguments for request parameters.
48
46
 
@@ -58,13 +56,19 @@ class Mt5TradingClient(Mt5DataClient):
58
56
  symbol_list = self.symbols_get()
59
57
  self.logger.info("Fetching and closing positions for symbols: %s", symbol_list)
60
58
  return {
61
- s: self._fetch_and_close_position(symbol=s, dry_run=dry_run, **kwargs)
59
+ s: self._fetch_and_close_position(
60
+ symbol=s,
61
+ order_filling_mode=order_filling_mode,
62
+ dry_run=dry_run,
63
+ **kwargs,
64
+ )
62
65
  for s in symbol_list
63
66
  }
64
67
 
65
68
  def _fetch_and_close_position(
66
69
  self,
67
70
  symbol: str | None = None,
71
+ order_filling_mode: Literal["IOC", "FOK", "RETURN"] = "IOC",
68
72
  dry_run: bool = False,
69
73
  **kwargs: Any, # noqa: ANN401
70
74
  ) -> list[dict[str, Any]]:
@@ -72,6 +76,7 @@ class Mt5TradingClient(Mt5DataClient):
72
76
 
73
77
  Args:
74
78
  symbol: Optional symbol filter.
79
+ order_filling_mode: Order filling mode, either "IOC", "FOK", or "RETURN".
75
80
  dry_run: If True, only check the order without sending it.
76
81
  **kwargs: Additional keyword arguments for request parameters.
77
82
 
@@ -84,10 +89,6 @@ class Mt5TradingClient(Mt5DataClient):
84
89
  return []
85
90
  else:
86
91
  self.logger.info("Closing open positions for symbol: %s", symbol)
87
- order_filling_type = getattr(
88
- self.mt5,
89
- f"ORDER_FILLING_{self.order_filling_mode}",
90
- )
91
92
  return [
92
93
  self._send_or_check_order(
93
94
  request={
@@ -99,7 +100,10 @@ class Mt5TradingClient(Mt5DataClient):
99
100
  if p["type"] == self.mt5.POSITION_TYPE_BUY
100
101
  else self.mt5.ORDER_TYPE_BUY
101
102
  ),
102
- "type_filling": order_filling_type,
103
+ "type_filling": getattr(
104
+ self.mt5,
105
+ f"ORDER_FILLING_{order_filling_mode}",
106
+ ),
103
107
  "type_time": self.mt5.ORDER_TIME_GTC,
104
108
  "position": p["ticket"],
105
109
  **kwargs,
@@ -263,41 +267,69 @@ class Mt5TradingClient(Mt5DataClient):
263
267
  )
264
268
  return []
265
269
 
266
- def calculate_minimum_order_margins(self, symbol: str) -> dict[str, float]:
267
- """Calculate minimum order margins for a given symbol.
270
+ def calculate_minimum_order_margin(
271
+ self,
272
+ symbol: str,
273
+ order_side: Literal["BUY", "SELL"],
274
+ ) -> dict[str, float]:
275
+ """Calculate the minimum order margins for a given symbol.
268
276
 
269
277
  Args:
270
- symbol: Symbol for which to calculate minimum order margins.
278
+ symbol: Symbol for which to calculate the minimum order margins.
279
+ order_side: Optional side of the order, either "BUY" or "SELL".
271
280
 
272
281
  Returns:
273
- Dictionary with margin information.
274
-
275
- Raises:
276
- Mt5TradingError: If margin calculation fails.
282
+ Dictionary with minimum volume and margin for the specified order side.
277
283
  """
278
284
  symbol_info = self.symbol_info_as_dict(symbol=symbol)
279
285
  symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
280
- min_ask_order_margin = self.order_calc_margin(
281
- action=self.mt5.ORDER_TYPE_BUY,
286
+ margin = self.order_calc_margin(
287
+ action=getattr(self.mt5, f"ORDER_TYPE_{order_side.upper()}"),
282
288
  symbol=symbol,
283
289
  volume=symbol_info["volume_min"],
284
- price=symbol_info_tick["ask"],
290
+ price=(
291
+ symbol_info_tick["bid"]
292
+ if order_side == "SELL"
293
+ else symbol_info_tick["ask"]
294
+ ),
285
295
  )
286
- min_bid_order_margin = self.order_calc_margin(
287
- action=self.mt5.ORDER_TYPE_SELL,
296
+ if margin:
297
+ return {"volume": symbol_info["volume_min"], "margin": margin}
298
+ else:
299
+ self.logger.warning(
300
+ "No margin available for symbol: %s with order side: %s",
301
+ symbol,
302
+ order_side,
303
+ )
304
+ return {"volume": symbol_info["volume_min"], "margin": 0.0}
305
+
306
+ def calculate_volume_by_margin(
307
+ self,
308
+ symbol: str,
309
+ margin: float,
310
+ order_side: Literal["BUY", "SELL"],
311
+ ) -> float:
312
+ """Calculate volume based on margin for a given symbol and order side.
313
+
314
+ Args:
315
+ symbol: Symbol for which to calculate the volume.
316
+ margin: Margin amount to use for the calculation.
317
+ order_side: Side of the order, either "BUY" or "SELL".
318
+
319
+ Returns:
320
+ Calculated volume as a float.
321
+ """
322
+ min_order_margin_dict = self.calculate_minimum_order_margin(
288
323
  symbol=symbol,
289
- volume=symbol_info["volume_min"],
290
- price=symbol_info_tick["bid"],
324
+ order_side=order_side,
291
325
  )
292
- min_order_margins = {"ask": min_ask_order_margin, "bid": min_bid_order_margin}
293
- self.logger.info("Minimum order margins for %s: %s", symbol, min_order_margins)
294
- if all(min_order_margins.values()):
295
- return min_order_margins
296
- else:
297
- error_message = (
298
- f"Failed to calculate minimum order margins for symbol: {symbol}."
326
+ if min_order_margin_dict["margin"]:
327
+ return (
328
+ floor(margin / min_order_margin_dict["margin"])
329
+ * min_order_margin_dict["volume"]
299
330
  )
300
- raise Mt5TradingError(error_message)
331
+ else:
332
+ return 0.0
301
333
 
302
334
  def calculate_spread_ratio(
303
335
  self,
@@ -477,15 +509,15 @@ class Mt5TradingClient(Mt5DataClient):
477
509
  def calculate_new_position_margin_ratio(
478
510
  self,
479
511
  symbol: str,
480
- new_side: Literal["BUY", "SELL"] | None = None,
481
- new_volume: float = 0,
512
+ new_position_side: Literal["BUY", "SELL"] | None = None,
513
+ new_position_volume: float = 0,
482
514
  ) -> float:
483
515
  """Calculate the margin ratio for a new position.
484
516
 
485
517
  Args:
486
518
  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.
519
+ new_position_side: Side of the new position, either "BUY" or "SELL".
520
+ new_position_volume: Volume of the new position.
489
521
 
490
522
  Returns:
491
523
  float: Margin ratio for the new position as a fraction of account equity.
@@ -499,20 +531,20 @@ class Mt5TradingClient(Mt5DataClient):
499
531
  positions_df["signed_margin"].sum() if positions_df.size else 0
500
532
  )
501
533
  symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
502
- if new_volume == 0:
534
+ if not (new_position_side and new_position_volume):
503
535
  new_signed_margin = 0
504
- elif new_side == "BUY":
536
+ elif new_position_side.upper() == "BUY":
505
537
  new_signed_margin = self.order_calc_margin(
506
538
  action=self.mt5.ORDER_TYPE_BUY,
507
539
  symbol=symbol,
508
- volume=new_volume,
540
+ volume=new_position_volume,
509
541
  price=symbol_info_tick["ask"],
510
542
  )
511
- elif new_side == "SELL":
543
+ elif new_position_side.upper() == "SELL":
512
544
  new_signed_margin = -self.order_calc_margin(
513
545
  action=self.mt5.ORDER_TYPE_SELL,
514
546
  symbol=symbol,
515
- volume=new_volume,
547
+ volume=new_position_volume,
516
548
  price=symbol_info_tick["bid"],
517
549
  )
518
550
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdmt5
3
- Version: 0.1.5
3
+ Version: 0.1.7
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>
@@ -42,6 +42,8 @@ Pandas-based data handler for MetaTrader 5
42
42
  - 🚀 **Context Manager Support**: Clean initialization and cleanup with `with` statements
43
43
  - 📈 **Time Series Ready**: OHLCV data with proper datetime indexing
44
44
  - 🛡️ **Robust Error Handling**: Custom exceptions with detailed MT5 error information
45
+ - 💰 **Advanced Trading Operations**: Position management, margin calculations, and risk analysis tools
46
+ - 🧪 **Dry Run Mode**: Test trading strategies without executing real trades
45
47
 
46
48
  ## Requirements
47
49
 
@@ -189,15 +191,13 @@ Extends Mt5Client with pandas DataFrame and dictionary conversions:
189
191
 
190
192
  Advanced trading operations client that extends Mt5DataClient:
191
193
 
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
194
  - **Position Management**:
196
195
  - `close_open_positions()` - Close all positions for specified symbol(s)
197
196
  - `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
197
+ - `update_sltp_for_open_positions()` - Modify stop loss and take profit levels for open positions
198
+ - **Margin Calculations**:
199
+ - `calculate_minimum_order_margin()` - Calculate minimum required margin for a specific order side
200
+ - `calculate_volume_by_margin()` - Calculate maximum volume for given margin amount
201
201
  - `calculate_spread_ratio()` - Calculate normalized bid-ask spread ratio
202
202
  - `calculate_new_position_margin_ratio()` - Calculate margin ratio for potential new positions
203
203
  - **Simplified Data Access**:
@@ -210,7 +210,6 @@ Advanced trading operations client that extends Mt5DataClient:
210
210
  - Comprehensive error handling with `Mt5TradingError`
211
211
  - Support for batch operations on multiple symbols
212
212
  - Automatic position closing with proper order type reversal
213
- - Dry run mode for strategy testing without real trades
214
213
 
215
214
  ### Configuration
216
215
 
@@ -287,8 +286,8 @@ with Mt5DataClient(config=config) as client:
287
286
  ```python
288
287
  from pdmt5 import Mt5TradingClient
289
288
 
290
- # Create trading client with specific order filling mode
291
- with Mt5TradingClient(config=config, order_filling_mode="IOC") as trader:
289
+ # Create trading client
290
+ with Mt5TradingClient(config=config) as trader:
292
291
  # Place a market buy order
293
292
  order_result = trader.place_market_order(
294
293
  symbol="EURUSD",
@@ -299,44 +298,33 @@ with Mt5TradingClient(config=config, order_filling_mode="IOC") as trader:
299
298
  )
300
299
  print(f"Order placed: {order_result['retcode']}")
301
300
 
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']}")
301
+ # Update stop loss and take profit for open positions
302
+ update_results = trader.update_sltp_for_open_positions(
303
+ symbol="EURUSD",
304
+ stop_loss=1.0950, # New stop loss
305
+ take_profit=1.1050 # New take profit
306
+ )
307
+ for result in update_results:
308
+ print(f"Position updated: {result['retcode']}")
312
309
 
313
310
  # Calculate margin ratio for a new position
314
311
  margin_ratio = trader.calculate_new_position_margin_ratio(
315
312
  symbol="EURUSD",
316
- new_side="SELL",
317
- new_volume=0.2
313
+ new_position_side="SELL",
314
+ new_position_volume=0.2
318
315
  )
319
316
  print(f"New position margin ratio: {margin_ratio:.2%}")
320
317
 
321
- # Close all EURUSD positions
322
- results = trader.close_open_positions(symbols="EURUSD")
318
+ # Close all EURUSD positions with specific order filling mode
319
+ results = trader.close_open_positions(
320
+ symbols="EURUSD",
321
+ order_filling_mode="FOK" # Fill or Kill
322
+ )
323
323
 
324
324
  if results:
325
325
  for symbol, close_results in results.items():
326
326
  for result in close_results:
327
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
328
  ```
341
329
 
342
330
  ### Market Analysis with Mt5TradingClient
@@ -347,10 +335,18 @@ with Mt5TradingClient(config=config) as trader:
347
335
  spread_ratio = trader.calculate_spread_ratio("EURUSD")
348
336
  print(f"EURUSD spread ratio: {spread_ratio:.5f}")
349
337
 
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']}")
338
+ # Get minimum order margin for BUY and SELL
339
+ buy_margin = trader.calculate_minimum_order_margin("EURUSD", "BUY")
340
+ sell_margin = trader.calculate_minimum_order_margin("EURUSD", "SELL")
341
+ print(f"Minimum BUY margin: {buy_margin['margin']} (volume: {buy_margin['volume']})")
342
+ print(f"Minimum SELL margin: {sell_margin['margin']} (volume: {sell_margin['volume']})")
343
+
344
+ # Calculate volume by margin
345
+ available_margin = 1000.0
346
+ max_buy_volume = trader.calculate_volume_by_margin("EURUSD", available_margin, "BUY")
347
+ max_sell_volume = trader.calculate_volume_by_margin("EURUSD", available_margin, "SELL")
348
+ print(f"Max BUY volume for ${available_margin}: {max_buy_volume}")
349
+ print(f"Max SELL volume for ${available_margin}: {max_sell_volume}")
354
350
 
355
351
  # Get recent OHLC data with custom timeframe
356
352
  rates_df = trader.fetch_latest_rates_as_df(
@@ -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=TJjxq2PP2eIRM8pe4G0DzxLkLjOFR-0ZV0rpzDwl-TU,19488
4
+ pdmt5/trading.py,sha256=LRHfh1bjC_day4A4Az4Q5TM4cBgZB18pWxj3Wfyo5Yg,20608
5
5
  pdmt5/utils.py,sha256=Ll5Q3OE5h1A_sZ_qVEnOPGniFlT6_MmHfuu0zqeLdeU,3913
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,,
6
+ pdmt5-0.1.7.dist-info/METADATA,sha256=c5HJ6Xm3upGHXdGQ3j1sOBE_QHY_kBVCRJJWPdgunt0,16175
7
+ pdmt5-0.1.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ pdmt5-0.1.7.dist-info/licenses/LICENSE,sha256=iABrdaUGOBWLYotFupB_PGe8arV5o7rVhn-_vK6P704,1073
9
+ pdmt5-0.1.7.dist-info/RECORD,,
File without changes