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.
- {pdmt5-0.1.6 → pdmt5-0.1.7}/PKG-INFO +8 -21
- {pdmt5-0.1.6 → pdmt5-0.1.7}/README.md +7 -20
- {pdmt5-0.1.6 → pdmt5-0.1.7}/docs/api/trading.md +18 -17
- {pdmt5-0.1.6 → pdmt5-0.1.7}/pdmt5/trading.py +40 -28
- {pdmt5-0.1.6 → pdmt5-0.1.7}/pyproject.toml +1 -1
- {pdmt5-0.1.6 → pdmt5-0.1.7}/test/test_trading.py +79 -8
- {pdmt5-0.1.6 → pdmt5-0.1.7}/uv.lock +1 -1
- {pdmt5-0.1.6 → pdmt5-0.1.7}/.claude/settings.json +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/.github/FUNDING.yml +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/.github/copilot-instructions.md +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/.github/dependabot.yml +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/.github/workflows/ci.yml +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/.gitignore +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/CLAUDE.md +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/LICENSE +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/docs/api/dataframe.md +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/docs/api/index.md +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/docs/api/mt5.md +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/docs/api/utils.md +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/docs/index.md +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/mkdocs.yml +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/pdmt5/__init__.py +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/pdmt5/dataframe.py +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/pdmt5/mt5.py +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/pdmt5/utils.py +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/renovate.json +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/test/__init__.py +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/test/test_dataframe.py +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/test/test_init.py +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.7}/test/test_mt5.py +0 -0
- {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.
|
|
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
|
|
294
|
-
with Mt5TradingClient(config=config
|
|
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(
|
|
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
|
|
271
|
-
with Mt5TradingClient(config=config
|
|
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(
|
|
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
|
-
|
|
74
|
-
# IOC (Immediate or Cancel) - default
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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(
|
|
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":
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
200
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
File without changes
|