pdmt5 0.1.8__tar.gz → 0.2.0__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 → pdmt5-0.2.0}/.github/workflows/ci.yml +4 -13
- pdmt5-0.2.0/.github/workflows/pr-agent.yml +34 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/PKG-INFO +4 -5
- {pdmt5-0.1.8 → pdmt5-0.2.0}/README.md +3 -4
- {pdmt5-0.1.8 → pdmt5-0.2.0}/pdmt5/trading.py +143 -28
- {pdmt5-0.1.8 → pdmt5-0.2.0}/pyproject.toml +1 -1
- {pdmt5-0.1.8 → pdmt5-0.2.0}/tests/test_trading.py +125 -9
- {pdmt5-0.1.8 → pdmt5-0.2.0}/uv.lock +2 -2
- {pdmt5-0.1.8 → pdmt5-0.2.0}/.claude/settings.json +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/.github/FUNDING.yml +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/.github/copilot-instructions.md +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/.github/dependabot.yml +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/.github/workflows/claude.yml +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/.gitignore +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/CLAUDE.md +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/LICENSE +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/docs/api/dataframe.md +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/docs/api/index.md +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/docs/api/mt5.md +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/docs/api/trading.md +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/docs/api/utils.md +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/docs/index.md +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/mkdocs.yml +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/pdmt5/__init__.py +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/pdmt5/dataframe.py +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/pdmt5/mt5.py +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/pdmt5/utils.py +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/renovate.json +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/tests/__init__.py +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/tests/test_dataframe.py +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/tests/test_init.py +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/tests/test_mt5.py +0 -0
- {pdmt5-0.1.8 → pdmt5-0.2.0}/tests/test_utils.py +0 -0
|
@@ -30,25 +30,16 @@ jobs:
|
|
|
30
30
|
uses: dceoy/gh-actions-for-devops/.github/workflows/python-package-lint-and-scan.yml@main
|
|
31
31
|
with:
|
|
32
32
|
package-path: .
|
|
33
|
-
python-version: 3.x
|
|
34
33
|
runs-on: windows-latest
|
|
35
34
|
python-test:
|
|
36
35
|
if: >
|
|
37
36
|
github.event_name == 'push'
|
|
38
37
|
|| github.event_name == 'pull_request'
|
|
39
38
|
|| (github.event_name == 'workflow_dispatch' && inputs.workflow == 'lint-and-test')
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
|
|
44
|
-
- name: Set up uv
|
|
45
|
-
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
|
46
|
-
- name: Install the package
|
|
47
|
-
run: >
|
|
48
|
-
uv sync
|
|
49
|
-
- name: Run unit tests with pytest
|
|
50
|
-
run: >
|
|
51
|
-
uv run pytest
|
|
39
|
+
uses: dceoy/gh-actions-for-devops/.github/workflows/python-package-test.yml@main
|
|
40
|
+
with:
|
|
41
|
+
package-path: .
|
|
42
|
+
runs-on: windows-latest
|
|
52
43
|
dependabot-auto-merge:
|
|
53
44
|
if: >
|
|
54
45
|
github.event_name == 'pull_request' && github.actor == 'dependabot[bot]'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: PR-agent
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types:
|
|
6
|
+
- opened
|
|
7
|
+
- reopened
|
|
8
|
+
- ready_for_review
|
|
9
|
+
issue_comment:
|
|
10
|
+
types:
|
|
11
|
+
- created
|
|
12
|
+
- edited
|
|
13
|
+
- deleted
|
|
14
|
+
jobs:
|
|
15
|
+
pr-agent:
|
|
16
|
+
if: >
|
|
17
|
+
github.event.sender.type != 'Bot'
|
|
18
|
+
&& (
|
|
19
|
+
github.event_name == 'pull_request'
|
|
20
|
+
|| github.event_name == 'issue_comment'
|
|
21
|
+
)
|
|
22
|
+
uses: dceoy/gh-actions-for-devops/.github/workflows/pr-agent.yml@main
|
|
23
|
+
permissions:
|
|
24
|
+
contents: write
|
|
25
|
+
pull-requests: write
|
|
26
|
+
issues: write
|
|
27
|
+
id-token: write
|
|
28
|
+
with:
|
|
29
|
+
auto-describe: true
|
|
30
|
+
auto-review: true
|
|
31
|
+
auto-improve: true
|
|
32
|
+
secrets:
|
|
33
|
+
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
|
34
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pdmt5
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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>
|
|
@@ -53,14 +53,13 @@ Pandas-based data handler for MetaTrader 5
|
|
|
53
53
|
|
|
54
54
|
## Installation
|
|
55
55
|
|
|
56
|
-
###
|
|
56
|
+
### Using pip
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
|
-
|
|
60
|
-
pip install -U --no-cache-dir ./pdmt5
|
|
59
|
+
pip install -U pdmt5 MetaTrader5
|
|
61
60
|
```
|
|
62
61
|
|
|
63
|
-
### Using uv
|
|
62
|
+
### Using uv
|
|
64
63
|
|
|
65
64
|
```bash
|
|
66
65
|
git clone https://github.com/dceoy/pdmt5.git
|
|
@@ -30,14 +30,13 @@ Pandas-based data handler for MetaTrader 5
|
|
|
30
30
|
|
|
31
31
|
## Installation
|
|
32
32
|
|
|
33
|
-
###
|
|
33
|
+
### Using pip
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
|
|
37
|
-
pip install -U --no-cache-dir ./pdmt5
|
|
36
|
+
pip install -U pdmt5 MetaTrader5
|
|
38
37
|
```
|
|
39
38
|
|
|
40
|
-
### Using uv
|
|
39
|
+
### Using uv
|
|
41
40
|
|
|
42
41
|
```bash
|
|
43
42
|
git clone https://github.com/dceoy/pdmt5.git
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from datetime import timedelta
|
|
6
|
+
from functools import cached_property
|
|
6
7
|
from math import floor
|
|
7
8
|
from typing import TYPE_CHECKING, Any, Literal
|
|
8
9
|
|
|
@@ -28,6 +29,67 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
28
29
|
|
|
29
30
|
model_config = ConfigDict(frozen=True)
|
|
30
31
|
|
|
32
|
+
@cached_property
|
|
33
|
+
def mt5_successful_trade_retcodes(self) -> set[int]:
|
|
34
|
+
"""Set of successful trade return codes.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Set of successful trade return codes.
|
|
38
|
+
"""
|
|
39
|
+
return {
|
|
40
|
+
self.mt5.TRADE_RETCODE_PLACED, # 10008
|
|
41
|
+
self.mt5.TRADE_RETCODE_DONE, # 10009
|
|
42
|
+
self.mt5.TRADE_RETCODE_DONE_PARTIAL, # 10010
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@cached_property
|
|
46
|
+
def mt5_failed_trade_retcodes(self) -> set[int]:
|
|
47
|
+
"""Set of failed trade return codes.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Set of failed trade return codes.
|
|
51
|
+
"""
|
|
52
|
+
return {
|
|
53
|
+
self.mt5.TRADE_RETCODE_REQUOTE, # 10004
|
|
54
|
+
self.mt5.TRADE_RETCODE_REJECT, # 10006
|
|
55
|
+
self.mt5.TRADE_RETCODE_CANCEL, # 10007
|
|
56
|
+
self.mt5.TRADE_RETCODE_ERROR, # 10011
|
|
57
|
+
self.mt5.TRADE_RETCODE_TIMEOUT, # 10012
|
|
58
|
+
self.mt5.TRADE_RETCODE_INVALID, # 10013
|
|
59
|
+
self.mt5.TRADE_RETCODE_INVALID_VOLUME, # 10014
|
|
60
|
+
self.mt5.TRADE_RETCODE_INVALID_PRICE, # 10015
|
|
61
|
+
self.mt5.TRADE_RETCODE_INVALID_STOPS, # 10016
|
|
62
|
+
self.mt5.TRADE_RETCODE_TRADE_DISABLED, # 10017
|
|
63
|
+
self.mt5.TRADE_RETCODE_MARKET_CLOSED, # 10018
|
|
64
|
+
self.mt5.TRADE_RETCODE_NO_MONEY, # 10019
|
|
65
|
+
self.mt5.TRADE_RETCODE_PRICE_CHANGED, # 10020
|
|
66
|
+
self.mt5.TRADE_RETCODE_PRICE_OFF, # 10021
|
|
67
|
+
self.mt5.TRADE_RETCODE_INVALID_EXPIRATION, # 10022
|
|
68
|
+
self.mt5.TRADE_RETCODE_ORDER_CHANGED, # 10023
|
|
69
|
+
self.mt5.TRADE_RETCODE_TOO_MANY_REQUESTS, # 10024
|
|
70
|
+
self.mt5.TRADE_RETCODE_NO_CHANGES, # 10025
|
|
71
|
+
self.mt5.TRADE_RETCODE_SERVER_DISABLES_AT, # 10026
|
|
72
|
+
self.mt5.TRADE_RETCODE_CLIENT_DISABLES_AT, # 10027
|
|
73
|
+
self.mt5.TRADE_RETCODE_LOCKED, # 10028
|
|
74
|
+
self.mt5.TRADE_RETCODE_FROZEN, # 10029
|
|
75
|
+
self.mt5.TRADE_RETCODE_INVALID_FILL, # 10030
|
|
76
|
+
self.mt5.TRADE_RETCODE_CONNECTION, # 10031
|
|
77
|
+
self.mt5.TRADE_RETCODE_ONLY_REAL, # 10032
|
|
78
|
+
self.mt5.TRADE_RETCODE_LIMIT_ORDERS, # 10033
|
|
79
|
+
self.mt5.TRADE_RETCODE_LIMIT_VOLUME, # 10034
|
|
80
|
+
self.mt5.TRADE_RETCODE_INVALID_ORDER, # 10035
|
|
81
|
+
self.mt5.TRADE_RETCODE_POSITION_CLOSED, # 10036
|
|
82
|
+
self.mt5.TRADE_RETCODE_INVALID_CLOSE_VOLUME, # 10038
|
|
83
|
+
self.mt5.TRADE_RETCODE_CLOSE_ORDER_EXIST, # 10039
|
|
84
|
+
self.mt5.TRADE_RETCODE_LIMIT_POSITIONS, # 10040
|
|
85
|
+
self.mt5.TRADE_RETCODE_REJECT_CANCEL, # 10041
|
|
86
|
+
self.mt5.TRADE_RETCODE_LONG_ONLY, # 10042
|
|
87
|
+
self.mt5.TRADE_RETCODE_SHORT_ONLY, # 10043
|
|
88
|
+
self.mt5.TRADE_RETCODE_CLOSE_ONLY, # 10044
|
|
89
|
+
self.mt5.TRADE_RETCODE_FIFO_CLOSE, # 10045
|
|
90
|
+
self.mt5.TRADE_RETCODE_HEDGE_PROHIBITED, # 10046
|
|
91
|
+
}
|
|
92
|
+
|
|
31
93
|
def close_open_positions(
|
|
32
94
|
self,
|
|
33
95
|
symbols: str | list[str] | tuple[str, ...] | None = None,
|
|
@@ -116,12 +178,14 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
116
178
|
def _send_or_check_order(
|
|
117
179
|
self,
|
|
118
180
|
request: dict[str, Any],
|
|
181
|
+
raise_on_error: bool = False,
|
|
119
182
|
dry_run: bool = False,
|
|
120
183
|
) -> dict[str, Any]:
|
|
121
184
|
"""Send or check an order request.
|
|
122
185
|
|
|
123
186
|
Args:
|
|
124
187
|
request: Order request dictionary.
|
|
188
|
+
raise_on_error: If True, raise an error on operation failure.
|
|
125
189
|
dry_run: If True, only check the order without sending it.
|
|
126
190
|
|
|
127
191
|
Returns:
|
|
@@ -138,25 +202,21 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
138
202
|
response = self.order_send_as_dict(request=request)
|
|
139
203
|
order_func = "order_send"
|
|
140
204
|
retcode = response.get("retcode")
|
|
141
|
-
if (
|
|
142
|
-
dry_run and retcode
|
|
205
|
+
if (dry_run and retcode == 0) or (
|
|
206
|
+
not dry_run and retcode in self.mt5_successful_trade_retcodes
|
|
143
207
|
):
|
|
144
208
|
self.logger.info("response: %s", response)
|
|
145
209
|
return response
|
|
146
|
-
elif
|
|
147
|
-
self.mt5.TRADE_RETCODE_TRADE_DISABLED,
|
|
148
|
-
self.mt5.TRADE_RETCODE_MARKET_CLOSED,
|
|
149
|
-
self.mt5.TRADE_RETCODE_NO_CHANGES,
|
|
150
|
-
}:
|
|
151
|
-
self.logger.info("response: %s", response)
|
|
152
|
-
comment = response.get("comment", "Unknown error")
|
|
153
|
-
self.logger.warning("%s() failed and skipped. <= `%s`", order_func, comment)
|
|
154
|
-
return response
|
|
155
|
-
else:
|
|
210
|
+
elif raise_on_error:
|
|
156
211
|
self.logger.error("response: %s", response)
|
|
157
|
-
comment = response.get("comment"
|
|
212
|
+
comment = response.get("comment")
|
|
158
213
|
error_message = f"{order_func}() failed and aborted. <= `{comment}`"
|
|
159
214
|
raise Mt5TradingError(error_message)
|
|
215
|
+
else:
|
|
216
|
+
self.logger.warning("response: %s", response)
|
|
217
|
+
comment = response.get("comment")
|
|
218
|
+
self.logger.warning("%s() failed and skipped. <= `%s`", order_func, comment)
|
|
219
|
+
return response
|
|
160
220
|
|
|
161
221
|
def place_market_order(
|
|
162
222
|
self,
|
|
@@ -183,6 +243,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
183
243
|
Returns:
|
|
184
244
|
Dictionary with operation result.
|
|
185
245
|
"""
|
|
246
|
+
self.logger.info("Placing market order: %s %s %s", order_side, volume, symbol)
|
|
186
247
|
return self._send_or_check_order(
|
|
187
248
|
request={
|
|
188
249
|
"action": self.mt5.TRADE_ACTION_DEAL,
|
|
@@ -255,6 +316,13 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
255
316
|
if sl != p["sl"] or tp != p["tp"]
|
|
256
317
|
]
|
|
257
318
|
if order_requests:
|
|
319
|
+
self.logger.info(
|
|
320
|
+
"Updating SL/TP for %d positions for %s: %s/%s",
|
|
321
|
+
len(order_requests),
|
|
322
|
+
symbol,
|
|
323
|
+
sl,
|
|
324
|
+
tp,
|
|
325
|
+
)
|
|
258
326
|
return [
|
|
259
327
|
self._send_or_check_order(request=r, dry_run=dry_run)
|
|
260
328
|
for r in order_requests
|
|
@@ -294,15 +362,22 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
294
362
|
else symbol_info_tick["ask"]
|
|
295
363
|
),
|
|
296
364
|
)
|
|
365
|
+
result = {"volume": symbol_info["volume_min"], "margin": margin}
|
|
297
366
|
if margin:
|
|
298
|
-
|
|
367
|
+
self.logger.info(
|
|
368
|
+
"Calculated minimum %s order margin for %s: %s",
|
|
369
|
+
order_side,
|
|
370
|
+
symbol,
|
|
371
|
+
result,
|
|
372
|
+
)
|
|
299
373
|
else:
|
|
300
374
|
self.logger.warning(
|
|
301
|
-
"
|
|
302
|
-
symbol,
|
|
375
|
+
"Calculated minimum order margin to %s %s: %s",
|
|
303
376
|
order_side,
|
|
377
|
+
symbol,
|
|
378
|
+
result,
|
|
304
379
|
)
|
|
305
|
-
|
|
380
|
+
return result
|
|
306
381
|
|
|
307
382
|
def calculate_volume_by_margin(
|
|
308
383
|
self,
|
|
@@ -325,12 +400,19 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
325
400
|
order_side=order_side,
|
|
326
401
|
)
|
|
327
402
|
if min_order_margin_dict["margin"]:
|
|
328
|
-
|
|
403
|
+
result = (
|
|
329
404
|
floor(margin / min_order_margin_dict["margin"])
|
|
330
405
|
* min_order_margin_dict["volume"]
|
|
331
406
|
)
|
|
332
407
|
else:
|
|
333
|
-
|
|
408
|
+
result = 0.0
|
|
409
|
+
self.logger.info(
|
|
410
|
+
"Calculated volume by margin to %s %s: %s",
|
|
411
|
+
order_side,
|
|
412
|
+
symbol,
|
|
413
|
+
result,
|
|
414
|
+
)
|
|
415
|
+
return result
|
|
334
416
|
|
|
335
417
|
def calculate_spread_ratio(
|
|
336
418
|
self,
|
|
@@ -345,11 +427,13 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
345
427
|
Spread ratio as a float.
|
|
346
428
|
"""
|
|
347
429
|
symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
|
|
348
|
-
|
|
430
|
+
result = (
|
|
349
431
|
(symbol_info_tick["ask"] - symbol_info_tick["bid"])
|
|
350
432
|
/ (symbol_info_tick["ask"] + symbol_info_tick["bid"])
|
|
351
433
|
* 2
|
|
352
434
|
)
|
|
435
|
+
self.logger.info("Calculated spread ratio for %s: %s", symbol, result)
|
|
436
|
+
return result
|
|
353
437
|
|
|
354
438
|
def fetch_latest_rates_as_df(
|
|
355
439
|
self,
|
|
@@ -380,13 +464,20 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
380
464
|
)
|
|
381
465
|
raise Mt5TradingError(error_message) from e
|
|
382
466
|
else:
|
|
383
|
-
|
|
467
|
+
result = self.copy_rates_from_pos_as_df(
|
|
384
468
|
symbol=symbol,
|
|
385
469
|
timeframe=timeframe,
|
|
386
470
|
start_pos=0,
|
|
387
471
|
count=count,
|
|
388
472
|
index_keys=index_keys,
|
|
389
473
|
)
|
|
474
|
+
self.logger.info(
|
|
475
|
+
"Fetched latest %s rates for %s: %d rows",
|
|
476
|
+
granularity,
|
|
477
|
+
symbol,
|
|
478
|
+
result.shape[0],
|
|
479
|
+
)
|
|
480
|
+
return result
|
|
390
481
|
|
|
391
482
|
def fetch_latest_ticks_as_df(
|
|
392
483
|
self,
|
|
@@ -405,13 +496,19 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
405
496
|
pd.DataFrame: Tick data with time index.
|
|
406
497
|
"""
|
|
407
498
|
last_tick_time = self.symbol_info_tick_as_dict(symbol=symbol)["time"]
|
|
408
|
-
|
|
499
|
+
result = self.copy_ticks_range_as_df(
|
|
409
500
|
symbol=symbol,
|
|
410
501
|
date_from=(last_tick_time - timedelta(seconds=seconds)),
|
|
411
502
|
date_to=(last_tick_time + timedelta(seconds=seconds)),
|
|
412
503
|
flags=self.mt5.COPY_TICKS_ALL,
|
|
413
504
|
index_keys=index_keys,
|
|
414
505
|
)
|
|
506
|
+
self.logger.info(
|
|
507
|
+
"Fetched latest ticks for %s: %d rows",
|
|
508
|
+
symbol,
|
|
509
|
+
result.shape[0],
|
|
510
|
+
)
|
|
511
|
+
return result
|
|
415
512
|
|
|
416
513
|
def collect_entry_deals_as_df(
|
|
417
514
|
self,
|
|
@@ -437,14 +534,20 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
437
534
|
index_keys=index_keys,
|
|
438
535
|
)
|
|
439
536
|
if deals_df.empty:
|
|
440
|
-
|
|
537
|
+
result = deals_df
|
|
441
538
|
else:
|
|
442
|
-
|
|
539
|
+
result = deals_df.pipe(
|
|
443
540
|
lambda d: d[
|
|
444
541
|
d["entry"]
|
|
445
542
|
& d["type"].isin({self.mt5.DEAL_TYPE_BUY, self.mt5.DEAL_TYPE_SELL})
|
|
446
543
|
]
|
|
447
544
|
)
|
|
545
|
+
self.logger.info(
|
|
546
|
+
"Collected entry deals for %s: %d rows",
|
|
547
|
+
symbol,
|
|
548
|
+
result.shape[0],
|
|
549
|
+
)
|
|
550
|
+
return result
|
|
448
551
|
|
|
449
552
|
def fetch_positions_with_metrics_as_df(
|
|
450
553
|
self,
|
|
@@ -460,7 +563,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
460
563
|
"""
|
|
461
564
|
positions_df = self.positions_get_as_df(symbol=symbol)
|
|
462
565
|
if positions_df.empty:
|
|
463
|
-
|
|
566
|
+
result = positions_df
|
|
464
567
|
else:
|
|
465
568
|
symbol_info_tick = self.symbol_info_tick_as_dict(symbol=symbol)
|
|
466
569
|
ask_margin = self.order_calc_margin(
|
|
@@ -475,7 +578,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
475
578
|
volume=1,
|
|
476
579
|
price=symbol_info_tick["bid"],
|
|
477
580
|
)
|
|
478
|
-
|
|
581
|
+
result = (
|
|
479
582
|
positions_df.assign(
|
|
480
583
|
elapsed_seconds=lambda d: (
|
|
481
584
|
symbol_info_tick["time"] - d["time"]
|
|
@@ -506,6 +609,12 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
506
609
|
)
|
|
507
610
|
.drop(columns=["buy_i", "sell_i", "sign", "underlier_increase_ratio"])
|
|
508
611
|
)
|
|
612
|
+
self.logger.info(
|
|
613
|
+
"Fetched positions with metrics for %s: %d rows",
|
|
614
|
+
symbol,
|
|
615
|
+
result.shape[0],
|
|
616
|
+
)
|
|
617
|
+
return result
|
|
509
618
|
|
|
510
619
|
def calculate_new_position_margin_ratio(
|
|
511
620
|
self,
|
|
@@ -525,7 +634,7 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
525
634
|
"""
|
|
526
635
|
account_info = self.account_info_as_dict()
|
|
527
636
|
if not account_info["equity"]:
|
|
528
|
-
|
|
637
|
+
result = 0.0
|
|
529
638
|
else:
|
|
530
639
|
positions_df = self.fetch_positions_with_metrics_as_df(symbol=symbol)
|
|
531
640
|
current_signed_margin = (
|
|
@@ -550,6 +659,12 @@ class Mt5TradingClient(Mt5DataClient):
|
|
|
550
659
|
)
|
|
551
660
|
else:
|
|
552
661
|
new_signed_margin = 0
|
|
553
|
-
|
|
662
|
+
result = abs(
|
|
554
663
|
(new_signed_margin + current_signed_margin) / account_info["equity"]
|
|
555
664
|
)
|
|
665
|
+
self.logger.info(
|
|
666
|
+
"Calculated new position margin ratio for %s: %s",
|
|
667
|
+
symbol,
|
|
668
|
+
result,
|
|
669
|
+
)
|
|
670
|
+
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pdmt5"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
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"}]
|
|
@@ -20,7 +20,7 @@ Mt5TradingClient.model_rebuild()
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
@pytest.fixture(autouse=True)
|
|
23
|
-
def mock_mt5_import(
|
|
23
|
+
def mock_mt5_import( # noqa: PLR0915
|
|
24
24
|
request: pytest.FixtureRequest,
|
|
25
25
|
mocker: MockerFixture,
|
|
26
26
|
) -> Generator[ModuleType | None, None, None]:
|
|
@@ -69,10 +69,50 @@ def mock_mt5_import(
|
|
|
69
69
|
mock_mt5.ORDER_FILLING_FOK = 2
|
|
70
70
|
mock_mt5.ORDER_FILLING_RETURN = 3
|
|
71
71
|
mock_mt5.ORDER_TIME_GTC = 0
|
|
72
|
+
|
|
73
|
+
# Trade return codes
|
|
74
|
+
mock_mt5.TRADE_RETCODE_REQUOTE = 10004
|
|
75
|
+
mock_mt5.TRADE_RETCODE_REJECT = 10006
|
|
76
|
+
mock_mt5.TRADE_RETCODE_CANCEL = 10007
|
|
77
|
+
mock_mt5.TRADE_RETCODE_PLACED = 10008
|
|
72
78
|
mock_mt5.TRADE_RETCODE_DONE = 10009
|
|
79
|
+
mock_mt5.TRADE_RETCODE_DONE_PARTIAL = 10010
|
|
80
|
+
mock_mt5.TRADE_RETCODE_ERROR = 10011
|
|
81
|
+
mock_mt5.TRADE_RETCODE_TIMEOUT = 10012
|
|
82
|
+
mock_mt5.TRADE_RETCODE_INVALID = 10013
|
|
83
|
+
mock_mt5.TRADE_RETCODE_INVALID_VOLUME = 10014
|
|
84
|
+
mock_mt5.TRADE_RETCODE_INVALID_PRICE = 10015
|
|
85
|
+
mock_mt5.TRADE_RETCODE_INVALID_STOPS = 10016
|
|
73
86
|
mock_mt5.TRADE_RETCODE_TRADE_DISABLED = 10017
|
|
74
87
|
mock_mt5.TRADE_RETCODE_MARKET_CLOSED = 10018
|
|
88
|
+
mock_mt5.TRADE_RETCODE_NO_MONEY = 10019
|
|
89
|
+
mock_mt5.TRADE_RETCODE_PRICE_CHANGED = 10020
|
|
90
|
+
mock_mt5.TRADE_RETCODE_PRICE_OFF = 10021
|
|
91
|
+
mock_mt5.TRADE_RETCODE_INVALID_EXPIRATION = 10022
|
|
92
|
+
mock_mt5.TRADE_RETCODE_ORDER_CHANGED = 10023
|
|
93
|
+
mock_mt5.TRADE_RETCODE_TOO_MANY_REQUESTS = 10024
|
|
75
94
|
mock_mt5.TRADE_RETCODE_NO_CHANGES = 10025
|
|
95
|
+
mock_mt5.TRADE_RETCODE_SERVER_DISABLES_AT = 10026
|
|
96
|
+
mock_mt5.TRADE_RETCODE_CLIENT_DISABLES_AT = 10027
|
|
97
|
+
mock_mt5.TRADE_RETCODE_LOCKED = 10028
|
|
98
|
+
mock_mt5.TRADE_RETCODE_FROZEN = 10029
|
|
99
|
+
mock_mt5.TRADE_RETCODE_INVALID_FILL = 10030
|
|
100
|
+
mock_mt5.TRADE_RETCODE_CONNECTION = 10031
|
|
101
|
+
mock_mt5.TRADE_RETCODE_ONLY_REAL = 10032
|
|
102
|
+
mock_mt5.TRADE_RETCODE_LIMIT_ORDERS = 10033
|
|
103
|
+
mock_mt5.TRADE_RETCODE_LIMIT_VOLUME = 10034
|
|
104
|
+
mock_mt5.TRADE_RETCODE_INVALID_ORDER = 10035
|
|
105
|
+
mock_mt5.TRADE_RETCODE_POSITION_CLOSED = 10036
|
|
106
|
+
mock_mt5.TRADE_RETCODE_INVALID_CLOSE_VOLUME = 10038
|
|
107
|
+
mock_mt5.TRADE_RETCODE_CLOSE_ORDER_EXIST = 10039
|
|
108
|
+
mock_mt5.TRADE_RETCODE_LIMIT_POSITIONS = 10040
|
|
109
|
+
mock_mt5.TRADE_RETCODE_REJECT_CANCEL = 10041
|
|
110
|
+
mock_mt5.TRADE_RETCODE_LONG_ONLY = 10042
|
|
111
|
+
mock_mt5.TRADE_RETCODE_SHORT_ONLY = 10043
|
|
112
|
+
mock_mt5.TRADE_RETCODE_CLOSE_ONLY = 10044
|
|
113
|
+
mock_mt5.TRADE_RETCODE_FIFO_CLOSE = 10045
|
|
114
|
+
mock_mt5.TRADE_RETCODE_HEDGE_PROHIBITED = 10046
|
|
115
|
+
|
|
76
116
|
mock_mt5.RES_S_OK = 1
|
|
77
117
|
mock_mt5.DEAL_TYPE_BUY = 0
|
|
78
118
|
mock_mt5.DEAL_TYPE_SELL = 1
|
|
@@ -612,15 +652,15 @@ class TestMt5TradingClient:
|
|
|
612
652
|
"type": 1,
|
|
613
653
|
}
|
|
614
654
|
|
|
615
|
-
# Mock failure response
|
|
616
|
-
mock_mt5_import.order_send.return_value.retcode =
|
|
655
|
+
# Mock failure response with error retcode
|
|
656
|
+
mock_mt5_import.order_send.return_value.retcode = 10006
|
|
617
657
|
mock_mt5_import.order_send.return_value._asdict.return_value = {
|
|
618
|
-
"retcode":
|
|
658
|
+
"retcode": 10006,
|
|
619
659
|
"comment": "Invalid request",
|
|
620
660
|
}
|
|
621
661
|
|
|
622
662
|
with pytest.raises(Mt5TradingError, match=r"order_send\(\) failed and aborted"):
|
|
623
|
-
client._send_or_check_order(request)
|
|
663
|
+
client._send_or_check_order(request, raise_on_error=True)
|
|
624
664
|
|
|
625
665
|
def test_send_or_check_order_dry_run_failure(
|
|
626
666
|
self,
|
|
@@ -638,17 +678,17 @@ class TestMt5TradingClient:
|
|
|
638
678
|
"type": 1,
|
|
639
679
|
}
|
|
640
680
|
|
|
641
|
-
# Mock failure response
|
|
642
|
-
mock_mt5_import.order_check.return_value.retcode =
|
|
681
|
+
# Mock failure response with non-zero retcode for dry run
|
|
682
|
+
mock_mt5_import.order_check.return_value.retcode = 10013
|
|
643
683
|
mock_mt5_import.order_check.return_value._asdict.return_value = {
|
|
644
|
-
"retcode":
|
|
684
|
+
"retcode": 10013,
|
|
645
685
|
"comment": "Invalid request",
|
|
646
686
|
}
|
|
647
687
|
|
|
648
688
|
with pytest.raises(
|
|
649
689
|
Mt5TradingError, match=r"order_check\(\) failed and aborted"
|
|
650
690
|
):
|
|
651
|
-
client._send_or_check_order(request, dry_run=True)
|
|
691
|
+
client._send_or_check_order(request, raise_on_error=True, dry_run=True)
|
|
652
692
|
|
|
653
693
|
def test_send_or_check_order_dry_run_override(
|
|
654
694
|
self,
|
|
@@ -1896,3 +1936,79 @@ class TestMt5TradingClient:
|
|
|
1896
1936
|
assert isinstance(result, list)
|
|
1897
1937
|
assert len(result) == 2
|
|
1898
1938
|
assert all(r["retcode"] == 10009 for r in result)
|
|
1939
|
+
|
|
1940
|
+
def test_mt5_successful_trade_retcodes_property(
|
|
1941
|
+
self, mock_mt5_import: ModuleType
|
|
1942
|
+
) -> None:
|
|
1943
|
+
"""Test mt5_successful_trade_retcodes property returns correct set of codes."""
|
|
1944
|
+
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
1945
|
+
|
|
1946
|
+
# Get the property value
|
|
1947
|
+
retcodes = client.mt5_successful_trade_retcodes
|
|
1948
|
+
|
|
1949
|
+
# Verify it's a set
|
|
1950
|
+
assert isinstance(retcodes, set)
|
|
1951
|
+
|
|
1952
|
+
# Verify the expected codes are present
|
|
1953
|
+
assert retcodes == {
|
|
1954
|
+
mock_mt5_import.TRADE_RETCODE_PLACED, # 10008
|
|
1955
|
+
mock_mt5_import.TRADE_RETCODE_DONE, # 10009
|
|
1956
|
+
mock_mt5_import.TRADE_RETCODE_DONE_PARTIAL, # 10010
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
def test_mt5_failed_trade_retcodes_property(
|
|
1960
|
+
self, mock_mt5_import: ModuleType
|
|
1961
|
+
) -> None:
|
|
1962
|
+
"""Test mt5_failed_trade_retcodes property returns correct set of codes."""
|
|
1963
|
+
client = Mt5TradingClient(mt5=mock_mt5_import)
|
|
1964
|
+
|
|
1965
|
+
# Get the property value
|
|
1966
|
+
retcodes = client.mt5_failed_trade_retcodes
|
|
1967
|
+
|
|
1968
|
+
# Verify it's a set
|
|
1969
|
+
assert isinstance(retcodes, set)
|
|
1970
|
+
|
|
1971
|
+
# Verify it contains the expected codes
|
|
1972
|
+
expected_codes = {
|
|
1973
|
+
mock_mt5_import.TRADE_RETCODE_REQUOTE, # 10004
|
|
1974
|
+
mock_mt5_import.TRADE_RETCODE_REJECT, # 10006
|
|
1975
|
+
mock_mt5_import.TRADE_RETCODE_CANCEL, # 10007
|
|
1976
|
+
mock_mt5_import.TRADE_RETCODE_ERROR, # 10011
|
|
1977
|
+
mock_mt5_import.TRADE_RETCODE_TIMEOUT, # 10012
|
|
1978
|
+
mock_mt5_import.TRADE_RETCODE_INVALID, # 10013
|
|
1979
|
+
mock_mt5_import.TRADE_RETCODE_INVALID_VOLUME, # 10014
|
|
1980
|
+
mock_mt5_import.TRADE_RETCODE_INVALID_PRICE, # 10015
|
|
1981
|
+
mock_mt5_import.TRADE_RETCODE_INVALID_STOPS, # 10016
|
|
1982
|
+
mock_mt5_import.TRADE_RETCODE_TRADE_DISABLED, # 10017
|
|
1983
|
+
mock_mt5_import.TRADE_RETCODE_MARKET_CLOSED, # 10018
|
|
1984
|
+
mock_mt5_import.TRADE_RETCODE_NO_MONEY, # 10019
|
|
1985
|
+
mock_mt5_import.TRADE_RETCODE_PRICE_CHANGED, # 10020
|
|
1986
|
+
mock_mt5_import.TRADE_RETCODE_PRICE_OFF, # 10021
|
|
1987
|
+
mock_mt5_import.TRADE_RETCODE_INVALID_EXPIRATION, # 10022
|
|
1988
|
+
mock_mt5_import.TRADE_RETCODE_ORDER_CHANGED, # 10023
|
|
1989
|
+
mock_mt5_import.TRADE_RETCODE_TOO_MANY_REQUESTS, # 10024
|
|
1990
|
+
mock_mt5_import.TRADE_RETCODE_NO_CHANGES, # 10025
|
|
1991
|
+
mock_mt5_import.TRADE_RETCODE_SERVER_DISABLES_AT, # 10026
|
|
1992
|
+
mock_mt5_import.TRADE_RETCODE_CLIENT_DISABLES_AT, # 10027
|
|
1993
|
+
mock_mt5_import.TRADE_RETCODE_LOCKED, # 10028
|
|
1994
|
+
mock_mt5_import.TRADE_RETCODE_FROZEN, # 10029
|
|
1995
|
+
mock_mt5_import.TRADE_RETCODE_INVALID_FILL, # 10030
|
|
1996
|
+
mock_mt5_import.TRADE_RETCODE_CONNECTION, # 10031
|
|
1997
|
+
mock_mt5_import.TRADE_RETCODE_ONLY_REAL, # 10032
|
|
1998
|
+
mock_mt5_import.TRADE_RETCODE_LIMIT_ORDERS, # 10033
|
|
1999
|
+
mock_mt5_import.TRADE_RETCODE_LIMIT_VOLUME, # 10034
|
|
2000
|
+
mock_mt5_import.TRADE_RETCODE_INVALID_ORDER, # 10035
|
|
2001
|
+
mock_mt5_import.TRADE_RETCODE_POSITION_CLOSED, # 10036
|
|
2002
|
+
mock_mt5_import.TRADE_RETCODE_INVALID_CLOSE_VOLUME, # 10038
|
|
2003
|
+
mock_mt5_import.TRADE_RETCODE_CLOSE_ORDER_EXIST, # 10039
|
|
2004
|
+
mock_mt5_import.TRADE_RETCODE_LIMIT_POSITIONS, # 10040
|
|
2005
|
+
mock_mt5_import.TRADE_RETCODE_REJECT_CANCEL, # 10041
|
|
2006
|
+
mock_mt5_import.TRADE_RETCODE_LONG_ONLY, # 10042
|
|
2007
|
+
mock_mt5_import.TRADE_RETCODE_SHORT_ONLY, # 10043
|
|
2008
|
+
mock_mt5_import.TRADE_RETCODE_CLOSE_ONLY, # 10044
|
|
2009
|
+
mock_mt5_import.TRADE_RETCODE_FIFO_CLOSE, # 10045
|
|
2010
|
+
mock_mt5_import.TRADE_RETCODE_HEDGE_PROHIBITED, # 10046
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
# Verify all expected codes are present
|
|
2014
|
+
assert retcodes == expected_codes
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
-
revision =
|
|
2
|
+
revision = 3
|
|
3
3
|
requires-python = ">=3.11"
|
|
4
4
|
resolution-markers = [
|
|
5
5
|
"python_full_version >= '3.12'",
|
|
@@ -613,7 +613,7 @@ wheels = [
|
|
|
613
613
|
|
|
614
614
|
[[package]]
|
|
615
615
|
name = "pdmt5"
|
|
616
|
-
version = "0.
|
|
616
|
+
version = "0.2.0"
|
|
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
|
|
File without changes
|
|
File without changes
|