From 43f632bc40c4531965bdd2167543d6d7d95ae646 Mon Sep 17 00:00:00 2001 From: Daichi Narushima <1938249+dceoy@users.noreply.github.com> Date: Sun, 28 Jun 2026 01:23:17 +0900 Subject: [PATCH] Reorganize CLI help text and command grouping for data/execution clarity (#85) * feat: clarify CLI/docs scope as generic MT5 data and execution infrastructure - Update app help text and module docstring to describe mt5cli as MT5 data and execution utilities rather than export-only tooling - Group CLI commands under rich_help_panel sections: Data / Export, Execution, and Collection; command names are unchanged for compatibility - Expand order-send docstring to explicitly flag it as the expert raw-request live-trading path; preserve --yes gate - Split docs/index.md Trading section into "Trading State" (read-only) and "Execution (live / mutating)" with close-positions now documented - Add TestHelpText tests verifying top-level panel grouping, order-send expert/live language, and close-positions safety gate coverage Closes #78 Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_018yH6esaqc5D1cmo1dK2Ur9 * chore: trim trailing whitespace in docs/index.md table Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_018yH6esaqc5D1cmo1dK2Ur9 * chore: bump version to 1.0.1 Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_018yH6esaqc5D1cmo1dK2Ur9 * chore: update uv.lock for version 1.0.1 Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_018yH6esaqc5D1cmo1dK2Ur9 * fix: address review feedback on CLI/docs scope PR - Remove dead help invocation in test_order_send_help_mentions_expert_and_raw (the result was immediately overwritten by result2) - Strengthen assertion from `or` to `and`; both "raw" and "expert" are present in the docstring so disjunction masked a potential regression - Split into two `assert` statements to satisfy PT018 (ruff) - Fix docs/index.md inaccuracy: order-check has no --yes gate; clarify that only order-send and close-positions require confirmation for live execution - Move order-check from "Execution" rich_help_panel to "Data / Export" so the Execution panel name is truthful (order-check is read-only) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_018yH6esaqc5D1cmo1dK2Ur9 * docs: move order-check out of Execution section into Trading State order-check is read-only and now lives in the CLI's Data / Export panel, so documenting it under "Execution (live / mutating)" was inconsistent. Moved it to the Trading State table. The Execution section now only lists order-send and close-positions, both of which require --yes. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_018yH6esaqc5D1cmo1dK2Ur9 --------- Co-authored-by: Claude --- docs/index.md | 38 ++++++++++++++++--------- mt5cli/cli.py | 71 +++++++++++++++++++++++++++-------------------- pyproject.toml | 2 +- tests/test_cli.py | 60 +++++++++++++++++++++++++++++++++++++++ uv.lock | 2 +- 5 files changed, 128 insertions(+), 45 deletions(-) diff --git a/docs/index.md b/docs/index.md index 415e72e..1313189 100644 --- a/docs/index.md +++ b/docs/index.md @@ -147,20 +147,32 @@ mt5cli --login 12345 --password mypass --server MyBroker-Demo \ | `minimum-margins` | Export minimum-volume margin summary | | `market-book` | Export market depth (order book) | -### Trading +### Trading State -| Command | Description | -| ---------------------- | ----------------------------------------------------------- | -| `orders` | Export active orders | -| `positions` | Export open positions | -| `history-orders` | Export historical orders | -| `history-deals` | Export historical deals | -| `recent-history-deals` | Export historical deals from a trailing window | -| `mt5-summary` | Export terminal/account status summary | -| `order-check` | Check funds sufficiency for a trade request | -| `order-send` | Send a trade request to the trade server (`--yes` required) | +| Command | Description | +| ---------------------- | --------------------------------------------------------------------- | +| `orders` | Export active orders | +| `positions` | Export open positions | +| `history-orders` | Export historical orders | +| `history-deals` | Export historical deals | +| `recent-history-deals` | Export historical deals from a trailing window | +| `mt5-summary` | Export terminal/account status summary | +| `order-check` | Check funds sufficiency for a trade request (read-only, no `--yes`) | -Use `order-check` to validate a request payload before running `order-send --yes`. +### Execution (live / mutating) + +These commands send requests to the live trade server and can place or close +real trades. Both require `--yes` for live execution. + +| Command | Description | +| ----------------- | ---------------------------------------------------------------------------------------------------- | +| `order-send` | Send a **raw** trade request directly to MT5 (`--yes` required; expert path — no extra validation) | +| `close-positions` | Close open positions by `--symbol` or `--ticket` (`--yes` required for live; `--dry-run` to preview) | + +Use `order-check` (Trading State) to validate funds before running `order-send --yes`. +`close-positions` is the safer high-level alternative that builds correct close +requests automatically. `order-send` is the expert raw path — downstream +applications should prefer dedicated closing helpers or their own risk controls. ### Bulk Collection @@ -217,7 +229,7 @@ See the [History schema diagram](api/history.md#entity-relationship-diagram) for Browse the API documentation for detailed module information: -- [CLI Module](api/cli.md) - CLI application with export commands +- [CLI Module](api/cli.md) - CLI application with data export and execution commands - [SDK Module](api/sdk.md) - Programmatic read-only data collection API - [Utils Module](api/utils.md) - Constants, parameter types, parsers, and export utilities diff --git a/mt5cli/cli.py b/mt5cli/cli.py index 51d36ea..6d39818 100644 --- a/mt5cli/cli.py +++ b/mt5cli/cli.py @@ -1,4 +1,4 @@ -"""Command-line interface for MetaTrader 5 data export.""" +"""Command-line interface for MetaTrader 5 data and execution utilities.""" from __future__ import annotations @@ -55,7 +55,12 @@ class _ExportContext: app = typer.Typer( name="mt5cli", - help="Export MetaTrader5 data to CSV, JSON, Parquet, or SQLite3.", + help=( + "MT5 data and execution utilities — read market data, inspect account" + " state, and send trade requests. Data commands write to CSV, JSON," + " Parquet, or SQLite3. Execution commands (order-send, close-positions)" + " require --yes for live mutations." + ), ) _REQUEST_OPTION_HELP = ( @@ -151,7 +156,7 @@ def _callback( # pyright: ignore[reportUnusedFunction] typer.Option("--log-level", help="Logging level."), ] = LogLevel.WARNING, ) -> None: - """Configure shared options for all export commands. + """Configure shared connection and output options. Raises: typer.BadParameter: If the output format cannot be determined. @@ -183,7 +188,7 @@ def _callback( # pyright: ignore[reportUnusedFunction] # --------------------------------------------------------------------------- -@app.command() +@app.command(rich_help_panel="Data / Export") def rates_from( ctx: typer.Context, symbol: Annotated[str, typer.Option(help="Symbol name.")], @@ -210,7 +215,7 @@ def rates_from( ) -@app.command() +@app.command(rich_help_panel="Data / Export") def rates_from_pos( ctx: typer.Context, symbol: Annotated[str, typer.Option(help="Symbol name.")], @@ -236,7 +241,7 @@ def rates_from_pos( ) -@app.command() +@app.command(rich_help_panel="Data / Export") def latest_rates( ctx: typer.Context, symbol: Annotated[str, typer.Option(help="Symbol name.")], @@ -265,7 +270,7 @@ def latest_rates( ) -@app.command() +@app.command(rich_help_panel="Data / Export") def rates_range( ctx: typer.Context, symbol: Annotated[str, typer.Option(help="Symbol name.")], @@ -292,7 +297,7 @@ def rates_range( ) -@app.command() +@app.command(rich_help_panel="Data / Export") def ticks_from( ctx: typer.Context, symbol: Annotated[str, typer.Option(help="Symbol name.")], @@ -316,7 +321,7 @@ def ticks_from( ) -@app.command() +@app.command(rich_help_panel="Data / Export") def ticks_range( ctx: typer.Context, symbol: Annotated[str, typer.Option(help="Symbol name.")], @@ -340,7 +345,7 @@ def ticks_range( ) -@app.command() +@app.command(rich_help_panel="Data / Export") def ticks_recent( ctx: typer.Context, symbol: Annotated[str, typer.Option(help="Symbol name.")], @@ -377,19 +382,19 @@ def ticks_recent( ) -@app.command() +@app.command(rich_help_panel="Data / Export") def account_info(ctx: typer.Context) -> None: """Export account information.""" _export_command(ctx, lambda client: client.account_info()) -@app.command() +@app.command(rich_help_panel="Data / Export") def terminal_info(ctx: typer.Context) -> None: """Export terminal information.""" _export_command(ctx, lambda client: client.terminal_info()) -@app.command() +@app.command(rich_help_panel="Data / Export") def symbols( ctx: typer.Context, group: Annotated[ @@ -401,7 +406,7 @@ def symbols( _export_command(ctx, lambda client: client.symbols(group=group)) -@app.command() +@app.command(rich_help_panel="Data / Export") def symbol_info( ctx: typer.Context, symbol: Annotated[str, typer.Option(help="Symbol name.")], @@ -410,7 +415,7 @@ def symbol_info( _export_command(ctx, lambda client: client.symbol_info(symbol)) -@app.command() +@app.command(rich_help_panel="Data / Export") def minimum_margins( ctx: typer.Context, symbol: Annotated[str, typer.Option(help="Symbol name.")], @@ -419,7 +424,7 @@ def minimum_margins( _export_command(ctx, lambda client: client.minimum_margins(symbol)) -@app.command() +@app.command(rich_help_panel="Data / Export") def orders( ctx: typer.Context, symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None, @@ -433,7 +438,7 @@ def orders( ) -@app.command() +@app.command(rich_help_panel="Data / Export") def positions( ctx: typer.Context, symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None, @@ -447,7 +452,7 @@ def positions( ) -@app.command() +@app.command(rich_help_panel="Data / Export") def history_orders( ctx: typer.Context, date_from: Annotated[ @@ -477,7 +482,7 @@ def history_orders( ) -@app.command() +@app.command(rich_help_panel="Data / Export") def history_deals( ctx: typer.Context, date_from: Annotated[ @@ -507,7 +512,7 @@ def history_deals( ) -@app.command() +@app.command(rich_help_panel="Data / Export") def recent_history_deals( ctx: typer.Context, hours: Annotated[float, typer.Option(help="Lookback window in hours.")], @@ -530,25 +535,25 @@ def recent_history_deals( ) -@app.command() +@app.command(rich_help_panel="Data / Export") def mt5_summary(ctx: typer.Context) -> None: """Export a compact terminal/account status summary.""" _export_command(ctx, lambda client: client.mt5_summary_as_df()) -@app.command() +@app.command(rich_help_panel="Data / Export") def version(ctx: typer.Context) -> None: """Export MetaTrader5 version information.""" _export_command(ctx, lambda client: client.version()) -@app.command() +@app.command(rich_help_panel="Data / Export") def last_error(ctx: typer.Context) -> None: """Export the last error information.""" _export_command(ctx, lambda client: client.last_error()) -@app.command() +@app.command(rich_help_panel="Data / Export") def symbol_info_tick( ctx: typer.Context, symbol: Annotated[str, typer.Option(help="Symbol name.")], @@ -557,7 +562,7 @@ def symbol_info_tick( _export_command(ctx, lambda client: client.symbol_info_tick(symbol)) -@app.command() +@app.command(rich_help_panel="Data / Export") def market_book( ctx: typer.Context, symbol: Annotated[str, typer.Option(help="Symbol name.")], @@ -566,7 +571,7 @@ def market_book( _export_command(ctx, lambda client: client.market_book(symbol)) -@app.command() +@app.command(rich_help_panel="Data / Export") def order_check( ctx: typer.Context, request: Annotated[ @@ -578,7 +583,7 @@ def order_check( _export_command(ctx, lambda client: client.order_check(request)) -@app.command() +@app.command(rich_help_panel="Execution") def order_send( ctx: typer.Context, request: Annotated[ @@ -590,7 +595,13 @@ def order_send( typer.Option("--yes", help="Confirm the live trade request."), ] = False, ) -> None: - """Send a trading operation request to the trade server. + """Send a raw trade request to the trade server (expert path, live execution). + + Passes the request JSON directly to MT5 ``order_send``. This is the + low-level expert path — it places real trades on the connected account + with no additional validation beyond what MT5 itself performs. Use + ``order-check`` first to validate funds sufficiency. Prefer + ``close-positions`` for closing open positions. ``--yes`` is required. Raises: typer.BadParameter: If --yes is not provided. @@ -628,7 +639,7 @@ def _execution_results_to_df(results: list[OrderExecutionResult]) -> pd.DataFram return pd.DataFrame(rows) -@app.command() +@app.command(rich_help_panel="Execution") def close_positions( ctx: typer.Context, symbol: Annotated[ @@ -691,7 +702,7 @@ def close_positions( _execute_export(ctx, lambda: df) -@app.command() +@app.command(rich_help_panel="Collection") def collect_history( ctx: typer.Context, symbol: Annotated[ diff --git a/pyproject.toml b/pyproject.toml index 3259a98..7f0a2b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mt5cli" -version = "1.0.0" +version = "1.0.1" description = "Generic MT5 data and execution infrastructure for Python applications" authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}] maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}] diff --git a/tests/test_cli.py b/tests/test_cli.py index ad015dd..4ab8663 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -740,6 +740,66 @@ class TestCommands: assert "must be a JSON object" in normalize_cli_output(result.output) +# --------------------------------------------------------------------------- +# Help text / scope tests +# --------------------------------------------------------------------------- + + +class TestHelpText: + """Tests verifying CLI help text matches the documented scope.""" + + def test_top_level_help_mentions_execution(self) -> None: + """Top-level help must describe execution utilities, not export only.""" + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + output = normalize_cli_output(result.output) + assert "execution" in output.lower() + + def test_top_level_help_has_execution_panel(self) -> None: + """Top-level help must show an Execution command group.""" + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Execution" in result.output + + def test_top_level_help_has_data_export_panel(self) -> None: + """Top-level help must show a Data / Export command group.""" + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Data / Export" in result.output + + def test_order_send_help_mentions_expert_and_raw(self) -> None: + """order-send help must communicate it is the expert raw-request path.""" + result2 = runner.invoke( + app, + ["-o", "out.csv", "order-send", "--help"], + ) + assert result2.exit_code == 0 + output = normalize_cli_output(result2.output) + assert "raw" in output.lower() + assert "expert" in output.lower() + + def test_order_send_help_mentions_live_execution(self) -> None: + """order-send help must warn about live execution.""" + result = runner.invoke( + app, + ["-o", "out.csv", "order-send", "--help"], + ) + assert result.exit_code == 0 + output = normalize_cli_output(result.output) + assert "live" in output.lower() + + def test_close_positions_help_mentions_dry_run_and_yes(self) -> None: + """close-positions help must document both safety gates.""" + result = runner.invoke( + app, + ["-o", "out.csv", "close-positions", "--help"], + ) + assert result.exit_code == 0 + output = normalize_cli_output(result.output) + assert "--dry-run" in output + assert "--yes" in output + + # --------------------------------------------------------------------------- # close-positions command # --------------------------------------------------------------------------- diff --git a/uv.lock b/uv.lock index d4373aa..bff611d 100644 --- a/uv.lock +++ b/uv.lock @@ -487,7 +487,7 @@ wheels = [ [[package]] name = "mt5cli" -version = "1.0.0" +version = "1.0.1" source = { editable = "." } dependencies = [ { name = "click" },