mt5cli 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """mt5cli: Command-line tool for MetaTrader 5."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ from .cli import detect_format, export_dataframe
6
+
7
+ __version__ = version(__package__) if __package__ else None
8
+
9
+ __all__ = [
10
+ "detect_format",
11
+ "export_dataframe",
12
+ ]
mt5cli/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Entry point for running mt5cli as a module via ``python -m mt5cli``."""
2
+
3
+ from mt5cli.cli import main
4
+
5
+ main()
mt5cli/cli.py ADDED
@@ -0,0 +1,752 @@
1
+ """Command-line interface for MetaTrader 5 data export."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sqlite3
7
+ from dataclasses import dataclass
8
+ from datetime import UTC, datetime
9
+ from enum import StrEnum
10
+ from pathlib import Path # noqa: TC003
11
+ from typing import TYPE_CHECKING, Annotated, cast
12
+
13
+ import click
14
+ import typer
15
+ from pdmt5 import Mt5Config, Mt5DataClient
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Callable
19
+
20
+ import pandas as pd
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Constants
26
+ # ---------------------------------------------------------------------------
27
+
28
+ TIMEFRAME_MAP: dict[str, int] = {
29
+ "M1": 1,
30
+ "M2": 2,
31
+ "M3": 3,
32
+ "M4": 4,
33
+ "M5": 5,
34
+ "M6": 6,
35
+ "M10": 10,
36
+ "M12": 12,
37
+ "M15": 15,
38
+ "M20": 20,
39
+ "M30": 30,
40
+ "H1": 16385,
41
+ "H2": 16386,
42
+ "H3": 16387,
43
+ "H4": 16388,
44
+ "H6": 16390,
45
+ "H8": 16392,
46
+ "H12": 16396,
47
+ "D1": 16408,
48
+ "W1": 32769,
49
+ "MN1": 49153,
50
+ }
51
+
52
+ TICK_FLAG_MAP: dict[str, int] = {
53
+ "ALL": 1,
54
+ "INFO": 2,
55
+ "TRADE": 4,
56
+ }
57
+
58
+ _FORMAT_EXTENSIONS: dict[str, str] = {
59
+ ".csv": "csv",
60
+ ".json": "json",
61
+ ".parquet": "parquet",
62
+ ".pq": "parquet",
63
+ ".db": "sqlite3",
64
+ ".sqlite": "sqlite3",
65
+ ".sqlite3": "sqlite3",
66
+ }
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Enums
70
+ # ---------------------------------------------------------------------------
71
+
72
+
73
+ class OutputFormat(StrEnum):
74
+ """Supported output file formats."""
75
+
76
+ csv = "csv"
77
+ json = "json"
78
+ parquet = "parquet"
79
+ sqlite3 = "sqlite3"
80
+
81
+
82
+ class LogLevel(StrEnum):
83
+ """Logging verbosity levels."""
84
+
85
+ DEBUG = "DEBUG"
86
+ INFO = "INFO"
87
+ WARNING = "WARNING"
88
+ ERROR = "ERROR"
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Click parameter types
93
+ # ---------------------------------------------------------------------------
94
+
95
+
96
+ class _DateTimeType(click.ParamType):
97
+ """Click parameter type for ISO 8601 datetime strings."""
98
+
99
+ name = "DATETIME"
100
+
101
+ def convert(
102
+ self,
103
+ value: object,
104
+ param: click.Parameter | None,
105
+ ctx: click.Context | None,
106
+ ) -> datetime:
107
+ """Convert a string value to a timezone-aware datetime.
108
+
109
+ Args:
110
+ value: Raw value from the command line.
111
+ param: Click parameter instance.
112
+ ctx: Click context.
113
+
114
+ Returns:
115
+ Parsed datetime.
116
+ """
117
+ if isinstance(value, datetime):
118
+ return value
119
+ try:
120
+ return parse_datetime(str(value))
121
+ except ValueError as exc:
122
+ self.fail(str(exc), param, ctx)
123
+
124
+
125
+ class _TimeframeType(click.ParamType):
126
+ """Click parameter type for MT5 timeframe values."""
127
+
128
+ name = "TIMEFRAME"
129
+
130
+ def convert(
131
+ self,
132
+ value: object,
133
+ param: click.Parameter | None,
134
+ ctx: click.Context | None,
135
+ ) -> int:
136
+ """Convert a string or integer value to a timeframe integer.
137
+
138
+ Args:
139
+ value: Raw value from the command line.
140
+ param: Click parameter instance.
141
+ ctx: Click context.
142
+
143
+ Returns:
144
+ Integer timeframe value.
145
+ """
146
+ if isinstance(value, int):
147
+ return value
148
+ try:
149
+ return parse_timeframe(str(value))
150
+ except ValueError as exc:
151
+ self.fail(str(exc), param, ctx)
152
+
153
+
154
+ class _TickFlagsType(click.ParamType):
155
+ """Click parameter type for MT5 tick copy flags."""
156
+
157
+ name = "FLAGS"
158
+
159
+ def convert(
160
+ self,
161
+ value: object,
162
+ param: click.Parameter | None,
163
+ ctx: click.Context | None,
164
+ ) -> int:
165
+ """Convert a string or integer value to a tick flags integer.
166
+
167
+ Args:
168
+ value: Raw value from the command line.
169
+ param: Click parameter instance.
170
+ ctx: Click context.
171
+
172
+ Returns:
173
+ Integer tick flag value.
174
+ """
175
+ if isinstance(value, int):
176
+ return value
177
+ try:
178
+ return parse_tick_flags(str(value))
179
+ except ValueError as exc:
180
+ self.fail(str(exc), param, ctx)
181
+
182
+
183
+ DATETIME_TYPE = _DateTimeType()
184
+ TIMEFRAME_TYPE = _TimeframeType()
185
+ TICK_FLAGS_TYPE = _TickFlagsType()
186
+
187
+ # ---------------------------------------------------------------------------
188
+ # Export context
189
+ # ---------------------------------------------------------------------------
190
+
191
+
192
+ @dataclass
193
+ class _ExportContext:
194
+ """Shared context data passed from the callback to each subcommand."""
195
+
196
+ output: Path
197
+ output_format: str
198
+ table: str
199
+ config: Mt5Config
200
+
201
+
202
+ # ---------------------------------------------------------------------------
203
+ # Public utility functions
204
+ # ---------------------------------------------------------------------------
205
+
206
+
207
+ def detect_format(
208
+ output_path: Path,
209
+ explicit_format: str | None = None,
210
+ ) -> str:
211
+ """Detect the output format from a file extension or explicit format string.
212
+
213
+ Args:
214
+ output_path: Path to the output file.
215
+ explicit_format: Explicitly specified format, if any.
216
+
217
+ Returns:
218
+ The detected format string.
219
+
220
+ Raises:
221
+ ValueError: If the format cannot be determined.
222
+ """
223
+ if explicit_format is not None:
224
+ return explicit_format
225
+ suffix = output_path.suffix.lower()
226
+ if suffix in _FORMAT_EXTENSIONS:
227
+ return _FORMAT_EXTENSIONS[suffix]
228
+ msg = (
229
+ f"Cannot detect format from extension '{suffix}'."
230
+ " Use --format to specify the output format."
231
+ )
232
+ raise ValueError(msg)
233
+
234
+
235
+ def export_dataframe(
236
+ df: pd.DataFrame,
237
+ output_path: Path,
238
+ output_format: str,
239
+ table_name: str = "data",
240
+ ) -> None:
241
+ """Export a pandas DataFrame to the specified file format.
242
+
243
+ Args:
244
+ df: DataFrame to export.
245
+ output_path: Path to the output file.
246
+ output_format: Output format (csv, json, parquet, or sqlite3).
247
+ table_name: Table name for SQLite3 output.
248
+
249
+ Raises:
250
+ ValueError: If the output format is not supported.
251
+ """
252
+ if output_format == "csv":
253
+ df.to_csv(output_path, index=False)
254
+ elif output_format == "json":
255
+ df.to_json(
256
+ output_path,
257
+ orient="records",
258
+ date_format="iso",
259
+ indent=2,
260
+ )
261
+ elif output_format == "parquet":
262
+ df.to_parquet(output_path, index=False)
263
+ elif output_format == "sqlite3":
264
+ with sqlite3.connect(output_path) as conn:
265
+ df.to_sql( # type: ignore[reportUnknownMemberType]
266
+ table_name,
267
+ conn,
268
+ if_exists="replace",
269
+ index=False,
270
+ )
271
+ else:
272
+ msg = f"Unsupported output format: {output_format}"
273
+ raise ValueError(msg)
274
+
275
+
276
+ def parse_datetime(value: str) -> datetime:
277
+ """Parse an ISO 8601 datetime string to a timezone-aware datetime.
278
+
279
+ Args:
280
+ value: ISO 8601 datetime string (e.g., '2024-01-01' or
281
+ '2024-01-01T12:00:00+00:00').
282
+
283
+ Returns:
284
+ Parsed datetime with UTC timezone if no timezone is specified.
285
+
286
+ Raises:
287
+ ValueError: If the string cannot be parsed.
288
+ """
289
+ try:
290
+ dt = datetime.fromisoformat(value)
291
+ except ValueError:
292
+ msg = f"Invalid datetime format: '{value}'. Use ISO 8601 format."
293
+ raise ValueError(msg) from None
294
+ if dt.tzinfo is None:
295
+ dt = dt.replace(tzinfo=UTC)
296
+ return dt
297
+
298
+
299
+ def parse_timeframe(value: str) -> int:
300
+ """Parse a timeframe string or integer value.
301
+
302
+ Args:
303
+ value: Timeframe name (e.g., 'M1', 'H1', 'D1') or integer value.
304
+
305
+ Returns:
306
+ Integer timeframe value.
307
+
308
+ Raises:
309
+ ValueError: If the timeframe is invalid.
310
+ """
311
+ upper = value.upper()
312
+ if upper in TIMEFRAME_MAP:
313
+ return TIMEFRAME_MAP[upper]
314
+ try:
315
+ return int(value)
316
+ except ValueError:
317
+ valid = ", ".join(TIMEFRAME_MAP)
318
+ msg = f"Invalid timeframe: '{value}'. Use one of: {valid}, or an integer."
319
+ raise ValueError(msg) from None
320
+
321
+
322
+ def parse_tick_flags(value: str) -> int:
323
+ """Parse tick flags string or integer value.
324
+
325
+ Args:
326
+ value: Tick flag name (ALL, INFO, TRADE) or integer value.
327
+
328
+ Returns:
329
+ Integer tick flag value.
330
+
331
+ Raises:
332
+ ValueError: If the flag is invalid.
333
+ """
334
+ upper = value.upper()
335
+ if upper in TICK_FLAG_MAP:
336
+ return TICK_FLAG_MAP[upper]
337
+ try:
338
+ return int(value)
339
+ except ValueError:
340
+ valid = ", ".join(TICK_FLAG_MAP)
341
+ msg = f"Invalid tick flags: '{value}'. Use one of: {valid}, or an integer."
342
+ raise ValueError(msg) from None
343
+
344
+
345
+ # ---------------------------------------------------------------------------
346
+ # Typer application
347
+ # ---------------------------------------------------------------------------
348
+
349
+ app = typer.Typer(
350
+ name="mt5cli",
351
+ help="Export MetaTrader5 data to CSV, JSON, Parquet, or SQLite3.",
352
+ )
353
+
354
+
355
+ def _get_export_context(ctx: typer.Context) -> _ExportContext:
356
+ return cast("_ExportContext", ctx.obj)
357
+
358
+
359
+ def _execute_export(
360
+ ctx: typer.Context,
361
+ fetch_fn: Callable[[Mt5DataClient], pd.DataFrame],
362
+ ) -> None:
363
+ """Execute the common connect-fetch-export-shutdown workflow.
364
+
365
+ Args:
366
+ ctx: Typer context carrying shared options.
367
+ fetch_fn: Callable that receives a connected client and returns a
368
+ DataFrame.
369
+ """
370
+ export_ctx = _get_export_context(ctx)
371
+ client = Mt5DataClient(config=export_ctx.config)
372
+ client.initialize_and_login_mt5()
373
+ try:
374
+ df = fetch_fn(client)
375
+ export_dataframe(
376
+ df=df,
377
+ output_path=export_ctx.output,
378
+ output_format=export_ctx.output_format,
379
+ table_name=export_ctx.table,
380
+ )
381
+ logger.info(
382
+ "Exported %d rows to %s (%s)",
383
+ len(df),
384
+ export_ctx.output,
385
+ export_ctx.output_format,
386
+ )
387
+ finally:
388
+ client.shutdown()
389
+
390
+
391
+ @app.callback()
392
+ def _callback( # pyright: ignore[reportUnusedFunction]
393
+ ctx: typer.Context,
394
+ output: Annotated[
395
+ Path,
396
+ typer.Option("--output", "-o", help="Output file path."),
397
+ ],
398
+ fmt: Annotated[
399
+ OutputFormat | None,
400
+ typer.Option(
401
+ "--format",
402
+ "-f",
403
+ help="Output format (auto-detected from extension if omitted).",
404
+ ),
405
+ ] = None,
406
+ table: Annotated[
407
+ str,
408
+ typer.Option(help="Table name for SQLite3 output."),
409
+ ] = "data",
410
+ login: Annotated[
411
+ int | None,
412
+ typer.Option(help="Trading account login."),
413
+ ] = None,
414
+ password: Annotated[
415
+ str | None,
416
+ typer.Option(help="Trading account password."),
417
+ ] = None,
418
+ server: Annotated[
419
+ str | None,
420
+ typer.Option(help="Trading server name."),
421
+ ] = None,
422
+ path: Annotated[
423
+ str | None,
424
+ typer.Option(help="Path to MetaTrader5 terminal EXE file."),
425
+ ] = None,
426
+ timeout: Annotated[
427
+ int | None,
428
+ typer.Option(help="Connection timeout in milliseconds."),
429
+ ] = None,
430
+ log_level: Annotated[
431
+ LogLevel,
432
+ typer.Option("--log-level", help="Logging level."),
433
+ ] = LogLevel.WARNING,
434
+ ) -> None:
435
+ """Configure shared options for all export commands.
436
+
437
+ Raises:
438
+ typer.BadParameter: If the output format cannot be determined.
439
+ """
440
+ logging.basicConfig(level=getattr(logging, log_level.value))
441
+ try:
442
+ output_format = detect_format(
443
+ output,
444
+ explicit_format=fmt.value if fmt is not None else None,
445
+ )
446
+ except ValueError as exc:
447
+ raise typer.BadParameter(str(exc)) from exc
448
+ ctx.obj = _ExportContext(
449
+ output=output,
450
+ output_format=output_format,
451
+ table=table,
452
+ config=Mt5Config(
453
+ path=path,
454
+ login=login,
455
+ password=password,
456
+ server=server,
457
+ timeout=timeout,
458
+ ),
459
+ )
460
+
461
+
462
+ # ---------------------------------------------------------------------------
463
+ # Subcommands
464
+ # ---------------------------------------------------------------------------
465
+
466
+
467
+ @app.command()
468
+ def rates_from(
469
+ ctx: typer.Context,
470
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
471
+ timeframe: Annotated[
472
+ int,
473
+ typer.Option(
474
+ click_type=TIMEFRAME_TYPE,
475
+ help="Timeframe (e.g., M1, H1, D1, or integer).",
476
+ ),
477
+ ],
478
+ date_from: Annotated[
479
+ datetime,
480
+ typer.Option(
481
+ click_type=DATETIME_TYPE,
482
+ help="Start date in ISO 8601 format.",
483
+ ),
484
+ ],
485
+ count: Annotated[int, typer.Option(help="Number of records.")],
486
+ ) -> None:
487
+ """Export rates from a start date."""
488
+ _execute_export(
489
+ ctx,
490
+ lambda c: c.copy_rates_from_as_df(
491
+ symbol=symbol,
492
+ timeframe=timeframe,
493
+ date_from=date_from,
494
+ count=count,
495
+ ),
496
+ )
497
+
498
+
499
+ @app.command()
500
+ def rates_from_pos(
501
+ ctx: typer.Context,
502
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
503
+ timeframe: Annotated[
504
+ int,
505
+ typer.Option(
506
+ click_type=TIMEFRAME_TYPE,
507
+ help="Timeframe.",
508
+ ),
509
+ ],
510
+ start_pos: Annotated[int, typer.Option(help="Start position (0 = current bar).")],
511
+ count: Annotated[int, typer.Option(help="Number of records.")],
512
+ ) -> None:
513
+ """Export rates from a start position."""
514
+ _execute_export(
515
+ ctx,
516
+ lambda c: c.copy_rates_from_pos_as_df(
517
+ symbol=symbol,
518
+ timeframe=timeframe,
519
+ start_pos=start_pos,
520
+ count=count,
521
+ ),
522
+ )
523
+
524
+
525
+ @app.command()
526
+ def rates_range(
527
+ ctx: typer.Context,
528
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
529
+ timeframe: Annotated[
530
+ int,
531
+ typer.Option(
532
+ click_type=TIMEFRAME_TYPE,
533
+ help="Timeframe.",
534
+ ),
535
+ ],
536
+ date_from: Annotated[
537
+ datetime,
538
+ typer.Option(click_type=DATETIME_TYPE, help="Start date."),
539
+ ],
540
+ date_to: Annotated[
541
+ datetime,
542
+ typer.Option(click_type=DATETIME_TYPE, help="End date."),
543
+ ],
544
+ ) -> None:
545
+ """Export rates for a date range."""
546
+ _execute_export(
547
+ ctx,
548
+ lambda c: c.copy_rates_range_as_df(
549
+ symbol=symbol,
550
+ timeframe=timeframe,
551
+ date_from=date_from,
552
+ date_to=date_to,
553
+ ),
554
+ )
555
+
556
+
557
+ @app.command()
558
+ def ticks_from(
559
+ ctx: typer.Context,
560
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
561
+ date_from: Annotated[
562
+ datetime,
563
+ typer.Option(click_type=DATETIME_TYPE, help="Start date."),
564
+ ],
565
+ count: Annotated[int, typer.Option(help="Number of ticks.")],
566
+ flags: Annotated[
567
+ int,
568
+ typer.Option(
569
+ click_type=TICK_FLAGS_TYPE,
570
+ help="Tick flags (ALL, INFO, TRADE, or integer).",
571
+ ),
572
+ ],
573
+ ) -> None:
574
+ """Export ticks from a start date."""
575
+ _execute_export(
576
+ ctx,
577
+ lambda c: c.copy_ticks_from_as_df(
578
+ symbol=symbol,
579
+ date_from=date_from,
580
+ count=count,
581
+ flags=flags,
582
+ ),
583
+ )
584
+
585
+
586
+ @app.command()
587
+ def ticks_range(
588
+ ctx: typer.Context,
589
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
590
+ date_from: Annotated[
591
+ datetime,
592
+ typer.Option(click_type=DATETIME_TYPE, help="Start date."),
593
+ ],
594
+ date_to: Annotated[
595
+ datetime,
596
+ typer.Option(click_type=DATETIME_TYPE, help="End date."),
597
+ ],
598
+ flags: Annotated[
599
+ int,
600
+ typer.Option(click_type=TICK_FLAGS_TYPE, help="Tick flags."),
601
+ ],
602
+ ) -> None:
603
+ """Export ticks for a date range."""
604
+ _execute_export(
605
+ ctx,
606
+ lambda c: c.copy_ticks_range_as_df(
607
+ symbol=symbol,
608
+ date_from=date_from,
609
+ date_to=date_to,
610
+ flags=flags,
611
+ ),
612
+ )
613
+
614
+
615
+ @app.command()
616
+ def account_info(ctx: typer.Context) -> None:
617
+ """Export account information."""
618
+ _execute_export(ctx, lambda c: c.account_info_as_df())
619
+
620
+
621
+ @app.command()
622
+ def terminal_info(ctx: typer.Context) -> None:
623
+ """Export terminal information."""
624
+ _execute_export(ctx, lambda c: c.terminal_info_as_df())
625
+
626
+
627
+ @app.command()
628
+ def symbols(
629
+ ctx: typer.Context,
630
+ group: Annotated[
631
+ str | None,
632
+ typer.Option(help="Symbol group filter (e.g., *USD*)."),
633
+ ] = None,
634
+ ) -> None:
635
+ """Export symbol list."""
636
+ _execute_export(
637
+ ctx,
638
+ lambda c: c.symbols_get_as_df(group=group),
639
+ )
640
+
641
+
642
+ @app.command()
643
+ def symbol_info(
644
+ ctx: typer.Context,
645
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
646
+ ) -> None:
647
+ """Export symbol details."""
648
+ _execute_export(
649
+ ctx,
650
+ lambda c: c.symbol_info_as_df(symbol=symbol),
651
+ )
652
+
653
+
654
+ @app.command()
655
+ def orders(
656
+ ctx: typer.Context,
657
+ symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
658
+ group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
659
+ ticket: Annotated[int | None, typer.Option(help="Ticket filter.")] = None,
660
+ ) -> None:
661
+ """Export active orders."""
662
+ _execute_export(
663
+ ctx,
664
+ lambda c: c.orders_get_as_df(
665
+ symbol=symbol,
666
+ group=group,
667
+ ticket=ticket,
668
+ ),
669
+ )
670
+
671
+
672
+ @app.command()
673
+ def positions(
674
+ ctx: typer.Context,
675
+ symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
676
+ group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
677
+ ticket: Annotated[int | None, typer.Option(help="Ticket filter.")] = None,
678
+ ) -> None:
679
+ """Export open positions."""
680
+ _execute_export(
681
+ ctx,
682
+ lambda c: c.positions_get_as_df(
683
+ symbol=symbol,
684
+ group=group,
685
+ ticket=ticket,
686
+ ),
687
+ )
688
+
689
+
690
+ @app.command()
691
+ def history_orders(
692
+ ctx: typer.Context,
693
+ date_from: Annotated[
694
+ datetime | None,
695
+ typer.Option(click_type=DATETIME_TYPE, help="Start date."),
696
+ ] = None,
697
+ date_to: Annotated[
698
+ datetime | None,
699
+ typer.Option(click_type=DATETIME_TYPE, help="End date."),
700
+ ] = None,
701
+ group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
702
+ symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
703
+ ticket: Annotated[int | None, typer.Option(help="Order ticket.")] = None,
704
+ position: Annotated[int | None, typer.Option(help="Position ticket.")] = None,
705
+ ) -> None:
706
+ """Export historical orders."""
707
+ _execute_export(
708
+ ctx,
709
+ lambda c: c.history_orders_get_as_df(
710
+ date_from=date_from,
711
+ date_to=date_to,
712
+ group=group,
713
+ symbol=symbol,
714
+ ticket=ticket,
715
+ position=position,
716
+ ),
717
+ )
718
+
719
+
720
+ @app.command()
721
+ def history_deals(
722
+ ctx: typer.Context,
723
+ date_from: Annotated[
724
+ datetime | None,
725
+ typer.Option(click_type=DATETIME_TYPE, help="Start date."),
726
+ ] = None,
727
+ date_to: Annotated[
728
+ datetime | None,
729
+ typer.Option(click_type=DATETIME_TYPE, help="End date."),
730
+ ] = None,
731
+ group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
732
+ symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
733
+ ticket: Annotated[int | None, typer.Option(help="Order ticket.")] = None,
734
+ position: Annotated[int | None, typer.Option(help="Position ticket.")] = None,
735
+ ) -> None:
736
+ """Export historical deals."""
737
+ _execute_export(
738
+ ctx,
739
+ lambda c: c.history_deals_get_as_df(
740
+ date_from=date_from,
741
+ date_to=date_to,
742
+ group=group,
743
+ symbol=symbol,
744
+ ticket=ticket,
745
+ position=position,
746
+ ),
747
+ )
748
+
749
+
750
+ def main() -> None:
751
+ """Run the mt5cli CLI."""
752
+ app()
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: mt5cli
3
+ Version: 0.1.0
4
+ Summary: Command-line tool for MetaTrader 5
5
+ Project-URL: Repository, https://github.com/dceoy/mt5cli.git
6
+ Author-email: dceoy <dceoy@users.noreply.github.com>
7
+ Maintainer-email: dceoy <dceoy@users.noreply.github.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Financial and Insurance Industry
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Office/Business :: Financial :: Investment
18
+ Requires-Python: <3.14,>=3.11
19
+ Requires-Dist: click>=8.1.0
20
+ Requires-Dist: pdmt5>=0.2.3
21
+ Requires-Dist: pyarrow>=19.0.0
22
+ Requires-Dist: typer>=0.15.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # mt5cli
26
+
27
+ [![CI/CD](https://github.com/dceoy/mt5cli/actions/workflows/ci.yml/badge.svg)](https://github.com/dceoy/mt5cli/actions/workflows/ci.yml)
28
+
29
+ Command-line tool for exporting MetaTrader 5 data to CSV, JSON, Parquet, and SQLite3.
30
+
31
+ Built on top of [pdmt5](https://github.com/dceoy/pdmt5), a pandas-based data handler for MetaTrader 5.
32
+
33
+ ## Features
34
+
35
+ - **Multi-format export**: CSV, JSON, Parquet, and SQLite3 output formats
36
+ - **Auto-detection**: Format detection from file extensions
37
+ - **Comprehensive data access**: Rates, ticks, account info, symbols, orders, positions, and trading history
38
+ - **Flexible timeframes**: Named timeframes (M1, H1, D1, etc.) and numeric values
39
+ - **Connection management**: Optional credentials, server, and timeout configuration
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install -U mt5cli MetaTrader5
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ```bash
50
+ # Export account information to CSV
51
+ mt5cli -o account.csv account-info
52
+
53
+ # Export EURUSD M1 rates to Parquet
54
+ mt5cli -o rates.parquet rates-from --symbol EURUSD --timeframe M1 \
55
+ --date-from 2024-01-01 --count 1000
56
+
57
+ # Export ticks to JSON
58
+ mt5cli -o ticks.json ticks-from --symbol EURUSD \
59
+ --date-from 2024-01-01 --count 500 --flags ALL
60
+
61
+ # Export symbols to SQLite3 with custom table name
62
+ mt5cli -o data.db --table symbols symbols --group "*USD*"
63
+
64
+ # Export with connection credentials
65
+ mt5cli --login 12345 --password mypass --server MyBroker-Demo \
66
+ -o positions.csv positions
67
+ ```
68
+
69
+ Run as a Python module:
70
+
71
+ ```bash
72
+ python -m mt5cli -o account.csv account-info
73
+ ```
74
+
75
+ ## Commands
76
+
77
+ | Command | Description |
78
+ | ---------------- | ---------------------------------- |
79
+ | `rates-from` | Export rates from a start date |
80
+ | `rates-from-pos` | Export rates from a start position |
81
+ | `rates-range` | Export rates for a date range |
82
+ | `ticks-from` | Export ticks from a start date |
83
+ | `ticks-range` | Export ticks for a date range |
84
+ | `account-info` | Export account information |
85
+ | `terminal-info` | Export terminal information |
86
+ | `symbols` | Export symbol list |
87
+ | `symbol-info` | Export symbol details |
88
+ | `orders` | Export active orders |
89
+ | `positions` | Export open positions |
90
+ | `history-orders` | Export historical orders |
91
+ | `history-deals` | Export historical deals |
92
+
93
+ ## Requirements
94
+
95
+ - Python 3.11+
96
+ - Windows OS (MetaTrader 5 requirement)
97
+ - MetaTrader 5 platform installed
98
+
99
+ ## Development
100
+
101
+ ```bash
102
+ git clone https://github.com/dceoy/mt5cli.git
103
+ cd mt5cli
104
+ uv sync
105
+ ```
106
+
107
+ ## License
108
+
109
+ [MIT](LICENSE)
@@ -0,0 +1,8 @@
1
+ mt5cli/__init__.py,sha256=OPh4k17J0bseeZEcr81tMBq1gCU57D25sfHmS86E7SE,261
2
+ mt5cli/__main__.py,sha256=9bRiqj9W8GmXw2imj5Xa7GxnvNBfIV4O9h-q5WTD2Po,112
3
+ mt5cli/cli.py,sha256=7d9is4ZfM9YSk30vfEyRPJjFwxXp3NHk5vMBnl4Sv68,19758
4
+ mt5cli-0.1.0.dist-info/METADATA,sha256=yKmbsMBB_1LsAr1fQ3toFuw0ahMES-ozqJ8yOk1AQ3g,3524
5
+ mt5cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
6
+ mt5cli-0.1.0.dist-info/entry_points.txt,sha256=JaoS82nFTu2PMePNXofpIv4hUwsUbUI7PFH1AHtt06c,43
7
+ mt5cli-0.1.0.dist-info/licenses/LICENSE,sha256=UWFWn4nTWvSPYJEOmGN3LaX3lhK4rJ7fYoX92ruXg3A,1073
8
+ mt5cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mt5cli = mt5cli.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daichi Narushima
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.