mt5cli 0.7.0__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.
- {mt5cli-0.7.0 → mt5cli-0.7.1}/PKG-INFO +1 -1
- {mt5cli-0.7.0 → mt5cli-0.7.1}/mt5cli/cli.py +53 -52
- {mt5cli-0.7.0 → mt5cli-0.7.1}/mt5cli/history.py +93 -50
- {mt5cli-0.7.0 → mt5cli-0.7.1}/mt5cli/sdk.py +3 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/pyproject.toml +1 -1
- mt5cli-0.7.1/tests/conftest.py +52 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/tests/test_cli.py +0 -32
- {mt5cli-0.7.0 → mt5cli-0.7.1}/tests/test_sdk.py +0 -26
- {mt5cli-0.7.0 → mt5cli-0.7.1}/uv.lock +1 -1
- {mt5cli-0.7.0 → mt5cli-0.7.1}/.agents/skills/local-qa/SKILL.md +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/.agents/skills/local-qa/scripts/qa.sh +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/.agents/skills/mt5cli/SKILL.md +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/.claude/agents/codex.md +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/.claude/settings.json +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/.github/FUNDING.yml +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/.github/dependabot.yml +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/.github/renovate.json +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/.github/workflows/ci.yml +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/.github/workflows/claude.yml +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/.github/workflows/release.yml +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/.gitignore +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/AGENTS.md +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/CLAUDE.md +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/LICENSE +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/README.md +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/docs/api/cli.md +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/docs/api/history.md +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/docs/api/index.md +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/docs/api/sdk.md +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/docs/api/trading.md +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/docs/api/utils.md +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/docs/index.md +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/mkdocs.yml +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/mt5cli/__init__.py +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/mt5cli/__main__.py +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/mt5cli/trading.py +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/mt5cli/utils.py +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/tests/__init__.py +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/tests/test_history.py +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/tests/test_trading.py +0 -0
- {mt5cli-0.7.0 → mt5cli-0.7.1}/tests/test_utils.py +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
219
|
-
_execute_export(
|
|
226
|
+
_export_command(
|
|
220
227
|
ctx,
|
|
221
|
-
lambda: client.copy_rates_from_pos(
|
|
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
|
-
|
|
244
|
-
_execute_export(
|
|
255
|
+
_export_command(
|
|
245
256
|
ctx,
|
|
246
|
-
lambda: client.latest_rates(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -350,10 +363,9 @@ def ticks_recent(
|
|
|
350
363
|
] = "ALL", # pyright: ignore[reportArgumentType]
|
|
351
364
|
) -> None:
|
|
352
365
|
"""Export ticks from a recent time window."""
|
|
353
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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()
|
|
@@ -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
|
-
|
|
1351
|
-
|
|
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
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
-
|
|
1388
|
-
|
|
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
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
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
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
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
|
-
|
|
1529
|
-
_record_dedup_scope(
|
|
1574
|
+
_record_symbol_time_dedup(
|
|
1530
1575
|
dedup_scopes,
|
|
1576
|
+
written_tables,
|
|
1531
1577
|
Dataset.ticks,
|
|
1532
|
-
|
|
1533
|
-
|
|
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
|
-
|
|
1568
|
-
_record_dedup_scope(
|
|
1612
|
+
_record_symbol_time_dedup(
|
|
1569
1613
|
dedup_scopes,
|
|
1614
|
+
written_tables,
|
|
1570
1615
|
Dataset.history_orders,
|
|
1571
|
-
|
|
1572
|
-
|
|
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
|
-
|
|
1666
|
-
_record_dedup_scope(
|
|
1709
|
+
_record_symbol_time_dedup(
|
|
1667
1710
|
dedup_scopes,
|
|
1711
|
+
written_tables,
|
|
1668
1712
|
Dataset.history_deals,
|
|
1669
|
-
|
|
1670
|
-
|
|
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mt5cli"
|
|
3
|
-
version = "0.7.
|
|
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"}]
|
|
@@ -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
|
|
|
@@ -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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|