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.
- {pdmt5-0.1.0 → pdmt5-0.1.2}/PKG-INFO +1 -1
- {pdmt5-0.1.0 → pdmt5-0.1.2}/pdmt5/dataframe.py +7 -10
- {pdmt5-0.1.0 → pdmt5-0.1.2}/pdmt5/mt5.py +0 -3
- {pdmt5-0.1.0 → pdmt5-0.1.2}/pdmt5/trading.py +9 -1
- {pdmt5-0.1.0 → pdmt5-0.1.2}/pyproject.toml +1 -1
- {pdmt5-0.1.0 → pdmt5-0.1.2}/test/test_dataframe.py +6 -9
- {pdmt5-0.1.0 → pdmt5-0.1.2}/test/test_mt5.py +0 -2
- {pdmt5-0.1.0 → pdmt5-0.1.2}/test/test_trading.py +175 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/uv.lock +1 -1
- {pdmt5-0.1.0 → pdmt5-0.1.2}/.claude/settings.json +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/.github/FUNDING.yml +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/.github/copilot-instructions.md +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/.github/dependabot.yml +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/.github/workflows/ci.yml +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/.gitignore +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/CLAUDE.md +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/LICENSE +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/README.md +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/docs/api/dataframe.md +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/docs/api/index.md +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/docs/api/mt5.md +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/docs/api/trading.md +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/docs/api/utils.md +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/docs/index.md +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/mkdocs.yml +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/pdmt5/__init__.py +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/pdmt5/utils.py +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/renovate.json +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/test/__init__.py +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/test/test_init.py +0 -0
- {pdmt5-0.1.0 → pdmt5-0.1.2}/test/test_utils.py +0 -0
|
@@ -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
|
|
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
|
-
|
|
79
|
-
|
|
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(**
|
|
95
|
-
|
|
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
|
|
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)
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1905
|
+
client.initialize_and_login_mt5()
|
|
1909
1906
|
|
|
1910
|
-
assert "MT5
|
|
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,
|
|
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
|