pdmt5 0.1.6__tar.gz → 0.1.8__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.8/.github/workflows/claude.yml +59 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/CLAUDE.md +2 -2
- {pdmt5-0.1.6 → pdmt5-0.1.8}/PKG-INFO +9 -22
- {pdmt5-0.1.6 → pdmt5-0.1.8}/README.md +8 -21
- {pdmt5-0.1.6 → pdmt5-0.1.8}/docs/api/trading.md +18 -17
- {pdmt5-0.1.6 → pdmt5-0.1.8}/pdmt5/trading.py +41 -28
- {pdmt5-0.1.6 → pdmt5-0.1.8}/pyproject.toml +7 -5
- {pdmt5-0.1.6/test → pdmt5-0.1.8/tests}/test_trading.py +107 -8
- {pdmt5-0.1.6 → pdmt5-0.1.8}/uv.lock +1 -1
- {pdmt5-0.1.6 → pdmt5-0.1.8}/.claude/settings.json +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/.github/FUNDING.yml +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/.github/copilot-instructions.md +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/.github/dependabot.yml +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/.github/workflows/ci.yml +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/.gitignore +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/LICENSE +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/docs/api/dataframe.md +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/docs/api/index.md +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/docs/api/mt5.md +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/docs/api/utils.md +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/docs/index.md +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/mkdocs.yml +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/pdmt5/__init__.py +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/pdmt5/dataframe.py +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/pdmt5/mt5.py +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/pdmt5/utils.py +0 -0
- {pdmt5-0.1.6 → pdmt5-0.1.8}/renovate.json +0 -0
- {pdmt5-0.1.6/test → pdmt5-0.1.8/tests}/__init__.py +0 -0
- {pdmt5-0.1.6/test → pdmt5-0.1.8/tests}/test_dataframe.py +0 -0
- {pdmt5-0.1.6/test → pdmt5-0.1.8/tests}/test_init.py +0 -0
- {pdmt5-0.1.6/test → pdmt5-0.1.8/tests}/test_mt5.py +0 -0
- {pdmt5-0.1.6/test → pdmt5-0.1.8/tests}/test_utils.py +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Claude Code
|
|
3
|
+
on:
|
|
4
|
+
issue_comment:
|
|
5
|
+
types:
|
|
6
|
+
- created
|
|
7
|
+
pull_request_review_comment:
|
|
8
|
+
types:
|
|
9
|
+
- created
|
|
10
|
+
issues:
|
|
11
|
+
types:
|
|
12
|
+
- opened
|
|
13
|
+
- assigned
|
|
14
|
+
pull_request_review:
|
|
15
|
+
types:
|
|
16
|
+
- submitted
|
|
17
|
+
jobs:
|
|
18
|
+
claude:
|
|
19
|
+
if: >
|
|
20
|
+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude'))
|
|
21
|
+
|| (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude'))
|
|
22
|
+
|| (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude'))
|
|
23
|
+
|| (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
26
|
+
pull-requests: read
|
|
27
|
+
issues: read
|
|
28
|
+
id-token: write
|
|
29
|
+
actions: read # Required for Claude to read CI results on PRs
|
|
30
|
+
uses: dceoy/gh-actions-for-devops/.github/workflows/claude-code-action.yml@main
|
|
31
|
+
with:
|
|
32
|
+
# This is an optional setting that allows Claude to read CI results on PRs
|
|
33
|
+
additional-permissions: |
|
|
34
|
+
actions: read
|
|
35
|
+
|
|
36
|
+
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
|
|
37
|
+
# model: "claude-opus-4-20250514"
|
|
38
|
+
|
|
39
|
+
# Optional: Customize the trigger phrase (default: @claude)
|
|
40
|
+
# trigger-phrase: "/claude"
|
|
41
|
+
|
|
42
|
+
# Optional: Trigger when specific user is assigned to an issue
|
|
43
|
+
# assignee-trigger: "claude-bot"
|
|
44
|
+
|
|
45
|
+
# Optional: Allow Claude to run specific commands
|
|
46
|
+
# allowed-tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
|
|
47
|
+
|
|
48
|
+
# Optional: Add custom instructions for Claude to customize its behavior for your project
|
|
49
|
+
# custom-instructions: |
|
|
50
|
+
# Follow our coding standards
|
|
51
|
+
# Ensure all new code has tests
|
|
52
|
+
# Use TypeScript for new files
|
|
53
|
+
|
|
54
|
+
# Optional: Custom environment variables for Claude
|
|
55
|
+
# claude-env: |
|
|
56
|
+
# NODE_ENV: test
|
|
57
|
+
secrets:
|
|
58
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
59
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -26,7 +26,7 @@ uv run pyright .
|
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
# Run unit tests with pytest
|
|
29
|
-
uv run pytest
|
|
29
|
+
uv run pytest tests/ -v
|
|
30
30
|
|
|
31
31
|
# Type checking with pyright
|
|
32
32
|
uv run pyright .
|
|
@@ -68,7 +68,7 @@ uv run mkdocs gh-deploy
|
|
|
68
68
|
- `dataframe.py`: MT5 data client with pandas DataFrame conversion (`Mt5Config`, `Mt5DataClient`)
|
|
69
69
|
- `trading.py`: Trading operations client (`Mt5TradingClient`, `Mt5TradingError`)
|
|
70
70
|
- `utils.py`: Utility decorators and functions for time conversion and DataFrame indexing
|
|
71
|
-
- `
|
|
71
|
+
- `tests/`: Comprehensive test suite (pytest-based)
|
|
72
72
|
- `test_init.py`, `test_mt5.py`, `test_dataframe.py`, `test_trading.py`, `test_utils.py`
|
|
73
73
|
- `docs/`: MkDocs documentation with API reference
|
|
74
74
|
- `docs/api/`: Auto-generated API documentation for all modules
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pdmt5
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
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
|
|
@@ -405,7 +392,7 @@ cd pdmt5
|
|
|
405
392
|
uv sync
|
|
406
393
|
|
|
407
394
|
# Run tests
|
|
408
|
-
uv run pytest
|
|
395
|
+
uv run pytest tests/ -v
|
|
409
396
|
|
|
410
397
|
# Run type checking
|
|
411
398
|
uv run pyright .
|
|
@@ -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
|
|
@@ -382,7 +369,7 @@ cd pdmt5
|
|
|
382
369
|
uv sync
|
|
383
370
|
|
|
384
371
|
# Run tests
|
|
385
|
-
uv run pytest
|
|
372
|
+
uv run pytest tests/ -v
|
|
386
373
|
|
|
387
374
|
# Run type checking
|
|
388
375
|
uv run pyright .
|
|
@@ -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,
|
|
@@ -143,6 +146,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
143
146
|
elif retcode in {
|
|
144
147
|
self.mt5.TRADE_RETCODE_TRADE_DISABLED,
|
|
145
148
|
self.mt5.TRADE_RETCODE_MARKET_CLOSED,
|
|
149
|
+
self.mt5.TRADE_RETCODE_NO_CHANGES,
|
|
146
150
|
}:
|
|
147
151
|
self.logger.info("response: %s", response)
|
|
148
152
|
comment = response.get("comment", "Unknown error")
|
|
@@ -280,19 +284,25 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
280
284
|
"""
|
|
281
285
|
symbol_info = self.symbol_info_as_dict(symbol=symbol)
|
|
282
286
|
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
|
-
),
|
|
287
|
+
margin = self.order_calc_margin(
|
|
288
|
+
action=getattr(self.mt5, f"ORDER_TYPE_{order_side.upper()}"),
|
|
289
|
+
symbol=symbol,
|
|
290
|
+
volume=symbol_info["volume_min"],
|
|
291
|
+
price=(
|
|
292
|
+
symbol_info_tick["bid"]
|
|
293
|
+
if order_side == "SELL"
|
|
294
|
+
else symbol_info_tick["ask"]
|
|
294
295
|
),
|
|
295
|
-
|
|
296
|
+
)
|
|
297
|
+
if margin:
|
|
298
|
+
return {"volume": symbol_info["volume_min"], "margin": margin}
|
|
299
|
+
else:
|
|
300
|
+
self.logger.warning(
|
|
301
|
+
"No margin available for symbol: %s with order side: %s",
|
|
302
|
+
symbol,
|
|
303
|
+
order_side,
|
|
304
|
+
)
|
|
305
|
+
return {"volume": symbol_info["volume_min"], "margin": 0.0}
|
|
296
306
|
|
|
297
307
|
def calculate_volume_by_margin(
|
|
298
308
|
self,
|
|
@@ -314,10 +324,13 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
314
324
|
symbol=symbol,
|
|
315
325
|
order_side=order_side,
|
|
316
326
|
)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
327
|
+
if min_order_margin_dict["margin"]:
|
|
328
|
+
return (
|
|
329
|
+
floor(margin / min_order_margin_dict["margin"])
|
|
330
|
+
* min_order_margin_dict["volume"]
|
|
331
|
+
)
|
|
332
|
+
else:
|
|
333
|
+
return 0.0
|
|
321
334
|
|
|
322
335
|
def calculate_spread_ratio(
|
|
323
336
|
self,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pdmt5"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.8"
|
|
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"}]
|
|
@@ -46,7 +46,7 @@ required-environments = ["platform_system == 'Windows'"]
|
|
|
46
46
|
|
|
47
47
|
[tool.uv.build-backend]
|
|
48
48
|
source-include = ["pdmt5/**", "LICENSE"]
|
|
49
|
-
source-exclude = ["
|
|
49
|
+
source-exclude = ["tests/**"]
|
|
50
50
|
|
|
51
51
|
[tool.ruff]
|
|
52
52
|
line-length = 88
|
|
@@ -126,7 +126,7 @@ ignore = [
|
|
|
126
126
|
]
|
|
127
127
|
|
|
128
128
|
[tool.ruff.lint.per-file-ignores]
|
|
129
|
-
"
|
|
129
|
+
"tests/**/*.py" = [
|
|
130
130
|
"DOC201", # Missing return documentation
|
|
131
131
|
"DOC501", # Raised exception missing from docstring
|
|
132
132
|
"PLC2701", # Private name import
|
|
@@ -147,6 +147,8 @@ max-public-methods = 40
|
|
|
147
147
|
|
|
148
148
|
[tool.pyright]
|
|
149
149
|
exclude = ["build", ".venv"]
|
|
150
|
+
venvPath = "."
|
|
151
|
+
venv = ".venv"
|
|
150
152
|
typeCheckingMode = "strict"
|
|
151
153
|
reportUnknownArgumentType = "none"
|
|
152
154
|
reportUnknownMemberType = "none"
|
|
@@ -162,7 +164,7 @@ addopts = [
|
|
|
162
164
|
"--capture=no",
|
|
163
165
|
]
|
|
164
166
|
pythonpaths = ["."]
|
|
165
|
-
testpaths = ["
|
|
167
|
+
testpaths = ["tests"]
|
|
166
168
|
python_files = ["test_*.py", "*_test.py"]
|
|
167
169
|
python_classes = ["Test*"]
|
|
168
170
|
python_functions = ["test_*"]
|
|
@@ -172,7 +174,7 @@ minversion = "6.0"
|
|
|
172
174
|
source = ["pdmt5"]
|
|
173
175
|
omit = [
|
|
174
176
|
"**/__init__.py",
|
|
175
|
-
"
|
|
177
|
+
"tests/**",
|
|
176
178
|
]
|
|
177
179
|
|
|
178
180
|
[tool.coverage.report]
|
|
@@ -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
|
|
@@ -73,6 +72,7 @@ def mock_mt5_import(
|
|
|
73
72
|
mock_mt5.TRADE_RETCODE_DONE = 10009
|
|
74
73
|
mock_mt5.TRADE_RETCODE_TRADE_DISABLED = 10017
|
|
75
74
|
mock_mt5.TRADE_RETCODE_MARKET_CLOSED = 10018
|
|
75
|
+
mock_mt5.TRADE_RETCODE_NO_CHANGES = 10025
|
|
76
76
|
mock_mt5.RES_S_OK = 1
|
|
77
77
|
mock_mt5.DEAL_TYPE_BUY = 0
|
|
78
78
|
mock_mt5.DEAL_TYPE_SELL = 1
|
|
@@ -182,22 +182,30 @@ class TestMt5TradingClient:
|
|
|
182
182
|
def test_client_initialization_default(self, mock_mt5_import: ModuleType) -> None:
|
|
183
183
|
"""Test client initialization with default parameters."""
|
|
184
184
|
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
185
|
-
|
|
185
|
+
# Order filling mode is now a parameter, not an attribute
|
|
186
|
+
assert isinstance(client, Mt5TradingClient)
|
|
186
187
|
|
|
187
188
|
def test_client_initialization_custom(self, mock_mt5_import: ModuleType) -> None:
|
|
188
189
|
"""Test client initialization with custom parameters."""
|
|
190
|
+
# Order filling mode is now a parameter to methods, not a class attribute
|
|
189
191
|
client = Mt5TradingClient(
|
|
190
192
|
mt5=mock_mt5_import,
|
|
191
|
-
order_filling_mode="FOK",
|
|
192
193
|
)
|
|
193
|
-
assert client
|
|
194
|
+
assert isinstance(client, Mt5TradingClient)
|
|
194
195
|
|
|
195
196
|
def test_client_initialization_invalid_filling_mode(
|
|
196
197
|
self, mock_mt5_import: ModuleType
|
|
197
198
|
) -> None:
|
|
198
199
|
"""Test client initialization with invalid filling mode."""
|
|
199
|
-
|
|
200
|
-
|
|
200
|
+
# Order filling mode is now a parameter to methods, not a class attribute
|
|
201
|
+
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
202
|
+
# Test that the method validates the parameter
|
|
203
|
+
mock_mt5_import.initialize.return_value = True
|
|
204
|
+
client.initialize()
|
|
205
|
+
mock_mt5_import.positions_get.return_value = []
|
|
206
|
+
# Should not raise as validation happens at method level
|
|
207
|
+
result = client._fetch_and_close_position(order_filling_mode="IOC") # type: ignore[arg-type]
|
|
208
|
+
assert result == []
|
|
201
209
|
|
|
202
210
|
def test_close_position_no_positions(
|
|
203
211
|
self,
|
|
@@ -561,6 +569,33 @@ class TestMt5TradingClient:
|
|
|
561
569
|
|
|
562
570
|
assert result["retcode"] == 10018
|
|
563
571
|
|
|
572
|
+
def test_send_or_check_order_no_changes(
|
|
573
|
+
self,
|
|
574
|
+
mock_mt5_import: ModuleType,
|
|
575
|
+
) -> None:
|
|
576
|
+
"""Test _send_or_check_order with no changes return code."""
|
|
577
|
+
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
578
|
+
mock_mt5_import.initialize.return_value = True
|
|
579
|
+
client.initialize()
|
|
580
|
+
|
|
581
|
+
request = {
|
|
582
|
+
"action": 1,
|
|
583
|
+
"symbol": "EURUSD",
|
|
584
|
+
"volume": 0.1,
|
|
585
|
+
"type": 1,
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
# Mock no changes response
|
|
589
|
+
mock_mt5_import.order_send.return_value.retcode = 10025
|
|
590
|
+
mock_mt5_import.order_send.return_value._asdict.return_value = {
|
|
591
|
+
"retcode": 10025,
|
|
592
|
+
"comment": "No changes",
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
result = client._send_or_check_order(request)
|
|
596
|
+
|
|
597
|
+
assert result["retcode"] == 10025
|
|
598
|
+
|
|
564
599
|
def test_send_or_check_order_failure(
|
|
565
600
|
self,
|
|
566
601
|
mock_mt5_import: ModuleType,
|
|
@@ -733,7 +768,7 @@ class TestMt5TradingClient:
|
|
|
733
768
|
mock_position_buy: MockPositionInfo,
|
|
734
769
|
) -> None:
|
|
735
770
|
"""Test that order filling mode constants are used correctly."""
|
|
736
|
-
client = Mt5TradingClient(mt5=mock_mt5_import
|
|
771
|
+
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
737
772
|
mock_mt5_import.initialize.return_value = True
|
|
738
773
|
client.initialize()
|
|
739
774
|
|
|
@@ -745,7 +780,8 @@ class TestMt5TradingClient:
|
|
|
745
780
|
"retcode": 10009
|
|
746
781
|
}
|
|
747
782
|
|
|
748
|
-
|
|
783
|
+
# Call _fetch_and_close_position with FOK mode
|
|
784
|
+
client._fetch_and_close_position("EURUSD", order_filling_mode="FOK")
|
|
749
785
|
|
|
750
786
|
# Verify that ORDER_FILLING_FOK was used
|
|
751
787
|
call_args = mock_mt5_import.order_send.call_args[0][0]
|
|
@@ -973,6 +1009,69 @@ class TestMt5TradingClient:
|
|
|
973
1009
|
expected_volume = 5 * 0.01
|
|
974
1010
|
assert result == expected_volume
|
|
975
1011
|
|
|
1012
|
+
def test_calculate_minimum_order_margin_no_margin(
|
|
1013
|
+
self,
|
|
1014
|
+
mock_mt5_import: ModuleType,
|
|
1015
|
+
) -> None:
|
|
1016
|
+
"""Test calculation when order_calc_margin returns zero."""
|
|
1017
|
+
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
1018
|
+
mock_mt5_import.initialize.return_value = True
|
|
1019
|
+
client.initialize()
|
|
1020
|
+
|
|
1021
|
+
# Mock symbol info
|
|
1022
|
+
mock_mt5_import.symbol_info.return_value._asdict.return_value = {
|
|
1023
|
+
"volume_min": 0.01,
|
|
1024
|
+
"name": "EURUSD",
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
# Mock symbol tick info
|
|
1028
|
+
mock_mt5_import.symbol_info_tick.return_value._asdict.return_value = {
|
|
1029
|
+
"ask": 1.1000,
|
|
1030
|
+
"bid": 1.0998,
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
# Mock order_calc_margin to return 0.0 (no margin required)
|
|
1034
|
+
mock_mt5_import.order_calc_margin.return_value = 0.0
|
|
1035
|
+
|
|
1036
|
+
result = client.calculate_minimum_order_margin("EURUSD", "BUY")
|
|
1037
|
+
|
|
1038
|
+
assert result == {"volume": 0.01, "margin": 0.0}
|
|
1039
|
+
mock_mt5_import.order_calc_margin.assert_called_once_with(
|
|
1040
|
+
mock_mt5_import.ORDER_TYPE_BUY,
|
|
1041
|
+
"EURUSD",
|
|
1042
|
+
0.01,
|
|
1043
|
+
1.1000,
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
def test_calculate_volume_by_margin_zero_margin(
|
|
1047
|
+
self,
|
|
1048
|
+
mock_mt5_import: ModuleType,
|
|
1049
|
+
) -> None:
|
|
1050
|
+
"""Test calculation when minimum order margin is zero."""
|
|
1051
|
+
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
1052
|
+
mock_mt5_import.initialize.return_value = True
|
|
1053
|
+
client.initialize()
|
|
1054
|
+
|
|
1055
|
+
# Mock symbol info
|
|
1056
|
+
mock_mt5_import.symbol_info.return_value._asdict.return_value = {
|
|
1057
|
+
"volume_min": 0.01,
|
|
1058
|
+
"name": "EURUSD",
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
# Mock symbol tick info
|
|
1062
|
+
mock_mt5_import.symbol_info_tick.return_value._asdict.return_value = {
|
|
1063
|
+
"ask": 1.1000,
|
|
1064
|
+
"bid": 1.0998,
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
# Mock order_calc_margin to return 0.0 (no margin required)
|
|
1068
|
+
mock_mt5_import.order_calc_margin.return_value = 0.0
|
|
1069
|
+
|
|
1070
|
+
result = client.calculate_volume_by_margin("EURUSD", 1000.0, "BUY")
|
|
1071
|
+
|
|
1072
|
+
# Should return 0.0 when margin is zero
|
|
1073
|
+
assert result == 0.0
|
|
1074
|
+
|
|
976
1075
|
def test_calculate_spread_ratio(
|
|
977
1076
|
self,
|
|
978
1077
|
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
|