mt5cli 0.6.1__tar.gz → 0.7.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.
- {mt5cli-0.6.1 → mt5cli-0.7.0}/PKG-INFO +8 -2
- {mt5cli-0.6.1 → mt5cli-0.7.0}/README.md +6 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/docs/index.md +6 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/mt5cli/cli.py +2 -2
- {mt5cli-0.6.1 → mt5cli-0.7.0}/mt5cli/history.py +9 -9
- {mt5cli-0.6.1 → mt5cli-0.7.0}/mt5cli/sdk.py +1 -5
- {mt5cli-0.6.1 → mt5cli-0.7.0}/mt5cli/utils.py +31 -50
- {mt5cli-0.6.1 → mt5cli-0.7.0}/pyproject.toml +2 -2
- {mt5cli-0.6.1 → mt5cli-0.7.0}/tests/test_cli.py +5 -5
- {mt5cli-0.6.1 → mt5cli-0.7.0}/tests/test_history.py +17 -1
- {mt5cli-0.6.1 → mt5cli-0.7.0}/tests/test_sdk.py +5 -5
- {mt5cli-0.6.1 → mt5cli-0.7.0}/tests/test_utils.py +51 -14
- {mt5cli-0.6.1 → mt5cli-0.7.0}/uv.lock +5 -5
- {mt5cli-0.6.1 → mt5cli-0.7.0}/.agents/skills/local-qa/SKILL.md +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/.agents/skills/local-qa/scripts/qa.sh +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/.agents/skills/mt5cli/SKILL.md +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/.claude/agents/codex.md +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/.claude/settings.json +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/.github/FUNDING.yml +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/.github/dependabot.yml +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/.github/renovate.json +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/.github/workflows/ci.yml +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/.github/workflows/claude.yml +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/.github/workflows/release.yml +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/.gitignore +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/AGENTS.md +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/CLAUDE.md +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/LICENSE +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/docs/api/cli.md +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/docs/api/history.md +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/docs/api/index.md +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/docs/api/sdk.md +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/docs/api/trading.md +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/docs/api/utils.md +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/mkdocs.yml +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/mt5cli/__init__.py +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/mt5cli/__main__.py +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/mt5cli/trading.py +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/tests/__init__.py +0 -0
- {mt5cli-0.6.1 → mt5cli-0.7.0}/tests/test_trading.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mt5cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Command-line tool for MetaTrader 5
|
|
5
5
|
Project-URL: Repository, https://github.com/dceoy/mt5cli.git
|
|
6
6
|
Author-email: dceoy <dceoy@users.noreply.github.com>
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
17
17
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
18
18
|
Requires-Python: <3.14,>=3.11
|
|
19
19
|
Requires-Dist: click>=8.1.0
|
|
20
|
-
Requires-Dist: pdmt5>=0.
|
|
20
|
+
Requires-Dist: pdmt5>=0.3.0
|
|
21
21
|
Requires-Dist: pyarrow>=19.0.0
|
|
22
22
|
Requires-Dist: typer>=0.15.0
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
@@ -30,6 +30,12 @@ Command-line tool for exporting MetaTrader 5 data to CSV, JSON, Parquet, and SQL
|
|
|
30
30
|
|
|
31
31
|
Built on top of [pdmt5](https://github.com/dceoy/pdmt5), a pandas-based data handler for MetaTrader 5.
|
|
32
32
|
|
|
33
|
+
## Architecture
|
|
34
|
+
|
|
35
|
+
- **pdmt5** — canonical MT5 client, DataFrame/trading primitives, and MT5 constant parsing (`TIMEFRAME_*`, `COPY_TICKS_*`, order types).
|
|
36
|
+
- **mt5cli** — CLI commands, CSV/JSON/Parquet/SQLite export, SQLite history collection, rate views, and local batch/automation SDK helpers built on pdmt5.
|
|
37
|
+
- **mt5api** — sibling HTTP adapter for remote MT5 access; not a dependency of mt5cli.
|
|
38
|
+
|
|
33
39
|
## Features
|
|
34
40
|
|
|
35
41
|
- **Multi-format export**: CSV, JSON, Parquet, and SQLite3 output formats
|
|
@@ -6,6 +6,12 @@ Command-line tool for exporting MetaTrader 5 data to CSV, JSON, Parquet, and SQL
|
|
|
6
6
|
|
|
7
7
|
Built on top of [pdmt5](https://github.com/dceoy/pdmt5), a pandas-based data handler for MetaTrader 5.
|
|
8
8
|
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
- **pdmt5** — canonical MT5 client, DataFrame/trading primitives, and MT5 constant parsing (`TIMEFRAME_*`, `COPY_TICKS_*`, order types).
|
|
12
|
+
- **mt5cli** — CLI commands, CSV/JSON/Parquet/SQLite export, SQLite history collection, rate views, and local batch/automation SDK helpers built on pdmt5.
|
|
13
|
+
- **mt5api** — sibling HTTP adapter for remote MT5 access; not a dependency of mt5cli.
|
|
14
|
+
|
|
9
15
|
## Features
|
|
10
16
|
|
|
11
17
|
- **Multi-format export**: CSV, JSON, Parquet, and SQLite3 output formats
|
|
@@ -6,6 +6,12 @@ Command-line tool for MetaTrader 5 data export.
|
|
|
6
6
|
|
|
7
7
|
mt5cli is a CLI application that exports MetaTrader 5 trading data to multiple file formats. It is built on top of [pdmt5](https://github.com/dceoy/pdmt5), a pandas-based data handler for MetaTrader 5.
|
|
8
8
|
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
- **pdmt5** — canonical MT5 client, DataFrame/trading primitives, and MT5 constant parsing (`TIMEFRAME_*`, `COPY_TICKS_*`, order types).
|
|
12
|
+
- **mt5cli** — CLI commands, CSV/JSON/Parquet/SQLite export, SQLite history collection, rate views, and local batch/automation SDK helpers built on pdmt5.
|
|
13
|
+
- **mt5api** — sibling HTTP adapter for remote MT5 access; not a dependency of mt5cli.
|
|
14
|
+
|
|
9
15
|
## Features
|
|
10
16
|
|
|
11
17
|
- **Multi-format export**: CSV, JSON, Parquet, and SQLite3 output formats
|
|
@@ -347,7 +347,7 @@ def ticks_recent(
|
|
|
347
347
|
click_type=TICK_FLAGS_TYPE,
|
|
348
348
|
help="Tick flags (ALL, INFO, TRADE, or integer).",
|
|
349
349
|
),
|
|
350
|
-
] =
|
|
350
|
+
] = "ALL", # pyright: ignore[reportArgumentType]
|
|
351
351
|
) -> None:
|
|
352
352
|
"""Export ticks from a recent time window."""
|
|
353
353
|
client = _sdk_client(ctx)
|
|
@@ -656,7 +656,7 @@ def collect_history(
|
|
|
656
656
|
click_type=TICK_FLAGS_TYPE,
|
|
657
657
|
help="Tick copy flags (ALL, INFO, TRADE, or integer).",
|
|
658
658
|
),
|
|
659
|
-
] =
|
|
659
|
+
] = "ALL", # pyright: ignore[reportArgumentType]
|
|
660
660
|
if_exists: Annotated[
|
|
661
661
|
IfExists,
|
|
662
662
|
typer.Option(
|
|
@@ -10,9 +10,10 @@ from pathlib import Path
|
|
|
10
10
|
from typing import TYPE_CHECKING, Literal, cast
|
|
11
11
|
|
|
12
12
|
import pandas as pd
|
|
13
|
+
from pdmt5 import get_timeframe_name as _get_timeframe_name
|
|
13
14
|
|
|
14
15
|
from .utils import (
|
|
15
|
-
|
|
16
|
+
TIMEFRAME_NAMES,
|
|
16
17
|
Dataset,
|
|
17
18
|
IfExists,
|
|
18
19
|
parse_datetime,
|
|
@@ -27,7 +28,7 @@ if TYPE_CHECKING:
|
|
|
27
28
|
|
|
28
29
|
logger = logging.getLogger(__name__)
|
|
29
30
|
|
|
30
|
-
DEFAULT_HISTORY_TIMEFRAMES: tuple[str, ...] =
|
|
31
|
+
DEFAULT_HISTORY_TIMEFRAMES: tuple[str, ...] = TIMEFRAME_NAMES
|
|
31
32
|
|
|
32
33
|
_HISTORY_DEDUP_KEYS: dict[Dataset, tuple[tuple[str, ...], ...]] = {
|
|
33
34
|
Dataset.rates: (("symbol", "timeframe", "time"), ("symbol", "time")),
|
|
@@ -80,7 +81,7 @@ def resolve_history_timeframes(
|
|
|
80
81
|
seen: set[int] = set()
|
|
81
82
|
resolved: list[int] = []
|
|
82
83
|
for value in raw:
|
|
83
|
-
tf =
|
|
84
|
+
tf = parse_timeframe(value)
|
|
84
85
|
if tf not in seen:
|
|
85
86
|
seen.add(tf)
|
|
86
87
|
resolved.append(tf)
|
|
@@ -93,17 +94,16 @@ def resolve_history_tick_flags(flags: int | str) -> int:
|
|
|
93
94
|
Returns:
|
|
94
95
|
Integer tick flag value.
|
|
95
96
|
"""
|
|
96
|
-
if isinstance(flags, int):
|
|
97
|
-
return flags
|
|
98
97
|
return parse_tick_flags(flags)
|
|
99
98
|
|
|
100
99
|
|
|
101
100
|
def resolve_granularity_name(timeframe: int) -> str:
|
|
102
101
|
"""Return a granularity name for a timeframe integer when known."""
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
try:
|
|
103
|
+
name = _get_timeframe_name(timeframe)
|
|
104
|
+
except ValueError:
|
|
105
|
+
return str(timeframe)
|
|
106
|
+
return name.removeprefix("TIMEFRAME_")
|
|
107
107
|
|
|
108
108
|
|
|
109
109
|
def drop_forming_rate_bar(df_rate: pd.DataFrame) -> pd.DataFrame:
|
|
@@ -144,14 +144,10 @@ __all__ = [
|
|
|
144
144
|
|
|
145
145
|
|
|
146
146
|
def _coerce_timeframe(timeframe: int | str) -> int:
|
|
147
|
-
if isinstance(timeframe, int):
|
|
148
|
-
return timeframe
|
|
149
147
|
return parse_timeframe(timeframe)
|
|
150
148
|
|
|
151
149
|
|
|
152
150
|
def _coerce_tick_flags(flags: int | str) -> int:
|
|
153
|
-
if isinstance(flags, int):
|
|
154
|
-
return flags
|
|
155
151
|
return parse_tick_flags(flags)
|
|
156
152
|
|
|
157
153
|
|
|
@@ -1171,7 +1167,7 @@ def collect_history(
|
|
|
1171
1167
|
*,
|
|
1172
1168
|
datasets: set[Dataset] | None = None,
|
|
1173
1169
|
timeframe: int | str = 1,
|
|
1174
|
-
flags: int | str =
|
|
1170
|
+
flags: int | str = "ALL",
|
|
1175
1171
|
if_exists: IfExists = IfExists.FAIL,
|
|
1176
1172
|
with_views: bool = False,
|
|
1177
1173
|
config: Mt5Config | None = None,
|
|
@@ -10,6 +10,9 @@ from pathlib import Path
|
|
|
10
10
|
from typing import TYPE_CHECKING, Any, TypeGuard
|
|
11
11
|
|
|
12
12
|
import click
|
|
13
|
+
from pdmt5 import COPY_TICKS_MAP, TIMEFRAME_MAP
|
|
14
|
+
from pdmt5 import parse_copy_ticks as _parse_copy_ticks
|
|
15
|
+
from pdmt5 import parse_timeframe as _parse_timeframe
|
|
13
16
|
|
|
14
17
|
if TYPE_CHECKING:
|
|
15
18
|
from collections.abc import Sequence
|
|
@@ -20,35 +23,15 @@ if TYPE_CHECKING:
|
|
|
20
23
|
# Constants
|
|
21
24
|
# ---------------------------------------------------------------------------
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"M2": 2,
|
|
26
|
-
"M3": 3,
|
|
27
|
-
"M4": 4,
|
|
28
|
-
"M5": 5,
|
|
29
|
-
"M6": 6,
|
|
30
|
-
"M10": 10,
|
|
31
|
-
"M12": 12,
|
|
32
|
-
"M15": 15,
|
|
33
|
-
"M20": 20,
|
|
34
|
-
"M30": 30,
|
|
35
|
-
"H1": 16385,
|
|
36
|
-
"H2": 16386,
|
|
37
|
-
"H3": 16387,
|
|
38
|
-
"H4": 16388,
|
|
39
|
-
"H6": 16390,
|
|
40
|
-
"H8": 16392,
|
|
41
|
-
"H12": 16396,
|
|
42
|
-
"D1": 16408,
|
|
43
|
-
"W1": 32769,
|
|
44
|
-
"MN1": 49153,
|
|
45
|
-
}
|
|
26
|
+
# Backward-compatible snapshot; prefer ``COPY_TICKS_MAP`` from pdmt5 directly.
|
|
27
|
+
TICK_FLAG_MAP: dict[str, int] = dict(COPY_TICKS_MAP)
|
|
46
28
|
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
29
|
+
TIMEFRAME_NAMES: tuple[str, ...] = tuple(
|
|
30
|
+
name for name in TIMEFRAME_MAP if not name.startswith("TIMEFRAME_")
|
|
31
|
+
)
|
|
32
|
+
_TICK_FLAG_NAMES: tuple[str, ...] = tuple(
|
|
33
|
+
name for name in COPY_TICKS_MAP if not name.startswith("COPY_TICKS_")
|
|
34
|
+
)
|
|
52
35
|
|
|
53
36
|
_FORMAT_EXTENSIONS: dict[str, str] = {
|
|
54
37
|
".csv": "csv",
|
|
@@ -160,10 +143,8 @@ class _TimeframeType(click.ParamType):
|
|
|
160
143
|
Returns:
|
|
161
144
|
Integer timeframe value.
|
|
162
145
|
"""
|
|
163
|
-
if isinstance(value, int):
|
|
164
|
-
return value
|
|
165
146
|
try:
|
|
166
|
-
return parse_timeframe(
|
|
147
|
+
return parse_timeframe(value)
|
|
167
148
|
except ValueError as exc:
|
|
168
149
|
self.fail(str(exc), param, ctx)
|
|
169
150
|
|
|
@@ -189,10 +170,8 @@ class _TickFlagsType(click.ParamType):
|
|
|
189
170
|
Returns:
|
|
190
171
|
Integer tick flag value.
|
|
191
172
|
"""
|
|
192
|
-
if isinstance(value, int):
|
|
193
|
-
return value
|
|
194
173
|
try:
|
|
195
|
-
return parse_tick_flags(
|
|
174
|
+
return parse_tick_flags(value)
|
|
196
175
|
except ValueError as exc:
|
|
197
176
|
self.fail(str(exc), param, ctx)
|
|
198
177
|
|
|
@@ -370,7 +349,7 @@ def parse_datetime(value: str) -> datetime:
|
|
|
370
349
|
return dt
|
|
371
350
|
|
|
372
351
|
|
|
373
|
-
def parse_timeframe(value:
|
|
352
|
+
def parse_timeframe(value: object) -> int:
|
|
374
353
|
"""Parse a timeframe string or integer value.
|
|
375
354
|
|
|
376
355
|
Args:
|
|
@@ -382,37 +361,39 @@ def parse_timeframe(value: str) -> int:
|
|
|
382
361
|
Raises:
|
|
383
362
|
ValueError: If the timeframe is invalid.
|
|
384
363
|
"""
|
|
385
|
-
upper = value.upper()
|
|
386
|
-
if upper in TIMEFRAME_MAP:
|
|
387
|
-
return TIMEFRAME_MAP[upper]
|
|
388
364
|
try:
|
|
389
|
-
return
|
|
365
|
+
return _parse_timeframe(value)
|
|
390
366
|
except ValueError:
|
|
391
|
-
|
|
392
|
-
|
|
367
|
+
display = value if isinstance(value, str) else repr(value)
|
|
368
|
+
valid = ", ".join(TIMEFRAME_NAMES)
|
|
369
|
+
msg = (
|
|
370
|
+
f"Invalid timeframe: '{display}'. "
|
|
371
|
+
f"Use one of: {valid}, or a supported integer."
|
|
372
|
+
)
|
|
393
373
|
raise ValueError(msg) from None
|
|
394
374
|
|
|
395
375
|
|
|
396
|
-
def parse_tick_flags(value:
|
|
376
|
+
def parse_tick_flags(value: object) -> int:
|
|
397
377
|
"""Parse tick flags string or integer value.
|
|
398
378
|
|
|
399
379
|
Args:
|
|
400
|
-
value: Tick flag name (ALL, INFO, TRADE) or integer value.
|
|
380
|
+
value: Tick flag name (ALL, INFO, TRADE, COPY_TICKS_*) or integer value.
|
|
401
381
|
|
|
402
382
|
Returns:
|
|
403
|
-
Integer tick flag value
|
|
383
|
+
Integer tick flag value compatible with MetaTrader 5 ``COPY_TICKS_*``.
|
|
404
384
|
|
|
405
385
|
Raises:
|
|
406
386
|
ValueError: If the flag is invalid.
|
|
407
387
|
"""
|
|
408
|
-
upper = value.upper()
|
|
409
|
-
if upper in TICK_FLAG_MAP:
|
|
410
|
-
return TICK_FLAG_MAP[upper]
|
|
411
388
|
try:
|
|
412
|
-
return
|
|
389
|
+
return _parse_copy_ticks(value)
|
|
413
390
|
except ValueError:
|
|
414
|
-
|
|
415
|
-
|
|
391
|
+
display = value if isinstance(value, str) else repr(value)
|
|
392
|
+
valid = ", ".join(_TICK_FLAG_NAMES)
|
|
393
|
+
msg = (
|
|
394
|
+
f"Invalid tick flags: '{display}'. "
|
|
395
|
+
f"Use one of: {valid}, or a supported integer."
|
|
396
|
+
)
|
|
416
397
|
raise ValueError(msg) from None
|
|
417
398
|
|
|
418
399
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mt5cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "Command-line tool 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"}]
|
|
@@ -9,7 +9,7 @@ license-files = ["LICENSE"]
|
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">= 3.11, < 3.14"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"pdmt5
|
|
12
|
+
"pdmt5>=0.3.0",
|
|
13
13
|
"click >= 8.1.0",
|
|
14
14
|
"pyarrow >= 19.0.0",
|
|
15
15
|
"typer >= 0.15.0",
|
|
@@ -317,7 +317,7 @@ class TestCommands:
|
|
|
317
317
|
symbol="EURUSD",
|
|
318
318
|
date_from=datetime(2024, 1, 1, tzinfo=UTC),
|
|
319
319
|
count=100,
|
|
320
|
-
flags
|
|
320
|
+
flags=-1,
|
|
321
321
|
)
|
|
322
322
|
|
|
323
323
|
def test_ticks_range(
|
|
@@ -348,7 +348,7 @@ class TestCommands:
|
|
|
348
348
|
symbol="EURUSD",
|
|
349
349
|
date_from=datetime(2024, 1, 1, tzinfo=UTC),
|
|
350
350
|
date_to=datetime(2024, 2, 1, tzinfo=UTC),
|
|
351
|
-
flags=
|
|
351
|
+
flags=1,
|
|
352
352
|
)
|
|
353
353
|
|
|
354
354
|
def test_ticks_recent(
|
|
@@ -381,7 +381,7 @@ class TestCommands:
|
|
|
381
381
|
symbol="EURUSD",
|
|
382
382
|
date_from=datetime(2024, 1, 2, tzinfo=UTC) - timedelta(seconds=120),
|
|
383
383
|
count=500,
|
|
384
|
-
flags
|
|
384
|
+
flags=-1,
|
|
385
385
|
)
|
|
386
386
|
mock_client.copy_ticks_range_as_df.assert_not_called()
|
|
387
387
|
|
|
@@ -1000,7 +1000,7 @@ class TestCollectHistory:
|
|
|
1000
1000
|
symbol="EURUSD",
|
|
1001
1001
|
date_from=datetime(2024, 1, 1, tzinfo=UTC),
|
|
1002
1002
|
date_to=datetime(2024, 2, 1, tzinfo=UTC),
|
|
1003
|
-
flags
|
|
1003
|
+
flags=-1,
|
|
1004
1004
|
)
|
|
1005
1005
|
with sqlite3.connect(output) as conn:
|
|
1006
1006
|
tables = {
|
|
@@ -1213,7 +1213,7 @@ class TestCollectHistory:
|
|
|
1213
1213
|
symbol="EURUSD",
|
|
1214
1214
|
date_from=datetime(2024, 1, 1, tzinfo=UTC),
|
|
1215
1215
|
date_to=datetime(2024, 2, 1, tzinfo=UTC),
|
|
1216
|
-
flags
|
|
1216
|
+
flags=-1,
|
|
1217
1217
|
)
|
|
1218
1218
|
|
|
1219
1219
|
def test_collect_history_with_views(
|
|
@@ -517,6 +517,9 @@ class TestResolveHistorySettings:
|
|
|
517
517
|
"""Test default timeframes include all fixed MT5 values."""
|
|
518
518
|
resolved = resolve_history_timeframes(None)
|
|
519
519
|
assert len(resolved) == len(DEFAULT_HISTORY_TIMEFRAMES)
|
|
520
|
+
assert not any(
|
|
521
|
+
name.startswith("TIMEFRAME_") for name in DEFAULT_HISTORY_TIMEFRAMES
|
|
522
|
+
)
|
|
520
523
|
assert 1 in resolved
|
|
521
524
|
assert TIMEFRAME_MAP["H1"] in resolved
|
|
522
525
|
|
|
@@ -526,7 +529,7 @@ class TestResolveHistorySettings:
|
|
|
526
529
|
|
|
527
530
|
def test_resolve_history_tick_flags(self) -> None:
|
|
528
531
|
"""Test tick flag resolution."""
|
|
529
|
-
assert resolve_history_tick_flags("ALL") == 1
|
|
532
|
+
assert resolve_history_tick_flags("ALL") == -1
|
|
530
533
|
assert resolve_history_tick_flags(2) == 2
|
|
531
534
|
|
|
532
535
|
def test_resolve_granularity_name_falls_back_to_integer(self) -> None:
|
|
@@ -534,6 +537,17 @@ class TestResolveHistorySettings:
|
|
|
534
537
|
assert resolve_granularity_name(999) == "999"
|
|
535
538
|
assert resolve_granularity_name(1) == "M1"
|
|
536
539
|
|
|
540
|
+
def test_resolve_granularity_name_strips_official_prefix(
|
|
541
|
+
self,
|
|
542
|
+
mocker: MockerFixture,
|
|
543
|
+
) -> None:
|
|
544
|
+
"""Test official pdmt5 timeframe names are normalized to short aliases."""
|
|
545
|
+
mocker.patch(
|
|
546
|
+
"mt5cli.history._get_timeframe_name",
|
|
547
|
+
return_value="TIMEFRAME_H1",
|
|
548
|
+
)
|
|
549
|
+
assert resolve_granularity_name(16385) == "H1"
|
|
550
|
+
|
|
537
551
|
|
|
538
552
|
class TestDropFormingRateBar:
|
|
539
553
|
"""Tests for drop_forming_rate_bar."""
|
|
@@ -1754,6 +1768,8 @@ class TestIncrementalIntegration:
|
|
|
1754
1768
|
"""Test invalid tick flags raise ValueError."""
|
|
1755
1769
|
with pytest.raises(ValueError, match="Invalid tick flags"):
|
|
1756
1770
|
resolve_history_tick_flags("BAD")
|
|
1771
|
+
with pytest.raises(ValueError, match="Invalid tick flags"):
|
|
1772
|
+
resolve_history_tick_flags(7)
|
|
1757
1773
|
|
|
1758
1774
|
def test_resolve_history_timeframes_invalid(self) -> None:
|
|
1759
1775
|
"""Test invalid timeframes raise ValueError."""
|
|
@@ -390,7 +390,7 @@ class TestMt5CliClient:
|
|
|
390
390
|
symbol="EURUSD",
|
|
391
391
|
date_from=datetime(2024, 1, 1, tzinfo=UTC),
|
|
392
392
|
count=100,
|
|
393
|
-
flags=
|
|
393
|
+
flags=1,
|
|
394
394
|
)
|
|
395
395
|
|
|
396
396
|
def test_history_orders_accepts_string_dates(
|
|
@@ -951,7 +951,7 @@ class TestUpdateHistory:
|
|
|
951
951
|
assert kwargs["symbol"] == "EURUSD"
|
|
952
952
|
assert kwargs["date_from"] == expected_start
|
|
953
953
|
assert kwargs["date_to"] == date_to
|
|
954
|
-
assert kwargs["flags"] == 1
|
|
954
|
+
assert kwargs["flags"] == -1
|
|
955
955
|
return pd.DataFrame({
|
|
956
956
|
"time": ["2024-01-01T12:00:00+00:00"],
|
|
957
957
|
"time_msc": [1_704_110_400_000],
|
|
@@ -1119,7 +1119,7 @@ class TestRecentTicks:
|
|
|
1119
1119
|
symbol="EURUSD",
|
|
1120
1120
|
date_from=end - timedelta(seconds=60),
|
|
1121
1121
|
count=100,
|
|
1122
|
-
flags=
|
|
1122
|
+
flags=1,
|
|
1123
1123
|
)
|
|
1124
1124
|
client.copy_ticks_range_as_df.assert_not_called()
|
|
1125
1125
|
|
|
@@ -1149,7 +1149,7 @@ class TestRecentTicks:
|
|
|
1149
1149
|
assert kwargs["symbol"] == "EURUSD"
|
|
1150
1150
|
assert kwargs["date_to"] == tick.time
|
|
1151
1151
|
assert kwargs["date_from"] == tick.time - timedelta(seconds=30)
|
|
1152
|
-
assert kwargs["flags"] == 1
|
|
1152
|
+
assert kwargs["flags"] == -1
|
|
1153
1153
|
|
|
1154
1154
|
def test_recent_ticks_rejects_unsupported_tick_time(
|
|
1155
1155
|
self,
|
|
@@ -1219,7 +1219,7 @@ class TestRecentTicks:
|
|
|
1219
1219
|
symbol="EURUSD",
|
|
1220
1220
|
date_from=end - timedelta(seconds=60),
|
|
1221
1221
|
date_to=end,
|
|
1222
|
-
flags
|
|
1222
|
+
flags=-1,
|
|
1223
1223
|
)
|
|
1224
1224
|
|
|
1225
1225
|
|
|
@@ -274,8 +274,14 @@ class TestParseTimeframe:
|
|
|
274
274
|
assert parse_timeframe(value) == expected
|
|
275
275
|
|
|
276
276
|
def test_integer_timeframe(self) -> None:
|
|
277
|
-
"""Test parsing integer
|
|
278
|
-
assert parse_timeframe("
|
|
277
|
+
"""Test parsing supported integer timeframes."""
|
|
278
|
+
assert parse_timeframe("1") == 1
|
|
279
|
+
assert parse_timeframe(16385) == 16385
|
|
280
|
+
|
|
281
|
+
def test_unsupported_integer_timeframe_raises(self) -> None:
|
|
282
|
+
"""Test that unsupported integer timeframes raise ValueError."""
|
|
283
|
+
with pytest.raises(ValueError, match="Invalid timeframe"):
|
|
284
|
+
parse_timeframe("42")
|
|
279
285
|
|
|
280
286
|
def test_invalid_timeframe_raises(self) -> None:
|
|
281
287
|
"""Test that invalid timeframe raises ValueError."""
|
|
@@ -288,15 +294,21 @@ class TestParseTickFlags:
|
|
|
288
294
|
|
|
289
295
|
@pytest.mark.parametrize(
|
|
290
296
|
("value", "expected"),
|
|
291
|
-
[("ALL", 1), ("info",
|
|
297
|
+
[("ALL", -1), ("info", 1), ("TRADE", 2), ("COPY_TICKS_ALL", -1)],
|
|
292
298
|
)
|
|
293
299
|
def test_named_flag(self, value: str, expected: int) -> None:
|
|
294
300
|
"""Test parsing named tick flags."""
|
|
295
301
|
assert parse_tick_flags(value) == expected
|
|
296
302
|
|
|
297
303
|
def test_integer_flag(self) -> None:
|
|
298
|
-
"""Test parsing integer tick
|
|
299
|
-
assert parse_tick_flags("
|
|
304
|
+
"""Test parsing supported integer tick flags."""
|
|
305
|
+
assert parse_tick_flags("-1") == -1
|
|
306
|
+
assert parse_tick_flags(2) == 2
|
|
307
|
+
|
|
308
|
+
def test_unsupported_integer_flag_raises(self) -> None:
|
|
309
|
+
"""Test that unsupported integer tick flags raise ValueError."""
|
|
310
|
+
with pytest.raises(ValueError, match="Invalid tick flags"):
|
|
311
|
+
parse_tick_flags("7")
|
|
300
312
|
|
|
301
313
|
def test_invalid_flag_raises(self) -> None:
|
|
302
314
|
"""Test that invalid flag raises ValueError."""
|
|
@@ -355,8 +367,11 @@ class TestConstants:
|
|
|
355
367
|
assert key in TIMEFRAME_MAP
|
|
356
368
|
|
|
357
369
|
def test_tick_flag_map_has_expected_keys(self) -> None:
|
|
358
|
-
"""Test that TICK_FLAG_MAP contains standard flags."""
|
|
359
|
-
assert
|
|
370
|
+
"""Test that TICK_FLAG_MAP contains standard flags with MT5 values."""
|
|
371
|
+
assert {"ALL", "INFO", "TRADE"} <= set(TICK_FLAG_MAP)
|
|
372
|
+
assert TICK_FLAG_MAP["ALL"] == -1
|
|
373
|
+
assert TICK_FLAG_MAP["INFO"] == 1
|
|
374
|
+
assert TICK_FLAG_MAP["TRADE"] == 2
|
|
360
375
|
|
|
361
376
|
@pytest.mark.parametrize(
|
|
362
377
|
("dataset", "expected"),
|
|
@@ -403,26 +418,48 @@ class TestTimeframeType:
|
|
|
403
418
|
"""Test converting a string to timeframe integer."""
|
|
404
419
|
assert TIMEFRAME_TYPE.convert("H1", None, None) == 16385
|
|
405
420
|
|
|
406
|
-
def
|
|
407
|
-
"""Test
|
|
408
|
-
assert TIMEFRAME_TYPE.convert(
|
|
421
|
+
def test_convert_int(self) -> None:
|
|
422
|
+
"""Test converting supported integer timeframe values."""
|
|
423
|
+
assert TIMEFRAME_TYPE.convert(16385, None, None) == 16385
|
|
424
|
+
|
|
425
|
+
def test_convert_unsupported_int(self) -> None:
|
|
426
|
+
"""Test that unsupported integer values raise BadParameter."""
|
|
427
|
+
with pytest.raises(Exception, match="Invalid timeframe"):
|
|
428
|
+
TIMEFRAME_TYPE.convert(42, None, None)
|
|
409
429
|
|
|
410
430
|
def test_convert_invalid(self) -> None:
|
|
411
431
|
"""Test that invalid values raise BadParameter."""
|
|
412
432
|
with pytest.raises(Exception, match="Invalid timeframe"):
|
|
413
433
|
TIMEFRAME_TYPE.convert("bad", None, None)
|
|
414
434
|
|
|
435
|
+
@pytest.mark.parametrize("value", [True, False, None, 1.5])
|
|
436
|
+
def test_convert_invalid_types(self, value: object) -> None:
|
|
437
|
+
"""Test that bool, float, and None values raise BadParameter."""
|
|
438
|
+
with pytest.raises(Exception, match="Invalid timeframe"):
|
|
439
|
+
TIMEFRAME_TYPE.convert(value, None, None)
|
|
440
|
+
|
|
415
441
|
|
|
416
442
|
class TestTickFlagsType:
|
|
417
443
|
"""Tests for _TickFlagsType."""
|
|
418
444
|
|
|
419
445
|
def test_convert_string(self) -> None:
|
|
420
446
|
"""Test converting a string to tick flags integer."""
|
|
421
|
-
assert TICK_FLAGS_TYPE.convert("ALL", None, None) == 1
|
|
447
|
+
assert TICK_FLAGS_TYPE.convert("ALL", None, None) == -1
|
|
448
|
+
|
|
449
|
+
def test_convert_int(self) -> None:
|
|
450
|
+
"""Test converting supported integer tick flag values."""
|
|
451
|
+
assert TICK_FLAGS_TYPE.convert(2, None, None) == 2
|
|
422
452
|
|
|
423
|
-
def
|
|
424
|
-
"""Test that integer values
|
|
425
|
-
|
|
453
|
+
def test_convert_unsupported_int(self) -> None:
|
|
454
|
+
"""Test that unsupported integer values raise BadParameter."""
|
|
455
|
+
with pytest.raises(Exception, match="Invalid tick flags"):
|
|
456
|
+
TICK_FLAGS_TYPE.convert(7, None, None)
|
|
457
|
+
|
|
458
|
+
@pytest.mark.parametrize("value", [True, False, None, 1.5])
|
|
459
|
+
def test_convert_invalid_types(self, value: object) -> None:
|
|
460
|
+
"""Test that bool, float, and None values raise BadParameter."""
|
|
461
|
+
with pytest.raises(Exception, match="Invalid tick flags"):
|
|
462
|
+
TICK_FLAGS_TYPE.convert(value, None, None)
|
|
426
463
|
|
|
427
464
|
def test_convert_invalid(self) -> None:
|
|
428
465
|
"""Test that invalid values raise BadParameter."""
|
|
@@ -487,7 +487,7 @@ wheels = [
|
|
|
487
487
|
|
|
488
488
|
[[package]]
|
|
489
489
|
name = "mt5cli"
|
|
490
|
-
version = "0.
|
|
490
|
+
version = "0.7.0"
|
|
491
491
|
source = { editable = "." }
|
|
492
492
|
dependencies = [
|
|
493
493
|
{ name = "click" },
|
|
@@ -513,7 +513,7 @@ dev = [
|
|
|
513
513
|
[package.metadata]
|
|
514
514
|
requires-dist = [
|
|
515
515
|
{ name = "click", specifier = ">=8.1.0" },
|
|
516
|
-
{ name = "pdmt5", specifier = ">=0.
|
|
516
|
+
{ name = "pdmt5", specifier = ">=0.3.0" },
|
|
517
517
|
{ name = "pyarrow", specifier = ">=19.0.0" },
|
|
518
518
|
{ name = "typer", specifier = ">=0.15.0" },
|
|
519
519
|
]
|
|
@@ -684,16 +684,16 @@ wheels = [
|
|
|
684
684
|
|
|
685
685
|
[[package]]
|
|
686
686
|
name = "pdmt5"
|
|
687
|
-
version = "0.
|
|
687
|
+
version = "0.3.0"
|
|
688
688
|
source = { registry = "https://pypi.org/simple" }
|
|
689
689
|
dependencies = [
|
|
690
690
|
{ name = "metatrader5", marker = "sys_platform == 'win32'" },
|
|
691
691
|
{ name = "pandas" },
|
|
692
692
|
{ name = "pydantic" },
|
|
693
693
|
]
|
|
694
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
694
|
+
sdist = { url = "https://files.pythonhosted.org/packages/bf/cc/c8fa3a01e0e34178fec8527992f7bb8eda5881477ce23aaacaa9b2ef7bec/pdmt5-0.3.0.tar.gz", hash = "sha256:bb612d5c2695eafac9b2a7b74756e13bd383d7e5517bd90c9a2efa92492c484c", size = 215100, upload-time = "2026-06-11T13:26:46.976Z" }
|
|
695
695
|
wheels = [
|
|
696
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
696
|
+
{ url = "https://files.pythonhosted.org/packages/f2/03/b12cc4c9db983d971c9172b3765161b6d91136d0624e6718a04dd815e7a1/pdmt5-0.3.0-py3-none-any.whl", hash = "sha256:5388b406cc583202600cfe22c9d781679b1d931b1ed5a2b5dcf37c566149b49f", size = 26250, upload-time = "2026-06-11T13:26:45.689Z" },
|
|
697
697
|
]
|
|
698
698
|
|
|
699
699
|
[[package]]
|
|
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
|