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 +12 -0
- mt5cli/__main__.py +5 -0
- mt5cli/cli.py +752 -0
- mt5cli-0.1.0.dist-info/METADATA +109 -0
- mt5cli-0.1.0.dist-info/RECORD +8 -0
- mt5cli-0.1.0.dist-info/WHEEL +4 -0
- mt5cli-0.1.0.dist-info/entry_points.txt +2 -0
- mt5cli-0.1.0.dist-info/licenses/LICENSE +21 -0
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
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
|
+
[](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,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.
|