pdmt5 0.1.6__tar.gz → 0.1.7__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.6 → pdmt5-0.1.7}/PKG-INFO +8 -21
  2. {pdmt5-0.1.6 → pdmt5-0.1.7}/README.md +7 -20
  3. {pdmt5-0.1.6 → pdmt5-0.1.7}/docs/api/trading.md +18 -17
  4. {pdmt5-0.1.6 → pdmt5-0.1.7}/pdmt5/trading.py +40 -28
  5. {pdmt5-0.1.6 → pdmt5-0.1.7}/pyproject.toml +1 -1
  6. {pdmt5-0.1.6 → pdmt5-0.1.7}/test/test_trading.py +79 -8
  7. {pdmt5-0.1.6 → pdmt5-0.1.7}/uv.lock +1 -1
  8. {pdmt5-0.1.6 → pdmt5-0.1.7}/.claude/settings.json +0 -0
  9. {pdmt5-0.1.6 → pdmt5-0.1.7}/.github/FUNDING.yml +0 -0
  10. {pdmt5-0.1.6 → pdmt5-0.1.7}/.github/copilot-instructions.md +0 -0
  11. {pdmt5-0.1.6 → pdmt5-0.1.7}/.github/dependabot.yml +0 -0
  12. {pdmt5-0.1.6 → pdmt5-0.1.7}/.github/workflows/ci.yml +0 -0
  13. {pdmt5-0.1.6 → pdmt5-0.1.7}/.gitignore +0 -0
  14. {pdmt5-0.1.6 → pdmt5-0.1.7}/CLAUDE.md +0 -0
  15. {pdmt5-0.1.6 → pdmt5-0.1.7}/LICENSE +0 -0
  16. {pdmt5-0.1.6 → pdmt5-0.1.7}/docs/api/dataframe.md +0 -0
  17. {pdmt5-0.1.6 → pdmt5-0.1.7}/docs/api/index.md +0 -0
  18. {pdmt5-0.1.6 → pdmt5-0.1.7}/docs/api/mt5.md +0 -0
  19. {pdmt5-0.1.6 → pdmt5-0.1.7}/docs/api/utils.md +0 -0
  20. {pdmt5-0.1.6 → pdmt5-0.1.7}/docs/index.md +0 -0
  21. {pdmt5-0.1.6 → pdmt5-0.1.7}/mkdocs.yml +0 -0
  22. {pdmt5-0.1.6 → pdmt5-0.1.7}/pdmt5/__init__.py +0 -0
  23. {pdmt5-0.1.6 → pdmt5-0.1.7}/pdmt5/dataframe.py +0 -0
  24. {pdmt5-0.1.6 → pdmt5-0.1.7}/pdmt5/mt5.py +0 -0
  25. {pdmt5-0.1.6 → pdmt5-0.1.7}/pdmt5/utils.py +0 -0
  26. {pdmt5-0.1.6 → pdmt5-0.1.7}/renovate.json +0 -0
  27. {pdmt5-0.1.6 → pdmt5-0.1.7}/test/__init__.py +0 -0
  28. {pdmt5-0.1.6 → pdmt5-0.1.7}/test/test_dataframe.py +0 -0
  29. {pdmt5-0.1.6 → pdmt5-0.1.7}/test/test_init.py +0 -0
  30. {pdmt5-0.1.6 → pdmt5-0.1.7}/test/test_mt5.py +0 -0
  31. {pdmt5-0.1.6 → pdmt5-0.1.7}/test/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdmt5
3
- Version: 0.1.6
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>
@@ -191,9 +191,6 @@ Extends Mt5Client with pandas DataFrame and dictionary conversions:
191
191
 
192
192
  Advanced trading operations client that extends Mt5DataClient:
193
193
 
194
- - **Trading Configuration**:
195
- - `order_filling_mode` - Order execution mode: "IOC" (default), "FOK", or "RETURN"
196
- - `dry_run` - Test mode flag for simulating trades without execution
197
194
  - **Position Management**:
198
195
  - `close_open_positions()` - Close all positions for specified symbol(s)
199
196
  - `place_market_order()` - Place market orders with configurable side, volume, and execution modes
@@ -213,7 +210,6 @@ Advanced trading operations client that extends Mt5DataClient:
213
210
  - Comprehensive error handling with `Mt5TradingError`
214
211
  - Support for batch operations on multiple symbols
215
212
  - Automatic position closing with proper order type reversal
216
- - Dry run mode for strategy testing without real trades
217
213
 
218
214
  ### Configuration
219
215
 
@@ -290,8 +286,8 @@ with Mt5DataClient(config=config) as client:
290
286
  ```python
291
287
  from pdmt5 import Mt5TradingClient
292
288
 
293
- # Create trading client with specific order filling mode
294
- with Mt5TradingClient(config=config, order_filling_mode="IOC") as trader:
289
+ # Create trading client
290
+ with Mt5TradingClient(config=config) as trader:
295
291
  # Place a market buy order
296
292
  order_result = trader.place_market_order(
297
293
  symbol="EURUSD",
@@ -319,25 +315,16 @@ with Mt5TradingClient(config=config, order_filling_mode="IOC") as trader:
319
315
  )
320
316
  print(f"New position margin ratio: {margin_ratio:.2%}")
321
317
 
322
- # Close all EURUSD positions
323
- 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
+ )
324
323
 
325
324
  if results:
326
325
  for symbol, close_results in results.items():
327
326
  for result in close_results:
328
327
  print(f"Closed position {result.get('position')} with result: {result['retcode']}")
329
-
330
- # Using dry run mode for testing
331
- trader_dry = Mt5TradingClient(config=config, dry_run=True)
332
- with trader_dry:
333
- # Test placing an order without actual execution
334
- test_order = trader_dry.place_market_order(
335
- symbol="GBPUSD",
336
- volume=0.1,
337
- order_side="SELL",
338
- dry_run=True # Override instance setting
339
- )
340
- print(f"Test order validation: {test_order['retcode']}")
341
328
  ```
342
329
 
343
330
  ### Market Analysis with Mt5TradingClient
@@ -168,9 +168,6 @@ Extends Mt5Client with pandas DataFrame and dictionary conversions:
168
168
 
169
169
  Advanced trading operations client that extends Mt5DataClient:
170
170
 
171
- - **Trading Configuration**:
172
- - `order_filling_mode` - Order execution mode: "IOC" (default), "FOK", or "RETURN"
173
- - `dry_run` - Test mode flag for simulating trades without execution
174
171
  - **Position Management**:
175
172
  - `close_open_positions()` - Close all positions for specified symbol(s)
176
173
  - `place_market_order()` - Place market orders with configurable side, volume, and execution modes
@@ -190,7 +187,6 @@ Advanced trading operations client that extends Mt5DataClient:
190
187
  - Comprehensive error handling with `Mt5TradingError`
191
188
  - Support for batch operations on multiple symbols
192
189
  - Automatic position closing with proper order type reversal
193
- - Dry run mode for strategy testing without real trades
194
190
 
195
191
  ### Configuration
196
192
 
@@ -267,8 +263,8 @@ with Mt5DataClient(config=config) as client:
267
263
  ```python
268
264
  from pdmt5 import Mt5TradingClient
269
265
 
270
- # Create trading client with specific order filling mode
271
- with Mt5TradingClient(config=config, order_filling_mode="IOC") as trader:
266
+ # Create trading client
267
+ with Mt5TradingClient(config=config) as trader:
272
268
  # Place a market buy order
273
269
  order_result = trader.place_market_order(
274
270
  symbol="EURUSD",
@@ -296,25 +292,16 @@ with Mt5TradingClient(config=config, order_filling_mode="IOC") as trader:
296
292
  )
297
293
  print(f"New position margin ratio: {margin_ratio:.2%}")
298
294
 
299
- # Close all EURUSD positions
300
- results = trader.close_open_positions(symbols="EURUSD")
295
+ # Close all EURUSD positions with specific order filling mode
296
+ results = trader.close_open_positions(
297
+ symbols="EURUSD",
298
+ order_filling_mode="FOK" # Fill or Kill
299
+ )
301
300
 
302
301
  if results:
303
302
  for symbol, close_results in results.items():
304
303
  for result in close_results:
305
304
  print(f"Closed position {result.get('position')} with result: {result['retcode']}")
306
-
307
- # Using dry run mode for testing
308
- trader_dry = Mt5TradingClient(config=config, dry_run=True)
309
- with trader_dry:
310
- # Test placing an order without actual execution
311
- test_order = trader_dry.place_market_order(
312
- symbol="GBPUSD",
313
- volume=0.1,
314
- order_side="SELL",
315
- dry_run=True # Override instance setting
316
- )
317
- print(f"Test order validation: {test_order['retcode']}")
318
305
  ```
319
306
 
320
307
  ### Market Analysis with Mt5TradingClient
@@ -70,33 +70,34 @@ with client:
70
70
  ### Order Filling Modes
71
71
 
72
72
  ```python
73
- # Configure different order filling modes
74
- # IOC (Immediate or Cancel) - default
75
- client_ioc = Mt5TradingClient(
76
- config=config,
77
- order_filling_mode="IOC"
78
- )
73
+ with Mt5TradingClient(config=config) as client:
74
+ # Use IOC (Immediate or Cancel) - default
75
+ results_ioc = client.close_open_positions(
76
+ symbols="EURUSD",
77
+ order_filling_mode="IOC"
78
+ )
79
79
 
80
- # FOK (Fill or Kill)
81
- client_fok = Mt5TradingClient(
82
- config=config,
83
- order_filling_mode="FOK"
84
- )
80
+ # Use FOK (Fill or Kill)
81
+ results_fok = client.close_open_positions(
82
+ symbols="GBPUSD",
83
+ order_filling_mode="FOK"
84
+ )
85
85
 
86
- # RETURN (Return if not filled)
87
- client_return = Mt5TradingClient(
88
- config=config,
89
- order_filling_mode="RETURN"
90
- )
86
+ # Use RETURN (Return if not filled)
87
+ results_return = client.close_open_positions(
88
+ symbols="USDJPY",
89
+ order_filling_mode="RETURN"
90
+ )
91
91
  ```
92
92
 
93
93
  ### Custom Order Parameters
94
94
 
95
95
  ```python
96
96
  with client:
97
- # Close positions with custom parameters
97
+ # Close positions with custom parameters and order filling mode
98
98
  results = client.close_open_positions(
99
99
  "EURUSD",
100
+ order_filling_mode="IOC", # Specify per method call
100
101
  comment="Closing all EURUSD positions",
101
102
  deviation=10 # Maximum price deviation
102
103
  )
@@ -6,7 +6,7 @@ from datetime import timedelta
6
6
  from math import floor
7
7
  from typing import TYPE_CHECKING, Any, Literal
8
8
 
9
- from pydantic import ConfigDict, Field
9
+ from pydantic import ConfigDict
10
10
 
11
11
  from .dataframe import Mt5DataClient
12
12
  from .mt5 import Mt5RuntimeError
@@ -27,15 +27,11 @@ class Mt5TradingClient(Mt5DataClient):
27
27
  """
28
28
 
29
29
  model_config = ConfigDict(frozen=True)
30
- order_filling_mode: Literal["IOC", "FOK", "RETURN"] = Field(
31
- default="IOC",
32
- description="Order filling mode: 'IOC' (Immediate or Cancel), "
33
- "'FOK' (Fill or Kill), 'RETURN' (Return if not filled)",
34
- )
35
30
 
36
31
  def close_open_positions(
37
32
  self,
38
33
  symbols: str | list[str] | tuple[str, ...] | None = None,
34
+ order_filling_mode: Literal["IOC", "FOK", "RETURN"] = "IOC",
39
35
  dry_run: bool = False,
40
36
  **kwargs: Any, # noqa: ANN401
41
37
  ) -> dict[str, list[dict[str, Any]]]:
@@ -44,6 +40,7 @@ class Mt5TradingClient(Mt5DataClient):
44
40
  Args:
45
41
  symbols: Optional symbol or list of symbols to filter positions.
46
42
  If None, all symbols will be considered.
43
+ order_filling_mode: Order filling mode, either "IOC", "FOK", or "RETURN".
47
44
  dry_run: If True, only check the order without sending it.
48
45
  **kwargs: Additional keyword arguments for request parameters.
49
46
 
@@ -59,13 +56,19 @@ class Mt5TradingClient(Mt5DataClient):
59
56
  symbol_list = self.symbols_get()
60
57
  self.logger.info("Fetching and closing positions for symbols: %s", symbol_list)
61
58
  return {
62
- 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
+ )
63
65
  for s in symbol_list
64
66
  }
65
67
 
66
68
  def _fetch_and_close_position(
67
69
  self,
68
70
  symbol: str | None = None,
71
+ order_filling_mode: Literal["IOC", "FOK", "RETURN"] = "IOC",
69
72
  dry_run: bool = False,
70
73
  **kwargs: Any, # noqa: ANN401
71
74
  ) -> list[dict[str, Any]]:
@@ -73,6 +76,7 @@ class Mt5TradingClient(Mt5DataClient):
73
76
 
74
77
  Args:
75
78
  symbol: Optional symbol filter.
79
+ order_filling_mode: Order filling mode, either "IOC", "FOK", or "RETURN".
76
80
  dry_run: If True, only check the order without sending it.
77
81
  **kwargs: Additional keyword arguments for request parameters.
78
82
 
@@ -85,10 +89,6 @@ class Mt5TradingClient(Mt5DataClient):
85
89
  return []
86
90
  else:
87
91
  self.logger.info("Closing open positions for symbol: %s", symbol)
88
- order_filling_type = getattr(
89
- self.mt5,
90
- f"ORDER_FILLING_{self.order_filling_mode}",
91
- )
92
92
  return [
93
93
  self._send_or_check_order(
94
94
  request={
@@ -100,7 +100,10 @@ class Mt5TradingClient(Mt5DataClient):
100
100
  if p["type"] == self.mt5.POSITION_TYPE_BUY
101
101
  else self.mt5.ORDER_TYPE_BUY
102
102
  ),
103
- "type_filling": order_filling_type,
103
+ "type_filling": getattr(
104
+ self.mt5,
105
+ f"ORDER_FILLING_{order_filling_mode}",
106
+ ),
104
107
  "type_time": self.mt5.ORDER_TIME_GTC,
105
108
  "position": p["ticket"],
106
109
  **kwargs,
@@ -280,19 +283,25 @@ class Mt5TradingClient(Mt5DataClient):
280
283
  """
281
284
  symbol_info = self.symbol_info_as_dict(symbol=symbol)
282
285
  symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
283
- return {
284
- "volume": symbol_info["volume_min"],
285
- "margin": self.order_calc_margin(
286
- action=getattr(self.mt5, f"ORDER_TYPE_{order_side.upper()}"),
287
- symbol=symbol,
288
- volume=symbol_info["volume_min"],
289
- price=(
290
- symbol_info_tick["bid"]
291
- if order_side == "SELL"
292
- else symbol_info_tick["ask"]
293
- ),
286
+ margin = self.order_calc_margin(
287
+ action=getattr(self.mt5, f"ORDER_TYPE_{order_side.upper()}"),
288
+ symbol=symbol,
289
+ volume=symbol_info["volume_min"],
290
+ price=(
291
+ symbol_info_tick["bid"]
292
+ if order_side == "SELL"
293
+ else symbol_info_tick["ask"]
294
294
  ),
295
- }
295
+ )
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}
296
305
 
297
306
  def calculate_volume_by_margin(
298
307
  self,
@@ -314,10 +323,13 @@ class Mt5TradingClient(Mt5DataClient):
314
323
  symbol=symbol,
315
324
  order_side=order_side,
316
325
  )
317
- return (
318
- floor(margin / min_order_margin_dict["margin"])
319
- * min_order_margin_dict["volume"]
320
- )
326
+ if min_order_margin_dict["margin"]:
327
+ return (
328
+ floor(margin / min_order_margin_dict["margin"])
329
+ * min_order_margin_dict["volume"]
330
+ )
331
+ else:
332
+ return 0.0
321
333
 
322
334
  def calculate_spread_ratio(
323
335
  self,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pdmt5"
3
- version = "0.1.6"
3
+ version = "0.1.7"
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"}]
@@ -10,7 +10,6 @@ from typing import NamedTuple
10
10
  import numpy as np
11
11
  import pandas as pd
12
12
  import pytest
13
- from pydantic import ValidationError
14
13
  from pytest_mock import MockerFixture
15
14
 
16
15
  from pdmt5.mt5 import Mt5RuntimeError
@@ -182,22 +181,30 @@ class TestMt5TradingClient:
182
181
  def test_client_initialization_default(self, mock_mt5_import: ModuleType) -> None:
183
182
  """Test client initialization with default parameters."""
184
183
  client = Mt5TradingClient(mt5=mock_mt5_import)
185
- assert client.order_filling_mode == "IOC"
184
+ # Order filling mode is now a parameter, not an attribute
185
+ assert isinstance(client, Mt5TradingClient)
186
186
 
187
187
  def test_client_initialization_custom(self, mock_mt5_import: ModuleType) -> None:
188
188
  """Test client initialization with custom parameters."""
189
+ # Order filling mode is now a parameter to methods, not a class attribute
189
190
  client = Mt5TradingClient(
190
191
  mt5=mock_mt5_import,
191
- order_filling_mode="FOK",
192
192
  )
193
- assert client.order_filling_mode == "FOK"
193
+ assert isinstance(client, Mt5TradingClient)
194
194
 
195
195
  def test_client_initialization_invalid_filling_mode(
196
196
  self, mock_mt5_import: ModuleType
197
197
  ) -> None:
198
198
  """Test client initialization with invalid filling mode."""
199
- with pytest.raises(ValidationError):
200
- Mt5TradingClient(mt5=mock_mt5_import, order_filling_mode="INVALID") # type: ignore[arg-type]
199
+ # Order filling mode is now a parameter to methods, not a class attribute
200
+ client = Mt5TradingClient(mt5=mock_mt5_import)
201
+ # Test that the method validates the parameter
202
+ mock_mt5_import.initialize.return_value = True
203
+ client.initialize()
204
+ mock_mt5_import.positions_get.return_value = []
205
+ # Should not raise as validation happens at method level
206
+ result = client._fetch_and_close_position(order_filling_mode="IOC") # type: ignore[arg-type]
207
+ assert result == []
201
208
 
202
209
  def test_close_position_no_positions(
203
210
  self,
@@ -733,7 +740,7 @@ class TestMt5TradingClient:
733
740
  mock_position_buy: MockPositionInfo,
734
741
  ) -> None:
735
742
  """Test that order filling mode constants are used correctly."""
736
- client = Mt5TradingClient(mt5=mock_mt5_import, order_filling_mode="FOK")
743
+ client = Mt5TradingClient(mt5=mock_mt5_import)
737
744
  mock_mt5_import.initialize.return_value = True
738
745
  client.initialize()
739
746
 
@@ -745,7 +752,8 @@ class TestMt5TradingClient:
745
752
  "retcode": 10009
746
753
  }
747
754
 
748
- client.close_open_positions("EURUSD")
755
+ # Call _fetch_and_close_position with FOK mode
756
+ client._fetch_and_close_position("EURUSD", order_filling_mode="FOK")
749
757
 
750
758
  # Verify that ORDER_FILLING_FOK was used
751
759
  call_args = mock_mt5_import.order_send.call_args[0][0]
@@ -973,6 +981,69 @@ class TestMt5TradingClient:
973
981
  expected_volume = 5 * 0.01
974
982
  assert result == expected_volume
975
983
 
984
+ def test_calculate_minimum_order_margin_no_margin(
985
+ self,
986
+ mock_mt5_import: ModuleType,
987
+ ) -> None:
988
+ """Test calculation when order_calc_margin returns zero."""
989
+ client = Mt5TradingClient(mt5=mock_mt5_import)
990
+ mock_mt5_import.initialize.return_value = True
991
+ client.initialize()
992
+
993
+ # Mock symbol info
994
+ mock_mt5_import.symbol_info.return_value._asdict.return_value = {
995
+ "volume_min": 0.01,
996
+ "name": "EURUSD",
997
+ }
998
+
999
+ # Mock symbol tick info
1000
+ mock_mt5_import.symbol_info_tick.return_value._asdict.return_value = {
1001
+ "ask": 1.1000,
1002
+ "bid": 1.0998,
1003
+ }
1004
+
1005
+ # Mock order_calc_margin to return 0.0 (no margin required)
1006
+ mock_mt5_import.order_calc_margin.return_value = 0.0
1007
+
1008
+ result = client.calculate_minimum_order_margin("EURUSD", "BUY")
1009
+
1010
+ assert result == {"volume": 0.01, "margin": 0.0}
1011
+ mock_mt5_import.order_calc_margin.assert_called_once_with(
1012
+ mock_mt5_import.ORDER_TYPE_BUY,
1013
+ "EURUSD",
1014
+ 0.01,
1015
+ 1.1000,
1016
+ )
1017
+
1018
+ def test_calculate_volume_by_margin_zero_margin(
1019
+ self,
1020
+ mock_mt5_import: ModuleType,
1021
+ ) -> None:
1022
+ """Test calculation when minimum order margin is zero."""
1023
+ client = Mt5TradingClient(mt5=mock_mt5_import)
1024
+ mock_mt5_import.initialize.return_value = True
1025
+ client.initialize()
1026
+
1027
+ # Mock symbol info
1028
+ mock_mt5_import.symbol_info.return_value._asdict.return_value = {
1029
+ "volume_min": 0.01,
1030
+ "name": "EURUSD",
1031
+ }
1032
+
1033
+ # Mock symbol tick info
1034
+ mock_mt5_import.symbol_info_tick.return_value._asdict.return_value = {
1035
+ "ask": 1.1000,
1036
+ "bid": 1.0998,
1037
+ }
1038
+
1039
+ # Mock order_calc_margin to return 0.0 (no margin required)
1040
+ mock_mt5_import.order_calc_margin.return_value = 0.0
1041
+
1042
+ result = client.calculate_volume_by_margin("EURUSD", 1000.0, "BUY")
1043
+
1044
+ # Should return 0.0 when margin is zero
1045
+ assert result == 0.0
1046
+
976
1047
  def test_calculate_spread_ratio(
977
1048
  self,
978
1049
  mock_mt5_import: ModuleType,
@@ -613,7 +613,7 @@ wheels = [
613
613
 
614
614
  [[package]]
615
615
  name = "pdmt5"
616
- version = "0.1.6"
616
+ version = "0.1.7"
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