pdmt5 0.1.0__tar.gz → 0.1.2__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.0 → pdmt5-0.1.2}/PKG-INFO +1 -1
  2. {pdmt5-0.1.0 → pdmt5-0.1.2}/pdmt5/dataframe.py +7 -10
  3. {pdmt5-0.1.0 → pdmt5-0.1.2}/pdmt5/mt5.py +0 -3
  4. {pdmt5-0.1.0 → pdmt5-0.1.2}/pdmt5/trading.py +9 -1
  5. {pdmt5-0.1.0 → pdmt5-0.1.2}/pyproject.toml +1 -1
  6. {pdmt5-0.1.0 → pdmt5-0.1.2}/test/test_dataframe.py +6 -9
  7. {pdmt5-0.1.0 → pdmt5-0.1.2}/test/test_mt5.py +0 -2
  8. {pdmt5-0.1.0 → pdmt5-0.1.2}/test/test_trading.py +175 -0
  9. {pdmt5-0.1.0 → pdmt5-0.1.2}/uv.lock +1 -1
  10. {pdmt5-0.1.0 → pdmt5-0.1.2}/.claude/settings.json +0 -0
  11. {pdmt5-0.1.0 → pdmt5-0.1.2}/.github/FUNDING.yml +0 -0
  12. {pdmt5-0.1.0 → pdmt5-0.1.2}/.github/copilot-instructions.md +0 -0
  13. {pdmt5-0.1.0 → pdmt5-0.1.2}/.github/dependabot.yml +0 -0
  14. {pdmt5-0.1.0 → pdmt5-0.1.2}/.github/workflows/ci.yml +0 -0
  15. {pdmt5-0.1.0 → pdmt5-0.1.2}/.gitignore +0 -0
  16. {pdmt5-0.1.0 → pdmt5-0.1.2}/CLAUDE.md +0 -0
  17. {pdmt5-0.1.0 → pdmt5-0.1.2}/LICENSE +0 -0
  18. {pdmt5-0.1.0 → pdmt5-0.1.2}/README.md +0 -0
  19. {pdmt5-0.1.0 → pdmt5-0.1.2}/docs/api/dataframe.md +0 -0
  20. {pdmt5-0.1.0 → pdmt5-0.1.2}/docs/api/index.md +0 -0
  21. {pdmt5-0.1.0 → pdmt5-0.1.2}/docs/api/mt5.md +0 -0
  22. {pdmt5-0.1.0 → pdmt5-0.1.2}/docs/api/trading.md +0 -0
  23. {pdmt5-0.1.0 → pdmt5-0.1.2}/docs/api/utils.md +0 -0
  24. {pdmt5-0.1.0 → pdmt5-0.1.2}/docs/index.md +0 -0
  25. {pdmt5-0.1.0 → pdmt5-0.1.2}/mkdocs.yml +0 -0
  26. {pdmt5-0.1.0 → pdmt5-0.1.2}/pdmt5/__init__.py +0 -0
  27. {pdmt5-0.1.0 → pdmt5-0.1.2}/pdmt5/utils.py +0 -0
  28. {pdmt5-0.1.0 → pdmt5-0.1.2}/renovate.json +0 -0
  29. {pdmt5-0.1.0 → pdmt5-0.1.2}/test/__init__.py +0 -0
  30. {pdmt5-0.1.0 → pdmt5-0.1.2}/test/test_init.py +0 -0
  31. {pdmt5-0.1.0 → pdmt5-0.1.2}/test/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdmt5
3
- Version: 0.1.0
3
+ Version: 0.1.2
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 @@ class Mt5Config(BaseModel):
29
29
  timeout: int | None = Field(
30
30
  default=None, description="Connection timeout in milliseconds"
31
31
  )
32
- portable: bool | None = Field(default=None, description="Use portable mode")
33
32
 
34
33
 
35
34
  class Mt5DataClient(Mt5Client):
@@ -51,14 +50,13 @@ class Mt5DataClient(Mt5Client):
51
50
  description="Number of retry attempts for connection initialization",
52
51
  )
53
52
 
54
- def initialize_mt5(
53
+ def initialize_and_login_mt5(
55
54
  self,
56
55
  path: str | None = None,
57
56
  login: int | None = None,
58
57
  password: str | None = None,
59
58
  server: str | None = None,
60
59
  timeout: int | None = None,
61
- portable: bool | None = None,
62
60
  ) -> None:
63
61
  """Initialize MetaTrader5 connection with retry logic.
64
62
 
@@ -70,18 +68,16 @@ class Mt5DataClient(Mt5Client):
70
68
  password: Account password (overrides config).
71
69
  server: Server name (overrides config).
72
70
  timeout: Connection timeout (overrides config).
73
- portable: Use portable mode (overrides config).
74
71
 
75
72
  Raises:
76
73
  Mt5RuntimeError: If initialization fails after retries.
77
74
  """
78
- initialize_kwargs = {
79
- "path": path or self.config.path,
75
+ path = path or self.config.path
76
+ login_kwargs = {
80
77
  "login": login or self.config.login,
81
78
  "password": password or self.config.password,
82
79
  "server": server or self.config.server,
83
80
  "timeout": timeout or self.config.timeout,
84
- "portable": portable if portable is not None else self.config.portable,
85
81
  }
86
82
  for i in range(1 + max(0, self.retry_count)):
87
83
  if i:
@@ -91,11 +87,12 @@ class Mt5DataClient(Mt5Client):
91
87
  self.retry_count,
92
88
  )
93
89
  time.sleep(i)
94
- if self.initialize(**initialize_kwargs): # type: ignore[reportArgumentType]
95
- self.logger.info("MT5 initialization successful.")
90
+ if self.initialize(path=path, **login_kwargs) and (
91
+ (not login_kwargs["login"]) or self.login(**login_kwargs)
92
+ ):
96
93
  return
97
94
  error_message = (
98
- f"MT5 initialization failed after {self.retry_count} retries:"
95
+ f"MT5 initialize and login failed after {self.retry_count} retries:"
99
96
  f" {self.last_error()}"
100
97
  )
101
98
  raise Mt5RuntimeError(error_message)
@@ -106,7 +106,6 @@ class Mt5Client(BaseModel):
106
106
  password: str | None = None,
107
107
  server: str | None = None,
108
108
  timeout: int | None = None,
109
- portable: bool | None = None,
110
109
  ) -> bool:
111
110
  """Establish a connection with the MetaTrader 5 terminal.
112
111
 
@@ -116,7 +115,6 @@ class Mt5Client(BaseModel):
116
115
  password: Trading account password.
117
116
  server: Trade server address.
118
117
  timeout: Connection timeout in milliseconds.
119
- portable: Use portable mode.
120
118
 
121
119
  Returns:
122
120
  True if successful, False otherwise.
@@ -135,7 +133,6 @@ class Mt5Client(BaseModel):
135
133
  "password": password,
136
134
  "server": server,
137
135
  "timeout": timeout,
138
- "portable": portable,
139
136
  }.items()
140
137
  if v is not None
141
138
  },
@@ -32,6 +32,7 @@ class Mt5TradingClient(Mt5DataClient):
32
32
  def close_open_positions(
33
33
  self,
34
34
  symbols: str | list[str] | tuple[str, ...] | None = None,
35
+ dry_run: bool | None = None,
35
36
  **kwargs: Any, # noqa: ANN401
36
37
  ) -> dict[str, list[dict[str, Any]]]:
37
38
  """Close all open positions for specified symbols.
@@ -39,6 +40,8 @@ class Mt5TradingClient(Mt5DataClient):
39
40
  Args:
40
41
  symbols: Optional symbol or list of symbols to filter positions.
41
42
  If None, all symbols will be considered.
43
+ dry_run: Optional flag to enable dry run mode. If None, uses the instance's
44
+ `dry_run` attribute.
42
45
  **kwargs: Additional keyword arguments for request parameters.
43
46
 
44
47
  Returns:
@@ -53,18 +56,22 @@ class Mt5TradingClient(Mt5DataClient):
53
56
  symbol_list = self.symbols_get()
54
57
  self.logger.info("Fetching and closing positions for symbols: %s", symbol_list)
55
58
  return {
56
- s: self._fetch_and_close_position(symbol=s, **kwargs) for s in symbol_list
59
+ s: self._fetch_and_close_position(symbol=s, dry_run=dry_run, **kwargs)
60
+ for s in symbol_list
57
61
  }
58
62
 
59
63
  def _fetch_and_close_position(
60
64
  self,
61
65
  symbol: str | None = None,
66
+ dry_run: bool | None = None,
62
67
  **kwargs: Any, # noqa: ANN401
63
68
  ) -> list[dict[str, Any]]:
64
69
  """Close all open positions for a specific symbol.
65
70
 
66
71
  Args:
67
72
  symbol: Optional symbol filter.
73
+ dry_run: Optional flag to enable dry run mode. If None, uses the instance's
74
+ `dry_run` attribute.
68
75
  **kwargs: Additional keyword arguments for request parameters.
69
76
 
70
77
  Returns:
@@ -96,6 +103,7 @@ class Mt5TradingClient(Mt5DataClient):
96
103
  "position": p["ticket"],
97
104
  **kwargs,
98
105
  },
106
+ dry_run=dry_run,
99
107
  )
100
108
  for p in positions_dict
101
109
  ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pdmt5"
3
- version = "0.1.0"
3
+ version = "0.1.2"
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"}]
@@ -397,7 +397,6 @@ class TestMt5Config:
397
397
  assert config.password is None
398
398
  assert config.server is None
399
399
  assert config.timeout is None
400
- assert config.portable is None
401
400
 
402
401
  def test_custom_config(self) -> None:
403
402
  """Test custom configuration."""
@@ -406,13 +405,11 @@ class TestMt5Config:
406
405
  password="secret",
407
406
  server="Demo-Server",
408
407
  timeout=30000,
409
- portable=True,
410
408
  )
411
409
  assert config.login == 123456
412
410
  assert config.password == "secret" # noqa: S105
413
411
  assert config.server == "Demo-Server"
414
412
  assert config.timeout == 30000
415
- assert config.portable is True
416
413
 
417
414
  def test_config_immutable(self) -> None:
418
415
  """Test that config is immutable."""
@@ -478,11 +475,11 @@ class TestMt5DataClient:
478
475
 
479
476
  client = Mt5DataClient(mt5=mock_mt5_import, retry_count=0)
480
477
  pattern = (
481
- r"MT5 initialization failed after 0 retries: "
478
+ r"MT5 initialize and login failed after 0 retries: "
482
479
  r"\(1, 'Connection failed'\)"
483
480
  )
484
481
  with pytest.raises(Mt5RuntimeError, match=pattern):
485
- client.initialize_mt5()
482
+ client.initialize_and_login_mt5()
486
483
 
487
484
  def test_initialize_already_initialized(
488
485
  self, mock_mt5_import: ModuleType | None
@@ -1864,7 +1861,7 @@ class TestMt5DataClientRetryLogic:
1864
1861
  mock_mt5_import.last_error.return_value = (1, "Test error")
1865
1862
 
1866
1863
  # Should succeed on third attempt
1867
- client.initialize_mt5()
1864
+ client.initialize_and_login_mt5()
1868
1865
 
1869
1866
  assert mock_mt5_import.initialize.call_count == 3
1870
1867
 
@@ -1884,7 +1881,7 @@ class TestMt5DataClientRetryLogic:
1884
1881
  mock_sleep = mocker.patch("pdmt5.dataframe.time.sleep")
1885
1882
 
1886
1883
  # Should succeed on second attempt
1887
- client.initialize_mt5()
1884
+ client.initialize_and_login_mt5()
1888
1885
 
1889
1886
  assert mock_mt5_import.initialize.call_count == 2
1890
1887
  mock_sleep.assert_called_once_with(1)
@@ -1905,9 +1902,9 @@ class TestMt5DataClientRetryLogic:
1905
1902
  mock_sleep = mocker.patch("pdmt5.dataframe.time.sleep")
1906
1903
 
1907
1904
  with pytest.raises(Mt5RuntimeError) as exc_info:
1908
- client.initialize_mt5()
1905
+ client.initialize_and_login_mt5()
1909
1906
 
1910
- assert "MT5 initialization failed after" in str(exc_info.value)
1907
+ assert "MT5 initialize and login failed after" in str(exc_info.value)
1911
1908
  assert mock_mt5_import.initialize.call_count == 3 # All attempts made
1912
1909
  # Check that sleep was called for retries
1913
1910
  assert mock_sleep.call_count == 2
@@ -88,7 +88,6 @@ class TestMt5Client:
88
88
  password="secret",
89
89
  server="Demo",
90
90
  timeout=60000,
91
- portable=True,
92
91
  )
93
92
 
94
93
  assert result is True
@@ -98,7 +97,6 @@ class TestMt5Client:
98
97
  password="secret",
99
98
  server="Demo",
100
99
  timeout=60000,
101
- portable=True,
102
100
  )
103
101
 
104
102
  def test_initialize_failure(self, client: Mt5Client, mock_mt5: Mock) -> None:
@@ -228,6 +228,90 @@ class TestMt5TradingClient:
228
228
  assert result["EURUSD"][0]["retcode"] == 10009
229
229
  mock_mt5_import.order_send.assert_called_once()
230
230
 
231
+ def test_close_position_with_positions_dry_run(
232
+ self,
233
+ mock_mt5_import: ModuleType,
234
+ mock_position_buy: MockPositionInfo,
235
+ ) -> None:
236
+ """Test close_position with existing positions in dry run mode."""
237
+ client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=True)
238
+ mock_mt5_import.initialize.return_value = True
239
+ client.initialize()
240
+
241
+ # Mock positions
242
+ mock_mt5_import.positions_get.return_value = [mock_position_buy]
243
+
244
+ mock_mt5_import.order_check.return_value.retcode = 0
245
+ mock_mt5_import.order_check.return_value._asdict.return_value = {
246
+ "retcode": 0,
247
+ "result": "check_success",
248
+ }
249
+
250
+ result = client.close_open_positions("EURUSD")
251
+
252
+ assert len(result["EURUSD"]) == 1
253
+ assert result["EURUSD"][0]["retcode"] == 0
254
+ mock_mt5_import.order_check.assert_called_once()
255
+ mock_mt5_import.order_send.assert_not_called()
256
+
257
+ def test_close_position_with_dry_run_override(
258
+ self,
259
+ mock_mt5_import: ModuleType,
260
+ mock_position_buy: MockPositionInfo,
261
+ ) -> None:
262
+ """Test close_position with dry_run parameter override."""
263
+ # Client initialized with dry_run=False
264
+ client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=False)
265
+ mock_mt5_import.initialize.return_value = True
266
+ client.initialize()
267
+
268
+ # Mock positions
269
+ mock_mt5_import.positions_get.return_value = [mock_position_buy]
270
+
271
+ mock_mt5_import.order_check.return_value.retcode = 0
272
+ mock_mt5_import.order_check.return_value._asdict.return_value = {
273
+ "retcode": 0,
274
+ "result": "check_success",
275
+ }
276
+
277
+ # Override with dry_run=True
278
+ result = client.close_open_positions("EURUSD", dry_run=True)
279
+
280
+ assert len(result["EURUSD"]) == 1
281
+ assert result["EURUSD"][0]["retcode"] == 0
282
+ # Should use order_check instead of order_send
283
+ mock_mt5_import.order_check.assert_called_once()
284
+ mock_mt5_import.order_send.assert_not_called()
285
+
286
+ def test_close_position_with_real_mode_override(
287
+ self,
288
+ mock_mt5_import: ModuleType,
289
+ mock_position_buy: MockPositionInfo,
290
+ ) -> None:
291
+ """Test close_position with real mode override."""
292
+ # Client initialized with dry_run=True
293
+ client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=True)
294
+ mock_mt5_import.initialize.return_value = True
295
+ client.initialize()
296
+
297
+ # Mock positions
298
+ mock_mt5_import.positions_get.return_value = [mock_position_buy]
299
+
300
+ mock_mt5_import.order_send.return_value.retcode = 10009
301
+ mock_mt5_import.order_send.return_value._asdict.return_value = {
302
+ "retcode": 10009,
303
+ "result": "send_success",
304
+ }
305
+
306
+ # Override with dry_run=False
307
+ result = client.close_open_positions("EURUSD", dry_run=False)
308
+
309
+ assert len(result["EURUSD"]) == 1
310
+ assert result["EURUSD"][0]["retcode"] == 10009
311
+ # Should use order_send instead of order_check
312
+ mock_mt5_import.order_send.assert_called_once()
313
+ mock_mt5_import.order_check.assert_not_called()
314
+
231
315
  def test_close_open_positions_all_symbols(
232
316
  self,
233
317
  mock_mt5_import: ModuleType,
@@ -319,6 +403,39 @@ class TestMt5TradingClient:
319
403
  assert call_args["comment"] == "custom_close"
320
404
  assert call_args["magic"] == 12345
321
405
 
406
+ def test_close_open_positions_with_kwargs_and_dry_run(
407
+ self,
408
+ mock_mt5_import: ModuleType,
409
+ mock_position_buy: MockPositionInfo,
410
+ ) -> None:
411
+ """Test close_open_positions with additional kwargs and dry_run override."""
412
+ client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=False)
413
+ mock_mt5_import.initialize.return_value = True
414
+ client.initialize()
415
+
416
+ # Mock positions
417
+ mock_mt5_import.positions_get.return_value = [mock_position_buy]
418
+
419
+ mock_mt5_import.order_check.return_value.retcode = 0
420
+ mock_mt5_import.order_check.return_value._asdict.return_value = {
421
+ "retcode": 0,
422
+ "result": "check_success",
423
+ }
424
+
425
+ # Pass custom kwargs with dry_run override
426
+ result = client.close_open_positions(
427
+ "EURUSD", dry_run=True, comment="custom_close", magic=12345
428
+ )
429
+
430
+ assert len(result["EURUSD"]) == 1
431
+ assert result["EURUSD"][0]["retcode"] == 0
432
+
433
+ # Check that kwargs were passed through to order_check
434
+ call_args = mock_mt5_import.order_check.call_args[0][0]
435
+ assert call_args["comment"] == "custom_close"
436
+ assert call_args["magic"] == 12345
437
+ mock_mt5_import.order_send.assert_not_called()
438
+
322
439
  def test_send_or_check_order_dry_run_success(
323
440
  self,
324
441
  mock_mt5_import: ModuleType,
@@ -611,6 +728,64 @@ class TestMt5TradingClient:
611
728
  call_args = mock_mt5_import.order_send.call_args[0][0]
612
729
  assert call_args["type"] == mock_mt5_import.ORDER_TYPE_BUY
613
730
 
731
+ def test_fetch_and_close_position_with_dry_run(
732
+ self,
733
+ mock_mt5_import: ModuleType,
734
+ mock_position_buy: MockPositionInfo,
735
+ mock_position_sell: MockPositionInfo,
736
+ ) -> None:
737
+ """Test _fetch_and_close_position with dry_run parameter."""
738
+ client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=False)
739
+ mock_mt5_import.initialize.return_value = True
740
+ client.initialize()
741
+
742
+ # Test with multiple positions and dry_run override
743
+ mock_mt5_import.positions_get.return_value = [
744
+ mock_position_buy,
745
+ mock_position_sell,
746
+ ]
747
+
748
+ mock_mt5_import.order_check.return_value.retcode = 0
749
+ mock_mt5_import.order_check.return_value._asdict.return_value = {
750
+ "retcode": 0,
751
+ "result": "check_success",
752
+ }
753
+
754
+ # Call internal method directly with dry_run=True
755
+ result = client._fetch_and_close_position(symbol="EURUSD", dry_run=True)
756
+
757
+ assert len(result) == 2
758
+ assert all(r["retcode"] == 0 for r in result)
759
+ assert mock_mt5_import.order_check.call_count == 2
760
+ mock_mt5_import.order_send.assert_not_called()
761
+
762
+ def test_fetch_and_close_position_inherits_instance_dry_run(
763
+ self,
764
+ mock_mt5_import: ModuleType,
765
+ mock_position_buy: MockPositionInfo,
766
+ ) -> None:
767
+ """Test _fetch_and_close_position inherits instance dry_run if not given."""
768
+ # Client initialized with dry_run=True
769
+ client = Mt5TradingClient(mt5=mock_mt5_import, dry_run=True)
770
+ mock_mt5_import.initialize.return_value = True
771
+ client.initialize()
772
+
773
+ mock_mt5_import.positions_get.return_value = [mock_position_buy]
774
+
775
+ mock_mt5_import.order_check.return_value.retcode = 0
776
+ mock_mt5_import.order_check.return_value._asdict.return_value = {
777
+ "retcode": 0,
778
+ "result": "check_success",
779
+ }
780
+
781
+ # Call without specifying dry_run - should use instance's dry_run=True
782
+ result = client._fetch_and_close_position(symbol="EURUSD")
783
+
784
+ assert len(result) == 1
785
+ assert result[0]["retcode"] == 0
786
+ mock_mt5_import.order_check.assert_called_once()
787
+ mock_mt5_import.order_send.assert_not_called()
788
+
614
789
  def test_calculate_minimum_order_margins_success(
615
790
  self,
616
791
  mock_mt5_import: ModuleType,
@@ -613,7 +613,7 @@ wheels = [
613
613
 
614
614
  [[package]]
615
615
  name = "pdmt5"
616
- version = "0.1.0"
616
+ version = "0.1.2"
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