mt5cli 0.6.1__tar.gz → 0.7.1__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 (41) hide show
  1. {mt5cli-0.6.1 → mt5cli-0.7.1}/PKG-INFO +8 -2
  2. {mt5cli-0.6.1 → mt5cli-0.7.1}/README.md +6 -0
  3. {mt5cli-0.6.1 → mt5cli-0.7.1}/docs/index.md +6 -0
  4. {mt5cli-0.6.1 → mt5cli-0.7.1}/mt5cli/cli.py +55 -54
  5. {mt5cli-0.6.1 → mt5cli-0.7.1}/mt5cli/history.py +102 -59
  6. {mt5cli-0.6.1 → mt5cli-0.7.1}/mt5cli/sdk.py +4 -5
  7. {mt5cli-0.6.1 → mt5cli-0.7.1}/mt5cli/utils.py +31 -50
  8. {mt5cli-0.6.1 → mt5cli-0.7.1}/pyproject.toml +2 -2
  9. mt5cli-0.7.1/tests/conftest.py +52 -0
  10. {mt5cli-0.6.1 → mt5cli-0.7.1}/tests/test_cli.py +5 -37
  11. {mt5cli-0.6.1 → mt5cli-0.7.1}/tests/test_history.py +17 -1
  12. {mt5cli-0.6.1 → mt5cli-0.7.1}/tests/test_sdk.py +5 -31
  13. {mt5cli-0.6.1 → mt5cli-0.7.1}/tests/test_utils.py +51 -14
  14. {mt5cli-0.6.1 → mt5cli-0.7.1}/uv.lock +5 -5
  15. {mt5cli-0.6.1 → mt5cli-0.7.1}/.agents/skills/local-qa/SKILL.md +0 -0
  16. {mt5cli-0.6.1 → mt5cli-0.7.1}/.agents/skills/local-qa/scripts/qa.sh +0 -0
  17. {mt5cli-0.6.1 → mt5cli-0.7.1}/.agents/skills/mt5cli/SKILL.md +0 -0
  18. {mt5cli-0.6.1 → mt5cli-0.7.1}/.claude/agents/codex.md +0 -0
  19. {mt5cli-0.6.1 → mt5cli-0.7.1}/.claude/settings.json +0 -0
  20. {mt5cli-0.6.1 → mt5cli-0.7.1}/.github/FUNDING.yml +0 -0
  21. {mt5cli-0.6.1 → mt5cli-0.7.1}/.github/dependabot.yml +0 -0
  22. {mt5cli-0.6.1 → mt5cli-0.7.1}/.github/renovate.json +0 -0
  23. {mt5cli-0.6.1 → mt5cli-0.7.1}/.github/workflows/ci.yml +0 -0
  24. {mt5cli-0.6.1 → mt5cli-0.7.1}/.github/workflows/claude.yml +0 -0
  25. {mt5cli-0.6.1 → mt5cli-0.7.1}/.github/workflows/release.yml +0 -0
  26. {mt5cli-0.6.1 → mt5cli-0.7.1}/.gitignore +0 -0
  27. {mt5cli-0.6.1 → mt5cli-0.7.1}/AGENTS.md +0 -0
  28. {mt5cli-0.6.1 → mt5cli-0.7.1}/CLAUDE.md +0 -0
  29. {mt5cli-0.6.1 → mt5cli-0.7.1}/LICENSE +0 -0
  30. {mt5cli-0.6.1 → mt5cli-0.7.1}/docs/api/cli.md +0 -0
  31. {mt5cli-0.6.1 → mt5cli-0.7.1}/docs/api/history.md +0 -0
  32. {mt5cli-0.6.1 → mt5cli-0.7.1}/docs/api/index.md +0 -0
  33. {mt5cli-0.6.1 → mt5cli-0.7.1}/docs/api/sdk.md +0 -0
  34. {mt5cli-0.6.1 → mt5cli-0.7.1}/docs/api/trading.md +0 -0
  35. {mt5cli-0.6.1 → mt5cli-0.7.1}/docs/api/utils.md +0 -0
  36. {mt5cli-0.6.1 → mt5cli-0.7.1}/mkdocs.yml +0 -0
  37. {mt5cli-0.6.1 → mt5cli-0.7.1}/mt5cli/__init__.py +0 -0
  38. {mt5cli-0.6.1 → mt5cli-0.7.1}/mt5cli/__main__.py +0 -0
  39. {mt5cli-0.6.1 → mt5cli-0.7.1}/mt5cli/trading.py +0 -0
  40. {mt5cli-0.6.1 → mt5cli-0.7.1}/tests/__init__.py +0 -0
  41. {mt5cli-0.6.1 → mt5cli-0.7.1}/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.1
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
@@ -96,6 +96,15 @@ def _sdk_client(ctx: typer.Context) -> sdk.Mt5CliClient:
96
96
  return sdk.Mt5CliClient(config=export_ctx.config)
97
97
 
98
98
 
99
+ def _export_command(
100
+ ctx: typer.Context,
101
+ fetch_fn: Callable[[sdk.Mt5CliClient], pd.DataFrame],
102
+ ) -> None:
103
+ """Create an SDK client, fetch a DataFrame, and export it."""
104
+ client = _sdk_client(ctx)
105
+ _execute_export(ctx, lambda: fetch_fn(client))
106
+
107
+
99
108
  @app.callback()
100
109
  def _callback( # pyright: ignore[reportUnusedFunction]
101
110
  ctx: typer.Context,
@@ -193,10 +202,9 @@ def rates_from(
193
202
  count: Annotated[int, typer.Option(help="Number of records.")],
194
203
  ) -> None:
195
204
  """Export rates from a start date."""
196
- client = _sdk_client(ctx)
197
- _execute_export(
205
+ _export_command(
198
206
  ctx,
199
- lambda: client.copy_rates_from(symbol, timeframe, date_from, count),
207
+ lambda client: client.copy_rates_from(symbol, timeframe, date_from, count),
200
208
  )
201
209
 
202
210
 
@@ -215,10 +223,14 @@ def rates_from_pos(
215
223
  count: Annotated[int, typer.Option(help="Number of records.")],
216
224
  ) -> None:
217
225
  """Export rates from a start position."""
218
- client = _sdk_client(ctx)
219
- _execute_export(
226
+ _export_command(
220
227
  ctx,
221
- lambda: client.copy_rates_from_pos(symbol, timeframe, start_pos, count),
228
+ lambda client: client.copy_rates_from_pos(
229
+ symbol,
230
+ timeframe,
231
+ start_pos,
232
+ count,
233
+ ),
222
234
  )
223
235
 
224
236
 
@@ -240,10 +252,14 @@ def latest_rates(
240
252
  ] = 0,
241
253
  ) -> None:
242
254
  """Export latest rates from a start position."""
243
- client = _sdk_client(ctx)
244
- _execute_export(
255
+ _export_command(
245
256
  ctx,
246
- lambda: client.latest_rates(symbol, timeframe, count, start_pos=start_pos),
257
+ lambda client: client.latest_rates(
258
+ symbol,
259
+ timeframe,
260
+ count,
261
+ start_pos=start_pos,
262
+ ),
247
263
  )
248
264
 
249
265
 
@@ -268,10 +284,9 @@ def rates_range(
268
284
  ],
269
285
  ) -> None:
270
286
  """Export rates for a date range."""
271
- client = _sdk_client(ctx)
272
- _execute_export(
287
+ _export_command(
273
288
  ctx,
274
- lambda: client.copy_rates_range(symbol, timeframe, date_from, date_to),
289
+ lambda client: client.copy_rates_range(symbol, timeframe, date_from, date_to),
275
290
  )
276
291
 
277
292
 
@@ -293,10 +308,9 @@ def ticks_from(
293
308
  ],
294
309
  ) -> None:
295
310
  """Export ticks from a start date."""
296
- client = _sdk_client(ctx)
297
- _execute_export(
311
+ _export_command(
298
312
  ctx,
299
- lambda: client.copy_ticks_from(symbol, date_from, count, flags),
313
+ lambda client: client.copy_ticks_from(symbol, date_from, count, flags),
300
314
  )
301
315
 
302
316
 
@@ -318,10 +332,9 @@ def ticks_range(
318
332
  ],
319
333
  ) -> None:
320
334
  """Export ticks for a date range."""
321
- client = _sdk_client(ctx)
322
- _execute_export(
335
+ _export_command(
323
336
  ctx,
324
- lambda: client.copy_ticks_range(symbol, date_from, date_to, flags),
337
+ lambda client: client.copy_ticks_range(symbol, date_from, date_to, flags),
325
338
  )
326
339
 
327
340
 
@@ -347,13 +360,12 @@ def ticks_recent(
347
360
  click_type=TICK_FLAGS_TYPE,
348
361
  help="Tick flags (ALL, INFO, TRADE, or integer).",
349
362
  ),
350
- ] = 1,
363
+ ] = "ALL", # pyright: ignore[reportArgumentType]
351
364
  ) -> None:
352
365
  """Export ticks from a recent time window."""
353
- client = _sdk_client(ctx)
354
- _execute_export(
366
+ _export_command(
355
367
  ctx,
356
- lambda: client.recent_ticks(
368
+ lambda client: client.recent_ticks(
357
369
  symbol,
358
370
  seconds,
359
371
  date_to=date_to,
@@ -366,13 +378,13 @@ def ticks_recent(
366
378
  @app.command()
367
379
  def account_info(ctx: typer.Context) -> None:
368
380
  """Export account information."""
369
- _execute_export(ctx, _sdk_client(ctx).account_info)
381
+ _export_command(ctx, lambda client: client.account_info())
370
382
 
371
383
 
372
384
  @app.command()
373
385
  def terminal_info(ctx: typer.Context) -> None:
374
386
  """Export terminal information."""
375
- _execute_export(ctx, _sdk_client(ctx).terminal_info)
387
+ _export_command(ctx, lambda client: client.terminal_info())
376
388
 
377
389
 
378
390
  @app.command()
@@ -384,8 +396,7 @@ def symbols(
384
396
  ] = None,
385
397
  ) -> None:
386
398
  """Export symbol list."""
387
- client = _sdk_client(ctx)
388
- _execute_export(ctx, lambda: client.symbols(group=group))
399
+ _export_command(ctx, lambda client: client.symbols(group=group))
389
400
 
390
401
 
391
402
  @app.command()
@@ -394,8 +405,7 @@ def symbol_info(
394
405
  symbol: Annotated[str, typer.Option(help="Symbol name.")],
395
406
  ) -> None:
396
407
  """Export symbol details."""
397
- client = _sdk_client(ctx)
398
- _execute_export(ctx, lambda: client.symbol_info(symbol))
408
+ _export_command(ctx, lambda client: client.symbol_info(symbol))
399
409
 
400
410
 
401
411
  @app.command()
@@ -404,8 +414,7 @@ def minimum_margins(
404
414
  symbol: Annotated[str, typer.Option(help="Symbol name.")],
405
415
  ) -> None:
406
416
  """Export minimum-volume buy and sell margin requirements."""
407
- client = _sdk_client(ctx)
408
- _execute_export(ctx, lambda: client.minimum_margins(symbol))
417
+ _export_command(ctx, lambda client: client.minimum_margins(symbol))
409
418
 
410
419
 
411
420
  @app.command()
@@ -416,10 +425,9 @@ def orders(
416
425
  ticket: Annotated[int | None, typer.Option(help="Ticket filter.")] = None,
417
426
  ) -> None:
418
427
  """Export active orders."""
419
- client = _sdk_client(ctx)
420
- _execute_export(
428
+ _export_command(
421
429
  ctx,
422
- lambda: client.orders(symbol=symbol, group=group, ticket=ticket),
430
+ lambda client: client.orders(symbol=symbol, group=group, ticket=ticket),
423
431
  )
424
432
 
425
433
 
@@ -431,10 +439,9 @@ def positions(
431
439
  ticket: Annotated[int | None, typer.Option(help="Ticket filter.")] = None,
432
440
  ) -> None:
433
441
  """Export open positions."""
434
- client = _sdk_client(ctx)
435
- _execute_export(
442
+ _export_command(
436
443
  ctx,
437
- lambda: client.positions(symbol=symbol, group=group, ticket=ticket),
444
+ lambda client: client.positions(symbol=symbol, group=group, ticket=ticket),
438
445
  )
439
446
 
440
447
 
@@ -455,10 +462,9 @@ def history_orders(
455
462
  position: Annotated[int | None, typer.Option(help="Position ticket.")] = None,
456
463
  ) -> None:
457
464
  """Export historical orders."""
458
- client = _sdk_client(ctx)
459
- _execute_export(
465
+ _export_command(
460
466
  ctx,
461
- lambda: client.history_orders(
467
+ lambda client: client.history_orders(
462
468
  date_from=date_from,
463
469
  date_to=date_to,
464
470
  group=group,
@@ -486,10 +492,9 @@ def history_deals(
486
492
  position: Annotated[int | None, typer.Option(help="Position ticket.")] = None,
487
493
  ) -> None:
488
494
  """Export historical deals."""
489
- client = _sdk_client(ctx)
490
- _execute_export(
495
+ _export_command(
491
496
  ctx,
492
- lambda: client.history_deals(
497
+ lambda client: client.history_deals(
493
498
  date_from=date_from,
494
499
  date_to=date_to,
495
500
  group=group,
@@ -512,10 +517,9 @@ def recent_history_deals(
512
517
  symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
513
518
  ) -> None:
514
519
  """Export historical deals from a recent trailing window."""
515
- client = _sdk_client(ctx)
516
- _execute_export(
520
+ _export_command(
517
521
  ctx,
518
- lambda: client.recent_history_deals(
522
+ lambda client: client.recent_history_deals(
519
523
  hours,
520
524
  date_to=date_to,
521
525
  group=group,
@@ -527,20 +531,19 @@ def recent_history_deals(
527
531
  @app.command()
528
532
  def mt5_summary(ctx: typer.Context) -> None:
529
533
  """Export a compact terminal/account status summary."""
530
- client = _sdk_client(ctx)
531
- _execute_export(ctx, client.mt5_summary_as_df)
534
+ _export_command(ctx, lambda client: client.mt5_summary_as_df())
532
535
 
533
536
 
534
537
  @app.command()
535
538
  def version(ctx: typer.Context) -> None:
536
539
  """Export MetaTrader5 version information."""
537
- _execute_export(ctx, _sdk_client(ctx).version)
540
+ _export_command(ctx, lambda client: client.version())
538
541
 
539
542
 
540
543
  @app.command()
541
544
  def last_error(ctx: typer.Context) -> None:
542
545
  """Export the last error information."""
543
- _execute_export(ctx, _sdk_client(ctx).last_error)
546
+ _export_command(ctx, lambda client: client.last_error())
544
547
 
545
548
 
546
549
  @app.command()
@@ -549,8 +552,7 @@ def symbol_info_tick(
549
552
  symbol: Annotated[str, typer.Option(help="Symbol name.")],
550
553
  ) -> None:
551
554
  """Export the last tick for a symbol."""
552
- client = _sdk_client(ctx)
553
- _execute_export(ctx, lambda: client.symbol_info_tick(symbol))
555
+ _export_command(ctx, lambda client: client.symbol_info_tick(symbol))
554
556
 
555
557
 
556
558
  @app.command()
@@ -559,8 +561,7 @@ def market_book(
559
561
  symbol: Annotated[str, typer.Option(help="Symbol name.")],
560
562
  ) -> None:
561
563
  """Export market depth (order book) for a symbol."""
562
- client = _sdk_client(ctx)
563
- _execute_export(ctx, lambda: client.market_book(symbol))
564
+ _export_command(ctx, lambda client: client.market_book(symbol))
564
565
 
565
566
 
566
567
  @app.command()
@@ -656,7 +657,7 @@ def collect_history(
656
657
  click_type=TICK_FLAGS_TYPE,
657
658
  help="Tick copy flags (ALL, INFO, TRADE, or integer).",
658
659
  ),
659
- ] = 1,
660
+ ] = "ALL", # pyright: ignore[reportArgumentType]
660
661
  if_exists: Annotated[
661
662
  IfExists,
662
663
  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:
@@ -1332,6 +1332,50 @@ def create_rate_compatibility_views(conn: sqlite3.Connection) -> None:
1332
1332
  )
1333
1333
 
1334
1334
 
1335
+ def _stream_symbol_frames(
1336
+ conn: sqlite3.Connection,
1337
+ symbols: Sequence[str],
1338
+ dataset: Dataset,
1339
+ if_exists: IfExists,
1340
+ written_columns: dict[Dataset, set[str]],
1341
+ fetch_frame: Callable[[str], pd.DataFrame],
1342
+ ) -> bool:
1343
+ """Stream per-symbol frames into SQLite.
1344
+
1345
+ Returns:
1346
+ True if the dataset table was written.
1347
+ """
1348
+ table_exists = False
1349
+ for sym in symbols:
1350
+ table_exists = write_streamed_frame(
1351
+ conn,
1352
+ fetch_frame(sym),
1353
+ dataset,
1354
+ table_exists,
1355
+ if_exists,
1356
+ written_columns,
1357
+ )
1358
+ return table_exists
1359
+
1360
+
1361
+ def _record_symbol_time_dedup(
1362
+ dedup_scopes: dict[Dataset, list[DedupScope]],
1363
+ written_tables: set[Dataset],
1364
+ dataset: Dataset,
1365
+ symbol: str,
1366
+ start_date: datetime,
1367
+ ) -> None:
1368
+ """Record a symbol-scoped deduplication window after an incremental write."""
1369
+ written_tables.add(dataset)
1370
+ _record_dedup_scope(
1371
+ dedup_scopes,
1372
+ dataset,
1373
+ "symbol = ? AND time >= ?",
1374
+ (symbol, start_date),
1375
+ frozenset({"symbol", "time"}),
1376
+ )
1377
+
1378
+
1335
1379
  def write_rates_dataset(
1336
1380
  conn: sqlite3.Connection,
1337
1381
  client: Mt5DataClient,
@@ -1347,8 +1391,8 @@ def write_rates_dataset(
1347
1391
  Returns:
1348
1392
  True if the rates table was written.
1349
1393
  """
1350
- table_exists = False
1351
- for sym in symbols:
1394
+
1395
+ def _fetch_rates_frame(sym: str) -> pd.DataFrame:
1352
1396
  frame = client.copy_rates_range_as_df(
1353
1397
  symbol=sym,
1354
1398
  timeframe=timeframe,
@@ -1358,15 +1402,16 @@ def write_rates_dataset(
1358
1402
  if len(frame.columns) != 0:
1359
1403
  frame.insert(0, "symbol", sym)
1360
1404
  frame.insert(1, "timeframe", timeframe)
1361
- table_exists = write_streamed_frame(
1362
- conn,
1363
- frame,
1364
- Dataset.rates,
1365
- table_exists,
1366
- if_exists,
1367
- written_columns,
1368
- )
1369
- return table_exists
1405
+ return frame
1406
+
1407
+ return _stream_symbol_frames(
1408
+ conn,
1409
+ symbols,
1410
+ Dataset.rates,
1411
+ if_exists,
1412
+ written_columns,
1413
+ _fetch_rates_frame,
1414
+ )
1370
1415
 
1371
1416
 
1372
1417
  def write_ticks_dataset(
@@ -1384,8 +1429,8 @@ def write_ticks_dataset(
1384
1429
  Returns:
1385
1430
  True if the ticks table was written.
1386
1431
  """
1387
- table_exists = False
1388
- for sym in symbols:
1432
+
1433
+ def _fetch_ticks_frame(sym: str) -> pd.DataFrame:
1389
1434
  frame = client.copy_ticks_range_as_df(
1390
1435
  symbol=sym,
1391
1436
  date_from=date_from,
@@ -1394,15 +1439,16 @@ def write_ticks_dataset(
1394
1439
  ).drop(columns=["symbol"], errors="ignore")
1395
1440
  if len(frame.columns) != 0:
1396
1441
  frame.insert(0, "symbol", sym)
1397
- table_exists = write_streamed_frame(
1398
- conn,
1399
- frame,
1400
- Dataset.ticks,
1401
- table_exists,
1402
- if_exists,
1403
- written_columns,
1404
- )
1405
- return table_exists
1442
+ return frame
1443
+
1444
+ return _stream_symbol_frames(
1445
+ conn,
1446
+ symbols,
1447
+ Dataset.ticks,
1448
+ if_exists,
1449
+ written_columns,
1450
+ _fetch_ticks_frame,
1451
+ )
1406
1452
 
1407
1453
 
1408
1454
  def write_history_dataset(
@@ -1437,22 +1483,22 @@ def write_history_dataset(
1437
1483
  if_exists,
1438
1484
  written_columns,
1439
1485
  )
1440
- for sym in symbols:
1441
- frame = fetch(date_from=date_from, date_to=date_to, symbol=sym)
1442
- frame = filter_trade_history_frame(
1443
- frame,
1486
+
1487
+ def _fetch_history_frame(sym: str) -> pd.DataFrame:
1488
+ return filter_trade_history_frame(
1489
+ fetch(date_from=date_from, date_to=date_to, symbol=sym),
1444
1490
  [sym],
1445
1491
  include_account_events=False,
1446
1492
  )
1447
- table_exists = write_streamed_frame(
1448
- conn,
1449
- frame,
1450
- dataset,
1451
- table_exists,
1452
- if_exists,
1453
- written_columns,
1454
- )
1455
- return table_exists
1493
+
1494
+ return _stream_symbol_frames(
1495
+ conn,
1496
+ symbols,
1497
+ dataset,
1498
+ if_exists,
1499
+ written_columns,
1500
+ _fetch_history_frame,
1501
+ )
1456
1502
 
1457
1503
 
1458
1504
  def _write_incremental_rates(
@@ -1525,13 +1571,12 @@ def _write_incremental_ticks(
1525
1571
  IfExists.APPEND,
1526
1572
  written_columns,
1527
1573
  ):
1528
- written_tables.add(Dataset.ticks)
1529
- _record_dedup_scope(
1574
+ _record_symbol_time_dedup(
1530
1575
  dedup_scopes,
1576
+ written_tables,
1531
1577
  Dataset.ticks,
1532
- "symbol = ? AND time >= ?",
1533
- (symbol, start_date),
1534
- frozenset({"symbol", "time"}),
1578
+ symbol,
1579
+ start_date,
1535
1580
  )
1536
1581
 
1537
1582
 
@@ -1564,13 +1609,12 @@ def _write_incremental_history_orders(
1564
1609
  written_columns,
1565
1610
  include_account_events=False,
1566
1611
  ):
1567
- written_tables.add(Dataset.history_orders)
1568
- _record_dedup_scope(
1612
+ _record_symbol_time_dedup(
1569
1613
  dedup_scopes,
1614
+ written_tables,
1570
1615
  Dataset.history_orders,
1571
- "symbol = ? AND time >= ?",
1572
- (symbol, start_date),
1573
- frozenset({"symbol", "time"}),
1616
+ symbol,
1617
+ start_date,
1574
1618
  )
1575
1619
 
1576
1620
 
@@ -1662,13 +1706,12 @@ def _write_incremental_history_deals(
1662
1706
  written_columns,
1663
1707
  include_account_events=False,
1664
1708
  ):
1665
- written_tables.add(Dataset.history_deals)
1666
- _record_dedup_scope(
1709
+ _record_symbol_time_dedup(
1667
1710
  dedup_scopes,
1711
+ written_tables,
1668
1712
  Dataset.history_deals,
1669
- "symbol = ? AND time >= ?",
1670
- (symbol, start_date),
1671
- frozenset({"symbol", "time"}),
1713
+ symbol,
1714
+ start_date,
1672
1715
  )
1673
1716
 
1674
1717
 
@@ -64,6 +64,9 @@ _MT5_HISTORY_CLIENT_CALL_FUNCTIONS: frozenset[str] = frozenset({
64
64
  "write_ticks_dataset",
65
65
  "write_history_dataset",
66
66
  "_write_incremental_history_deals",
67
+ "_fetch_rates_frame",
68
+ "_fetch_ticks_frame",
69
+ "_fetch_history_frame",
67
70
  })
68
71
  _NON_CALLABLE_TYPE_ERROR = re.compile(r"^'[^']+' object is not callable$")
69
72
 
@@ -144,14 +147,10 @@ __all__ = [
144
147
 
145
148
 
146
149
  def _coerce_timeframe(timeframe: int | str) -> int:
147
- if isinstance(timeframe, int):
148
- return timeframe
149
150
  return parse_timeframe(timeframe)
150
151
 
151
152
 
152
153
  def _coerce_tick_flags(flags: int | str) -> int:
153
- if isinstance(flags, int):
154
- return flags
155
154
  return parse_tick_flags(flags)
156
155
 
157
156
 
@@ -1171,7 +1170,7 @@ def collect_history(
1171
1170
  *,
1172
1171
  datasets: set[Dataset] | None = None,
1173
1172
  timeframe: int | str = 1,
1174
- flags: int | str = 1,
1173
+ flags: int | str = "ALL",
1175
1174
  if_exists: IfExists = IfExists.FAIL,
1176
1175
  with_views: bool = False,
1177
1176
  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.1"
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",
@@ -0,0 +1,52 @@
1
+ """Shared pytest fixtures for mt5cli tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import MagicMock
6
+
7
+ import pandas as pd
8
+ import pytest
9
+ from pytest_mock import MockerFixture # noqa: TC002
10
+
11
+ _DATAFRAME_METHODS = (
12
+ "copy_rates_from_as_df",
13
+ "copy_rates_from_pos_as_df",
14
+ "copy_rates_range_as_df",
15
+ "copy_ticks_from_as_df",
16
+ "copy_ticks_range_as_df",
17
+ "account_info_as_df",
18
+ "terminal_info_as_df",
19
+ "symbols_get_as_df",
20
+ "symbol_info_as_df",
21
+ "orders_get_as_df",
22
+ "positions_get_as_df",
23
+ "history_orders_get_as_df",
24
+ "history_deals_get_as_df",
25
+ "version_as_df",
26
+ "last_error_as_df",
27
+ "symbol_info_tick_as_df",
28
+ "market_book_get_as_df",
29
+ "order_check_as_df",
30
+ "order_send_as_df",
31
+ )
32
+
33
+
34
+ def build_mock_mt5_data_client() -> MagicMock:
35
+ """Return a MagicMock Mt5DataClient with common DataFrame stubs."""
36
+ client = MagicMock()
37
+ sample_df = pd.DataFrame({"col": [1]})
38
+ for method_name in _DATAFRAME_METHODS:
39
+ getattr(client, method_name).return_value = sample_df
40
+ client.version.return_value = (5, 0, 1)
41
+ client.terminal_info.return_value = {"connected": True, "paths": ["terminal.exe"]}
42
+ client.account_info.return_value = {"login": 123, "limits": {"modes": ["demo"]}}
43
+ client.symbols_total.return_value = 42
44
+ return client
45
+
46
+
47
+ @pytest.fixture
48
+ def mock_client(mocker: MockerFixture) -> MagicMock:
49
+ """Create and patch a mock Mt5DataClient for CLI and SDK tests."""
50
+ client = build_mock_mt5_data_client()
51
+ mocker.patch("mt5cli.sdk.Mt5DataClient", return_value=client)
52
+ return client
@@ -69,38 +69,6 @@ class TestExecuteExport:
69
69
  # ---------------------------------------------------------------------------
70
70
 
71
71
 
72
- @pytest.fixture
73
- def mock_client(mocker: MockerFixture) -> MagicMock:
74
- """Create and patch a mock Mt5DataClient for CLI tests."""
75
- client = MagicMock()
76
- sample_df = pd.DataFrame({"col": [1]})
77
- client.copy_rates_from_as_df.return_value = sample_df
78
- client.copy_rates_from_pos_as_df.return_value = sample_df
79
- client.copy_rates_range_as_df.return_value = sample_df
80
- client.copy_ticks_from_as_df.return_value = sample_df
81
- client.copy_ticks_range_as_df.return_value = sample_df
82
- client.account_info_as_df.return_value = sample_df
83
- client.terminal_info_as_df.return_value = sample_df
84
- client.symbols_get_as_df.return_value = sample_df
85
- client.symbol_info_as_df.return_value = sample_df
86
- client.orders_get_as_df.return_value = sample_df
87
- client.positions_get_as_df.return_value = sample_df
88
- client.history_orders_get_as_df.return_value = sample_df
89
- client.history_deals_get_as_df.return_value = sample_df
90
- client.version_as_df.return_value = sample_df
91
- client.last_error_as_df.return_value = sample_df
92
- client.symbol_info_tick_as_df.return_value = sample_df
93
- client.market_book_get_as_df.return_value = sample_df
94
- client.order_check_as_df.return_value = sample_df
95
- client.order_send_as_df.return_value = sample_df
96
- client.version.return_value = (5, 0, 1)
97
- client.terminal_info.return_value = {"connected": True, "paths": ["terminal.exe"]}
98
- client.account_info.return_value = {"login": 123, "limits": {"modes": ["demo"]}}
99
- client.symbols_total.return_value = 42
100
- mocker.patch("mt5cli.sdk.Mt5DataClient", return_value=client)
101
- return client
102
-
103
-
104
72
  class TestCommands:
105
73
  """Tests for all CLI subcommands via CliRunner."""
106
74
 
@@ -317,7 +285,7 @@ class TestCommands:
317
285
  symbol="EURUSD",
318
286
  date_from=datetime(2024, 1, 1, tzinfo=UTC),
319
287
  count=100,
320
- flags=1,
288
+ flags=-1,
321
289
  )
322
290
 
323
291
  def test_ticks_range(
@@ -348,7 +316,7 @@ class TestCommands:
348
316
  symbol="EURUSD",
349
317
  date_from=datetime(2024, 1, 1, tzinfo=UTC),
350
318
  date_to=datetime(2024, 2, 1, tzinfo=UTC),
351
- flags=2,
319
+ flags=1,
352
320
  )
353
321
 
354
322
  def test_ticks_recent(
@@ -381,7 +349,7 @@ class TestCommands:
381
349
  symbol="EURUSD",
382
350
  date_from=datetime(2024, 1, 2, tzinfo=UTC) - timedelta(seconds=120),
383
351
  count=500,
384
- flags=1,
352
+ flags=-1,
385
353
  )
386
354
  mock_client.copy_ticks_range_as_df.assert_not_called()
387
355
 
@@ -1000,7 +968,7 @@ class TestCollectHistory:
1000
968
  symbol="EURUSD",
1001
969
  date_from=datetime(2024, 1, 1, tzinfo=UTC),
1002
970
  date_to=datetime(2024, 2, 1, tzinfo=UTC),
1003
- flags=1,
971
+ flags=-1,
1004
972
  )
1005
973
  with sqlite3.connect(output) as conn:
1006
974
  tables = {
@@ -1213,7 +1181,7 @@ class TestCollectHistory:
1213
1181
  symbol="EURUSD",
1214
1182
  date_from=datetime(2024, 1, 1, tzinfo=UTC),
1215
1183
  date_to=datetime(2024, 2, 1, tzinfo=UTC),
1216
- flags=1,
1184
+ flags=-1,
1217
1185
  )
1218
1186
 
1219
1187
  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."""
@@ -132,32 +132,6 @@ _DEALS_FIXTURE: dict[str, list[object]] = {
132
132
  }
133
133
 
134
134
 
135
- @pytest.fixture
136
- def mock_client(mocker: MockerFixture) -> MagicMock:
137
- """Create and patch a mock Mt5DataClient for SDK tests."""
138
- client = MagicMock()
139
- sample_df = pd.DataFrame({"col": [1]})
140
- client.copy_rates_from_as_df.return_value = sample_df
141
- client.copy_rates_from_pos_as_df.return_value = sample_df
142
- client.copy_rates_range_as_df.return_value = sample_df
143
- client.copy_ticks_from_as_df.return_value = sample_df
144
- client.copy_ticks_range_as_df.return_value = sample_df
145
- client.account_info_as_df.return_value = sample_df
146
- client.terminal_info_as_df.return_value = sample_df
147
- client.symbols_get_as_df.return_value = sample_df
148
- client.symbol_info_as_df.return_value = sample_df
149
- client.orders_get_as_df.return_value = sample_df
150
- client.positions_get_as_df.return_value = sample_df
151
- client.history_orders_get_as_df.return_value = sample_df
152
- client.history_deals_get_as_df.return_value = sample_df
153
- client.version_as_df.return_value = sample_df
154
- client.last_error_as_df.return_value = sample_df
155
- client.symbol_info_tick_as_df.return_value = sample_df
156
- client.market_book_get_as_df.return_value = sample_df
157
- mocker.patch("mt5cli.sdk.Mt5DataClient", return_value=client)
158
- return client
159
-
160
-
161
135
  def _build_history_client(mocker: MockerFixture) -> MagicMock:
162
136
  """Build a mocked Mt5DataClient with per-symbol history results."""
163
137
  client = MagicMock()
@@ -390,7 +364,7 @@ class TestMt5CliClient:
390
364
  symbol="EURUSD",
391
365
  date_from=datetime(2024, 1, 1, tzinfo=UTC),
392
366
  count=100,
393
- flags=2,
367
+ flags=1,
394
368
  )
395
369
 
396
370
  def test_history_orders_accepts_string_dates(
@@ -951,7 +925,7 @@ class TestUpdateHistory:
951
925
  assert kwargs["symbol"] == "EURUSD"
952
926
  assert kwargs["date_from"] == expected_start
953
927
  assert kwargs["date_to"] == date_to
954
- assert kwargs["flags"] == 1
928
+ assert kwargs["flags"] == -1
955
929
  return pd.DataFrame({
956
930
  "time": ["2024-01-01T12:00:00+00:00"],
957
931
  "time_msc": [1_704_110_400_000],
@@ -1119,7 +1093,7 @@ class TestRecentTicks:
1119
1093
  symbol="EURUSD",
1120
1094
  date_from=end - timedelta(seconds=60),
1121
1095
  count=100,
1122
- flags=2,
1096
+ flags=1,
1123
1097
  )
1124
1098
  client.copy_ticks_range_as_df.assert_not_called()
1125
1099
 
@@ -1149,7 +1123,7 @@ class TestRecentTicks:
1149
1123
  assert kwargs["symbol"] == "EURUSD"
1150
1124
  assert kwargs["date_to"] == tick.time
1151
1125
  assert kwargs["date_from"] == tick.time - timedelta(seconds=30)
1152
- assert kwargs["flags"] == 1
1126
+ assert kwargs["flags"] == -1
1153
1127
 
1154
1128
  def test_recent_ticks_rejects_unsupported_tick_time(
1155
1129
  self,
@@ -1219,7 +1193,7 @@ class TestRecentTicks:
1219
1193
  symbol="EURUSD",
1220
1194
  date_from=end - timedelta(seconds=60),
1221
1195
  date_to=end,
1222
- flags=1,
1196
+ flags=-1,
1223
1197
  )
1224
1198
 
1225
1199
 
@@ -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.1"
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