pdmt5 0.1.4__tar.gz → 0.1.5__tar.gz

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.
Files changed (31) hide show
  1. {pdmt5-0.1.4 → pdmt5-0.1.5}/.claude/settings.json +2 -2
  2. {pdmt5-0.1.4 → pdmt5-0.1.5}/PKG-INFO +44 -7
  3. {pdmt5-0.1.4 → pdmt5-0.1.5}/README.md +43 -6
  4. {pdmt5-0.1.4 → pdmt5-0.1.5}/pdmt5/trading.py +168 -17
  5. {pdmt5-0.1.4 → pdmt5-0.1.5}/pyproject.toml +1 -1
  6. {pdmt5-0.1.4 → pdmt5-0.1.5}/test/test_trading.py +503 -42
  7. {pdmt5-0.1.4 → pdmt5-0.1.5}/uv.lock +1 -1
  8. {pdmt5-0.1.4 → pdmt5-0.1.5}/.github/FUNDING.yml +0 -0
  9. {pdmt5-0.1.4 → pdmt5-0.1.5}/.github/copilot-instructions.md +0 -0
  10. {pdmt5-0.1.4 → pdmt5-0.1.5}/.github/dependabot.yml +0 -0
  11. {pdmt5-0.1.4 → pdmt5-0.1.5}/.github/workflows/ci.yml +0 -0
  12. {pdmt5-0.1.4 → pdmt5-0.1.5}/.gitignore +0 -0
  13. {pdmt5-0.1.4 → pdmt5-0.1.5}/CLAUDE.md +0 -0
  14. {pdmt5-0.1.4 → pdmt5-0.1.5}/LICENSE +0 -0
  15. {pdmt5-0.1.4 → pdmt5-0.1.5}/docs/api/dataframe.md +0 -0
  16. {pdmt5-0.1.4 → pdmt5-0.1.5}/docs/api/index.md +0 -0
  17. {pdmt5-0.1.4 → pdmt5-0.1.5}/docs/api/mt5.md +0 -0
  18. {pdmt5-0.1.4 → pdmt5-0.1.5}/docs/api/trading.md +0 -0
  19. {pdmt5-0.1.4 → pdmt5-0.1.5}/docs/api/utils.md +0 -0
  20. {pdmt5-0.1.4 → pdmt5-0.1.5}/docs/index.md +0 -0
  21. {pdmt5-0.1.4 → pdmt5-0.1.5}/mkdocs.yml +0 -0
  22. {pdmt5-0.1.4 → pdmt5-0.1.5}/pdmt5/__init__.py +0 -0
  23. {pdmt5-0.1.4 → pdmt5-0.1.5}/pdmt5/dataframe.py +0 -0
  24. {pdmt5-0.1.4 → pdmt5-0.1.5}/pdmt5/mt5.py +0 -0
  25. {pdmt5-0.1.4 → pdmt5-0.1.5}/pdmt5/utils.py +0 -0
  26. {pdmt5-0.1.4 → pdmt5-0.1.5}/renovate.json +0 -0
  27. {pdmt5-0.1.4 → pdmt5-0.1.5}/test/__init__.py +0 -0
  28. {pdmt5-0.1.4 → pdmt5-0.1.5}/test/test_dataframe.py +0 -0
  29. {pdmt5-0.1.4 → pdmt5-0.1.5}/test/test_init.py +0 -0
  30. {pdmt5-0.1.4 → pdmt5-0.1.5}/test/test_mt5.py +0 -0
  31. {pdmt5-0.1.4 → pdmt5-0.1.5}/test/test_utils.py +0 -0
@@ -6,10 +6,10 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "uv run ruff check --fix . && uv run ruff format . && uv run pyright . && uv run pytest"
9
+ "command": "uv run ruff format . && uv run ruff check --fix . && uv run pyright . && uv run pytest"
10
10
  }
11
11
  ]
12
12
  }
13
13
  ]
14
14
  }
15
- }
15
+ }
@@ -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
@@ -6,7 +6,6 @@ Pandas-based data handler for MetaTrader 5
6
6
  [![Python Version](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
  [![Platform](https://img.shields.io/badge/platform-Windows-blue.svg)](https://www.microsoft.com/windows)
9
- [![Version](https://img.shields.io/badge/version-0.1.4-green.svg)](https://github.com/dceoy/pdmt5)
10
9
 
11
10
  ## Overview
12
11
 
@@ -172,10 +171,12 @@ Advanced trading operations client that extends Mt5DataClient:
172
171
  - `dry_run` - Test mode flag for simulating trades without execution
173
172
  - **Position Management**:
174
173
  - `close_open_positions()` - Close all positions for specified symbol(s)
175
- - `send_or_check_order()` - Execute or validate orders based on dry_run mode
174
+ - `place_market_order()` - Place market orders with configurable side, volume, and execution modes
175
+ - `update_open_position_sltp()` - Modify stop loss and take profit levels for open positions
176
176
  - **Market Analysis**:
177
177
  - `calculate_minimum_order_margins()` - Calculate minimum required margins for buy/sell orders
178
178
  - `calculate_spread_ratio()` - Calculate normalized bid-ask spread ratio
179
+ - `calculate_new_position_margin_ratio()` - Calculate margin ratio for potential new positions
179
180
  - **Simplified Data Access**:
180
181
  - `fetch_latest_rates_as_df()` - Get recent OHLC data with timeframe strings (e.g., "M1", "H1", "D1")
181
182
  - `fetch_latest_ticks_as_df()` - Get tick data for specified seconds around last tick
@@ -265,18 +266,54 @@ from pdmt5 import Mt5TradingClient
265
266
 
266
267
  # Create trading client with specific order filling mode
267
268
  with Mt5TradingClient(config=config, order_filling_mode="IOC") as trader:
269
+ # Place a market buy order
270
+ order_result = trader.place_market_order(
271
+ symbol="EURUSD",
272
+ volume=0.1,
273
+ order_side="BUY",
274
+ order_filling_mode="IOC", # Immediate or Cancel
275
+ order_time_mode="GTC" # Good Till Cancelled
276
+ )
277
+ print(f"Order placed: {order_result['retcode']}")
278
+
279
+ # Update stop loss and take profit for an open position
280
+ if positions := trader.positions_get_as_df(symbol="EURUSD"):
281
+ position_ticket = positions.iloc[0]['ticket']
282
+ update_result = trader.update_open_position_sltp(
283
+ symbol="EURUSD",
284
+ position_ticket=position_ticket,
285
+ sl=1.0950, # New stop loss
286
+ tp=1.1050 # New take profit
287
+ )
288
+ print(f"Position updated: {update_result['retcode']}")
289
+
290
+ # Calculate margin ratio for a new position
291
+ margin_ratio = trader.calculate_new_position_margin_ratio(
292
+ symbol="EURUSD",
293
+ new_side="SELL",
294
+ new_volume=0.2
295
+ )
296
+ print(f"New position margin ratio: {margin_ratio:.2%}")
297
+
268
298
  # Close all EURUSD positions
269
299
  results = trader.close_open_positions(symbols="EURUSD")
270
300
 
271
301
  if results:
272
- for result in results:
273
- print(f"Closed position {result['position']} with result: {result['retcode']}")
302
+ for symbol, close_results in results.items():
303
+ for result in close_results:
304
+ print(f"Closed position {result.get('position')} with result: {result['retcode']}")
274
305
 
275
306
  # Using dry run mode for testing
276
307
  trader_dry = Mt5TradingClient(config=config, dry_run=True)
277
308
  with trader_dry:
278
- # Test closing positions without actual execution
279
- test_results = trader_dry.close_open_positions(symbols=["EURUSD", "GBPUSD"])
309
+ # Test placing an order without actual execution
310
+ test_order = trader_dry.place_market_order(
311
+ symbol="GBPUSD",
312
+ volume=0.1,
313
+ order_side="SELL",
314
+ dry_run=True # Override instance setting
315
+ )
316
+ print(f"Test order validation: {test_order['retcode']}")
280
317
  ```
281
318
 
282
319
  ### Market Analysis with Mt5TradingClient
@@ -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
  [project]
2
2
  name = "pdmt5"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  description = "Pandas-based data handler for MetaTrader 5"
5
5
  authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
6
6
  maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
@@ -183,17 +183,14 @@ class TestMt5TradingClient:
183
183
  """Test client initialization with default parameters."""
184
184
  client = Mt5TradingClient(mt5=mock_mt5_import)
185
185
  assert client.order_filling_mode == "IOC"
186
- assert client.dry_run is False
187
186
 
188
187
  def test_client_initialization_custom(self, mock_mt5_import: ModuleType) -> None:
189
188
  """Test client initialization with custom parameters."""
190
189
  client = Mt5TradingClient(
191
190
  mt5=mock_mt5_import,
192
191
  order_filling_mode="FOK",
193
- dry_run=True,
194
192
  )
195
193
  assert client.order_filling_mode == "FOK"
196
- assert client.dry_run is True
197
194
 
198
195
  def test_client_initialization_invalid_filling_mode(
199
196
  self, mock_mt5_import: ModuleType
@@ -250,7 +247,7 @@ class TestMt5TradingClient:
250
247
  mock_position_buy: MockPositionInfo,
251
248
  ) -> None:
252
249
  """Test close_position with existing positions in dry run mode."""
253
- client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=True)
250
+ client = Mt5TradingClient(mt5=mock_mt5_import)
254
251
  mock_mt5_import.initialize.return_value = True
255
252
  client.initialize()
256
253
 
@@ -263,7 +260,7 @@ class TestMt5TradingClient:
263
260
  "result": "check_success",
264
261
  }
265
262
 
266
- result = client.close_open_positions("EURUSD")
263
+ result = client.close_open_positions("EURUSD", dry_run=True)
267
264
 
268
265
  assert len(result["EURUSD"]) == 1
269
266
  assert result["EURUSD"][0]["retcode"] == 0
@@ -276,8 +273,8 @@ class TestMt5TradingClient:
276
273
  mock_position_buy: MockPositionInfo,
277
274
  ) -> None:
278
275
  """Test close_position with dry_run parameter override."""
279
- # Client initialized with dry_run=False
280
- client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=False)
276
+ # Client initialized without dry_run
277
+ client = Mt5TradingClient(mt5=mock_mt5_import)
281
278
  mock_mt5_import.initialize.return_value = True
282
279
  client.initialize()
283
280
 
@@ -305,8 +302,8 @@ class TestMt5TradingClient:
305
302
  mock_position_buy: MockPositionInfo,
306
303
  ) -> None:
307
304
  """Test close_position with real mode override."""
308
- # Client initialized with dry_run=True
309
- client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=True)
305
+ # Client initialized without dry_run
306
+ client = Mt5TradingClient(mt5=mock_mt5_import)
310
307
  mock_mt5_import.initialize.return_value = True
311
308
  client.initialize()
312
309
 
@@ -425,7 +422,7 @@ class TestMt5TradingClient:
425
422
  mock_position_buy: MockPositionInfo,
426
423
  ) -> None:
427
424
  """Test close_open_positions with additional kwargs and dry_run override."""
428
- client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=False)
425
+ client = Mt5TradingClient(mt5=mock_mt5_import)
429
426
  mock_mt5_import.initialize.return_value = True
430
427
  client.initialize()
431
428
 
@@ -456,8 +453,8 @@ class TestMt5TradingClient:
456
453
  self,
457
454
  mock_mt5_import: ModuleType,
458
455
  ) -> None:
459
- """Test send_or_check_order in dry run mode with success."""
460
- client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=True)
456
+ """Test _send_or_check_order in dry run mode with success."""
457
+ client = Mt5TradingClient(mt5=mock_mt5_import)
461
458
  mock_mt5_import.initialize.return_value = True
462
459
  client.initialize()
463
460
 
@@ -475,7 +472,7 @@ class TestMt5TradingClient:
475
472
  "result": "check_success",
476
473
  }
477
474
 
478
- result = client.send_or_check_order(request)
475
+ result = client._send_or_check_order(request, dry_run=True)
479
476
 
480
477
  assert result["retcode"] == 0
481
478
  assert result["result"] == "check_success"
@@ -485,8 +482,8 @@ class TestMt5TradingClient:
485
482
  self,
486
483
  mock_mt5_import: ModuleType,
487
484
  ) -> None:
488
- """Test send_or_check_order in real mode with success."""
489
- client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=False)
485
+ """Test _send_or_check_order in real mode with success."""
486
+ client = Mt5TradingClient(mt5=mock_mt5_import)
490
487
  mock_mt5_import.initialize.return_value = True
491
488
  client.initialize()
492
489
 
@@ -504,7 +501,7 @@ class TestMt5TradingClient:
504
501
  "result": "send_success",
505
502
  }
506
503
 
507
- result = client.send_or_check_order(request)
504
+ result = client._send_or_check_order(request)
508
505
 
509
506
  assert result["retcode"] == 10009
510
507
  assert result["result"] == "send_success"
@@ -514,8 +511,8 @@ class TestMt5TradingClient:
514
511
  self,
515
512
  mock_mt5_import: ModuleType,
516
513
  ) -> None:
517
- """Test send_or_check_order with trade disabled."""
518
- client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=False)
514
+ """Test _send_or_check_order with trade disabled."""
515
+ client = Mt5TradingClient(mt5=mock_mt5_import)
519
516
  mock_mt5_import.initialize.return_value = True
520
517
  client.initialize()
521
518
 
@@ -533,7 +530,7 @@ class TestMt5TradingClient:
533
530
  "comment": "Trade disabled",
534
531
  }
535
532
 
536
- result = client.send_or_check_order(request)
533
+ result = client._send_or_check_order(request)
537
534
 
538
535
  assert result["retcode"] == 10017
539
536
 
@@ -541,8 +538,8 @@ class TestMt5TradingClient:
541
538
  self,
542
539
  mock_mt5_import: ModuleType,
543
540
  ) -> None:
544
- """Test send_or_check_order with market closed."""
545
- client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=False)
541
+ """Test _send_or_check_order with market closed."""
542
+ client = Mt5TradingClient(mt5=mock_mt5_import)
546
543
  mock_mt5_import.initialize.return_value = True
547
544
  client.initialize()
548
545
 
@@ -560,7 +557,7 @@ class TestMt5TradingClient:
560
557
  "comment": "Market closed",
561
558
  }
562
559
 
563
- result = client.send_or_check_order(request)
560
+ result = client._send_or_check_order(request)
564
561
 
565
562
  assert result["retcode"] == 10018
566
563
 
@@ -568,8 +565,8 @@ class TestMt5TradingClient:
568
565
  self,
569
566
  mock_mt5_import: ModuleType,
570
567
  ) -> None:
571
- """Test send_or_check_order with failure."""
572
- client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=False)
568
+ """Test _send_or_check_order with failure."""
569
+ client = Mt5TradingClient(mt5=mock_mt5_import)
573
570
  mock_mt5_import.initialize.return_value = True
574
571
  client.initialize()
575
572
 
@@ -588,14 +585,14 @@ class TestMt5TradingClient:
588
585
  }
589
586
 
590
587
  with pytest.raises(Mt5TradingError, match=r"order_send\(\) failed and aborted"):
591
- client.send_or_check_order(request)
588
+ client._send_or_check_order(request)
592
589
 
593
590
  def test_send_or_check_order_dry_run_failure(
594
591
  self,
595
592
  mock_mt5_import: ModuleType,
596
593
  ) -> None:
597
- """Test send_or_check_order in dry run mode with failure."""
598
- client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=True)
594
+ """Test _send_or_check_order in dry run mode with failure."""
595
+ client = Mt5TradingClient(mt5=mock_mt5_import)
599
596
  mock_mt5_import.initialize.return_value = True
600
597
  client.initialize()
601
598
 
@@ -616,15 +613,15 @@ class TestMt5TradingClient:
616
613
  with pytest.raises(
617
614
  Mt5TradingError, match=r"order_check\(\) failed and aborted"
618
615
  ):
619
- client.send_or_check_order(request)
616
+ client._send_or_check_order(request, dry_run=True)
620
617
 
621
618
  def test_send_or_check_order_dry_run_override(
622
619
  self,
623
620
  mock_mt5_import: ModuleType,
624
621
  ) -> None:
625
- """Test send_or_check_order with dry_run parameter override."""
626
- # Client initialized with dry_run=False
627
- client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=False)
622
+ """Test _send_or_check_order with dry_run parameter override."""
623
+ # Client initialized without dry_run
624
+ client = Mt5TradingClient(mt5=mock_mt5_import)
628
625
  mock_mt5_import.initialize.return_value = True
629
626
  client.initialize()
630
627
 
@@ -643,7 +640,7 @@ class TestMt5TradingClient:
643
640
  }
644
641
 
645
642
  # Override with dry_run=True
646
- result = client.send_or_check_order(request, dry_run=True)
643
+ result = client._send_or_check_order(request, dry_run=True)
647
644
 
648
645
  assert result["retcode"] == 0
649
646
  assert result["result"] == "check_success"
@@ -655,9 +652,9 @@ class TestMt5TradingClient:
655
652
  self,
656
653
  mock_mt5_import: ModuleType,
657
654
  ) -> None:
658
- """Test send_or_check_order with real mode override."""
659
- # Client initialized with dry_run=True
660
- client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=True)
655
+ """Test _send_or_check_order with real mode override."""
656
+ # Client initialized without dry_run
657
+ client = Mt5TradingClient(mt5=mock_mt5_import)
661
658
  mock_mt5_import.initialize.return_value = True
662
659
  client.initialize()
663
660
 
@@ -676,7 +673,7 @@ class TestMt5TradingClient:
676
673
  }
677
674
 
678
675
  # Override with dry_run=False
679
- result = client.send_or_check_order(request, dry_run=False)
676
+ result = client._send_or_check_order(request, dry_run=False)
680
677
 
681
678
  assert result["retcode"] == 10009
682
679
  assert result["result"] == "send_success"
@@ -684,6 +681,52 @@ class TestMt5TradingClient:
684
681
  mock_mt5_import.order_send.assert_called_once_with(request)
685
682
  mock_mt5_import.order_check.assert_not_called()
686
683
 
684
+ def test_place_market_order(
685
+ self,
686
+ mock_mt5_import: ModuleType,
687
+ ) -> None:
688
+ """Test place_market_order method."""
689
+ client = Mt5TradingClient(mt5=mock_mt5_import)
690
+ mock_mt5_import.initialize.return_value = True
691
+ client.initialize()
692
+
693
+ # Mock MT5 constants
694
+ mock_mt5_import.ORDER_TYPE_BUY = 0
695
+ mock_mt5_import.ORDER_FILLING_IOC = 1
696
+ mock_mt5_import.ORDER_TIME_GTC = 0
697
+ mock_mt5_import.TRADE_ACTION_DEAL = 1
698
+
699
+ # Mock successful order send
700
+ mock_mt5_import.order_send.return_value.retcode = 10009
701
+ mock_mt5_import.order_send.return_value._asdict.return_value = {
702
+ "retcode": 10009,
703
+ "deal": 123456,
704
+ "order": 789012,
705
+ }
706
+
707
+ result = client.place_market_order(
708
+ symbol="EURUSD",
709
+ volume=0.1,
710
+ order_side="BUY",
711
+ order_filling_mode="IOC",
712
+ order_time_mode="GTC",
713
+ )
714
+
715
+ assert result["retcode"] == 10009
716
+ assert result["deal"] == 123456
717
+ assert result["order"] == 789012
718
+
719
+ # Verify the request was built correctly
720
+ expected_request = {
721
+ "action": 1, # TRADE_ACTION_DEAL
722
+ "symbol": "EURUSD",
723
+ "volume": 0.1,
724
+ "type": 0, # ORDER_TYPE_BUY
725
+ "type_filling": 1, # ORDER_FILLING_IOC
726
+ "type_time": 0, # ORDER_TIME_GTC
727
+ }
728
+ mock_mt5_import.order_send.assert_called_once_with(expected_request)
729
+
687
730
  def test_order_filling_mode_constants(
688
731
  self,
689
732
  mock_mt5_import: ModuleType,
@@ -751,7 +794,7 @@ class TestMt5TradingClient:
751
794
  mock_position_sell: MockPositionInfo,
752
795
  ) -> None:
753
796
  """Test _fetch_and_close_position with dry_run parameter."""
754
- client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=False)
797
+ client = Mt5TradingClient(mt5=mock_mt5_import)
755
798
  mock_mt5_import.initialize.return_value = True
756
799
  client.initialize()
757
800
 
@@ -780,9 +823,9 @@ class TestMt5TradingClient:
780
823
  mock_mt5_import: ModuleType,
781
824
  mock_position_buy: MockPositionInfo,
782
825
  ) -> None:
783
- """Test _fetch_and_close_position inherits instance dry_run if not given."""
784
- # Client initialized with dry_run=True
785
- client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=True)
826
+ """Test _fetch_and_close_position does not inherit dry_run from instance."""
827
+ # Client initialized without dry_run
828
+ client = Mt5TradingClient(mt5=mock_mt5_import)
786
829
  mock_mt5_import.initialize.return_value = True
787
830
  client.initialize()
788
831
 
@@ -794,8 +837,8 @@ class TestMt5TradingClient:
794
837
  "result": "check_success",
795
838
  }
796
839
 
797
- # Call without specifying dry_run - should use instance's dry_run=True
798
- result = client._fetch_and_close_position(symbol="EURUSD")
840
+ # Call with dry_run=True explicitly
841
+ result = client._fetch_and_close_position(symbol="EURUSD", dry_run=True)
799
842
 
800
843
  assert len(result) == 1
801
844
  assert result[0]["retcode"] == 0
@@ -1300,3 +1343,421 @@ class TestMt5TradingClient:
1300
1343
 
1301
1344
  # Verify order_calc_margin was called twice (ask and bid)
1302
1345
  assert mock_mt5_import.order_calc_margin.call_count == 2
1346
+
1347
+ def test_calculate_new_position_margin_ratio_no_equity(
1348
+ self, mock_mt5_import: ModuleType
1349
+ ) -> None:
1350
+ """Test calculating margin ratio when account has no equity."""
1351
+ client = Mt5TradingClient(mt5=mock_mt5_import)
1352
+ mock_mt5_import.initialize.return_value = True
1353
+ client.initialize()
1354
+
1355
+ # Mock account info with zero equity
1356
+ mock_mt5_import.account_info.return_value._asdict.return_value = {
1357
+ "equity": 0.0,
1358
+ }
1359
+
1360
+ result = client.calculate_new_position_margin_ratio(
1361
+ symbol="EURUSD", new_side="BUY", new_volume=0.1
1362
+ )
1363
+
1364
+ assert result == 0.0
1365
+
1366
+ def test_calculate_new_position_margin_ratio_buy_position(
1367
+ self, mock_mt5_import: ModuleType, mocker: MockerFixture
1368
+ ) -> None:
1369
+ """Test calculating margin ratio for a new buy position."""
1370
+ client = Mt5TradingClient(mt5=mock_mt5_import)
1371
+ mock_mt5_import.initialize.return_value = True
1372
+ client.initialize()
1373
+
1374
+ # Mock account info
1375
+ mock_mt5_import.account_info.return_value._asdict.return_value = {
1376
+ "equity": 10000.0,
1377
+ }
1378
+
1379
+ # Mock existing positions
1380
+ mock_position = mocker.MagicMock()
1381
+ mock_position._asdict.return_value = {
1382
+ "ticket": 12345,
1383
+ "symbol": "EURUSD",
1384
+ "volume": 0.1,
1385
+ "type": 0, # POSITION_TYPE_BUY
1386
+ "time": 1234567890,
1387
+ "price_open": 1.2,
1388
+ "price_current": 1.205,
1389
+ "profit": 5.0,
1390
+ "sl": 0.0,
1391
+ "tp": 0.0,
1392
+ "identifier": 12345,
1393
+ "reason": 0,
1394
+ "swap": 0.0,
1395
+ "magic": 0,
1396
+ "comment": "test",
1397
+ "external_id": "",
1398
+ }
1399
+ mock_mt5_import.positions_get.return_value = [mock_position]
1400
+
1401
+ # Mock symbol tick info
1402
+ mock_mt5_import.symbol_info_tick.return_value._asdict.return_value = {
1403
+ "time": pd.Timestamp("2009-02-14 00:31:30"),
1404
+ "ask": 1.1002,
1405
+ "bid": 1.1000,
1406
+ }
1407
+
1408
+ # Mock order calc margin
1409
+ mock_mt5_import.order_calc_margin.return_value = 1000.0
1410
+
1411
+ result = client.calculate_new_position_margin_ratio(
1412
+ symbol="EURUSD", new_side="BUY", new_volume=0.1
1413
+ )
1414
+
1415
+ # Should return (new_margin + current_margin) / equity
1416
+ # current_margin = 100.0 (from position), new_margin = 1000.0
1417
+ expected_ratio = abs((1000.0 + 100.0) / 10000.0)
1418
+ assert result == expected_ratio
1419
+
1420
+ def test_calculate_new_position_margin_ratio_sell_position(
1421
+ self, mock_mt5_import: ModuleType
1422
+ ) -> None:
1423
+ """Test calculating margin ratio for a new sell position."""
1424
+ client = Mt5TradingClient(mt5=mock_mt5_import)
1425
+ mock_mt5_import.initialize.return_value = True
1426
+ client.initialize()
1427
+
1428
+ # Mock account info
1429
+ mock_mt5_import.account_info.return_value._asdict.return_value = {
1430
+ "equity": 10000.0,
1431
+ }
1432
+
1433
+ # Mock empty positions
1434
+ mock_mt5_import.positions_get.return_value = []
1435
+
1436
+ # Mock symbol tick info
1437
+ mock_mt5_import.symbol_info_tick.return_value._asdict.return_value = {
1438
+ "time": pd.Timestamp("2009-02-14 00:31:30"),
1439
+ "ask": 1.1002,
1440
+ "bid": 1.1000,
1441
+ }
1442
+
1443
+ # Mock order calc margin
1444
+ mock_mt5_import.order_calc_margin.return_value = 1000.0
1445
+
1446
+ result = client.calculate_new_position_margin_ratio(
1447
+ symbol="EURUSD", new_side="SELL", new_volume=0.1
1448
+ )
1449
+
1450
+ # Should return abs(-new_margin / equity) for sell
1451
+ expected_ratio = abs(-1000.0 / 10000.0)
1452
+ assert result == expected_ratio
1453
+
1454
+ def test_calculate_new_position_margin_ratio_zero_volume(
1455
+ self, mock_mt5_import: ModuleType
1456
+ ) -> None:
1457
+ """Test calculating margin ratio with zero volume."""
1458
+ client = Mt5TradingClient(mt5=mock_mt5_import)
1459
+ mock_mt5_import.initialize.return_value = True
1460
+ client.initialize()
1461
+
1462
+ # Mock account info
1463
+ mock_mt5_import.account_info.return_value._asdict.return_value = {
1464
+ "equity": 10000.0,
1465
+ }
1466
+
1467
+ # Mock empty positions
1468
+ mock_mt5_import.positions_get.return_value = []
1469
+
1470
+ # Mock symbol tick info
1471
+ mock_mt5_import.symbol_info_tick.return_value._asdict.return_value = {
1472
+ "time": pd.Timestamp("2009-02-14 00:31:30"),
1473
+ "ask": 1.1002,
1474
+ "bid": 1.1000,
1475
+ }
1476
+
1477
+ result = client.calculate_new_position_margin_ratio(
1478
+ symbol="EURUSD", new_side="BUY", new_volume=0
1479
+ )
1480
+
1481
+ # Should return 0 since new_volume is 0
1482
+ assert result == 0.0
1483
+
1484
+ def test_calculate_new_position_margin_ratio_invalid_side(
1485
+ self, mock_mt5_import: ModuleType
1486
+ ) -> None:
1487
+ """Test calculating margin ratio with invalid side."""
1488
+ client = Mt5TradingClient(mt5=mock_mt5_import)
1489
+ mock_mt5_import.initialize.return_value = True
1490
+ client.initialize()
1491
+
1492
+ # Mock account info
1493
+ mock_mt5_import.account_info.return_value._asdict.return_value = {
1494
+ "equity": 10000.0,
1495
+ }
1496
+
1497
+ # Mock empty positions
1498
+ mock_mt5_import.positions_get.return_value = []
1499
+
1500
+ # Mock symbol tick info
1501
+ mock_mt5_import.symbol_info_tick.return_value._asdict.return_value = {
1502
+ "time": pd.Timestamp("2009-02-14 00:31:30"),
1503
+ "ask": 1.1002,
1504
+ "bid": 1.1000,
1505
+ }
1506
+
1507
+ result = client.calculate_new_position_margin_ratio(
1508
+ symbol="EURUSD", new_side=None, new_volume=0.1
1509
+ )
1510
+
1511
+ # Should return 0 since side is invalid
1512
+ assert result == 0.0
1513
+
1514
+ def test_update_sltp_for_open_positions(self, mock_mt5_import: ModuleType) -> None:
1515
+ """Test update_sltp_for_open_positions method."""
1516
+ client = Mt5TradingClient(mt5=mock_mt5_import)
1517
+ mock_mt5_import.initialize.return_value = True
1518
+ client.initialize()
1519
+
1520
+ # Mock MT5 constants
1521
+ mock_mt5_import.TRADE_ACTION_SLTP = 6
1522
+
1523
+ # Mock symbol info
1524
+ mock_mt5_import.symbol_info.return_value._asdict.return_value = {
1525
+ "digits": 5,
1526
+ }
1527
+
1528
+ # Mock positions for the symbol
1529
+ mock_position = MockPositionInfo(
1530
+ ticket=123456,
1531
+ time=123456789,
1532
+ type=0, # buy
1533
+ magic=0,
1534
+ identifier=123456,
1535
+ reason=0,
1536
+ volume=0.1,
1537
+ price_open=1.1000,
1538
+ sl=1.0900,
1539
+ tp=1.1100,
1540
+ price_current=1.1050,
1541
+ swap=0.0,
1542
+ profit=50.0,
1543
+ symbol="EURUSD",
1544
+ comment="test",
1545
+ external_id="",
1546
+ )
1547
+ mock_mt5_import.positions_get.return_value = [mock_position]
1548
+
1549
+ # Mock successful order send
1550
+ mock_mt5_import.order_send.return_value.retcode = 10009
1551
+ mock_mt5_import.order_send.return_value._asdict.return_value = {
1552
+ "retcode": 10009,
1553
+ "deal": 0,
1554
+ "order": 789012,
1555
+ }
1556
+
1557
+ result = client.update_sltp_for_open_positions(
1558
+ symbol="EURUSD",
1559
+ tickets=[123456],
1560
+ stop_loss=1.0950,
1561
+ take_profit=1.1050,
1562
+ )
1563
+
1564
+ # Now returns a list of dictionaries
1565
+ assert isinstance(result, list)
1566
+ assert len(result) == 1
1567
+ assert result[0]["retcode"] == 10009
1568
+ assert result[0]["order"] == 789012
1569
+
1570
+ def test_update_sltp_for_open_positions_no_positions(
1571
+ self, mock_mt5_import: ModuleType
1572
+ ) -> None:
1573
+ """Test update_sltp_for_open_positions when no positions exist for symbol."""
1574
+ client = Mt5TradingClient(mt5=mock_mt5_import)
1575
+ mock_mt5_import.initialize.return_value = True
1576
+ client.initialize()
1577
+
1578
+ # Mock empty positions result
1579
+ mock_mt5_import.positions_get.return_value = []
1580
+
1581
+ result = client.update_sltp_for_open_positions(
1582
+ symbol="EURUSD",
1583
+ tickets=[123456],
1584
+ stop_loss=1.0950,
1585
+ take_profit=1.1050,
1586
+ )
1587
+
1588
+ # Should return empty list and log warning
1589
+ assert result == []
1590
+ # Verify positions_get was called with correct symbol
1591
+ mock_mt5_import.positions_get.assert_called_with(symbol="EURUSD")
1592
+
1593
+ def test_update_sltp_for_open_positions_no_matching_tickets(
1594
+ self, mock_mt5_import: ModuleType
1595
+ ) -> None:
1596
+ """Test update_sltp_for_open_positions when positions exist but no tickets match.""" # noqa: E501
1597
+ client = Mt5TradingClient(mt5=mock_mt5_import)
1598
+ mock_mt5_import.initialize.return_value = True
1599
+ client.initialize()
1600
+
1601
+ # Mock MT5 constants
1602
+ mock_mt5_import.TRADE_ACTION_SLTP = 6
1603
+
1604
+ # Mock symbol info
1605
+ mock_mt5_import.symbol_info.return_value._asdict.return_value = {
1606
+ "digits": 5,
1607
+ }
1608
+
1609
+ # Mock positions with different tickets
1610
+ mock_position = MockPositionInfo(
1611
+ ticket=999999, # Different ticket
1612
+ time=123456789,
1613
+ type=0, # buy
1614
+ magic=0,
1615
+ identifier=999999,
1616
+ reason=0,
1617
+ volume=0.1,
1618
+ price_open=1.1000,
1619
+ sl=1.0900,
1620
+ tp=1.1100,
1621
+ price_current=1.1050,
1622
+ swap=0.0,
1623
+ profit=50.0,
1624
+ symbol="EURUSD",
1625
+ comment="test",
1626
+ external_id="",
1627
+ )
1628
+ mock_mt5_import.positions_get.return_value = [mock_position]
1629
+
1630
+ result = client.update_sltp_for_open_positions(
1631
+ symbol="EURUSD",
1632
+ tickets=[123456], # This ticket doesn't exist
1633
+ stop_loss=1.0950,
1634
+ take_profit=1.1050,
1635
+ )
1636
+
1637
+ # Should return empty list and log warning
1638
+ assert result == []
1639
+
1640
+ def test_update_sltp_for_open_positions_same_sltp_values(
1641
+ self, mock_mt5_import: ModuleType
1642
+ ) -> None:
1643
+ """Test update_sltp_for_open_positions when SL/TP values are already the same.""" # noqa: E501
1644
+ client = Mt5TradingClient(mt5=mock_mt5_import)
1645
+ mock_mt5_import.initialize.return_value = True
1646
+ client.initialize()
1647
+
1648
+ # Mock MT5 constants
1649
+ mock_mt5_import.TRADE_ACTION_SLTP = 6
1650
+
1651
+ # Mock symbol info
1652
+ mock_mt5_import.symbol_info.return_value._asdict.return_value = {
1653
+ "digits": 5,
1654
+ }
1655
+
1656
+ # Mock positions with same SL/TP as requested
1657
+ mock_position = MockPositionInfo(
1658
+ ticket=123456,
1659
+ time=123456789,
1660
+ type=0, # buy
1661
+ magic=0,
1662
+ identifier=123456,
1663
+ reason=0,
1664
+ volume=0.1,
1665
+ price_open=1.1000,
1666
+ sl=1.0950, # Same as requested stop_loss
1667
+ tp=1.1050, # Same as requested take_profit
1668
+ price_current=1.1050,
1669
+ swap=0.0,
1670
+ profit=50.0,
1671
+ symbol="EURUSD",
1672
+ comment="test",
1673
+ external_id="",
1674
+ )
1675
+ mock_mt5_import.positions_get.return_value = [mock_position]
1676
+
1677
+ result = client.update_sltp_for_open_positions(
1678
+ symbol="EURUSD",
1679
+ tickets=[123456],
1680
+ stop_loss=1.0950, # Same as position's sl
1681
+ take_profit=1.1050, # Same as position's tp
1682
+ )
1683
+
1684
+ # Should return empty list since no update is needed
1685
+ assert result == []
1686
+ # Verify order_send was NOT called
1687
+ mock_mt5_import.order_send.assert_not_called()
1688
+
1689
+ def test_update_sltp_for_open_positions_no_tickets(
1690
+ self, mock_mt5_import: ModuleType
1691
+ ) -> None:
1692
+ """Test update_sltp_for_open_positions without specifying tickets."""
1693
+ client = Mt5TradingClient(mt5=mock_mt5_import)
1694
+ mock_mt5_import.initialize.return_value = True
1695
+ client.initialize()
1696
+
1697
+ # Mock MT5 constants
1698
+ mock_mt5_import.TRADE_ACTION_SLTP = 6
1699
+
1700
+ # Mock symbol info
1701
+ mock_mt5_import.symbol_info.return_value._asdict.return_value = {
1702
+ "digits": 5,
1703
+ }
1704
+
1705
+ # Mock positions for the symbol
1706
+ mock_position1 = MockPositionInfo(
1707
+ ticket=123456,
1708
+ time=123456789,
1709
+ type=0, # buy
1710
+ magic=0,
1711
+ identifier=123456,
1712
+ reason=0,
1713
+ volume=0.1,
1714
+ price_open=1.1000,
1715
+ sl=1.0900,
1716
+ tp=1.1100,
1717
+ price_current=1.1050,
1718
+ swap=0.0,
1719
+ profit=50.0,
1720
+ symbol="EURUSD",
1721
+ comment="test",
1722
+ external_id="",
1723
+ )
1724
+ mock_position2 = MockPositionInfo(
1725
+ ticket=654321,
1726
+ time=123456789,
1727
+ type=1, # sell
1728
+ magic=0,
1729
+ identifier=654321,
1730
+ reason=0,
1731
+ volume=0.2,
1732
+ price_open=1.1050,
1733
+ sl=1.1150,
1734
+ tp=1.0950,
1735
+ price_current=1.1050,
1736
+ swap=0.0,
1737
+ profit=-20.0,
1738
+ symbol="EURUSD",
1739
+ comment="test2",
1740
+ external_id="",
1741
+ )
1742
+ mock_mt5_import.positions_get.return_value = [mock_position1, mock_position2]
1743
+
1744
+ # Mock successful order send
1745
+ mock_mt5_import.order_send.return_value.retcode = 10009
1746
+ mock_mt5_import.order_send.return_value._asdict.return_value = {
1747
+ "retcode": 10009,
1748
+ "deal": 0,
1749
+ "order": 789012,
1750
+ }
1751
+
1752
+ # Call without tickets to update all positions
1753
+ result = client.update_sltp_for_open_positions(
1754
+ symbol="EURUSD",
1755
+ tickets=None, # No tickets specified
1756
+ stop_loss=1.0950,
1757
+ take_profit=1.1050,
1758
+ )
1759
+
1760
+ # Should return results for both positions
1761
+ assert isinstance(result, list)
1762
+ assert len(result) == 2
1763
+ assert all(r["retcode"] == 10009 for r in result)
@@ -613,7 +613,7 @@ wheels = [
613
613
 
614
614
  [[package]]
615
615
  name = "pdmt5"
616
- version = "0.1.4"
616
+ version = "0.1.5"
617
617
  source = { editable = "." }
618
618
  dependencies = [
619
619
  { name = "metatrader5", marker = "sys_platform == 'win32'" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes