mt5cli 1.2.3__tar.gz → 1.2.4__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.
- {mt5cli-1.2.3 → mt5cli-1.2.4}/.github/workflows/ci.yml +2 -2
- {mt5cli-1.2.3 → mt5cli-1.2.4}/PKG-INFO +1 -2
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/cli.py +1 -122
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/client.py +15 -8
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/history.py +1 -5
- {mt5cli-1.2.3 → mt5cli-1.2.4}/pyproject.toml +65 -64
- {mt5cli-1.2.3 → mt5cli-1.2.4}/tests/conftest.py +2 -1
- {mt5cli-1.2.3 → mt5cli-1.2.4}/tests/test_cli.py +12 -101
- {mt5cli-1.2.3 → mt5cli-1.2.4}/tests/test_client.py +4 -10
- {mt5cli-1.2.3 → mt5cli-1.2.4}/tests/test_contracts.py +6 -3
- {mt5cli-1.2.3 → mt5cli-1.2.4}/tests/test_history.py +2 -1
- {mt5cli-1.2.3 → mt5cli-1.2.4}/tests/test_telemetry.py +4 -1
- {mt5cli-1.2.3 → mt5cli-1.2.4}/tests/test_trading.py +4 -2
- {mt5cli-1.2.3 → mt5cli-1.2.4}/uv.lock +1 -3
- {mt5cli-1.2.3 → mt5cli-1.2.4}/.agents/skills/local-qa/SKILL.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/.agents/skills/local-qa/scripts/qa.sh +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/.agents/skills/mt5cli/SKILL.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/.agents/skills/pr-feedback-triage/SKILL.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/.claude/agents/codex.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/.claude/settings.json +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/.github/FUNDING.yml +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/.github/dependabot.yml +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/.github/renovate.json +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/.github/workflows/claude.yml +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/.github/workflows/release.yml +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/.gitignore +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/AGENTS.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/CLAUDE.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/LICENSE +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/README.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/docs/api/cli.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/docs/api/client.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/docs/api/converters.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/docs/api/exceptions.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/docs/api/grafana.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/docs/api/history.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/docs/api/index.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/docs/api/marketdata.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/docs/api/observability.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/docs/api/public-contract.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/docs/api/schemas.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/docs/api/telemetry.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/docs/api/trading.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/docs/api/utils.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/docs/index.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/examples/grafana/README.md +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/examples/grafana/compose.yml +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/examples/grafana/dashboards/mt5cli-market.json +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/examples/grafana/dashboards/mt5cli-overview.json +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/examples/grafana/dashboards/mt5cli-trades.json +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/examples/grafana/provisioning/dashboards/mt5cli.yml +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/examples/grafana/provisioning/datasources/mt5cli-sqlite.yml +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mkdocs.yml +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/__init__.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/__main__.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/contract.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/converters.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/exceptions.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/grafana.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/marketdata.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/observability.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/retry.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/schemas.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/telemetry.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/trading.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/mt5cli/utils.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/tests/__init__.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/tests/test_examples.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/tests/test_grafana.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/tests/test_marketdata.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/tests/test_observability.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/tests/test_packaging.py +0 -0
- {mt5cli-1.2.3 → mt5cli-1.2.4}/tests/test_utils.py +0 -0
|
@@ -59,10 +59,10 @@ jobs:
|
|
|
59
59
|
contents: read
|
|
60
60
|
runs-on: windows-latest
|
|
61
61
|
steps:
|
|
62
|
-
- uses: actions/checkout@
|
|
62
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6.0.2
|
|
63
63
|
with:
|
|
64
64
|
persist-credentials: false
|
|
65
|
-
- uses: astral-sh/setup-uv@
|
|
65
|
+
- uses: astral-sh/setup-uv@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
|
|
66
66
|
- name: Check CLI help with minimum Click and Typer versions
|
|
67
67
|
run: uv run --with click==8.4.0 --with typer==0.26.0 mt5cli --help
|
|
68
68
|
python-docs-deploy:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mt5cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.4
|
|
4
4
|
Summary: Generic MT5 data and execution infrastructure for Python applications
|
|
5
5
|
Project-URL: Repository, https://github.com/dceoy/mt5cli.git
|
|
6
6
|
Author-email: dceoy <dceoy@users.noreply.github.com>
|
|
@@ -16,7 +16,6 @@ Classifier: Programming Language :: Python
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
17
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
18
18
|
Requires-Python: <3.14,>=3.11
|
|
19
|
-
Requires-Dist: click>=8.4
|
|
20
19
|
Requires-Dist: pandas>=2.2.2
|
|
21
20
|
Requires-Dist: pdmt5>=1.2.0
|
|
22
21
|
Requires-Dist: typer>=0.26.0
|
|
@@ -6,11 +6,10 @@ import json
|
|
|
6
6
|
import logging
|
|
7
7
|
from contextlib import closing
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
-
from datetime import datetime
|
|
9
|
+
from datetime import datetime # noqa: TC003
|
|
10
10
|
from pathlib import Path # noqa: TC003
|
|
11
11
|
from typing import TYPE_CHECKING, Annotated, Any, cast
|
|
12
12
|
|
|
13
|
-
import click
|
|
14
13
|
import pandas as pd
|
|
15
14
|
import typer
|
|
16
15
|
|
|
@@ -46,126 +45,6 @@ if TYPE_CHECKING:
|
|
|
46
45
|
|
|
47
46
|
logger = logging.getLogger(__name__)
|
|
48
47
|
|
|
49
|
-
# ---------------------------------------------------------------------------
|
|
50
|
-
# Click parameter types
|
|
51
|
-
# ---------------------------------------------------------------------------
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class _DateTimeType(click.ParamType[datetime]):
|
|
55
|
-
"""Click parameter type for ISO 8601 datetime strings."""
|
|
56
|
-
|
|
57
|
-
name = "DATETIME"
|
|
58
|
-
|
|
59
|
-
def convert(
|
|
60
|
-
self,
|
|
61
|
-
value: object,
|
|
62
|
-
param: click.Parameter | None = None,
|
|
63
|
-
ctx: click.Context | None = None,
|
|
64
|
-
) -> datetime:
|
|
65
|
-
"""Convert a string value to a timezone-aware datetime.
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
value: Raw value from the command line.
|
|
69
|
-
param: Click parameter instance.
|
|
70
|
-
ctx: Click context.
|
|
71
|
-
|
|
72
|
-
Returns:
|
|
73
|
-
Parsed datetime.
|
|
74
|
-
"""
|
|
75
|
-
if isinstance(value, datetime):
|
|
76
|
-
return value
|
|
77
|
-
try:
|
|
78
|
-
return parse_datetime(str(value))
|
|
79
|
-
except ValueError as exc:
|
|
80
|
-
self.fail(str(exc), param, ctx)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class _TimeframeType(click.ParamType[int]):
|
|
84
|
-
"""Click parameter type for MT5 timeframe values."""
|
|
85
|
-
|
|
86
|
-
name = "TIMEFRAME"
|
|
87
|
-
|
|
88
|
-
def convert(
|
|
89
|
-
self,
|
|
90
|
-
value: object,
|
|
91
|
-
param: click.Parameter | None = None,
|
|
92
|
-
ctx: click.Context | None = None,
|
|
93
|
-
) -> int:
|
|
94
|
-
"""Convert a string or integer value to a timeframe integer.
|
|
95
|
-
|
|
96
|
-
Args:
|
|
97
|
-
value: Raw value from the command line.
|
|
98
|
-
param: Click parameter instance.
|
|
99
|
-
ctx: Click context.
|
|
100
|
-
|
|
101
|
-
Returns:
|
|
102
|
-
Integer timeframe value.
|
|
103
|
-
"""
|
|
104
|
-
try:
|
|
105
|
-
return parse_timeframe(value)
|
|
106
|
-
except ValueError as exc:
|
|
107
|
-
self.fail(str(exc), param, ctx)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
class _TickFlagsType(click.ParamType[int]):
|
|
111
|
-
"""Click parameter type for MT5 tick copy flags."""
|
|
112
|
-
|
|
113
|
-
name = "FLAGS"
|
|
114
|
-
|
|
115
|
-
def convert(
|
|
116
|
-
self,
|
|
117
|
-
value: object,
|
|
118
|
-
param: click.Parameter | None = None,
|
|
119
|
-
ctx: click.Context | None = None,
|
|
120
|
-
) -> int:
|
|
121
|
-
"""Convert a string or integer value to a tick flags integer.
|
|
122
|
-
|
|
123
|
-
Args:
|
|
124
|
-
value: Raw value from the command line.
|
|
125
|
-
param: Click parameter instance.
|
|
126
|
-
ctx: Click context.
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
Integer tick flag value.
|
|
130
|
-
"""
|
|
131
|
-
try:
|
|
132
|
-
return parse_tick_flags(value)
|
|
133
|
-
except ValueError as exc:
|
|
134
|
-
self.fail(str(exc), param, ctx)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
class _RequestType(click.ParamType[dict[str, Any]]):
|
|
138
|
-
"""Click parameter type for JSON order requests."""
|
|
139
|
-
|
|
140
|
-
name = "REQUEST"
|
|
141
|
-
|
|
142
|
-
def convert(
|
|
143
|
-
self,
|
|
144
|
-
value: object,
|
|
145
|
-
param: click.Parameter | None = None,
|
|
146
|
-
ctx: click.Context | None = None,
|
|
147
|
-
) -> dict[str, Any]:
|
|
148
|
-
"""Convert a raw CLI value to an order request dictionary.
|
|
149
|
-
|
|
150
|
-
Args:
|
|
151
|
-
value: Raw value from the command line.
|
|
152
|
-
param: Click parameter instance.
|
|
153
|
-
ctx: Click context.
|
|
154
|
-
|
|
155
|
-
Returns:
|
|
156
|
-
Parsed request dictionary.
|
|
157
|
-
"""
|
|
158
|
-
try:
|
|
159
|
-
return parse_request(str(value))
|
|
160
|
-
except ValueError as exc:
|
|
161
|
-
self.fail(str(exc), param, ctx)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
DATETIME_TYPE = _DateTimeType()
|
|
165
|
-
TIMEFRAME_TYPE = _TimeframeType()
|
|
166
|
-
TICK_FLAGS_TYPE = _TickFlagsType()
|
|
167
|
-
REQUEST_TYPE = _RequestType()
|
|
168
|
-
|
|
169
48
|
|
|
170
49
|
def _parse_datetime_parameter(value: str) -> datetime:
|
|
171
50
|
try:
|
|
@@ -355,12 +355,14 @@ def _shutdown_client(client: Mt5DataClient, *, raise_on_error: bool) -> None:
|
|
|
355
355
|
failure is raised as the stable normalized exception. When False (an
|
|
356
356
|
initialization or session-body exception is already propagating), the
|
|
357
357
|
failure is logged so the primary exception passes through unchanged.
|
|
358
|
-
|
|
358
|
+
|
|
359
|
+
"""
|
|
359
360
|
try:
|
|
360
361
|
client.shutdown()
|
|
361
362
|
except Exception as exc:
|
|
362
363
|
if raise_on_error:
|
|
363
|
-
|
|
364
|
+
normalized = normalize_mt5_exception(exc)
|
|
365
|
+
raise normalized from exc
|
|
364
366
|
logger.warning("MT5 shutdown failed during cleanup", exc_info=True)
|
|
365
367
|
|
|
366
368
|
|
|
@@ -385,7 +387,7 @@ def _connected_client(
|
|
|
385
387
|
Yields:
|
|
386
388
|
Connected ``Mt5DataClient`` instance.
|
|
387
389
|
|
|
388
|
-
"""
|
|
390
|
+
"""
|
|
389
391
|
client = (
|
|
390
392
|
Mt5DataClient(config=config)
|
|
391
393
|
if retry_count is None
|
|
@@ -395,7 +397,8 @@ def _connected_client(
|
|
|
395
397
|
client.initialize_and_login_mt5()
|
|
396
398
|
except Exception as exc:
|
|
397
399
|
_shutdown_client(client, raise_on_error=False)
|
|
398
|
-
|
|
400
|
+
normalized = normalize_mt5_exception(exc)
|
|
401
|
+
raise normalized from exc
|
|
399
402
|
try:
|
|
400
403
|
yield client
|
|
401
404
|
except BaseException:
|
|
@@ -420,12 +423,14 @@ def _run_with_client(
|
|
|
420
423
|
|
|
421
424
|
Returns:
|
|
422
425
|
Value returned by ``fetch_fn``.
|
|
423
|
-
|
|
426
|
+
|
|
427
|
+
"""
|
|
424
428
|
with _connected_client(config, retry_count=retry_count) as client:
|
|
425
429
|
try:
|
|
426
430
|
return fetch_fn(client)
|
|
427
431
|
except Mt5RuntimeError as exc:
|
|
428
|
-
|
|
432
|
+
normalized = normalize_mt5_exception(exc)
|
|
433
|
+
raise normalized from exc
|
|
429
434
|
|
|
430
435
|
|
|
431
436
|
class _BaseMT5Client:
|
|
@@ -480,7 +485,8 @@ class _BaseMT5Client:
|
|
|
480
485
|
|
|
481
486
|
Returns:
|
|
482
487
|
This client instance.
|
|
483
|
-
|
|
488
|
+
|
|
489
|
+
"""
|
|
484
490
|
if self._client is not None:
|
|
485
491
|
return self
|
|
486
492
|
client = Mt5DataClient(config=self._config, retry_count=self._retry_count)
|
|
@@ -488,7 +494,8 @@ class _BaseMT5Client:
|
|
|
488
494
|
client.initialize_and_login_mt5()
|
|
489
495
|
except Exception as exc:
|
|
490
496
|
_shutdown_client(client, raise_on_error=False)
|
|
491
|
-
|
|
497
|
+
normalized = normalize_mt5_exception(exc)
|
|
498
|
+
raise normalized from exc
|
|
492
499
|
self._client = client
|
|
493
500
|
self._owns_client = True # only set when this method created the client
|
|
494
501
|
return self
|
|
@@ -2755,11 +2755,7 @@ class ThrottledHistoryUpdater:
|
|
|
2755
2755
|
When ``suppress_errors`` is False, recoverable update failures
|
|
2756
2756
|
propagate to the caller.
|
|
2757
2757
|
|
|
2758
|
-
|
|
2759
|
-
Mt5ConnectionError: When ``suppress_errors`` is False and the
|
|
2760
|
-
update fails with a recoverable MT5, SQLite, validation, or
|
|
2761
|
-
I/O error.
|
|
2762
|
-
""" # noqa: DOC502
|
|
2758
|
+
"""
|
|
2763
2759
|
if not self.should_update():
|
|
2764
2760
|
return False
|
|
2765
2761
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mt5cli"
|
|
3
|
-
version = "1.2.
|
|
3
|
+
version = "1.2.4"
|
|
4
4
|
description = "Generic MT5 data and execution infrastructure for Python applications"
|
|
5
5
|
authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
|
6
6
|
maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
|
@@ -11,7 +11,6 @@ requires-python = ">= 3.11, < 3.14"
|
|
|
11
11
|
dependencies = [
|
|
12
12
|
"pdmt5 >= 1.2.0",
|
|
13
13
|
"pandas >= 2.2.2",
|
|
14
|
-
"click >= 8.4",
|
|
15
14
|
"typer >= 0.26.0",
|
|
16
15
|
]
|
|
17
16
|
classifiers = [
|
|
@@ -66,67 +65,67 @@ preview = true
|
|
|
66
65
|
|
|
67
66
|
[tool.ruff.lint]
|
|
68
67
|
select = [
|
|
69
|
-
"F",
|
|
70
|
-
"E",
|
|
71
|
-
"W",
|
|
72
|
-
"C90",
|
|
73
|
-
"I",
|
|
74
|
-
"N",
|
|
75
|
-
"D",
|
|
76
|
-
"UP",
|
|
77
|
-
"S",
|
|
78
|
-
"B",
|
|
79
|
-
"C4",
|
|
80
|
-
"SIM",
|
|
81
|
-
"ARG",
|
|
82
|
-
"PD",
|
|
83
|
-
"PLC",
|
|
84
|
-
"PLE",
|
|
85
|
-
"PLR",
|
|
86
|
-
"PLW",
|
|
87
|
-
"FLY",
|
|
88
|
-
"NPY",
|
|
89
|
-
"PERF",
|
|
90
|
-
"FURB",
|
|
91
|
-
"RUF",
|
|
92
|
-
"YTT",
|
|
93
|
-
"ANN",
|
|
94
|
-
"ASYNC",
|
|
95
|
-
"BLE",
|
|
96
|
-
"FBT",
|
|
97
|
-
"A",
|
|
98
|
-
"COM",
|
|
99
|
-
"DTZ",
|
|
100
|
-
"T10",
|
|
101
|
-
"DJ",
|
|
102
|
-
"EM",
|
|
103
|
-
"EXE",
|
|
104
|
-
"FA",
|
|
105
|
-
"ISC",
|
|
106
|
-
"ICN",
|
|
107
|
-
"LOG",
|
|
108
|
-
"G",
|
|
109
|
-
"INP",
|
|
110
|
-
"PIE",
|
|
111
|
-
"T20",
|
|
112
|
-
"PYI",
|
|
113
|
-
"PT",
|
|
114
|
-
"Q",
|
|
115
|
-
"RSE",
|
|
116
|
-
"SLF",
|
|
117
|
-
"SLOT",
|
|
118
|
-
"TID",
|
|
119
|
-
"TCH",
|
|
120
|
-
"INT",
|
|
121
|
-
"PTH",
|
|
122
|
-
"TD",
|
|
123
|
-
"FIX",
|
|
124
|
-
"ERA",
|
|
125
|
-
"PGH",
|
|
126
|
-
"TRY",
|
|
127
|
-
"FAST",
|
|
128
|
-
"AIR",
|
|
129
|
-
"DOC",
|
|
68
|
+
"F", # Pyflakes (F)
|
|
69
|
+
"E", # pycodestyle error (E)
|
|
70
|
+
"W", # pycodestyle warning (W)
|
|
71
|
+
"C90", # mccabe (C90)
|
|
72
|
+
"I", # isort (I)
|
|
73
|
+
"N", # pep8-naming (N)
|
|
74
|
+
"D", # pydocstyle (D)
|
|
75
|
+
"UP", # pyupgrade (UP)
|
|
76
|
+
"S", # flake8-bandit (S)
|
|
77
|
+
"B", # flake8-bugbear (B)
|
|
78
|
+
"C4", # flake8-comprehensions (C4)
|
|
79
|
+
"SIM", # flake8-simplify (SIM)
|
|
80
|
+
"ARG", # flake8-unused-arguments (ARG)
|
|
81
|
+
"PD", # pandas-vet (PD)
|
|
82
|
+
"PLC", # Pylint convention (PLC)
|
|
83
|
+
"PLE", # Pylint error (PLE)
|
|
84
|
+
"PLR", # Pylint refactor (PLR)
|
|
85
|
+
"PLW", # Pylint warning (PLW)
|
|
86
|
+
"FLY", # flynt (FLY)
|
|
87
|
+
"NPY", # NumPy-specific rules (NPY)
|
|
88
|
+
"PERF", # Perflint (PERF)
|
|
89
|
+
"FURB", # refurb (FURB)
|
|
90
|
+
"RUF", # Ruff-specific rules (RUF)
|
|
91
|
+
"YTT", # flake8-2020 (YTT)
|
|
92
|
+
"ANN", # flake8-annotations (ANN)
|
|
93
|
+
"ASYNC", # flake8-async (ASYNC)
|
|
94
|
+
"BLE", # flake8-blind-except (BLE)
|
|
95
|
+
"FBT", # flake8-boolean-trap (FBT)
|
|
96
|
+
"A", # flake8-builtins (A)
|
|
97
|
+
"COM", # flake8-commas (COM)
|
|
98
|
+
"DTZ", # flake8-datetimez (DTZ)
|
|
99
|
+
"T10", # flake8-debugger (T10)
|
|
100
|
+
"DJ", # flake8-django (DJ)
|
|
101
|
+
"EM", # flake8-errmsg (EM)
|
|
102
|
+
"EXE", # flake8-executable (EXE)
|
|
103
|
+
"FA", # flake8-future-annotations (FA)
|
|
104
|
+
"ISC", # flake8-implicit-str-concat (ISC)
|
|
105
|
+
"ICN", # flake8-import-conventions (ICN)
|
|
106
|
+
"LOG", # flake8-logging (LOG)
|
|
107
|
+
"G", # flake8-logging-format (G)
|
|
108
|
+
"INP", # flake8-no-pep420 (INP)
|
|
109
|
+
"PIE", # flake8-pie (PIE)
|
|
110
|
+
"T20", # flake8-print (T20)
|
|
111
|
+
"PYI", # flake8-pyi (PYI)
|
|
112
|
+
"PT", # flake8-pytest-style (PT)
|
|
113
|
+
"Q", # flake8-quotes (Q)
|
|
114
|
+
"RSE", # flake8-raise (RSE)
|
|
115
|
+
"SLF", # flake8-self (SLF)
|
|
116
|
+
"SLOT", # flake8-slots (SLOT)
|
|
117
|
+
"TID", # flake8-tidy-imports (TID)
|
|
118
|
+
"TCH", # flake8-type-checking (TCH)
|
|
119
|
+
"INT", # flake8-gettext (INT)
|
|
120
|
+
"PTH", # flake8-use-pathlib (PTH)
|
|
121
|
+
"TD", # flake8-todos (TD)
|
|
122
|
+
"FIX", # flake8-fixme (FIX)
|
|
123
|
+
"ERA", # eradicate (ERA)
|
|
124
|
+
"PGH", # pygrep-hooks (PGH)
|
|
125
|
+
"TRY", # tryceratops (TRY)
|
|
126
|
+
"FAST", # FastAPI (FAST)
|
|
127
|
+
"AIR", # Airflow (AIR)
|
|
128
|
+
"DOC", # pydoclint (DOC)
|
|
130
129
|
]
|
|
131
130
|
ignore = [
|
|
132
131
|
"COM812", # missing-trailing-comma
|
|
@@ -137,7 +136,9 @@ ignore = [
|
|
|
137
136
|
[tool.ruff.lint.per-file-ignores]
|
|
138
137
|
# __all__ is derived from contract.STABLE_SDK_EXPORTS, which Ruff cannot
|
|
139
138
|
# resolve statically; contract tests enforce that imports and exports match.
|
|
140
|
-
"mt5cli/__init__.py" = [
|
|
139
|
+
"mt5cli/__init__.py" = [
|
|
140
|
+
"F401", # Unused import (for __all__)
|
|
141
|
+
]
|
|
141
142
|
"tests/**/*.py" = [
|
|
142
143
|
"DOC201", # Missing return documentation
|
|
143
144
|
"DOC501", # Raised exception missing from docstring
|
|
@@ -8,11 +8,12 @@ from unittest.mock import MagicMock
|
|
|
8
8
|
|
|
9
9
|
import pandas as pd
|
|
10
10
|
import pytest
|
|
11
|
-
from pytest_mock import MockerFixture # noqa: TC002
|
|
12
11
|
|
|
13
12
|
if TYPE_CHECKING:
|
|
14
13
|
from types import TracebackType
|
|
15
14
|
|
|
15
|
+
from pytest_mock import MockerFixture
|
|
16
|
+
|
|
16
17
|
_DATAFRAME_METHODS = (
|
|
17
18
|
"copy_rates_from_as_df",
|
|
18
19
|
"copy_rates_from_pos_as_df",
|
|
@@ -12,17 +12,14 @@ from unittest.mock import MagicMock
|
|
|
12
12
|
|
|
13
13
|
import pandas as pd
|
|
14
14
|
import pytest
|
|
15
|
-
from pytest_mock import MockerFixture # noqa: TC002
|
|
16
15
|
from typer.testing import CliRunner
|
|
17
16
|
|
|
18
17
|
if TYPE_CHECKING:
|
|
19
18
|
from pathlib import Path
|
|
20
19
|
|
|
20
|
+
from pytest_mock import MockerFixture
|
|
21
|
+
|
|
21
22
|
from mt5cli.cli import (
|
|
22
|
-
DATETIME_TYPE, # type: ignore[reportPrivateUsage]
|
|
23
|
-
REQUEST_TYPE, # type: ignore[reportPrivateUsage]
|
|
24
|
-
TICK_FLAGS_TYPE, # type: ignore[reportPrivateUsage]
|
|
25
|
-
TIMEFRAME_TYPE, # type: ignore[reportPrivateUsage]
|
|
26
23
|
_execute_export, # type: ignore[reportPrivateUsage]
|
|
27
24
|
_ExportContext, # type: ignore[reportPrivateUsage]
|
|
28
25
|
_parse_datetime_parameter, # type: ignore[reportPrivateUsage]
|
|
@@ -2181,111 +2178,25 @@ class TestMain:
|
|
|
2181
2178
|
mock_app.assert_called_once()
|
|
2182
2179
|
|
|
2183
2180
|
|
|
2184
|
-
class
|
|
2185
|
-
"""Tests for
|
|
2186
|
-
|
|
2187
|
-
def test_convert_string(self) -> None:
|
|
2188
|
-
"""Test converting a string to datetime."""
|
|
2189
|
-
result = DATETIME_TYPE.convert("2024-06-15", None, None)
|
|
2190
|
-
assert result == datetime(2024, 6, 15, tzinfo=UTC)
|
|
2191
|
-
|
|
2192
|
-
def test_convert_datetime_passthrough(self) -> None:
|
|
2193
|
-
"""Test that datetime values pass through unchanged."""
|
|
2194
|
-
dt = datetime(2024, 1, 1, tzinfo=UTC)
|
|
2195
|
-
assert DATETIME_TYPE.convert(dt, None, None) is dt
|
|
2196
|
-
|
|
2197
|
-
def test_convert_invalid(self) -> None:
|
|
2198
|
-
"""Test that invalid values raise BadParameter."""
|
|
2199
|
-
with pytest.raises(Exception, match="Invalid datetime"):
|
|
2200
|
-
DATETIME_TYPE.convert("bad", None, None)
|
|
2181
|
+
class TestTyperParameterParsers:
|
|
2182
|
+
"""Tests for Typer parameter parser callbacks."""
|
|
2201
2183
|
|
|
2202
|
-
def
|
|
2203
|
-
"""Test that
|
|
2184
|
+
def test_datetime_parser_invalid(self) -> None:
|
|
2185
|
+
"""Test that invalid datetimes raise a Typer BadParameter."""
|
|
2204
2186
|
with pytest.raises(Exception, match="Invalid datetime"):
|
|
2205
2187
|
_parse_datetime_parameter("bad")
|
|
2206
2188
|
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
"""Tests for _TimeframeType."""
|
|
2210
|
-
|
|
2211
|
-
@pytest.mark.parametrize(
|
|
2212
|
-
("value", "expected"),
|
|
2213
|
-
[("H1", 16385), (16385, 16385)],
|
|
2214
|
-
ids=["string", "int"],
|
|
2215
|
-
)
|
|
2216
|
-
def test_convert_valid(self, value: str | int, expected: int) -> None:
|
|
2217
|
-
"""Test converting valid string and integer timeframe values."""
|
|
2218
|
-
assert TIMEFRAME_TYPE.convert(value, None, None) == expected
|
|
2219
|
-
|
|
2220
|
-
@pytest.mark.parametrize(
|
|
2221
|
-
"value",
|
|
2222
|
-
[42, "bad"],
|
|
2223
|
-
ids=["unsupported-int", "invalid-string"],
|
|
2224
|
-
)
|
|
2225
|
-
def test_convert_invalid(self, value: object) -> None:
|
|
2226
|
-
"""Test that unsupported int and invalid string values raise BadParameter."""
|
|
2227
|
-
with pytest.raises(Exception, match="Invalid timeframe"):
|
|
2228
|
-
TIMEFRAME_TYPE.convert(value, None, None)
|
|
2229
|
-
|
|
2230
|
-
@pytest.mark.parametrize("value", [True, False, None, 1.5])
|
|
2231
|
-
def test_convert_invalid_types(self, value: object) -> None:
|
|
2232
|
-
"""Test that bool, float, and None values raise BadParameter."""
|
|
2233
|
-
with pytest.raises(Exception, match="Invalid timeframe"):
|
|
2234
|
-
TIMEFRAME_TYPE.convert(value, None, None)
|
|
2235
|
-
|
|
2236
|
-
def test_parser_invalid(self) -> None:
|
|
2237
|
-
"""Test that Typer receives a BadParameter with validation details."""
|
|
2189
|
+
def test_timeframe_parser_invalid(self) -> None:
|
|
2190
|
+
"""Test that invalid timeframes raise a Typer BadParameter."""
|
|
2238
2191
|
with pytest.raises(Exception, match="Invalid timeframe"):
|
|
2239
2192
|
_parse_timeframe_parameter("bad")
|
|
2240
2193
|
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
"""Tests for _TickFlagsType."""
|
|
2244
|
-
|
|
2245
|
-
@pytest.mark.parametrize(
|
|
2246
|
-
("value", "expected"),
|
|
2247
|
-
[("ALL", -1), (2, 2)],
|
|
2248
|
-
ids=["string", "int"],
|
|
2249
|
-
)
|
|
2250
|
-
def test_convert_valid(self, value: str | int, expected: int) -> None:
|
|
2251
|
-
"""Test converting valid string and integer tick flag values."""
|
|
2252
|
-
assert TICK_FLAGS_TYPE.convert(value, None, None) == expected
|
|
2253
|
-
|
|
2254
|
-
@pytest.mark.parametrize(
|
|
2255
|
-
"value",
|
|
2256
|
-
[7, "bad"],
|
|
2257
|
-
ids=["unsupported-int", "invalid-string"],
|
|
2258
|
-
)
|
|
2259
|
-
def test_convert_invalid(self, value: object) -> None:
|
|
2260
|
-
"""Test that unsupported int and invalid string values raise BadParameter."""
|
|
2261
|
-
with pytest.raises(Exception, match="Invalid tick flags"):
|
|
2262
|
-
TICK_FLAGS_TYPE.convert(value, None, None)
|
|
2263
|
-
|
|
2264
|
-
@pytest.mark.parametrize("value", [True, False, None, 1.5])
|
|
2265
|
-
def test_convert_invalid_types(self, value: object) -> None:
|
|
2266
|
-
"""Test that bool, float, and None values raise BadParameter."""
|
|
2267
|
-
with pytest.raises(Exception, match="Invalid tick flags"):
|
|
2268
|
-
TICK_FLAGS_TYPE.convert(value, None, None)
|
|
2269
|
-
|
|
2270
|
-
def test_parser_invalid(self) -> None:
|
|
2271
|
-
"""Test that Typer receives a BadParameter with validation details."""
|
|
2194
|
+
def test_tick_flags_parser_invalid(self) -> None:
|
|
2195
|
+
"""Test that invalid tick flags raise a Typer BadParameter."""
|
|
2272
2196
|
with pytest.raises(Exception, match="Invalid tick flags"):
|
|
2273
2197
|
_parse_tick_flags_parameter("bad")
|
|
2274
2198
|
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
"""Tests for _RequestType."""
|
|
2278
|
-
|
|
2279
|
-
def test_convert_string(self) -> None:
|
|
2280
|
-
"""Test converting a JSON string to a request dictionary."""
|
|
2281
|
-
assert REQUEST_TYPE.convert('{"action": 1}', None, None) == {"action": 1}
|
|
2282
|
-
|
|
2283
|
-
def test_convert_invalid(self) -> None:
|
|
2284
|
-
"""Test that invalid values raise BadParameter."""
|
|
2285
|
-
with pytest.raises(Exception, match="Invalid JSON request"):
|
|
2286
|
-
REQUEST_TYPE.convert("bad", None, None)
|
|
2287
|
-
|
|
2288
|
-
def test_parser_invalid(self) -> None:
|
|
2289
|
-
"""Test that Typer receives a BadParameter with validation details."""
|
|
2199
|
+
def test_request_parser_invalid(self) -> None:
|
|
2200
|
+
"""Test that invalid requests raise a Typer BadParameter."""
|
|
2290
2201
|
with pytest.raises(Exception, match="Invalid JSON request"):
|
|
2291
2202
|
_parse_request_parameter("bad")
|
|
@@ -24,7 +24,11 @@ from mt5cli.exceptions import Mt5ConnectionError
|
|
|
24
24
|
from mt5cli.marketdata import (
|
|
25
25
|
collect_latest_rates,
|
|
26
26
|
copy_rates_range,
|
|
27
|
+
minimum_margins,
|
|
28
|
+
mt5_summary,
|
|
29
|
+
mt5_summary_as_df,
|
|
27
30
|
recent_history_deals,
|
|
31
|
+
recent_ticks,
|
|
28
32
|
)
|
|
29
33
|
|
|
30
34
|
if TYPE_CHECKING:
|
|
@@ -774,8 +778,6 @@ class TestMT5ClientMethods:
|
|
|
774
778
|
mock_client.account_info.return_value = account_info_value
|
|
775
779
|
mock_client.symbols_total.return_value = 42
|
|
776
780
|
|
|
777
|
-
from mt5cli.marketdata import mt5_summary # noqa: PLC0415
|
|
778
|
-
|
|
779
781
|
assert mt5_summary() == expected
|
|
780
782
|
|
|
781
783
|
def test_mt5_summary_as_df_stringifies_nested_values(
|
|
@@ -794,8 +796,6 @@ class TestMT5ClientMethods:
|
|
|
794
796
|
)
|
|
795
797
|
mock_client.symbols_total.return_value = 42
|
|
796
798
|
|
|
797
|
-
from mt5cli.marketdata import mt5_summary_as_df # noqa: PLC0415
|
|
798
|
-
|
|
799
799
|
result = mt5_summary_as_df()
|
|
800
800
|
|
|
801
801
|
assert len(result) == 1
|
|
@@ -853,8 +853,6 @@ class TestRecentTicks:
|
|
|
853
853
|
"bid": [1.0],
|
|
854
854
|
})
|
|
855
855
|
mocker.patch("mt5cli.client.Mt5DataClient", return_value=client)
|
|
856
|
-
from mt5cli.marketdata import recent_ticks # noqa: PLC0415
|
|
857
|
-
|
|
858
856
|
result = recent_ticks(
|
|
859
857
|
"EURUSD",
|
|
860
858
|
60,
|
|
@@ -955,8 +953,6 @@ class TestRecentTicks:
|
|
|
955
953
|
"bid": [1.0, 1.1, 1.2],
|
|
956
954
|
})
|
|
957
955
|
mocker.patch("mt5cli.client.Mt5DataClient", return_value=client)
|
|
958
|
-
from mt5cli.marketdata import recent_ticks # noqa: PLC0415
|
|
959
|
-
|
|
960
956
|
result = recent_ticks(
|
|
961
957
|
"EURUSD",
|
|
962
958
|
60,
|
|
@@ -993,8 +989,6 @@ class TestMinimumMargins:
|
|
|
993
989
|
client.mt5.ORDER_TYPE_BUY = 0
|
|
994
990
|
client.mt5.ORDER_TYPE_SELL = 1
|
|
995
991
|
mocker.patch("mt5cli.client.Mt5DataClient", return_value=client)
|
|
996
|
-
from mt5cli.marketdata import minimum_margins # noqa: PLC0415
|
|
997
|
-
|
|
998
992
|
result = minimum_margins("EURUSD", config=build_config(login=123))
|
|
999
993
|
|
|
1000
994
|
pd.testing.assert_frame_equal(
|
|
@@ -4,13 +4,16 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
6
|
from datetime import UTC, datetime
|
|
7
|
+
from importlib import import_module
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import Protocol
|
|
9
|
+
from typing import TYPE_CHECKING, Protocol
|
|
9
10
|
|
|
10
11
|
import pandas as pd
|
|
11
12
|
import pytest
|
|
12
13
|
from pdmt5 import Mt5RuntimeError
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pytest_mock import MockerFixture
|
|
14
17
|
|
|
15
18
|
import mt5cli
|
|
16
19
|
import mt5cli.cli
|
|
@@ -564,7 +567,7 @@ class TestSingleConnectionLifecycleContract:
|
|
|
564
567
|
def test_sdk_compatibility_module_no_longer_exists(self) -> None:
|
|
565
568
|
"""The removed mt5cli.sdk module cannot be imported."""
|
|
566
569
|
with pytest.raises(ModuleNotFoundError):
|
|
567
|
-
|
|
570
|
+
import_module("mt5cli.sdk")
|
|
568
571
|
|
|
569
572
|
def test_package_root_mt5_session_is_the_client_module_session(self) -> None:
|
|
570
573
|
"""The package-root mt5_session is the same object as mt5cli.client's."""
|
|
@@ -10,12 +10,13 @@ from unittest.mock import MagicMock
|
|
|
10
10
|
|
|
11
11
|
import pandas as pd
|
|
12
12
|
import pytest
|
|
13
|
-
from pytest_mock import MockerFixture # noqa: TC002
|
|
14
13
|
|
|
15
14
|
if TYPE_CHECKING:
|
|
16
15
|
from collections.abc import Callable
|
|
17
16
|
from pathlib import Path
|
|
18
17
|
|
|
18
|
+
from pytest_mock import MockerFixture
|
|
19
|
+
|
|
19
20
|
from pdmt5 import TIMEFRAME_MAP, Mt5RuntimeError
|
|
20
21
|
|
|
21
22
|
from mt5cli import history
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
5
6
|
from unittest.mock import MagicMock
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
8
9
|
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pytest_mock import MockerFixture
|
|
10
13
|
|
|
11
14
|
from mt5cli.telemetry import (
|
|
12
15
|
_Mt5Metrics, # type: ignore[reportPrivateUsage]
|
|
@@ -5,14 +5,16 @@ from __future__ import annotations
|
|
|
5
5
|
import logging
|
|
6
6
|
from datetime import UTC, datetime
|
|
7
7
|
from types import SimpleNamespace
|
|
8
|
-
from typing import Any, cast, get_args
|
|
8
|
+
from typing import TYPE_CHECKING, Any, cast, get_args
|
|
9
9
|
from unittest.mock import MagicMock
|
|
10
10
|
|
|
11
11
|
import pandas as pd
|
|
12
12
|
import pytest
|
|
13
13
|
from numpy import int64 as np_int64
|
|
14
14
|
from pdmt5 import Mt5RuntimeError
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from pytest_mock import MockerFixture
|
|
16
18
|
|
|
17
19
|
from mt5cli import trading
|
|
18
20
|
from mt5cli.client import MT5Client
|
|
@@ -499,10 +499,9 @@ wheels = [
|
|
|
499
499
|
|
|
500
500
|
[[package]]
|
|
501
501
|
name = "mt5cli"
|
|
502
|
-
version = "1.2.
|
|
502
|
+
version = "1.2.4"
|
|
503
503
|
source = { editable = "." }
|
|
504
504
|
dependencies = [
|
|
505
|
-
{ name = "click" },
|
|
506
505
|
{ name = "pandas" },
|
|
507
506
|
{ name = "pdmt5" },
|
|
508
507
|
{ name = "typer" },
|
|
@@ -537,7 +536,6 @@ dev = [
|
|
|
537
536
|
|
|
538
537
|
[package.metadata]
|
|
539
538
|
requires-dist = [
|
|
540
|
-
{ name = "click", specifier = ">=8.4" },
|
|
541
539
|
{ name = "opentelemetry-api", marker = "extra == 'otel'" },
|
|
542
540
|
{ name = "opentelemetry-exporter-otlp-proto-http", marker = "extra == 'otel'" },
|
|
543
541
|
{ name = "opentelemetry-sdk", marker = "extra == 'otel'" },
|
|
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
|
|
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
|
|
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
|