379
-380
+ | @app.command()
-def account_info(ctx: typer.Context) -> None:
- """Export account information."""
- _export_command(ctx, lambda client: client.account_info())
+382
+383
| @app.command()
+def account_info(ctx: typer.Context) -> None:
+ """Export account information."""
+ _export_command(ctx, lambda client: client.account_info())
+
|
+
+
+
+
+
+
+
+
+
+ close_positions
+
+
+
+ close_positions(
+ ctx: Context,
+ symbol: Annotated[
+ list[str] | None,
+ Option(
+ "--symbol",
+ "-s",
+ help="Symbol to close (repeat for multiple symbols).",
+ ),
+ ] = None,
+ ticket: Annotated[
+ list[int] | None,
+ Option(
+ "--ticket",
+ "-t",
+ help="Position ticket to close (repeat for multiple tickets).",
+ ),
+ ] = None,
+ dry_run: Annotated[
+ bool,
+ Option(
+ "--dry-run",
+ help="Preview close orders without executing them.",
+ ),
+ ] = False,
+ yes: Annotated[
+ bool,
+ Option(
+ "--yes", help="Confirm live position closing."
+ ),
+ ] = False,
+) -> None
+
+
+
+
+ Close open positions by symbol or ticket.
+ Delegates to :func:mt5cli.trading.close_open_positions. At least one
+--symbol or --ticket must be provided to avoid accidentally closing
+all positions. Use --dry-run to preview without executing; --yes is
+required for live execution.
+ order-send is the expert raw-request path. close-positions is the
+safer high-level helper that builds correct close requests automatically.
+
+
+ Raises:
+
+
+
+ | Type |
+ Description |
+
+
+
+
+
+ BadParameter
+ |
+
+
+ If neither --symbol nor --ticket is given,
+or if --yes is missing for a live (non-dry-run) run.
+
+ |
+
+
+
+
+
+
+ Source code in mt5cli/cli.py
+ | @app.command()
+def close_positions(
+ ctx: typer.Context,
+ symbol: Annotated[
+ list[str] | None,
+ typer.Option(
+ "--symbol",
+ "-s",
+ help="Symbol to close (repeat for multiple symbols).",
+ ),
+ ] = None,
+ ticket: Annotated[
+ list[int] | None,
+ typer.Option(
+ "--ticket",
+ "-t",
+ help="Position ticket to close (repeat for multiple tickets).",
+ ),
+ ] = None,
+ dry_run: Annotated[
+ bool,
+ typer.Option("--dry-run", help="Preview close orders without executing them."),
+ ] = False,
+ yes: Annotated[
+ bool,
+ typer.Option("--yes", help="Confirm live position closing."),
+ ] = False,
+) -> None:
+ """Close open positions by symbol or ticket.
+
+ Delegates to :func:`mt5cli.trading.close_open_positions`. At least one
+ ``--symbol`` or ``--ticket`` must be provided to avoid accidentally closing
+ all positions. Use ``--dry-run`` to preview without executing; ``--yes`` is
+ required for live execution.
+
+ ``order-send`` is the expert raw-request path. ``close-positions`` is the
+ safer high-level helper that builds correct close requests automatically.
+
+ Raises:
+ typer.BadParameter: If neither ``--symbol`` nor ``--ticket`` is given,
+ or if ``--yes`` is missing for a live (non-dry-run) run.
+ """
+ if not symbol and not ticket:
+ msg = "Provide at least one --symbol or --ticket to close positions."
+ raise typer.BadParameter(msg)
+ if not dry_run and not yes:
+ msg = "Pass --yes to close live positions."
+ raise typer.BadParameter(msg, param_hint="--yes")
+ export_ctx = _get_export_context(ctx)
+ client = create_trading_client(config=export_ctx.config)
+ try:
+ results = close_open_positions(
+ client,
+ symbols=list(symbol) if symbol else None,
+ tickets=list(ticket) if ticket else None,
+ dry_run=dry_run,
+ )
+ finally:
+ client.shutdown()
+ df = _execution_results_to_df(results)
+ _execute_export(ctx, lambda: df)
|
@@ -388,85 +594,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 603
-604
-605
-606
-607
-608
-609
-610
-611
-612
-613
-614
-615
-616
-617
-618
-619
-620
-621
-622
-623
-624
-625
-626
-627
-628
-629
-630
-631
-632
-633
-634
-635
-636
-637
-638
-639
-640
-641
-642
-643
-644
-645
-646
-647
-648
-649
-650
-651
-652
-653
-654
-655
-656
-657
-658
-659
-660
-661
-662
-663
-664
-665
-666
-667
-668
-669
-670
-671
-672
-673
-674
-675
-676
-677
-678
-679
-680
-681
+ | @app.command()
-def collect_history(
- ctx: typer.Context,
- symbol: Annotated[
- list[str],
- typer.Option(
- "--symbol",
- "-s",
- help="Symbol to collect (repeat for multiple symbols).",
- ),
- ],
- date_from: Annotated[
- datetime,
- typer.Option(click_type=DATETIME_TYPE, help="Start date."),
- ],
- date_to: Annotated[
- datetime,
- typer.Option(click_type=DATETIME_TYPE, help="End date."),
- ],
- dataset: Annotated[
- list[Dataset] | None,
- typer.Option(
- "--dataset",
- help=(
- "Dataset to include (repeat for multiple)."
- " Defaults to all: rates, ticks, history-orders, history-deals."
- ),
- ),
- ] = None,
- timeframe: Annotated[
- int,
- typer.Option(
- click_type=TIMEFRAME_TYPE,
- help="Rates timeframe (e.g., M1, H1, D1).",
- ),
- ] = 1,
- flags: Annotated[
- int,
- typer.Option(
- click_type=TICK_FLAGS_TYPE,
- help="Tick copy flags (ALL, INFO, TRADE, or integer).",
- ),
- ] = "ALL", # pyright: ignore[reportArgumentType]
- if_exists: Annotated[
- IfExists,
- typer.Option(
- "--if-exists",
- help="Behavior when a target table already exists.",
- ),
- ] = IfExists.FAIL,
- with_views: Annotated[
- bool,
- typer.Option(
- "--with-views",
- help=(
- "Add cash_events and positions_reconstructed SQLite views"
- " derived from history_deals."
- ),
- ),
- ] = False,
-) -> None:
- """Collect historical datasets into a single SQLite database.
-
- Tables written depend on ``--dataset``: ``rates``, ``ticks``,
- ``history_orders``, ``history_deals``. History datasets are fetched per
- symbol and concatenated. Rates rows carry the requested ``timeframe`` so
- appended runs at different timeframes remain distinguishable.
-
- With ``--with-views`` (requires the ``history-deals`` dataset), optional
- views ``cash_events`` and ``positions_reconstructed`` are derived from
- ``history_deals`` when the required columns are present.
-
- Raises:
- typer.BadParameter: If the output format is not SQLite3.
- """
- export_ctx = _get_export_context(ctx)
- if export_ctx.output_format != "sqlite3":
- msg = (
- "collect-history requires SQLite3 output."
- " Use a .db/.sqlite/.sqlite3 extension or --format sqlite3."
- )
- raise typer.BadParameter(msg)
- datasets = set(dataset) if dataset else set(Dataset)
- sdk.collect_history(
- output=export_ctx.output,
- symbols=symbol,
- date_from=date_from,
- date_to=date_to,
- datasets=datasets,
- timeframe=timeframe,
- flags=flags,
- if_exists=if_exists,
- with_views=with_views,
- config=export_ctx.config,
- )
+697
+698
+699
+700
+701
+702
+703
+704
+705
+706
+707
+708
+709
+710
+711
+712
+713
+714
+715
+716
+717
+718
+719
+720
+721
+722
+723
+724
+725
+726
+727
+728
+729
+730
+731
+732
+733
+734
+735
+736
+737
+738
+739
+740
+741
+742
+743
+744
+745
+746
+747
+748
+749
+750
+751
+752
+753
+754
+755
+756
+757
+758
+759
+760
+761
+762
+763
+764
+765
+766
+767
+768
+769
+770
+771
+772
+773
+774
+775
| @app.command()
+def collect_history(
+ ctx: typer.Context,
+ symbol: Annotated[
+ list[str],
+ typer.Option(
+ "--symbol",
+ "-s",
+ help="Symbol to collect (repeat for multiple symbols).",
+ ),
+ ],
+ date_from: Annotated[
+ datetime,
+ typer.Option(click_type=DATETIME_TYPE, help="Start date."),
+ ],
+ date_to: Annotated[
+ datetime,
+ typer.Option(click_type=DATETIME_TYPE, help="End date."),
+ ],
+ dataset: Annotated[
+ list[Dataset] | None,
+ typer.Option(
+ "--dataset",
+ help=(
+ "Dataset to include (repeat for multiple)."
+ " Defaults to all: rates, ticks, history-orders, history-deals."
+ ),
+ ),
+ ] = None,
+ timeframe: Annotated[
+ int,
+ typer.Option(
+ click_type=TIMEFRAME_TYPE,
+ help="Rates timeframe (e.g., M1, H1, D1).",
+ ),
+ ] = 1,
+ flags: Annotated[
+ int,
+ typer.Option(
+ click_type=TICK_FLAGS_TYPE,
+ help="Tick copy flags (ALL, INFO, TRADE, or integer).",
+ ),
+ ] = "ALL", # pyright: ignore[reportArgumentType]
+ if_exists: Annotated[
+ IfExists,
+ typer.Option(
+ "--if-exists",
+ help="Behavior when a target table already exists.",
+ ),
+ ] = IfExists.FAIL,
+ with_views: Annotated[
+ bool,
+ typer.Option(
+ "--with-views",
+ help=(
+ "Add cash_events and positions_reconstructed SQLite views"
+ " derived from history_deals."
+ ),
+ ),
+ ] = False,
+) -> None:
+ """Collect historical datasets into a single SQLite database.
+
+ Tables written depend on ``--dataset``: ``rates``, ``ticks``,
+ ``history_orders``, ``history_deals``. History datasets are fetched per
+ symbol and concatenated. Rates rows carry the requested ``timeframe`` so
+ appended runs at different timeframes remain distinguishable.
+
+ With ``--with-views`` (requires the ``history-deals`` dataset), optional
+ views ``cash_events`` and ``positions_reconstructed`` are derived from
+ ``history_deals`` when the required columns are present.
+
+ Raises:
+ typer.BadParameter: If the output format is not SQLite3.
+ """
+ export_ctx = _get_export_context(ctx)
+ if export_ctx.output_format != "sqlite3":
+ msg = (
+ "collect-history requires SQLite3 output."
+ " Use a .db/.sqlite/.sqlite3 extension or --format sqlite3."
+ )
+ raise typer.BadParameter(msg)
+ datasets = set(dataset) if dataset else set(Dataset)
+ sdk.collect_history(
+ output=export_ctx.output,
+ symbols=symbol,
+ date_from=date_from,
+ date_to=date_to,
+ datasets=datasets,
+ timeframe=timeframe,
+ flags=flags,
+ if_exists=if_exists,
+ with_views=with_views,
+ config=export_ctx.config,
+ )
|
@@ -635,8 +841,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 479
-480
+ | @app.command()
-def history_deals(
- ctx: typer.Context,
- date_from: Annotated[
- datetime | None,
- typer.Option(click_type=DATETIME_TYPE, help="Start date."),
- ] = None,
- date_to: Annotated[
- datetime | None,
- typer.Option(click_type=DATETIME_TYPE, help="End date."),
- ] = None,
- group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
- symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
- ticket: Annotated[int | None, typer.Option(help="Order ticket.")] = None,
- position: Annotated[int | None, typer.Option(help="Position ticket.")] = None,
-) -> None:
- """Export historical deals."""
- _export_command(
- ctx,
- lambda client: client.history_deals(
- date_from=date_from,
- date_to=date_to,
- group=group,
- symbol=symbol,
- ticket=ticket,
- position=position,
- ),
- )
+506
+507
| @app.command()
+def history_deals(
+ ctx: typer.Context,
+ date_from: Annotated[
+ datetime | None,
+ typer.Option(click_type=DATETIME_TYPE, help="Start date."),
+ ] = None,
+ date_to: Annotated[
+ datetime | None,
+ typer.Option(click_type=DATETIME_TYPE, help="End date."),
+ ] = None,
+ group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
+ symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
+ ticket: Annotated[int | None, typer.Option(help="Order ticket.")] = None,
+ position: Annotated[int | None, typer.Option(help="Position ticket.")] = None,
+) -> None:
+ """Export historical deals."""
+ _export_command(
+ ctx,
+ lambda client: client.history_deals(
+ date_from=date_from,
+ date_to=date_to,
+ group=group,
+ symbol=symbol,
+ ticket=ticket,
+ position=position,
+ ),
+ )
|
@@ -748,8 +954,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 449
-450
+ | @app.command()
-def history_orders(
- ctx: typer.Context,
- date_from: Annotated[
- datetime | None,
- typer.Option(click_type=DATETIME_TYPE, help="Start date."),
- ] = None,
- date_to: Annotated[
- datetime | None,
- typer.Option(click_type=DATETIME_TYPE, help="End date."),
- ] = None,
- group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
- symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
- ticket: Annotated[int | None, typer.Option(help="Order ticket.")] = None,
- position: Annotated[int | None, typer.Option(help="Position ticket.")] = None,
-) -> None:
- """Export historical orders."""
- _export_command(
- ctx,
- lambda client: client.history_orders(
- date_from=date_from,
- date_to=date_to,
- group=group,
- symbol=symbol,
- ticket=ticket,
- position=position,
- ),
- )
+476
+477
| @app.command()
+def history_orders(
+ ctx: typer.Context,
+ date_from: Annotated[
+ datetime | None,
+ typer.Option(click_type=DATETIME_TYPE, help="Start date."),
+ ] = None,
+ date_to: Annotated[
+ datetime | None,
+ typer.Option(click_type=DATETIME_TYPE, help="End date."),
+ ] = None,
+ group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
+ symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
+ ticket: Annotated[int | None, typer.Option(help="Order ticket.")] = None,
+ position: Annotated[int | None, typer.Option(help="Position ticket.")] = None,
+) -> None:
+ """Export historical orders."""
+ _export_command(
+ ctx,
+ lambda client: client.history_orders(
+ date_from=date_from,
+ date_to=date_to,
+ group=group,
+ symbol=symbol,
+ ticket=ticket,
+ position=position,
+ ),
+ )
|
@@ -827,13 +1033,13 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 544
-545
+ | @app.command()
-def last_error(ctx: typer.Context) -> None:
- """Export the last error information."""
- _export_command(ctx, lambda client: client.last_error())
+547
+548
| @app.command()
+def last_error(ctx: typer.Context) -> None:
+ """Export the last error information."""
+ _export_command(ctx, lambda client: client.last_error())
|
@@ -879,8 +1085,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 238
-239
+ | @app.command()
-def latest_rates(
- ctx: typer.Context,
- symbol: Annotated[str, typer.Option(help="Symbol name.")],
- timeframe: Annotated[
- int,
- typer.Option(
- click_type=TIMEFRAME_TYPE,
- help="Timeframe.",
- ),
- ],
- count: Annotated[int, typer.Option(help="Number of records.")],
- start_pos: Annotated[
- int,
- typer.Option(help="Start position (0 = current bar)."),
- ] = 0,
-) -> None:
- """Export latest rates from a start position."""
- _export_command(
- ctx,
- lambda client: client.latest_rates(
- symbol,
- timeframe,
- count,
- start_pos=start_pos,
- ),
- )
+264
+265
| @app.command()
+def latest_rates(
+ ctx: typer.Context,
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
+ timeframe: Annotated[
+ int,
+ typer.Option(
+ click_type=TIMEFRAME_TYPE,
+ help="Timeframe.",
+ ),
+ ],
+ count: Annotated[int, typer.Option(help="Number of records.")],
+ start_pos: Annotated[
+ int,
+ typer.Option(help="Start position (0 = current bar)."),
+ ] = 0,
+) -> None:
+ """Export latest rates from a start position."""
+ _export_command(
+ ctx,
+ lambda client: client.latest_rates(
+ symbol,
+ timeframe,
+ count,
+ start_pos=start_pos,
+ ),
+ )
|
@@ -956,11 +1162,11 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- | def main() -> None:
- """Run the mt5cli CLI."""
- app()
+ | def main() -> None:
+ """Run the mt5cli CLI."""
+ app()
|
@@ -988,19 +1194,19 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 559
-560
+ | @app.command()
-def market_book(
- ctx: typer.Context,
- symbol: Annotated[str, typer.Option(help="Symbol name.")],
-) -> None:
- """Export market depth (order book) for a symbol."""
- _export_command(ctx, lambda client: client.market_book(symbol))
+565
+566
| @app.command()
+def market_book(
+ ctx: typer.Context,
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
+) -> None:
+ """Export market depth (order book) for a symbol."""
+ _export_command(ctx, lambda client: client.market_book(symbol))
|
@@ -1028,19 +1234,19 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 412
-413
+ | @app.command()
-def minimum_margins(
- ctx: typer.Context,
- symbol: Annotated[str, typer.Option(help="Symbol name.")],
-) -> None:
- """Export minimum-volume buy and sell margin requirements."""
- _export_command(ctx, lambda client: client.minimum_margins(symbol))
+418
+419
| @app.command()
+def minimum_margins(
+ ctx: typer.Context,
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
+) -> None:
+ """Export minimum-volume buy and sell margin requirements."""
+ _export_command(ctx, lambda client: client.minimum_margins(symbol))
|
@@ -1065,13 +1271,13 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 532
-533
+ | @app.command()
-def mt5_summary(ctx: typer.Context) -> None:
- """Export a compact terminal/account status summary."""
- _export_command(ctx, lambda client: client.mt5_summary_as_df())
+535
+536
| @app.command()
+def mt5_summary(ctx: typer.Context) -> None:
+ """Export a compact terminal/account status summary."""
+ _export_command(ctx, lambda client: client.mt5_summary_as_df())
|
@@ -1110,8 +1316,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 568
-569
+ | @app.command()
-def order_check(
- ctx: typer.Context,
- request: Annotated[
- dict[str, Any],
- typer.Option(click_type=REQUEST_TYPE, help=_REQUEST_OPTION_HELP),
- ],
-) -> None:
- """Check funds sufficiency for a trading operation."""
- _export_command(ctx, lambda client: client.order_check(request))
+577
+578
| @app.command()
+def order_check(
+ ctx: typer.Context,
+ request: Annotated[
+ dict[str, Any],
+ typer.Option(click_type=REQUEST_TYPE, help=_REQUEST_OPTION_HELP),
+ ],
+) -> None:
+ """Check funds sufficiency for a trading operation."""
+ _export_command(ctx, lambda client: client.order_check(request))
|
@@ -1196,8 +1402,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 580
-581
+ | @app.command()
-def order_send(
- ctx: typer.Context,
- request: Annotated[
- dict[str, Any],
- typer.Option(click_type=REQUEST_TYPE, help=_REQUEST_OPTION_HELP),
- ],
- yes: Annotated[
- bool,
- typer.Option("--yes", help="Confirm the live trade request."),
- ] = False,
-) -> None:
- """Send a trading operation request to the trade server.
-
- Raises:
- typer.BadParameter: If --yes is not provided.
- """
- if not yes:
- msg = "Pass --yes to send a live trade request."
- raise typer.BadParameter(msg, param_hint="--yes")
- _export_command(ctx, lambda client: client.order_send(request))
+600
+601
| @app.command()
+def order_send(
+ ctx: typer.Context,
+ request: Annotated[
+ dict[str, Any],
+ typer.Option(click_type=REQUEST_TYPE, help=_REQUEST_OPTION_HELP),
+ ],
+ yes: Annotated[
+ bool,
+ typer.Option("--yes", help="Confirm the live trade request."),
+ ] = False,
+) -> None:
+ """Send a trading operation request to the trade server.
+
+ Raises:
+ typer.BadParameter: If --yes is not provided.
+ """
+ if not yes:
+ msg = "Pass --yes to send a live trade request."
+ raise typer.BadParameter(msg, param_hint="--yes")
+ _export_command(ctx, lambda client: client.order_send(request))
|
@@ -1272,8 +1478,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 421
-422
+ | @app.command()
-def orders(
- ctx: typer.Context,
- symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
- group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
- ticket: Annotated[int | None, typer.Option(help="Ticket filter.")] = None,
-) -> None:
- """Export active orders."""
- _export_command(
- ctx,
- lambda client: client.orders(symbol=symbol, group=group, ticket=ticket),
- )
+432
+433
| @app.command()
+def orders(
+ ctx: typer.Context,
+ symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
+ group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
+ ticket: Annotated[int | None, typer.Option(help="Ticket filter.")] = None,
+) -> None:
+ """Export active orders."""
+ _export_command(
+ ctx,
+ lambda client: client.orders(symbol=symbol, group=group, ticket=ticket),
+ )
|
@@ -1330,8 +1536,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 435
-436
+ | @app.command()
-def positions(
- ctx: typer.Context,
- symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
- group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
- ticket: Annotated[int | None, typer.Option(help="Ticket filter.")] = None,
-) -> None:
- """Export open positions."""
- _export_command(
- ctx,
- lambda client: client.positions(symbol=symbol, group=group, ticket=ticket),
- )
+446
+447
| @app.command()
+def positions(
+ ctx: typer.Context,
+ symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
+ group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
+ ticket: Annotated[int | None, typer.Option(help="Ticket filter.")] = None,
+) -> None:
+ """Export open positions."""
+ _export_command(
+ ctx,
+ lambda client: client.positions(symbol=symbol, group=group, ticket=ticket),
+ )
|
@@ -1407,8 +1613,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 185
-186
+ | @app.command()
-def rates_from(
- ctx: typer.Context,
- symbol: Annotated[str, typer.Option(help="Symbol name.")],
- timeframe: Annotated[
- int,
- typer.Option(
- click_type=TIMEFRAME_TYPE,
- help="Timeframe (e.g., M1, H1, D1, or integer).",
- ),
- ],
- date_from: Annotated[
- datetime,
- typer.Option(
- click_type=DATETIME_TYPE,
- help="Start date in ISO 8601 format.",
- ),
- ],
- count: Annotated[int, typer.Option(help="Number of records.")],
-) -> None:
- """Export rates from a start date."""
- _export_command(
- ctx,
- lambda client: client.copy_rates_from(symbol, timeframe, date_from, count),
- )
+209
+210
| @app.command()
+def rates_from(
+ ctx: typer.Context,
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
+ timeframe: Annotated[
+ int,
+ typer.Option(
+ click_type=TIMEFRAME_TYPE,
+ help="Timeframe (e.g., M1, H1, D1, or integer).",
+ ),
+ ],
+ date_from: Annotated[
+ datetime,
+ typer.Option(
+ click_type=DATETIME_TYPE,
+ help="Start date in ISO 8601 format.",
+ ),
+ ],
+ count: Annotated[int, typer.Option(help="Number of records.")],
+) -> None:
+ """Export rates from a start date."""
+ _export_command(
+ ctx,
+ lambda client: client.copy_rates_from(symbol, timeframe, date_from, count),
+ )
|
@@ -1501,8 +1707,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 212
-213
+ | @app.command()
-def rates_from_pos(
- ctx: typer.Context,
- symbol: Annotated[str, typer.Option(help="Symbol name.")],
- timeframe: Annotated[
- int,
- typer.Option(
- click_type=TIMEFRAME_TYPE,
- help="Timeframe.",
- ),
- ],
- start_pos: Annotated[int, typer.Option(help="Start position (0 = current bar).")],
- count: Annotated[int, typer.Option(help="Number of records.")],
-) -> None:
- """Export rates from a start position."""
- _export_command(
- ctx,
- lambda client: client.copy_rates_from_pos(
- symbol,
- timeframe,
- start_pos,
- count,
- ),
- )
+235
+236
| @app.command()
+def rates_from_pos(
+ ctx: typer.Context,
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
+ timeframe: Annotated[
+ int,
+ typer.Option(
+ click_type=TIMEFRAME_TYPE,
+ help="Timeframe.",
+ ),
+ ],
+ start_pos: Annotated[int, typer.Option(help="Start position (0 = current bar).")],
+ count: Annotated[int, typer.Option(help="Number of records.")],
+) -> None:
+ """Export rates from a start position."""
+ _export_command(
+ ctx,
+ lambda client: client.copy_rates_from_pos(
+ symbol,
+ timeframe,
+ start_pos,
+ count,
+ ),
+ )
|
@@ -1606,8 +1812,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 267
-268
+ | @app.command()
-def rates_range(
- ctx: typer.Context,
- symbol: Annotated[str, typer.Option(help="Symbol name.")],
- timeframe: Annotated[
- int,
- typer.Option(
- click_type=TIMEFRAME_TYPE,
- help="Timeframe.",
- ),
- ],
- date_from: Annotated[
- datetime,
- typer.Option(click_type=DATETIME_TYPE, help="Start date."),
- ],
- date_to: Annotated[
- datetime,
- typer.Option(click_type=DATETIME_TYPE, help="End date."),
- ],
-) -> None:
- """Export rates for a date range."""
- _export_command(
- ctx,
- lambda client: client.copy_rates_range(symbol, timeframe, date_from, date_to),
- )
+291
+292
| @app.command()
+def rates_range(
+ ctx: typer.Context,
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
+ timeframe: Annotated[
+ int,
+ typer.Option(
+ click_type=TIMEFRAME_TYPE,
+ help="Timeframe.",
+ ),
+ ],
+ date_from: Annotated[
+ datetime,
+ typer.Option(click_type=DATETIME_TYPE, help="Start date."),
+ ],
+ date_to: Annotated[
+ datetime,
+ typer.Option(click_type=DATETIME_TYPE, help="End date."),
+ ],
+) -> None:
+ """Export rates for a date range."""
+ _export_command(
+ ctx,
+ lambda client: client.copy_rates_range(symbol, timeframe, date_from, date_to),
+ )
|
@@ -1702,8 +1908,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 509
-510
+ | @app.command()
-def recent_history_deals(
- ctx: typer.Context,
- hours: Annotated[float, typer.Option(help="Lookback window in hours.")],
- date_to: Annotated[
- datetime | None,
- typer.Option(click_type=DATETIME_TYPE, help="Window end date."),
- ] = None,
- group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
- symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
-) -> None:
- """Export historical deals from a recent trailing window."""
- _export_command(
- ctx,
- lambda client: client.recent_history_deals(
- hours,
- date_to=date_to,
- group=group,
- symbol=symbol,
- ),
- )
+529
+530
| @app.command()
+def recent_history_deals(
+ ctx: typer.Context,
+ hours: Annotated[float, typer.Option(help="Lookback window in hours.")],
+ date_to: Annotated[
+ datetime | None,
+ typer.Option(click_type=DATETIME_TYPE, help="Window end date."),
+ ] = None,
+ group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
+ symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
+) -> None:
+ """Export historical deals from a recent trailing window."""
+ _export_command(
+ ctx,
+ lambda client: client.recent_history_deals(
+ hours,
+ date_to=date_to,
+ group=group,
+ symbol=symbol,
+ ),
+ )
|
@@ -1770,19 +1976,19 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 403
-404
+ | @app.command()
-def symbol_info(
- ctx: typer.Context,
- symbol: Annotated[str, typer.Option(help="Symbol name.")],
-) -> None:
- """Export symbol details."""
- _export_command(ctx, lambda client: client.symbol_info(symbol))
+409
+410
| @app.command()
+def symbol_info(
+ ctx: typer.Context,
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
+) -> None:
+ """Export symbol details."""
+ _export_command(ctx, lambda client: client.symbol_info(symbol))
|
@@ -1810,19 +2016,19 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 550
-551
+ | @app.command()
-def symbol_info_tick(
- ctx: typer.Context,
- symbol: Annotated[str, typer.Option(help="Symbol name.")],
-) -> None:
- """Export the last tick for a symbol."""
- _export_command(ctx, lambda client: client.symbol_info_tick(symbol))
+556
+557
| @app.command()
+def symbol_info_tick(
+ ctx: typer.Context,
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
+) -> None:
+ """Export the last tick for a symbol."""
+ _export_command(ctx, lambda client: client.symbol_info_tick(symbol))
|
@@ -1853,8 +2059,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 391
-392
+ | @app.command()
-def symbols(
- ctx: typer.Context,
- group: Annotated[
- str | None,
- typer.Option(help="Symbol group filter (e.g., *USD*)."),
- ] = None,
-) -> None:
- """Export symbol list."""
- _export_command(ctx, lambda client: client.symbols(group=group))
+400
+401
| @app.command()
+def symbols(
+ ctx: typer.Context,
+ group: Annotated[
+ str | None,
+ typer.Option(help="Symbol group filter (e.g., *USD*)."),
+ ] = None,
+) -> None:
+ """Export symbol list."""
+ _export_command(ctx, lambda client: client.symbols(group=group))
|
@@ -1896,13 +2102,13 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 385
-386
+ | @app.command()
-def terminal_info(ctx: typer.Context) -> None:
- """Export terminal information."""
- _export_command(ctx, lambda client: client.terminal_info())
+388
+389
| @app.command()
+def terminal_info(ctx: typer.Context) -> None:
+ """Export terminal information."""
+ _export_command(ctx, lambda client: client.terminal_info())
|
@@ -1954,8 +2160,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 294
-295
+ | @app.command()
-def ticks_from(
- ctx: typer.Context,
- symbol: Annotated[str, typer.Option(help="Symbol name.")],
- date_from: Annotated[
- datetime,
- typer.Option(click_type=DATETIME_TYPE, help="Start date."),
- ],
- count: Annotated[int, typer.Option(help="Number of ticks.")],
- flags: Annotated[
- int,
- typer.Option(
- click_type=TICK_FLAGS_TYPE,
- help="Tick flags (ALL, INFO, TRADE, or integer).",
- ),
- ],
-) -> None:
- """Export ticks from a start date."""
- _export_command(
- ctx,
- lambda client: client.copy_ticks_from(symbol, date_from, count, flags),
- )
+315
+316
| @app.command()
+def ticks_from(
+ ctx: typer.Context,
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
+ date_from: Annotated[
+ datetime,
+ typer.Option(click_type=DATETIME_TYPE, help="Start date."),
+ ],
+ count: Annotated[int, typer.Option(help="Number of ticks.")],
+ flags: Annotated[
+ int,
+ typer.Option(
+ click_type=TICK_FLAGS_TYPE,
+ help="Tick flags (ALL, INFO, TRADE, or integer).",
+ ),
+ ],
+) -> None:
+ """Export ticks from a start date."""
+ _export_command(
+ ctx,
+ lambda client: client.copy_ticks_from(symbol, date_from, count, flags),
+ )
|
@@ -2055,8 +2261,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 318
-319
+ | @app.command()
-def ticks_range(
- ctx: typer.Context,
- symbol: Annotated[str, typer.Option(help="Symbol name.")],
- date_from: Annotated[
- datetime,
- typer.Option(click_type=DATETIME_TYPE, help="Start date."),
- ],
- date_to: Annotated[
- datetime,
- typer.Option(click_type=DATETIME_TYPE, help="End date."),
- ],
- flags: Annotated[
- int,
- typer.Option(click_type=TICK_FLAGS_TYPE, help="Tick flags."),
- ],
-) -> None:
- """Export ticks for a date range."""
- _export_command(
- ctx,
- lambda client: client.copy_ticks_range(symbol, date_from, date_to, flags),
- )
+339
+340
| @app.command()
+def ticks_range(
+ ctx: typer.Context,
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
+ date_from: Annotated[
+ datetime,
+ typer.Option(click_type=DATETIME_TYPE, help="Start date."),
+ ],
+ date_to: Annotated[
+ datetime,
+ typer.Option(click_type=DATETIME_TYPE, help="End date."),
+ ],
+ flags: Annotated[
+ int,
+ typer.Option(click_type=TICK_FLAGS_TYPE, help="Tick flags."),
+ ],
+) -> None:
+ """Export ticks for a date range."""
+ _export_command(
+ ctx,
+ lambda client: client.copy_ticks_range(symbol, date_from, date_to, flags),
+ )
|
@@ -2156,8 +2362,7 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 342
-343
+ | @app.command()
-def ticks_recent(
- ctx: typer.Context,
- symbol: Annotated[str, typer.Option(help="Symbol name.")],
- seconds: Annotated[
- float,
- typer.Option(help="Lookback window in seconds."),
- ],
- date_to: Annotated[
- datetime | None,
- typer.Option(click_type=DATETIME_TYPE, help="Window end date."),
- ] = None,
- count: Annotated[
- int,
- typer.Option(help="Maximum number of ticks to return."),
- ] = 10000,
- flags: Annotated[
- int,
- typer.Option(
- click_type=TICK_FLAGS_TYPE,
- help="Tick flags (ALL, INFO, TRADE, or integer).",
- ),
- ] = "ALL", # pyright: ignore[reportArgumentType]
-) -> None:
- """Export ticks from a recent time window."""
- _export_command(
- ctx,
- lambda client: client.recent_ticks(
- symbol,
- seconds,
- date_to=date_to,
- count=count,
- flags=flags,
- ),
- )
+376
+377
| @app.command()
+def ticks_recent(
+ ctx: typer.Context,
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
+ seconds: Annotated[
+ float,
+ typer.Option(help="Lookback window in seconds."),
+ ],
+ date_to: Annotated[
+ datetime | None,
+ typer.Option(click_type=DATETIME_TYPE, help="Window end date."),
+ ] = None,
+ count: Annotated[
+ int,
+ typer.Option(help="Maximum number of ticks to return."),
+ ] = 10000,
+ flags: Annotated[
+ int,
+ typer.Option(
+ click_type=TICK_FLAGS_TYPE,
+ help="Tick flags (ALL, INFO, TRADE, or integer).",
+ ),
+ ] = "ALL", # pyright: ignore[reportArgumentType]
+) -> None:
+ """Export ticks from a recent time window."""
+ _export_command(
+ ctx,
+ lambda client: client.recent_ticks(
+ symbol,
+ seconds,
+ date_to=date_to,
+ count=count,
+ flags=flags,
+ ),
+ )
|
@@ -2249,13 +2455,13 @@ views cash_events and positions_reconstructed are deri
Source code in mt5cli/cli.py
- 538
-539
+ | @app.command()
-def version(ctx: typer.Context) -> None:
- """Export MetaTrader5 version information."""
- _export_command(ctx, lambda client: client.version())
+541
+542
| @app.command()
+def version(ctx: typer.Context) -> None:
+ """Export MetaTrader5 version information."""
+ _export_command(ctx, lambda client: client.version())
|
diff --git a/api/public-contract/index.html b/api/public-contract/index.html
index 9af736d..13a96be 100644
--- a/api/public-contract/index.html
+++ b/api/public-contract/index.html
@@ -425,6 +425,10 @@ strategy entries, exits, Kelly sizing, or signal logic.
| OrderSide, OrderFillingMode, OrderTimeMode, PositionSide, ExecutionStatus |
Typed enums for order helpers |
+
+ProjectionMode |
+Literal type for calculate_symbol_group_margin_ratio projection |
+
MT5Client.order_send() and CLI order-send --yes are live execution paths.
@@ -546,6 +550,12 @@ the MT5 SDK helper.
|
+ calculate_symbol_group_margin_ratio supports an optional projection_mode
+parameter ("add" by default). Pass projection_mode="replace_symbol" to
+subtract current exposure for new_symbol before adding the candidate margin —
+useful for reversal-style projections. mt5cli only calculates broker-facing
+exposure; downstream applications own thresholds, risk guard actions, and
+strategy policy.
CLI commands
The Typer application in mt5cli.cli exposes file-export commands documented in
CLI Module and the project README. CLI commands:
@@ -556,7 +566,12 @@ the MT5 SDK helper.
Delegate to the same Python APIs described here; they are not duplicated
business logic.
- order-send requires --yes before placing live trades.
+ order-send is the expert raw-request path; it requires --yes and a fully
+constructed request payload. close-positions is the safer high-level helper
+that closes open positions by --symbol or --ticket using
+close_open_positions(). Both order-send --yes and close-positions --yes
+are live execution paths. close-positions --dry-run previews close orders
+without placing them and does not require --yes.
Internal helpers (not stable)
Do not import these for downstream contracts; they may change without a semver
notice:
diff --git a/api/trading/index.html b/api/trading/index.html
index 9a09473..e3e6e76 100644
--- a/api/trading/index.html
+++ b/api/trading/index.html
@@ -347,6 +347,28 @@
+
+ ProjectionMode
+
+
+
+ module-attribute
+
+
+
+ ProjectionMode = Literal['add', 'replace_symbol']
+
+
+
+
+
+
+
+
+
+
+
+
__all__
@@ -366,36 +388,37 @@
"OrderSide",
"OrderTimeMode",
"PositionSide",
- "calculate_account_projected_margin_ratio",
- "calculate_margin_and_volume",
- "calculate_new_position_margin_ratio",
- "calculate_positions_margin",
- "calculate_positions_margin_by_symbol",
- "calculate_positions_margin_safe",
- "calculate_projected_margin_ratio",
- "calculate_spread_ratio",
- "calculate_symbol_group_margin_ratio",
- "calculate_trailing_stop_updates",
- "calculate_volume_by_margin",
- "close_open_positions",
- "create_trading_client",
- "detect_position_side",
- "determine_order_limits",
- "ensure_symbol_selected",
- "estimate_order_margin",
- "extract_tick_price",
- "fetch_latest_closed_rates_for_trading_client",
- "fetch_latest_closed_rates_indexed",
- "get_account_snapshot",
- "get_positions_frame",
- "get_symbol_snapshot",
- "get_tick_snapshot",
- "mt5_trading_session",
- "normalize_order_volume",
- "place_market_order",
- "update_sltp_for_open_positions",
- "update_trailing_stop_loss_for_open_positions",
-]
+ "ProjectionMode",
+ "calculate_account_projected_margin_ratio",
+ "calculate_margin_and_volume",
+ "calculate_new_position_margin_ratio",
+ "calculate_positions_margin",
+ "calculate_positions_margin_by_symbol",
+ "calculate_positions_margin_safe",
+ "calculate_projected_margin_ratio",
+ "calculate_spread_ratio",
+ "calculate_symbol_group_margin_ratio",
+ "calculate_trailing_stop_updates",
+ "calculate_volume_by_margin",
+ "close_open_positions",
+ "create_trading_client",
+ "detect_position_side",
+ "determine_order_limits",
+ "ensure_symbol_selected",
+ "estimate_order_margin",
+ "extract_tick_price",
+ "fetch_latest_closed_rates_for_trading_client",
+ "fetch_latest_closed_rates_indexed",
+ "get_account_snapshot",
+ "get_positions_frame",
+ "get_symbol_snapshot",
+ "get_tick_snapshot",
+ "mt5_trading_session",
+ "normalize_order_volume",
+ "place_market_order",
+ "update_sltp_for_open_positions",
+ "update_trailing_stop_loss_for_open_positions",
+]
@@ -1034,9 +1057,7 @@ and positive volume are all supplied.
Source code in mt5cli/trading.py
- 866
-867
-868
+ | def calculate_account_projected_margin_ratio(
- client: Mt5TradingClient,
- *,
- symbol: str | None = None,
- new_position_side: OrderSide | None = None,
- new_position_volume: float = 0.0,
-) -> float:
- """Return account-wide current plus optional new-position margin over equity.
-
- Current exposure comes from the broker account snapshot ``margin`` field so
- unrelated open positions remain in the baseline. Optional projected
- exposure is added via :func:`estimate_order_margin` only when a symbol, side,
- and positive volume are all supplied.
-
- """
- account = get_account_snapshot(client)
- equity = _required_account_number(account, "equity", allow_zero=False)
- margin = _required_account_number(account, "margin", allow_zero=True)
- if symbol is not None and new_position_side is not None and new_position_volume > 0:
- margin += estimate_order_margin(
- client,
- symbol,
- new_position_side,
- new_position_volume,
- )
- return margin / equity
+891
+892
+893
| def calculate_account_projected_margin_ratio(
+ client: Mt5TradingClient,
+ *,
+ symbol: str | None = None,
+ new_position_side: OrderSide | None = None,
+ new_position_volume: float = 0.0,
+) -> float:
+ """Return account-wide current plus optional new-position margin over equity.
+
+ Current exposure comes from the broker account snapshot ``margin`` field so
+ unrelated open positions remain in the baseline. Optional projected
+ exposure is added via :func:`estimate_order_margin` only when a symbol, side,
+ and positive volume are all supplied.
+
+ """
+ account = get_account_snapshot(client)
+ equity = _required_account_number(account, "equity", allow_zero=False)
+ margin = _required_account_number(account, "margin", allow_zero=True)
+ if symbol is not None and new_position_side is not None and new_position_volume > 0:
+ margin += estimate_order_margin(
+ client,
+ symbol,
+ new_position_side,
+ new_position_volume,
+ )
+ return margin / equity
|
@@ -1241,20 +1264,7 @@ side when the post-reserve margin can afford it.
Source code in mt5cli/trading.py
- 977
- 978
- 979
- 980
- 981
- 982
- 983
- 984
- 985
- 986
- 987
- 988
- 989
- 990
+ | def calculate_margin_and_volume(
- client: Mt5TradingClient,
- symbol: str,
- unit_margin_ratio: float,
- preserved_margin_ratio: float,
-) -> MarginVolume:
- """Calculate tradable margin and volumes from account free margin.
-
- Applies ``preserved_margin_ratio`` to keep a reserve off ``margin_free``,
- then allocates ``unit_margin_ratio`` of the remainder as the margin budget
- for proportional volume sizing on both buy and sell sides. A
- ``unit_margin_ratio`` of ``0`` requests exactly one minimum valid unit per
- side when the post-reserve margin can afford it.
-
- Args:
- client: Connected ``Mt5TradingClient`` instance.
- symbol: Symbol used for minimum-lot margin and volume calculations.
- unit_margin_ratio: Fraction of post-reserve margin to allocate per unit.
- preserved_margin_ratio: Fraction of ``margin_free`` to preserve.
-
- Returns:
- Dictionary with ``margin_free``, ``available_margin``, ``trade_margin``,
- ``buy_volume``, and ``sell_volume``. Negative ``margin_free`` values are
- clamped to ``0.0`` before sizing.
- """
- _require_unit_ratio(unit_margin_ratio, "unit_margin_ratio")
- _require_unit_ratio(preserved_margin_ratio, "preserved_margin_ratio")
-
- account = client.account_info_as_dict()
- margin_free = max(0.0, float(account.get("margin_free") or 0.0))
- available_margin = margin_free * (1.0 - preserved_margin_ratio)
- trade_margin = available_margin * unit_margin_ratio
- if unit_margin_ratio == 0:
- buy_volume = _calculate_min_volume_if_affordable(
- client,
- symbol,
- available_margin,
- "BUY",
- )
- sell_volume = _calculate_min_volume_if_affordable(
- client,
- symbol,
- available_margin,
- "SELL",
- )
- else:
- buy_volume = calculate_volume_by_margin(client, symbol, trade_margin, "BUY")
- sell_volume = calculate_volume_by_margin(client, symbol, trade_margin, "SELL")
- try:
- symbol_info = get_symbol_snapshot(client, symbol)
- volume_min = float(symbol_info.get("volume_min") or 0.0)
- volume_max = float(symbol_info.get("volume_max") or 0.0)
- volume_step = float(symbol_info.get("volume_step") or 0.0)
- except AttributeError:
- volume_min = volume_max = volume_step = 0.0
- return {
- "margin_free": margin_free,
- "available_margin": available_margin,
- "trade_margin": trade_margin,
- "buy_volume": float(buy_volume),
- "sell_volume": float(sell_volume),
- "volume_min": volume_min,
- "volume_max": volume_max,
- "volume_step": volume_step,
- }
+1041
+1042
+1043
+1044
+1045
+1046
+1047
+1048
+1049
+1050
+1051
+1052
+1053
+1054
| def calculate_margin_and_volume(
+ client: Mt5TradingClient,
+ symbol: str,
+ unit_margin_ratio: float,
+ preserved_margin_ratio: float,
+) -> MarginVolume:
+ """Calculate tradable margin and volumes from account free margin.
+
+ Applies ``preserved_margin_ratio`` to keep a reserve off ``margin_free``,
+ then allocates ``unit_margin_ratio`` of the remainder as the margin budget
+ for proportional volume sizing on both buy and sell sides. A
+ ``unit_margin_ratio`` of ``0`` requests exactly one minimum valid unit per
+ side when the post-reserve margin can afford it.
+
+ Args:
+ client: Connected ``Mt5TradingClient`` instance.
+ symbol: Symbol used for minimum-lot margin and volume calculations.
+ unit_margin_ratio: Fraction of post-reserve margin to allocate per unit.
+ preserved_margin_ratio: Fraction of ``margin_free`` to preserve.
+
+ Returns:
+ Dictionary with ``margin_free``, ``available_margin``, ``trade_margin``,
+ ``buy_volume``, and ``sell_volume``. Negative ``margin_free`` values are
+ clamped to ``0.0`` before sizing.
+ """
+ _require_unit_ratio(unit_margin_ratio, "unit_margin_ratio")
+ _require_unit_ratio(preserved_margin_ratio, "preserved_margin_ratio")
+
+ account = client.account_info_as_dict()
+ margin_free = max(0.0, float(account.get("margin_free") or 0.0))
+ available_margin = margin_free * (1.0 - preserved_margin_ratio)
+ trade_margin = available_margin * unit_margin_ratio
+ if unit_margin_ratio == 0:
+ buy_volume = _calculate_min_volume_if_affordable(
+ client,
+ symbol,
+ available_margin,
+ "BUY",
+ )
+ sell_volume = _calculate_min_volume_if_affordable(
+ client,
+ symbol,
+ available_margin,
+ "SELL",
+ )
+ else:
+ buy_volume = calculate_volume_by_margin(client, symbol, trade_margin, "BUY")
+ sell_volume = calculate_volume_by_margin(client, symbol, trade_margin, "SELL")
+ try:
+ symbol_info = get_symbol_snapshot(client, symbol)
+ volume_min = float(symbol_info.get("volume_min") or 0.0)
+ volume_max = float(symbol_info.get("volume_max") or 0.0)
+ volume_step = float(symbol_info.get("volume_step") or 0.0)
+ except AttributeError:
+ volume_min = volume_max = volume_step = 0.0
+ return {
+ "margin_free": margin_free,
+ "available_margin": available_margin,
+ "trade_margin": trade_margin,
+ "buy_volume": float(buy_volume),
+ "sell_volume": float(sell_volume),
+ "volume_min": volume_min,
+ "volume_max": volume_max,
+ "volume_step": volume_step,
+ }
|
@@ -1428,9 +1451,7 @@ side when the post-reserve margin can afford it.
Source code in mt5cli/trading.py
- 801
-802
-803
+ | def calculate_new_position_margin_ratio(
- client: Mt5TradingClient,
- *,
- symbol: str,
- new_position_side: OrderSide | None = None,
- new_position_volume: float = 0.0,
-) -> float:
- """Return total margin/equity ratio after an optional hypothetical position.
-
- Raises:
- Mt5TradingError: If equity or required tick data is invalid.
- """
- account = get_account_snapshot(client)
- equity = float(account.get("equity") or 0.0)
- if equity <= 0:
- msg = "Account equity must be positive to calculate margin ratio."
- raise Mt5TradingError(msg)
- margin = float(account.get("margin") or 0.0)
- if new_position_side is not None and new_position_volume > 0:
- side = _normalize_order_side(new_position_side)
- price = extract_tick_price(
- get_tick_snapshot(client, symbol), "ask" if side == "BUY" else "bid"
- )
- if price is None:
- msg = f"Tick price is unavailable for {symbol!r}."
- raise Mt5TradingError(msg)
- order_type = (
- client.mt5.ORDER_TYPE_BUY if side == "BUY" else client.mt5.ORDER_TYPE_SELL
- )
- margin += float(
- client.order_calc_margin(order_type, symbol, new_position_volume, price),
- )
- return margin / equity
+833
+834
+835
| def calculate_new_position_margin_ratio(
+ client: Mt5TradingClient,
+ *,
+ symbol: str,
+ new_position_side: OrderSide | None = None,
+ new_position_volume: float = 0.0,
+) -> float:
+ """Return total margin/equity ratio after an optional hypothetical position.
+
+ Raises:
+ Mt5TradingError: If equity or required tick data is invalid.
+ """
+ account = get_account_snapshot(client)
+ equity = float(account.get("equity") or 0.0)
+ if equity <= 0:
+ msg = "Account equity must be positive to calculate margin ratio."
+ raise Mt5TradingError(msg)
+ margin = float(account.get("margin") or 0.0)
+ if new_position_side is not None and new_position_volume > 0:
+ side = _normalize_order_side(new_position_side)
+ price = extract_tick_price(
+ get_tick_snapshot(client, symbol), "ask" if side == "BUY" else "bid"
+ )
+ if price is None:
+ msg = f"Tick price is unavailable for {symbol!r}."
+ raise Mt5TradingError(msg)
+ order_type = (
+ client.mt5.ORDER_TYPE_BUY if side == "BUY" else client.mt5.ORDER_TYPE_SELL
+ )
+ margin += float(
+ client.order_calc_margin(order_type, symbol, new_position_volume, price),
+ )
+ return margin / equity
|
@@ -1592,9 +1615,7 @@ included.
Source code in mt5cli/trading.py
- 678
-679
-680
+ | def calculate_positions_margin(
- client: Mt5TradingClient,
- *,
- symbols: Sequence[str] | None = None,
-) -> float:
- """Return the sum of estimated current margin for open positions.
-
- Args:
- client: Connected ``Mt5TradingClient`` instance.
- symbols: Optional symbol filter. When omitted, all open positions are
- included.
-
- Returns:
- Total estimated margin, or ``0.0`` when no matching positions exist.
- """
- frame = get_positions_frame(client)
- if frame.empty or "symbol" not in frame.columns:
- return 0.0
- if symbols is not None:
- frame = frame[frame["symbol"].isin(list(symbols))]
- if frame.empty:
- return 0.0
- grouped_volumes: dict[tuple[str, OrderSide], float] = {}
- for _, row in frame.iterrows():
- symbol = row.get("symbol")
- if not isinstance(symbol, str) or not symbol:
- continue
- volume = row.get("volume")
- if not _is_positive_finite_number(volume):
- continue
- order_side = _order_side_from_position_type(client, row.get("type"))
- if order_side is None:
- continue
- key = (symbol, order_side)
- finite_volume = float(cast("float | int", volume))
- grouped_volumes[key] = grouped_volumes.get(key, 0.0) + finite_volume
- total = 0.0
- for (symbol, order_side), volume in grouped_volumes.items():
- total += estimate_order_margin(client, symbol, order_side, volume)
- return total
+717
+718
+719
| def calculate_positions_margin(
+ client: Mt5TradingClient,
+ *,
+ symbols: Sequence[str] | None = None,
+) -> float:
+ """Return the sum of estimated current margin for open positions.
+
+ Args:
+ client: Connected ``Mt5TradingClient`` instance.
+ symbols: Optional symbol filter. When omitted, all open positions are
+ included.
+
+ Returns:
+ Total estimated margin, or ``0.0`` when no matching positions exist.
+ """
+ frame = get_positions_frame(client)
+ if frame.empty or "symbol" not in frame.columns:
+ return 0.0
+ if symbols is not None:
+ frame = frame[frame["symbol"].isin(list(symbols))]
+ if frame.empty:
+ return 0.0
+ grouped_volumes: dict[tuple[str, OrderSide], float] = {}
+ for _, row in frame.iterrows():
+ symbol = row.get("symbol")
+ if not isinstance(symbol, str) or not symbol:
+ continue
+ volume = row.get("volume")
+ if not _is_positive_finite_number(volume):
+ continue
+ order_side = _order_side_from_position_type(client, row.get("type"))
+ if order_side is None:
+ continue
+ key = (symbol, order_side)
+ finite_volume = float(cast("float | int", volume))
+ grouped_volumes[key] = grouped_volumes.get(key, 0.0) + finite_volume
+ total = 0.0
+ for (symbol, order_side), volume in grouped_volumes.items():
+ total += estimate_order_margin(client, symbol, order_side, volume)
+ return total
|
@@ -1857,9 +1880,7 @@ When False, re-raise the first failure.
Source code in mt5cli/trading.py
- 720
-721
-722
+ | def calculate_positions_margin_by_symbol(
- client: Mt5TradingClient,
- *,
- symbols: Sequence[str],
- suppress_errors: bool = True,
-) -> dict[str, float]:
- """Return per-symbol estimated margin for open positions.
-
- Computes margin for each unique input symbol independently using the strict
- :func:`calculate_positions_margin` helper. Duplicates are deduplicated in
- first-seen order.
-
- Args:
- client: Connected ``Mt5TradingClient`` instance.
- symbols: Symbols to compute margin for.
- suppress_errors: When ``True``, log and skip symbols that raise
- ``Mt5TradingError``, ``Mt5RuntimeError``, or ``AttributeError``.
- When ``False``, re-raise the first failure.
-
- Returns:
- Mapping of symbol to margin total in first-seen unique-symbol order.
- Returns an empty dict when ``symbols`` is empty or all symbols fail
- with ``suppress_errors=True``.
-
- Raises:
- Mt5TradingError: When a symbol raises ``Mt5TradingError`` and
- ``suppress_errors=False``.
- Mt5RuntimeError: When a symbol raises ``Mt5RuntimeError`` and
+760
+761
+762
| def calculate_positions_margin_by_symbol(
+ client: Mt5TradingClient,
+ *,
+ symbols: Sequence[str],
+ suppress_errors: bool = True,
+) -> dict[str, float]:
+ """Return per-symbol estimated margin for open positions.
+
+ Computes margin for each unique input symbol independently using the strict
+ :func:`calculate_positions_margin` helper. Duplicates are deduplicated in
+ first-seen order.
+
+ Args:
+ client: Connected ``Mt5TradingClient`` instance.
+ symbols: Symbols to compute margin for.
+ suppress_errors: When ``True``, log and skip symbols that raise
+ ``Mt5TradingError``, ``Mt5RuntimeError``, or ``AttributeError``.
+ When ``False``, re-raise the first failure.
+
+ Returns:
+ Mapping of symbol to margin total in first-seen unique-symbol order.
+ Returns an empty dict when ``symbols`` is empty or all symbols fail
+ with ``suppress_errors=True``.
+
+ Raises:
+ Mt5TradingError: When a symbol raises ``Mt5TradingError`` and
``suppress_errors=False``.
- AttributeError: When a symbol raises ``AttributeError`` and
+ Mt5RuntimeError: When a symbol raises ``Mt5RuntimeError`` and
``suppress_errors=False``.
- """
- result: dict[str, float] = {}
- for symbol in dict.fromkeys(symbols):
- try:
- result[symbol] = calculate_positions_margin(client, symbols=[symbol])
- except (Mt5TradingError, Mt5RuntimeError, AttributeError) as exc:
- if not suppress_errors:
- raise
- _logger.warning("Skipping margin for %r: %s", symbol, exc)
- return result
+ AttributeError: When a symbol raises ``AttributeError`` and
+ ``suppress_errors=False``.
+ """
+ result: dict[str, float] = {}
+ for symbol in dict.fromkeys(symbols):
+ try:
+ result[symbol] = calculate_positions_margin(client, symbols=[symbol])
+ except (Mt5TradingError, Mt5RuntimeError, AttributeError) as exc:
+ if not suppress_errors:
+ raise
+ _logger.warning("Skipping margin for %r: %s", symbol, exc)
+ return result
|
@@ -2036,9 +2059,7 @@ When False, re-raise the first failure.
Source code in mt5cli/trading.py
- 763
-764
-765
+ | def calculate_positions_margin_safe(
- client: Mt5TradingClient,
- *,
- symbols: Sequence[str],
-) -> float:
- """Return the total estimated margin for open positions across symbols.
-
- Internally calls :func:`calculate_positions_margin_by_symbol` with
- ``suppress_errors=True``. Failed symbols are silently skipped.
-
- Args:
- client: Connected ``Mt5TradingClient`` instance.
- symbols: Symbols to include.
-
- Returns:
- Sum of per-symbol margins; ``0.0`` when no symbols or all fail.
- """
- return sum(
- calculate_positions_margin_by_symbol(client, symbols=symbols).values(),
- 0.0,
- )
+783
+784
+785
| def calculate_positions_margin_safe(
+ client: Mt5TradingClient,
+ *,
+ symbols: Sequence[str],
+) -> float:
+ """Return the total estimated margin for open positions across symbols.
+
+ Internally calls :func:`calculate_positions_margin_by_symbol` with
+ ``suppress_errors=True``. Failed symbols are silently skipped.
+
+ Args:
+ client: Connected ``Mt5TradingClient`` instance.
+ symbols: Symbols to include.
+
+ Returns:
+ Sum of per-symbol margins; ``0.0`` when no symbols or all fail.
+ """
+ return sum(
+ calculate_positions_margin_by_symbol(client, symbols=symbols).values(),
+ 0.0,
+ )
|
@@ -2118,9 +2141,7 @@ the composed MT5 helpers propagate to the caller.
Source code in mt5cli/trading.py
- 894
-895
-896
+ | def calculate_projected_margin_ratio(
- client: Mt5TradingClient,
- *,
- symbol: str,
- new_position_side: OrderSide | None = None,
- new_position_volume: float = 0.0,
-) -> float:
- """Return estimated current plus optional new-position margin over equity.
-
- Current exposure is estimated from open positions with
- :func:`calculate_positions_margin`. Optional projected exposure is added via
- :func:`estimate_order_margin`. Thresholds and guard actions are intentionally
- left to downstream applications.
-
- Account equity, position margin, and optional projected margin errors from
- the composed MT5 helpers propagate to the caller.
- """
- equity = _account_equity(client)
- margin = calculate_positions_margin(client, symbols=[symbol])
- if new_position_side is not None and new_position_volume > 0:
- margin += estimate_order_margin(
- client,
- symbol,
- new_position_side,
- new_position_volume,
- )
- return margin / equity
+920
+921
+922
| def calculate_projected_margin_ratio(
+ client: Mt5TradingClient,
+ *,
+ symbol: str,
+ new_position_side: OrderSide | None = None,
+ new_position_volume: float = 0.0,
+) -> float:
+ """Return estimated current plus optional new-position margin over equity.
+
+ Current exposure is estimated from open positions with
+ :func:`calculate_positions_margin`. Optional projected exposure is added via
+ :func:`estimate_order_margin`. Thresholds and guard actions are intentionally
+ left to downstream applications.
+
+ Account equity, position margin, and optional projected margin errors from
+ the composed MT5 helpers propagate to the caller.
+ """
+ equity = _account_equity(client)
+ margin = calculate_positions_margin(client, symbols=[symbol])
+ if new_position_side is not None and new_position_volume > 0:
+ margin += estimate_order_margin(
+ client,
+ symbol,
+ new_position_side,
+ new_position_volume,
+ )
+ return margin / equity
|
@@ -2220,9 +2243,7 @@ the composed MT5 helpers propagate to the caller.
Source code in mt5cli/trading.py
- 786
-787
-788
+ | def calculate_spread_ratio(client: Mt5TradingClient, symbol: str) -> float:
- """Return ``(ask - bid) / ((ask + bid) / 2)`` for the latest tick.
-
- Raises:
- Mt5TradingError: If bid or ask is unavailable.
- """
- tick = get_tick_snapshot(client, symbol)
- bid = extract_tick_price(tick, "bid")
- ask = extract_tick_price(tick, "ask")
- if bid is None or ask is None:
- msg = f"Tick bid/ask is unavailable for {symbol!r}."
- raise Mt5TradingError(msg)
- return (ask - bid) / ((ask + bid) / 2.0)
+798
+799
+800
| def calculate_spread_ratio(client: Mt5TradingClient, symbol: str) -> float:
+ """Return ``(ask - bid) / ((ask + bid) / 2)`` for the latest tick.
+
+ Raises:
+ Mt5TradingError: If bid or ask is unavailable.
+ """
+ tick = get_tick_snapshot(client, symbol)
+ bid = extract_tick_price(tick, "bid")
+ ask = extract_tick_price(tick, "ask")
+ if bid is None or ask is None:
+ msg = f"Tick bid/ask is unavailable for {symbol!r}."
+ raise Mt5TradingError(msg)
+ return (ask - bid) / ((ask + bid) / 2.0)
|
@@ -2272,7 +2295,13 @@ the composed MT5 helpers propagate to the caller.
(mt5cli.trading.OrderSide)" href="#mt5cli.trading.OrderSide">OrderSide | None = None,
new_position_volume: float = 0.0,
suppress_errors: bool = True,
-) -> float
+ projection_mode: ProjectionMode = "add",
+) -> float
@@ -2280,8 +2309,16 @@ the composed MT5 helpers propagate to the caller.
Return estimated symbol-group margin over account equity.
Per-symbol current exposure is summed with
:func:calculate_positions_margin_by_symbol. When new_symbol is inside
-the input symbol group, optional projected order margin is added for that
-symbol. Invalid equity always raises to fail closed.
+the input symbol group and candidate side/volume are provided, projected order
+margin is applied according to projection_mode:
+
+"add" (default): adds candidate margin to the group total.
+"replace_symbol": subtracts current margin for new_symbol, then
+ adds candidate margin. Useful for reversal-style projections where the new
+ order is intended to replace existing exposure for that symbol.
+
+ If the candidate margin estimation fails, the subtraction is also skipped so
+the operation is atomic. Invalid equity always raises to fail closed.
Raises:
@@ -2333,9 +2370,7 @@ lookup or projected margin lookup fails and suppress_errors is
Source code in mt5cli/trading.py
- 923
-924
-925
+ | def calculate_symbol_group_margin_ratio(
- client: Mt5TradingClient,
- *,
- symbols: Sequence[str],
- new_symbol: str | None = None,
- new_position_side: OrderSide | None = None,
- new_position_volume: float = 0.0,
- suppress_errors: bool = True,
-) -> float:
- """Return estimated symbol-group margin over account equity.
-
- Per-symbol current exposure is summed with
- :func:`calculate_positions_margin_by_symbol`. When ``new_symbol`` is inside
- the input symbol group, optional projected order margin is added for that
- symbol. Invalid equity always raises to fail closed.
-
- Raises:
- AttributeError: When symbol margin lookup or projected margin lookup
- fails and ``suppress_errors`` is ``False``.
- Mt5RuntimeError: When symbol margin lookup or projected margin lookup
- fails and ``suppress_errors`` is ``False``.
- Mt5TradingError: When account equity is invalid, or when symbol margin
- lookup or projected margin lookup fails and ``suppress_errors`` is
- ``False``.
- """
- equity = _account_equity(client)
- unique_symbols = list(dict.fromkeys(symbols))
- margin = sum(
- calculate_positions_margin_by_symbol(
- client,
- symbols=unique_symbols,
- suppress_errors=suppress_errors,
- ).values(),
- 0.0,
- )
- if (
- new_symbol in unique_symbols
- and new_position_side is not None
- and new_position_volume > 0
- ):
- try:
- margin += estimate_order_margin(
- client,
- new_symbol,
- new_position_side,
- new_position_volume,
- )
- except (Mt5TradingError, Mt5RuntimeError, AttributeError):
- if not suppress_errors:
- raise
- _logger.warning("Skipping projected margin for %r.", new_symbol)
- return margin / equity
+974
+975
+976
+977
+978
+979
+980
+981
+982
+983
+984
+985
+986
+987
| def calculate_symbol_group_margin_ratio(
+ client: Mt5TradingClient,
+ *,
+ symbols: Sequence[str],
+ new_symbol: str | None = None,
+ new_position_side: OrderSide | None = None,
+ new_position_volume: float = 0.0,
+ suppress_errors: bool = True,
+ projection_mode: ProjectionMode = "add",
+) -> float:
+ """Return estimated symbol-group margin over account equity.
+
+ Per-symbol current exposure is summed with
+ :func:`calculate_positions_margin_by_symbol`. When ``new_symbol`` is inside
+ the input symbol group and candidate side/volume are provided, projected order
+ margin is applied according to ``projection_mode``:
+
+ - ``"add"`` (default): adds candidate margin to the group total.
+ - ``"replace_symbol"``: subtracts current margin for ``new_symbol``, then
+ adds candidate margin. Useful for reversal-style projections where the new
+ order is intended to replace existing exposure for that symbol.
+
+ If the candidate margin estimation fails, the subtraction is also skipped so
+ the operation is atomic. Invalid equity always raises to fail closed.
+
+ Raises:
+ AttributeError: When symbol margin lookup or projected margin lookup
+ fails and ``suppress_errors`` is ``False``.
+ Mt5RuntimeError: When symbol margin lookup or projected margin lookup
+ fails and ``suppress_errors`` is ``False``.
+ Mt5TradingError: When account equity is invalid, or when symbol margin
+ lookup or projected margin lookup fails and ``suppress_errors`` is
+ ``False``.
+ """
+ equity = _account_equity(client)
+ unique_symbols = list(dict.fromkeys(symbols))
+ per_symbol = calculate_positions_margin_by_symbol(
+ client,
+ symbols=unique_symbols,
+ suppress_errors=suppress_errors,
+ )
+ margin = sum(per_symbol.values(), 0.0)
+ if (
+ new_symbol in unique_symbols
+ and new_position_side is not None
+ and new_position_volume > 0
+ ):
+ try:
+ candidate_margin = estimate_order_margin(
+ client,
+ new_symbol,
+ new_position_side,
+ new_position_volume,
+ )
+ except (Mt5TradingError, Mt5RuntimeError, AttributeError):
+ if not suppress_errors:
+ raise
+ _logger.warning("Skipping projected margin for %r.", new_symbol)
+ else:
+ if projection_mode == "replace_symbol":
+ margin -= per_symbol.get(new_symbol, 0.0)
+ margin += candidate_margin
+ return margin / equity
|
@@ -2470,20 +2529,7 @@ missing side-specific tick price are skipped.
Source code in mt5cli/trading.py
- 1380
-1381
-1382
-1383
-1384
-1385
-1386
-1387
-1388
-1389
-1390
-1391
-1392
-1393
+ | def calculate_trailing_stop_updates(
- client: Mt5TradingClient,
- *,
- symbol: str,
- trailing_stop_ratio: float,
-) -> dict[int, float]:
- """Return per-ticket trailing stop-loss updates for open symbol positions.
-
- Buy positions trail from bid using ``bid * (1 - trailing_stop_ratio)``.
- Sell positions trail from ask using ``ask * (1 + trailing_stop_ratio)``.
- Existing stop losses are preserved when they are already more favorable.
- Missing symbol metadata returns an empty update map. Positions with a
- missing side-specific tick price are skipped.
- """
- _require_protective_ratio(trailing_stop_ratio, "trailing_stop_ratio")
- positions = get_positions_frame(client, symbol=symbol)
- if positions.empty:
- return {}
- tick = get_tick_snapshot(client, symbol)
- bid = extract_tick_price(tick, "bid")
- ask = extract_tick_price(tick, "ask")
- digits = _symbol_digits(client, symbol)
- if digits is None:
- return {}
-
- updates: dict[int, float] = {}
- for row in positions.to_dict("records"):
- ticket = _position_ticket(row.get("ticket"))
- if ticket is None:
- continue
- next_sl = _trailing_stop_loss(
- client,
- position_type=row.get("type"),
- current_sl=_current_stop_loss(row.get("sl")),
- bid=bid,
- ask=ask,
- digits=digits,
- trailing_stop_ratio=trailing_stop_ratio,
- )
- if next_sl is None:
- continue
- updates[ticket] = next_sl
- return updates
+1422
+1423
+1424
+1425
+1426
+1427
+1428
+1429
+1430
+1431
+1432
+1433
+1434
+1435
| def calculate_trailing_stop_updates(
+ client: Mt5TradingClient,
+ *,
+ symbol: str,
+ trailing_stop_ratio: float,
+) -> dict[int, float]:
+ """Return per-ticket trailing stop-loss updates for open symbol positions.
+
+ Buy positions trail from bid using ``bid * (1 - trailing_stop_ratio)``.
+ Sell positions trail from ask using ``ask * (1 + trailing_stop_ratio)``.
+ Existing stop losses are preserved when they are already more favorable.
+ Missing symbol metadata returns an empty update map. Positions with a
+ missing side-specific tick price are skipped.
+ """
+ _require_protective_ratio(trailing_stop_ratio, "trailing_stop_ratio")
+ positions = get_positions_frame(client, symbol=symbol)
+ if positions.empty:
+ return {}
+ tick = get_tick_snapshot(client, symbol)
+ bid = extract_tick_price(tick, "bid")
+ ask = extract_tick_price(tick, "ask")
+ digits = _symbol_digits(client, symbol)
+ if digits is None:
+ return {}
+
+ updates: dict[int, float] = {}
+ for row in positions.to_dict("records"):
+ ticket = _position_ticket(row.get("ticket"))
+ if ticket is None:
+ continue
+ next_sl = _trailing_stop_loss(
+ client,
+ position_type=row.get("type"),
+ current_sl=_current_stop_loss(row.get("sl")),
+ bid=bid,
+ ask=ask,
+ digits=digits,
+ trailing_stop_ratio=trailing_stop_ratio,
+ )
+ if next_sl is None:
+ continue
+ updates[ticket] = next_sl
+ return updates
|
@@ -2655,20 +2714,7 @@ missing side-specific tick price are skipped.
Source code in mt5cli/trading.py
- 1044
-1045
-1046
-1047
-1048
-1049
-1050
-1051
-1052
-1053
-1054
-1055
-1056
-1057
+ | def calculate_volume_by_margin(
- client: Mt5TradingClient,
- symbol: str,
- available_margin: float,
- order_side: OrderSide,
-) -> float:
- """Calculate max normalized volume affordable for one side.
-
- Returns:
- Largest stepped volume whose actual margin (from ``order_calc_margin``)
- fits within ``available_margin``, rounded down to symbol volume
- constraints; ``0.0`` when no affordable step exists.
-
- Raises:
- Mt5TradingError: If symbol volume constraints or tick data are invalid.
- """
- if available_margin <= 0:
- return 0.0
- symbol_info = get_symbol_snapshot(client, symbol)
- volume_min = float(symbol_info.get("volume_min") or 0.0)
- volume_max = float(symbol_info.get("volume_max") or 0.0)
- volume_step = float(symbol_info.get("volume_step") or volume_min or 0.0)
- if volume_min <= 0 or volume_step <= 0:
- msg = f"Invalid volume constraints for {symbol!r}."
- raise Mt5TradingError(msg)
- side = _normalize_order_side(order_side)
- price = extract_tick_price(
- get_tick_snapshot(client, symbol), "ask" if side == "BUY" else "bid"
- )
- if price is None:
- msg = f"Tick price is unavailable for {symbol!r}."
- raise Mt5TradingError(msg)
- order_type = (
- client.mt5.ORDER_TYPE_BUY if side == "BUY" else client.mt5.ORDER_TYPE_SELL
- )
- min_margin = float(client.order_calc_margin(order_type, symbol, volume_min, price))
- if min_margin <= 0 or min_margin > available_margin:
- return 0.0
- lo = 0
- hi = int(
- max(
- 0,
- floor(
- (
- (
- min(available_margin / min_margin * volume_min, volume_max)
- if volume_max > 0
- else available_margin / min_margin * volume_min
- )
- - volume_min
- )
- / volume_step
- + 1e-12
- ),
- )
- )
- best = -1
-
- while lo <= hi:
- mid = (lo + hi) // 2
- normalized = round(volume_min + mid * volume_step, 10)
- actual = float(client.order_calc_margin(order_type, symbol, normalized, price))
-
- if actual > 0 and actual <= available_margin:
- best = mid
- lo = mid + 1
- else:
- hi = mid - 1
-
- return round(volume_min + best * volume_step, 10) if best >= 0 else 0.0
+1113
+1114
+1115
+1116
+1117
+1118
+1119
+1120
+1121
+1122
+1123
+1124
+1125
+1126
| def calculate_volume_by_margin(
+ client: Mt5TradingClient,
+ symbol: str,
+ available_margin: float,
+ order_side: OrderSide,
+) -> float:
+ """Calculate max normalized volume affordable for one side.
+
+ Returns:
+ Largest stepped volume whose actual margin (from ``order_calc_margin``)
+ fits within ``available_margin``, rounded down to symbol volume
+ constraints; ``0.0`` when no affordable step exists.
+
+ Raises:
+ Mt5TradingError: If symbol volume constraints or tick data are invalid.
+ """
+ if available_margin <= 0:
+ return 0.0
+ symbol_info = get_symbol_snapshot(client, symbol)
+ volume_min = float(symbol_info.get("volume_min") or 0.0)
+ volume_max = float(symbol_info.get("volume_max") or 0.0)
+ volume_step = float(symbol_info.get("volume_step") or volume_min or 0.0)
+ if volume_min <= 0 or volume_step <= 0:
+ msg = f"Invalid volume constraints for {symbol!r}."
+ raise Mt5TradingError(msg)
+ side = _normalize_order_side(order_side)
+ price = extract_tick_price(
+ get_tick_snapshot(client, symbol), "ask" if side == "BUY" else "bid"
+ )
+ if price is None:
+ msg = f"Tick price is unavailable for {symbol!r}."
+ raise Mt5TradingError(msg)
+ order_type = (
+ client.mt5.ORDER_TYPE_BUY if side == "BUY" else client.mt5.ORDER_TYPE_SELL
+ )
+ min_margin = float(client.order_calc_margin(order_type, symbol, volume_min, price))
+ if min_margin <= 0 or min_margin > available_margin:
+ return 0.0
+ lo = 0
+ hi = int(
+ max(
+ 0,
+ floor(
+ (
+ (
+ min(available_margin / min_margin * volume_min, volume_max)
+ if volume_max > 0
+ else available_margin / min_margin * volume_min
+ )
+ - volume_min
+ )
+ / volume_step
+ + 1e-12
+ ),
+ )
+ )
+ best = -1
+
+ while lo <= hi:
+ mid = (lo + hi) // 2
+ normalized = round(volume_min + mid * volume_step, 10)
+ actual = float(client.order_calc_margin(order_type, symbol, normalized, price))
+
+ if actual > 0 and actual <= available_margin:
+ best = mid
+ lo = mid + 1
+ else:
+ hi = mid - 1
+
+ return round(volume_min + best * volume_step, 10) if best >= 0 else 0.0
|
@@ -2847,20 +2906,7 @@ missing side-specific tick price are skipped.
Source code in mt5cli/trading.py
- 1304
-1305
-1306
-1307
-1308
-1309
-1310
-1311
-1312
-1313
-1314
-1315
-1316
-1317
+ | def close_open_positions(
- client: Mt5TradingClient,
- *,
- symbols: str | list[str] | None = None,
- tickets: list[int] | None = None,
- dry_run: bool = False,
-) -> list[OrderExecutionResult]:
- """Close matching open positions.
-
- Returns:
- Normalized execution results for matching positions.
- """
- positions = _filter_positions(
- get_positions_frame(client),
- symbols=symbols,
- tickets=tickets,
- )
- results: list[OrderExecutionResult] = []
- for row in positions.to_dict("records"):
- pos_type = row["type"]
- side: OrderSide = "SELL" if pos_type == client.mt5.POSITION_TYPE_BUY else "BUY"
- result = place_market_order(
- client,
- symbol=str(row["symbol"]),
- volume=float(row["volume"]),
- order_side=side,
- position=int(row["ticket"]),
- dry_run=dry_run,
- )
- results.append(result)
- return results
+1334
+1335
+1336
+1337
+1338
+1339
+1340
+1341
+1342
+1343
+1344
+1345
+1346
+1347
| def close_open_positions(
+ client: Mt5TradingClient,
+ *,
+ symbols: str | list[str] | None = None,
+ tickets: list[int] | None = None,
+ dry_run: bool = False,
+) -> list[OrderExecutionResult]:
+ """Close matching open positions.
+
+ Returns:
+ Normalized execution results for matching positions.
+ """
+ positions = _filter_positions(
+ get_positions_frame(client),
+ symbols=symbols,
+ tickets=tickets,
+ )
+ results: list[OrderExecutionResult] = []
+ for row in positions.to_dict("records"):
+ pos_type = row["type"]
+ side: OrderSide = "SELL" if pos_type == client.mt5.POSITION_TYPE_BUY else "BUY"
+ result = place_market_order(
+ client,
+ symbol=str(row["symbol"]),
+ volume=float(row["volume"]),
+ order_side=side,
+ position=int(row["ticket"]),
+ dry_run=dry_run,
+ )
+ results.append(result)
+ return results
|
@@ -2941,9 +3000,7 @@ missing side-specific tick price are skipped.
Source code in mt5cli/trading.py
- 511
-512
-513
+ | def create_trading_client(
- *,
- config: Mt5Config | None = None,
- login: int | str | None = None,
- password: str | None = None,
- server: str | None = None,
- path: str | None = None,
- timeout: int | None = None,
- retry_count: int = 0,
-) -> Mt5TradingClient:
- """Return an initialized and logged-in trading client."""
- mt5_config = _resolve_config(
- config=config,
- login=login,
- password=password,
- server=server,
- path=path,
- timeout=timeout,
- )
- client = Mt5TradingClient(config=mt5_config, retry_count=retry_count)
- try:
- client.initialize_and_login_mt5()
- except Exception:
- client.shutdown()
- raise
- return client
+536
+537
+538
| def create_trading_client(
+ *,
+ config: Mt5Config | None = None,
+ login: int | str | None = None,
+ password: str | None = None,
+ server: str | None = None,
+ path: str | None = None,
+ timeout: int | None = None,
+ retry_count: int = 0,
+) -> Mt5TradingClient:
+ """Return an initialized and logged-in trading client."""
+ mt5_config = _resolve_config(
+ config=config,
+ login=login,
+ password=password,
+ server=server,
+ path=path,
+ timeout=timeout,
+ )
+ client = Mt5TradingClient(config=mt5_config, retry_count=retry_count)
+ try:
+ client.initialize_and_login_mt5()
+ except Exception:
+ client.shutdown()
+ raise
+ return client
|
@@ -3128,9 +3187,7 @@ missing side-specific tick price are skipped.
Source code in mt5cli/trading.py
- 539
-540
-541
+ | def detect_position_side(
- client: Mt5TradingClient,
- symbol: str,
-) -> PositionSide | None:
- """Detect the net open position side for a symbol.
-
- Args:
- client: Connected ``Mt5TradingClient`` instance.
- symbol: Symbol to inspect.
-
- Returns:
- ``"long"`` when there are buy positions and no sell positions,
- ``"short"`` when there are sell positions and no buy positions, or
- ``None`` when no positions or mixed exposure exists.
- """
- positions = get_positions_frame(client, symbol=symbol)
- if positions.empty:
- return None
-
- buy_type = client.mt5.POSITION_TYPE_BUY
- sell_type = client.mt5.POSITION_TYPE_SELL
- buy_volume = _sum_position_volume(positions, buy_type)
- sell_volume = _sum_position_volume(positions, sell_type)
- if buy_volume > 0 and sell_volume == 0:
- return "long"
- if sell_volume > 0 and buy_volume == 0:
- return "short"
- return None
+566
+567
+568
| def detect_position_side(
+ client: Mt5TradingClient,
+ symbol: str,
+) -> PositionSide | None:
+ """Detect the net open position side for a symbol.
+
+ Args:
+ client: Connected ``Mt5TradingClient`` instance.
+ symbol: Symbol to inspect.
+
+ Returns:
+ ``"long"`` when there are buy positions and no sell positions,
+ ``"short"`` when there are sell positions and no buy positions, or
+ ``None`` when no positions or mixed exposure exists.
+ """
+ positions = get_positions_frame(client, symbol=symbol)
+ if positions.empty:
+ return None
+
+ buy_type = client.mt5.POSITION_TYPE_BUY
+ sell_type = client.mt5.POSITION_TYPE_SELL
+ buy_volume = _sum_position_volume(positions, buy_type)
+ sell_volume = _sum_position_volume(positions, sell_type)
+ if buy_volume > 0 and sell_volume == 0:
+ return "long"
+ if sell_volume > 0 and buy_volume == 0:
+ return "short"
+ return None
|
@@ -3378,20 +3437,7 @@ prices violate available trade_stops_level pre-validation.
Source code in mt5cli/trading.py
- 1116
-1117
-1118
-1119
-1120
-1121
-1122
-1123
-1124
-1125
-1126
-1127
-1128
-1129
+ | def determine_order_limits(
- client: Mt5TradingClient,
- symbol: str,
- side: PositionSide | str,
- stop_loss_limit_ratio: float | None = None,
- take_profit_limit_ratio: float | None = None,
-) -> OrderLimits:
- """Derive entry and protective order prices from current market quotes.
-
- Args:
- client: Connected ``Mt5TradingClient`` instance.
- symbol: Symbol used for the quote lookup.
- side: Position side as ``"long"``/``"short"`` (``"buy"``/``"sell"``
- aliases are accepted).
- stop_loss_limit_ratio: Relative distance from entry for stop loss in
- ``[0, 1)``. A value of ``0`` omits the stop loss.
- take_profit_limit_ratio: Relative distance from entry for take profit in
- ``[0, 1)``. A value of ``0`` omits the take profit.
-
- Returns:
- Dictionary with ``entry``, ``stop_loss``, and ``take_profit`` keys.
- Omitted protective levels are returned as ``None``.
-
- Raises:
- Mt5TradingError: If required tick data is invalid or computed SL/TP
- prices violate available ``trade_stops_level`` pre-validation.
- """
- stop_loss_ratio = stop_loss_limit_ratio or 0.0
- take_profit_ratio = take_profit_limit_ratio or 0.0
- _require_protective_ratio(stop_loss_ratio, "stop_loss_limit_ratio")
- _require_protective_ratio(take_profit_ratio, "take_profit_limit_ratio")
- normalized_side = _position_side_from_order_side(side)
- tick = get_tick_snapshot(client, symbol)
- entry_key = "ask" if normalized_side == "long" else "bid"
- entry = extract_tick_price(tick, entry_key)
- if entry is None:
- msg = f"Tick price is unavailable for {symbol!r}."
- raise Mt5TradingError(msg)
- try:
- symbol_info = get_symbol_snapshot(client, symbol)
- except (AttributeError, KeyError, TypeError, ValueError):
- symbol_info = {}
- try:
- digits = int(symbol_info.get("digits") or 8)
- except (TypeError, ValueError):
- digits = 8
- min_distance = _minimum_stop_distance(symbol_info)
-
- stop_loss: float | None = None
- if stop_loss_ratio > 0:
- if normalized_side == "long":
- stop_loss = entry * (1.0 - stop_loss_ratio)
- else:
- stop_loss = entry * (1.0 + stop_loss_ratio)
- stop_loss = round(stop_loss, digits)
-
- take_profit: float | None = None
- if take_profit_ratio > 0:
- if normalized_side == "long":
- take_profit = entry * (1.0 + take_profit_ratio)
- else:
- take_profit = entry * (1.0 - take_profit_ratio)
- take_profit = round(take_profit, digits)
-
- _validate_protective_prices(
- symbol=symbol,
- side=normalized_side,
- entry=entry,
- stop_loss=stop_loss,
- take_profit=take_profit,
- min_distance=min_distance,
- )
-
- return {
- "entry": entry,
- "stop_loss": stop_loss,
- "take_profit": take_profit,
- }
+1193
+1194
+1195
+1196
+1197
+1198
+1199
+1200
+1201
+1202
+1203
+1204
+1205
+1206
| def determine_order_limits(
+ client: Mt5TradingClient,
+ symbol: str,
+ side: PositionSide | str,
+ stop_loss_limit_ratio: float | None = None,
+ take_profit_limit_ratio: float | None = None,
+) -> OrderLimits:
+ """Derive entry and protective order prices from current market quotes.
+
+ Args:
+ client: Connected ``Mt5TradingClient`` instance.
+ symbol: Symbol used for the quote lookup.
+ side: Position side as ``"long"``/``"short"`` (``"buy"``/``"sell"``
+ aliases are accepted).
+ stop_loss_limit_ratio: Relative distance from entry for stop loss in
+ ``[0, 1)``. A value of ``0`` omits the stop loss.
+ take_profit_limit_ratio: Relative distance from entry for take profit in
+ ``[0, 1)``. A value of ``0`` omits the take profit.
+
+ Returns:
+ Dictionary with ``entry``, ``stop_loss``, and ``take_profit`` keys.
+ Omitted protective levels are returned as ``None``.
+
+ Raises:
+ Mt5TradingError: If required tick data is invalid or computed SL/TP
+ prices violate available ``trade_stops_level`` pre-validation.
+ """
+ stop_loss_ratio = stop_loss_limit_ratio or 0.0
+ take_profit_ratio = take_profit_limit_ratio or 0.0
+ _require_protective_ratio(stop_loss_ratio, "stop_loss_limit_ratio")
+ _require_protective_ratio(take_profit_ratio, "take_profit_limit_ratio")
+ normalized_side = _position_side_from_order_side(side)
+ tick = get_tick_snapshot(client, symbol)
+ entry_key = "ask" if normalized_side == "long" else "bid"
+ entry = extract_tick_price(tick, entry_key)
+ if entry is None:
+ msg = f"Tick price is unavailable for {symbol!r}."
+ raise Mt5TradingError(msg)
+ try:
+ symbol_info = get_symbol_snapshot(client, symbol)
+ except (AttributeError, KeyError, TypeError, ValueError):
+ symbol_info = {}
+ try:
+ digits = int(symbol_info.get("digits") or 8)
+ except (TypeError, ValueError):
+ digits = 8
+ min_distance = _minimum_stop_distance(symbol_info)
+
+ stop_loss: float | None = None
+ if stop_loss_ratio > 0:
+ if normalized_side == "long":
+ stop_loss = entry * (1.0 - stop_loss_ratio)
+ else:
+ stop_loss = entry * (1.0 + stop_loss_ratio)
+ stop_loss = round(stop_loss, digits)
+
+ take_profit: float | None = None
+ if take_profit_ratio > 0:
+ if normalized_side == "long":
+ take_profit = entry * (1.0 + take_profit_ratio)
+ else:
+ take_profit = entry * (1.0 - take_profit_ratio)
+ take_profit = round(take_profit, digits)
+
+ _validate_protective_prices(
+ symbol=symbol,
+ side=normalized_side,
+ entry=entry,
+ stop_loss=stop_loss,
+ take_profit=take_profit,
+ min_distance=min_distance,
+ )
+
+ return {
+ "entry": entry,
+ "stop_loss": stop_loss,
+ "take_profit": take_profit,
+ }
|
@@ -3630,9 +3689,7 @@ prices violate available trade_stops_level pre-validation.
Source code in mt5cli/trading.py
- 221
-222
-223
+ | def ensure_symbol_selected(client: Mt5TradingClient, symbol: str) -> None:
- """Ensure a symbol is visible in Market Watch before sending orders.
-
- Args:
- client: Connected ``Mt5TradingClient`` instance.
- symbol: Symbol to select.
-
- Raises:
- Mt5TradingError: If the symbol cannot be selected in Market Watch or
- ``symbol_select`` is unavailable on the client.
- """
- snapshot = get_symbol_snapshot(client, symbol)
- if snapshot.get("visible"):
- return
- select = getattr(client, "symbol_select", None)
- if not callable(select):
- msg = "MT5 client is missing required method: symbol_select"
- raise Mt5TradingError(msg)
- if select(symbol, enable=True):
- return
- last_error = getattr(client, "last_error", None)
- detail = f" ({last_error()})" if callable(last_error) else ""
- msg = f"Failed to select symbol {symbol!r} in Market Watch{detail}."
- raise Mt5TradingError(msg)
+244
+245
+246
| def ensure_symbol_selected(client: Mt5TradingClient, symbol: str) -> None:
+ """Ensure a symbol is visible in Market Watch before sending orders.
+
+ Args:
+ client: Connected ``Mt5TradingClient`` instance.
+ symbol: Symbol to select.
+
+ Raises:
+ Mt5TradingError: If the symbol cannot be selected in Market Watch or
+ ``symbol_select`` is unavailable on the client.
+ """
+ snapshot = get_symbol_snapshot(client, symbol)
+ if snapshot.get("visible"):
+ return
+ select = getattr(client, "symbol_select", None)
+ if not callable(select):
+ msg = "MT5 client is missing required method: symbol_select"
+ raise Mt5TradingError(msg)
+ if select(symbol, enable=True):
+ return
+ last_error = getattr(client, "last_error", None)
+ detail = f" ({last_error()})" if callable(last_error) else ""
+ msg = f"Failed to select symbol {symbol!r} in Market Watch{detail}."
+ raise Mt5TradingError(msg)
|
@@ -3757,9 +3816,7 @@ prices violate available trade_stops_level pre-validation.
Source code in mt5cli/trading.py
- 640
-641
-642
+ | def estimate_order_margin(
- client: Mt5TradingClient,
- symbol: str,
- order_side: OrderSide | str,
- volume: float,
-) -> float:
- """Estimate required margin for one order at the current market price.
-
- Returns:
- Positive finite margin required for the order at the current quote.
-
- Raises:
- Mt5TradingError: If volume, tick data, or margin estimation is invalid.
- """
- if not _is_positive_finite_number(volume):
- msg = "Volume must be a positive finite number to estimate order margin."
- raise Mt5TradingError(msg)
- side = _normalize_order_side(order_side)
- tick = get_tick_snapshot(client, symbol)
- price = extract_tick_price(tick, "ask" if side == "BUY" else "bid")
- if price is None:
- msg = f"Tick price is unavailable for {symbol!r}."
- raise Mt5TradingError(msg)
- order_type = (
- client.mt5.ORDER_TYPE_BUY if side == "BUY" else client.mt5.ORDER_TYPE_SELL
- )
- raw_margin = client.order_calc_margin(order_type, symbol, volume, price)
- try:
- margin = float(raw_margin)
- except (TypeError, ValueError) as exc:
- msg = f"Margin estimate is invalid for {symbol!r}."
- raise Mt5TradingError(msg) from exc
- if margin <= 0 or not isfinite(margin):
- msg = f"Margin estimate is invalid for {symbol!r}."
- raise Mt5TradingError(msg)
- return margin
+675
+676
+677
| def estimate_order_margin(
+ client: Mt5TradingClient,
+ symbol: str,
+ order_side: OrderSide | str,
+ volume: float,
+) -> float:
+ """Estimate required margin for one order at the current market price.
+
+ Returns:
+ Positive finite margin required for the order at the current quote.
+
+ Raises:
+ Mt5TradingError: If volume, tick data, or margin estimation is invalid.
+ """
+ if not _is_positive_finite_number(volume):
+ msg = "Volume must be a positive finite number to estimate order margin."
+ raise Mt5TradingError(msg)
+ side = _normalize_order_side(order_side)
+ tick = get_tick_snapshot(client, symbol)
+ price = extract_tick_price(tick, "ask" if side == "BUY" else "bid")
+ if price is None:
+ msg = f"Tick price is unavailable for {symbol!r}."
+ raise Mt5TradingError(msg)
+ order_type = (
+ client.mt5.ORDER_TYPE_BUY if side == "BUY" else client.mt5.ORDER_TYPE_SELL
+ )
+ raw_margin = client.order_calc_margin(order_type, symbol, volume, price)
+ try:
+ margin = float(raw_margin)
+ except (TypeError, ValueError) as exc:
+ msg = f"Margin estimate is invalid for {symbol!r}."
+ raise Mt5TradingError(msg) from exc
+ if margin <= 0 or not isfinite(margin):
+ msg = f"Margin estimate is invalid for {symbol!r}."
+ raise Mt5TradingError(msg)
+ return margin
|
@@ -3857,9 +3916,7 @@ Booleans are treated as non-numeric and return None.
Source code in mt5cli/trading.py
- 436
-437
-438
+ | def extract_tick_price(tick: Mapping[str, object], key: str) -> float | None:
- """Return a positive finite float from tick[key], or None if invalid.
-
- Accepts int, float, or numeric string values. Returns None when the key is
- missing, the value is None, non-numeric, NaN, infinite, zero, or negative.
- Booleans are treated as non-numeric and return None.
- """
- value = tick.get(key)
- if value is None or isinstance(value, bool):
- return None
- if isinstance(value, int | float):
- price = float(value)
- elif isinstance(value, str):
- try:
- price = float(value)
- except ValueError:
- return None
- else:
- return None
- if not isfinite(price) or price <= 0:
+457
+458
+459
| def extract_tick_price(tick: Mapping[str, object], key: str) -> float | None:
+ """Return a positive finite float from tick[key], or None if invalid.
+
+ Accepts int, float, or numeric string values. Returns None when the key is
+ missing, the value is None, non-numeric, NaN, infinite, zero, or negative.
+ Booleans are treated as non-numeric and return None.
+ """
+ value = tick.get(key)
+ if value is None or isinstance(value, bool):
+ return None
+ if isinstance(value, int | float):
+ price = float(value)
+ elif isinstance(value, str):
+ try:
+ price = float(value)
+ except ValueError:
+ return None
+ else:
return None
- return price
+ if not isfinite(price) or price <= 0:
+ return None
+ return price
|
@@ -3987,20 +4046,7 @@ malformed, or the time column is missing.
Source code in mt5cli/trading.py
- 1520
-1521
-1522
-1523
-1524
-1525
-1526
-1527
-1528
-1529
-1530
-1531
-1532
-1533
+ | def fetch_latest_closed_rates_for_trading_client(
- client: Mt5TradingClient,
- *,
- symbol: str,
- granularity: str,
- count: int,
-) -> pd.DataFrame:
- """Fetch the latest closed bars from a connected trading client.
-
- Returns:
- Up to ``count`` closed bars ordered oldest to newest.
-
- Raises:
- ValueError: If ``count`` is not positive, rate data is empty or
- malformed, or the ``time`` column is missing.
- Mt5TradingError: If the trading client cannot fetch rate data.
- """
- if count <= 0:
- msg = "count must be positive."
- raise ValueError(msg)
- fetch_method = getattr(client, "fetch_latest_rates_as_df", None)
- if not callable(fetch_method):
- msg = "MT5 trading client cannot fetch rate data."
- raise Mt5TradingError(msg)
- fetched = fetch_method(symbol, granularity, count + 1)
- if not isinstance(fetched, pd.DataFrame):
- msg = (
- f"Malformed rate data for {symbol!r} at granularity {granularity!r}: "
- "expected a DataFrame."
- )
- raise ValueError(msg) # noqa: TRY004
- frame = fetched
- frame = _ensure_rate_time_column(frame)
- if "time" not in frame.columns:
- msg = f"Rate data is missing a time column for {symbol!r}."
- raise ValueError(msg)
- closed = drop_forming_rate_bar(frame)
- if closed.empty:
- msg = (
- f"Rate data is empty for {symbol!r} at granularity {granularity!r} "
- f"with count {count}."
- )
- raise ValueError(msg)
- return closed.tail(count).reset_index(drop=True)
+1563
+1564
+1565
+1566
+1567
+1568
+1569
+1570
+1571
+1572
+1573
+1574
+1575
+1576
| def fetch_latest_closed_rates_for_trading_client(
+ client: Mt5TradingClient,
+ *,
+ symbol: str,
+ granularity: str,
+ count: int,
+) -> pd.DataFrame:
+ """Fetch the latest closed bars from a connected trading client.
+
+ Returns:
+ Up to ``count`` closed bars ordered oldest to newest.
+
+ Raises:
+ ValueError: If ``count`` is not positive, rate data is empty or
+ malformed, or the ``time`` column is missing.
+ Mt5TradingError: If the trading client cannot fetch rate data.
+ """
+ if count <= 0:
+ msg = "count must be positive."
+ raise ValueError(msg)
+ fetch_method = getattr(client, "fetch_latest_rates_as_df", None)
+ if not callable(fetch_method):
+ msg = "MT5 trading client cannot fetch rate data."
+ raise Mt5TradingError(msg)
+ fetched = fetch_method(symbol, granularity, count + 1)
+ if not isinstance(fetched, pd.DataFrame):
+ msg = (
+ f"Malformed rate data for {symbol!r} at granularity {granularity!r}: "
+ "expected a DataFrame."
+ )
+ raise ValueError(msg) # noqa: TRY004
+ frame = fetched
+ frame = _ensure_rate_time_column(frame)
+ if "time" not in frame.columns:
+ msg = f"Rate data is missing a time column for {symbol!r}."
+ raise ValueError(msg)
+ closed = drop_forming_rate_bar(frame)
+ if closed.empty:
+ msg = (
+ f"Rate data is empty for {symbol!r} at granularity {granularity!r} "
+ f"with count {count}."
+ )
+ raise ValueError(msg)
+ return closed.tail(count).reset_index(drop=True)
|
@@ -4256,20 +4315,7 @@ is invalid or unparseable.
Source code in mt5cli/trading.py
- 1605
-1606
-1607
-1608
-1609
-1610
-1611
-1612
-1613
-1614
-1615
-1616
-1617
-1618
+ | def fetch_latest_closed_rates_indexed(
- client: Mt5TradingClient,
- *,
- symbol: str,
- granularity: str,
- count: int,
-) -> pd.DataFrame:
- """Fetch the latest closed bars with a UTC DatetimeIndex from a trading client.
-
- Internally reuses :func:`fetch_latest_closed_rates_for_trading_client` for
- closed-bar detection and validation, then converts the ``time`` column to a
- UTC-aware :class:`~pandas.DatetimeIndex` named ``"time"`` and drops the
- original column. Intended for downstream time-series consumers that require
- a datetime index rather than a ``time`` column.
-
- Args:
- client: Connected trading client with rate-fetch capability.
- symbol: Symbol name.
- granularity: Timeframe string (for example ``"M1"``, ``"H1"``).
- count: Maximum number of closed bars to return.
-
- Returns:
- Up to ``count`` closed bars ordered oldest to newest, with a
- UTC-aware ``DatetimeIndex`` named ``"time"``. The original ``time``
- column is dropped.
-
- Raises:
- ValueError: If ``count`` is not positive, rate data is empty or
- malformed, the ``time`` column is missing, or timestamp data
- is invalid or unparseable.
- """
- frame = fetch_latest_closed_rates_for_trading_client(
- client,
- symbol=symbol,
- granularity=granularity,
- count=count,
- )
- if "time" not in frame.columns:
- msg = f"Rate data is missing a time column for {symbol!r}."
- raise ValueError(msg)
- idx = _rate_time_to_utc(frame["time"], symbol)
- idx.name = "time"
- result = frame.drop(columns=["time"])
- result.index = idx
- return result
+1649
+1650
+1651
+1652
+1653
+1654
+1655
+1656
+1657
+1658
+1659
+1660
+1661
+1662
| def fetch_latest_closed_rates_indexed(
+ client: Mt5TradingClient,
+ *,
+ symbol: str,
+ granularity: str,
+ count: int,
+) -> pd.DataFrame:
+ """Fetch the latest closed bars with a UTC DatetimeIndex from a trading client.
+
+ Internally reuses :func:`fetch_latest_closed_rates_for_trading_client` for
+ closed-bar detection and validation, then converts the ``time`` column to a
+ UTC-aware :class:`~pandas.DatetimeIndex` named ``"time"`` and drops the
+ original column. Intended for downstream time-series consumers that require
+ a datetime index rather than a ``time`` column.
+
+ Args:
+ client: Connected trading client with rate-fetch capability.
+ symbol: Symbol name.
+ granularity: Timeframe string (for example ``"M1"``, ``"H1"``).
+ count: Maximum number of closed bars to return.
+
+ Returns:
+ Up to ``count`` closed bars ordered oldest to newest, with a
+ UTC-aware ``DatetimeIndex`` named ``"time"``. The original ``time``
+ column is dropped.
+
+ Raises:
+ ValueError: If ``count`` is not positive, rate data is empty or
+ malformed, the ``time`` column is missing, or timestamp data
+ is invalid or unparseable.
+ """
+ frame = fetch_latest_closed_rates_for_trading_client(
+ client,
+ symbol=symbol,
+ granularity=granularity,
+ count=count,
+ )
+ if "time" not in frame.columns:
+ msg = f"Rate data is missing a time column for {symbol!r}."
+ raise ValueError(msg)
+ idx = _rate_time_to_utc(frame["time"], symbol)
+ idx.name = "time"
+ result = frame.drop(columns=["time"])
+ result.index = idx
+ return result
|
@@ -4371,23 +4430,23 @@ is invalid or unparseable.
Source code in mt5cli/trading.py
- 569
-570
-571
+ | def get_account_snapshot(
- client: Mt5TradingClient,
-) -> dict[str, float | int | str | None]:
- """Return normalized account state with stable keys."""
- value = _call_snapshot_method(client, "account_info_as_dict", "account_info")
- return cast(
- "dict[str, float | int | str | None]",
- _snapshot_from_value(value, _ACCOUNT_SNAPSHOT_FIELDS),
- )
+577
+578
+579
| def get_account_snapshot(
+ client: Mt5TradingClient,
+) -> dict[str, float | int | str | None]:
+ """Return normalized account state with stable keys."""
+ value = _call_snapshot_method(client, "account_info_as_dict", "account_info")
+ return cast(
+ "dict[str, float | int | str | None]",
+ _snapshot_from_value(value, _ACCOUNT_SNAPSHOT_FIELDS),
+ )
|
@@ -4414,25 +4473,25 @@ is invalid or unparseable.
Source code in mt5cli/trading.py
- 606
-607
-608
+ | def get_positions_frame(
- client: Mt5TradingClient,
- symbol: str | None = None,
-) -> pd.DataFrame:
- """Return open positions as a DataFrame with stable baseline columns."""
- frame = client.positions_get_as_df(symbol=symbol)
- for column in POSITION_COLUMNS:
- if column not in frame.columns:
- frame[column] = pd.Series(dtype="object")
- return frame
+615
+616
+617
| def get_positions_frame(
+ client: Mt5TradingClient,
+ symbol: str | None = None,
+) -> pd.DataFrame:
+ """Return open positions as a DataFrame with stable baseline columns."""
+ frame = client.positions_get_as_df(symbol=symbol)
+ for column in POSITION_COLUMNS:
+ if column not in frame.columns:
+ frame[column] = pd.Series(dtype="object")
+ return frame
|
@@ -4459,25 +4518,25 @@ is invalid or unparseable.
Source code in mt5cli/trading.py
- 580
-581
-582
+ | def get_symbol_snapshot(
- client: Mt5TradingClient,
- symbol: str,
-) -> dict[str, float | int | str | bool | None]:
- """Return normalized symbol metadata required for trading decisions."""
- method = getattr(client, "symbol_info_as_dict", None)
- value = method(symbol=symbol) if callable(method) else client.symbol_info(symbol)
- snapshot = _snapshot_from_value(value, _SYMBOL_SNAPSHOT_FIELDS)
- snapshot["symbol"] = snapshot.get("symbol") or symbol
- return cast("dict[str, float | int | str | bool | None]", snapshot)
+589
+590
+591
| def get_symbol_snapshot(
+ client: Mt5TradingClient,
+ symbol: str,
+) -> dict[str, float | int | str | bool | None]:
+ """Return normalized symbol metadata required for trading decisions."""
+ method = getattr(client, "symbol_info_as_dict", None)
+ value = method(symbol=symbol) if callable(method) else client.symbol_info(symbol)
+ snapshot = _snapshot_from_value(value, _SYMBOL_SNAPSHOT_FIELDS)
+ snapshot["symbol"] = snapshot.get("symbol") or symbol
+ return cast("dict[str, float | int | str | bool | None]", snapshot)
|
@@ -4504,9 +4563,7 @@ is invalid or unparseable.
Source code in mt5cli/trading.py
- 592
-593
-594
+ | def get_tick_snapshot(
- client: Mt5TradingClient,
- symbol: str,
-) -> dict[str, float | int | None]:
- """Return normalized latest tick data, including bid, ask, and timestamp."""
- method = getattr(client, "symbol_info_tick_as_dict", None)
- value = (
- method(symbol=symbol) if callable(method) else client.symbol_info_tick(symbol)
- )
- snapshot = _snapshot_from_value(value, _TICK_SNAPSHOT_FIELDS)
- snapshot["symbol"] = snapshot.get("symbol") or symbol
- return cast("dict[str, float | int | None]", snapshot)
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|