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.
Files changed (40) hide show
  1. {mt5cli-0.6.1 → mt5cli-0.7.0}/PKG-INFO +8 -2
  2. {mt5cli-0.6.1 → mt5cli-0.7.0}/README.md +6 -0
  3. {mt5cli-0.6.1 → mt5cli-0.7.0}/docs/index.md +6 -0
  4. {mt5cli-0.6.1 → mt5cli-0.7.0}/mt5cli/cli.py +2 -2
  5. {mt5cli-0.6.1 → mt5cli-0.7.0}/mt5cli/history.py +9 -9
  6. {mt5cli-0.6.1 → mt5cli-0.7.0}/mt5cli/sdk.py +1 -5
  7. {mt5cli-0.6.1 → mt5cli-0.7.0}/mt5cli/utils.py +31 -50
  8. {mt5cli-0.6.1 → mt5cli-0.7.0}/pyproject.toml +2 -2
  9. {mt5cli-0.6.1 → mt5cli-0.7.0}/tests/test_cli.py +5 -5
  10. {mt5cli-0.6.1 → mt5cli-0.7.0}/tests/test_history.py +17 -1
  11. {mt5cli-0.6.1 → mt5cli-0.7.0}/tests/test_sdk.py +5 -5
  12. {mt5cli-0.6.1 → mt5cli-0.7.0}/tests/test_utils.py +51 -14
  13. {mt5cli-0.6.1 → mt5cli-0.7.0}/uv.lock +5 -5
  14. {mt5cli-0.6.1 → mt5cli-0.7.0}/.agents/skills/local-qa/SKILL.md +0 -0
  15. {mt5cli-0.6.1 → mt5cli-0.7.0}/.agents/skills/local-qa/scripts/qa.sh +0 -0
  16. {mt5cli-0.6.1 → mt5cli-0.7.0}/.agents/skills/mt5cli/SKILL.md +0 -0
  17. {mt5cli-0.6.1 → mt5cli-0.7.0}/.claude/agents/codex.md +0 -0
  18. {mt5cli-0.6.1 → mt5cli-0.7.0}/.claude/settings.json +0 -0
  19. {mt5cli-0.6.1 → mt5cli-0.7.0}/.github/FUNDING.yml +0 -0
  20. {mt5cli-0.6.1 → mt5cli-0.7.0}/.github/dependabot.yml +0 -0
  21. {mt5cli-0.6.1 → mt5cli-0.7.0}/.github/renovate.json +0 -0
  22. {mt5cli-0.6.1 → mt5cli-0.7.0}/.github/workflows/ci.yml +0 -0
  23. {mt5cli-0.6.1 → mt5cli-0.7.0}/.github/workflows/claude.yml +0 -0
  24. {mt5cli-0.6.1 → mt5cli-0.7.0}/.github/workflows/release.yml +0 -0
  25. {mt5cli-0.6.1 → mt5cli-0.7.0}/.gitignore +0 -0
  26. {mt5cli-0.6.1 → mt5cli-0.7.0}/AGENTS.md +0 -0
  27. {mt5cli-0.6.1 → mt5cli-0.7.0}/CLAUDE.md +0 -0
  28. {mt5cli-0.6.1 → mt5cli-0.7.0}/LICENSE +0 -0
  29. {mt5cli-0.6.1 → mt5cli-0.7.0}/docs/api/cli.md +0 -0
  30. {mt5cli-0.6.1 → mt5cli-0.7.0}/docs/api/history.md +0 -0
  31. {mt5cli-0.6.1 → mt5cli-0.7.0}/docs/api/index.md +0 -0
  32. {mt5cli-0.6.1 → mt5cli-0.7.0}/docs/api/sdk.md +0 -0
  33. {mt5cli-0.6.1 → mt5cli-0.7.0}/docs/api/trading.md +0 -0
  34. {mt5cli-0.6.1 → mt5cli-0.7.0}/docs/api/utils.md +0 -0
  35. {mt5cli-0.6.1 → mt5cli-0.7.0}/mkdocs.yml +0 -0
  36. {mt5cli-0.6.1 → mt5cli-0.7.0}/mt5cli/__init__.py +0 -0
  37. {mt5cli-0.6.1 → mt5cli-0.7.0}/mt5cli/__main__.py +0 -0
  38. {mt5cli-0.6.1 → mt5cli-0.7.0}/mt5cli/trading.py +0 -0
  39. {mt5cli-0.6.1 → mt5cli-0.7.0}/tests/__init__.py +0 -0
  40. {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.6.1
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.2.3
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
- ] = 1,
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
- ] = 1,
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
- TIMEFRAME_MAP,
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, ...] = tuple(TIMEFRAME_MAP)
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 = value if isinstance(value, int) else parse_timeframe(str(value))
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
- for name, value in TIMEFRAME_MAP.items():
104
- if value == timeframe:
105
- return name
106
- return str(timeframe)
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 = 1,
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
- TIMEFRAME_MAP: dict[str, int] = {
24
- "M1": 1,
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
- TICK_FLAG_MAP: dict[str, int] = {
48
- "ALL": 1,
49
- "INFO": 2,
50
- "TRADE": 4,
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(str(value))
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(str(value))
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: str) -> int:
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 int(value)
365
+ return _parse_timeframe(value)
390
366
  except ValueError:
391
- valid = ", ".join(TIMEFRAME_MAP)
392
- msg = f"Invalid timeframe: '{value}'. Use one of: {valid}, or an integer."
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: str) -> int:
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 int(value)
389
+ return _parse_copy_ticks(value)
413
390
  except ValueError:
414
- valid = ", ".join(TICK_FLAG_MAP)
415
- msg = f"Invalid tick flags: '{value}'. Use one of: {valid}, or an integer."
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.6.1"
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 >= 0.2.3",
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=1,
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=2,
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=1,
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=1,
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=1,
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=2,
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=2,
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=1,
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 timeframe."""
278
- assert parse_timeframe("42") == 42
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", 2), ("TRADE", 4)],
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 flag."""
299
- assert parse_tick_flags("7") == 7
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 set(TICK_FLAG_MAP) == {"ALL", "INFO", "TRADE"}
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 test_convert_int_passthrough(self) -> None:
407
- """Test that integer values pass through unchanged."""
408
- assert TIMEFRAME_TYPE.convert(42, None, None) == 42
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 test_convert_int_passthrough(self) -> None:
424
- """Test that integer values pass through unchanged."""
425
- assert TICK_FLAGS_TYPE.convert(7, None, None) == 7
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.6.1"
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.2.3" },
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.2.3"
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/02/25/52d9d954504ccdd0fe91f715ab74c424d61234b237cc4160d3ebe20070f1/pdmt5-0.2.3.tar.gz", hash = "sha256:21384f5826fb0125fee3f93c90b108340f55ab53b1c819d229ceac162289d2ec", size = 226665, upload-time = "2026-02-05T13:28:21.071Z" }
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/c1/75/c5e52a9cf459b85b2dd52f83e70857571b1b45805c9fe610b3959a26ac15/pdmt5-0.2.3-py3-none-any.whl", hash = "sha256:f92246a05cfc3b7feb3ab0cc5b48768a4d84aad6b02e7a68060948f5828718a1", size = 22967, upload-time = "2026-02-05T13:28:19.523Z" },
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