mt5cli 1.1.3__tar.gz → 1.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. {mt5cli-1.1.3 → mt5cli-1.1.4}/PKG-INFO +33 -33
  2. {mt5cli-1.1.3 → mt5cli-1.1.4}/README.md +31 -31
  3. {mt5cli-1.1.3 → mt5cli-1.1.4}/docs/api/public-contract.md +6 -2
  4. {mt5cli-1.1.3 → mt5cli-1.1.4}/docs/index.md +4 -4
  5. {mt5cli-1.1.3 → mt5cli-1.1.4}/mt5cli/cli.py +22 -1
  6. {mt5cli-1.1.3 → mt5cli-1.1.4}/mt5cli/trading.py +30 -1
  7. {mt5cli-1.1.3 → mt5cli-1.1.4}/pyproject.toml +2 -2
  8. {mt5cli-1.1.3 → mt5cli-1.1.4}/tests/test_cli.py +87 -0
  9. {mt5cli-1.1.3 → mt5cli-1.1.4}/tests/test_trading.py +152 -0
  10. {mt5cli-1.1.3 → mt5cli-1.1.4}/uv.lock +5 -5
  11. {mt5cli-1.1.3 → mt5cli-1.1.4}/.agents/skills/local-qa/SKILL.md +0 -0
  12. {mt5cli-1.1.3 → mt5cli-1.1.4}/.agents/skills/local-qa/scripts/qa.sh +0 -0
  13. {mt5cli-1.1.3 → mt5cli-1.1.4}/.agents/skills/mt5cli/SKILL.md +0 -0
  14. {mt5cli-1.1.3 → mt5cli-1.1.4}/.agents/skills/pr-feedback-triage/SKILL.md +0 -0
  15. {mt5cli-1.1.3 → mt5cli-1.1.4}/.claude/agents/codex.md +0 -0
  16. {mt5cli-1.1.3 → mt5cli-1.1.4}/.claude/settings.json +0 -0
  17. {mt5cli-1.1.3 → mt5cli-1.1.4}/.github/FUNDING.yml +0 -0
  18. {mt5cli-1.1.3 → mt5cli-1.1.4}/.github/dependabot.yml +0 -0
  19. {mt5cli-1.1.3 → mt5cli-1.1.4}/.github/renovate.json +0 -0
  20. {mt5cli-1.1.3 → mt5cli-1.1.4}/.github/workflows/ci.yml +0 -0
  21. {mt5cli-1.1.3 → mt5cli-1.1.4}/.github/workflows/claude.yml +0 -0
  22. {mt5cli-1.1.3 → mt5cli-1.1.4}/.github/workflows/opencode.yml +0 -0
  23. {mt5cli-1.1.3 → mt5cli-1.1.4}/.github/workflows/release.yml +0 -0
  24. {mt5cli-1.1.3 → mt5cli-1.1.4}/.gitignore +0 -0
  25. {mt5cli-1.1.3 → mt5cli-1.1.4}/AGENTS.md +0 -0
  26. {mt5cli-1.1.3 → mt5cli-1.1.4}/CLAUDE.md +0 -0
  27. {mt5cli-1.1.3 → mt5cli-1.1.4}/LICENSE +0 -0
  28. {mt5cli-1.1.3 → mt5cli-1.1.4}/docs/api/cli.md +0 -0
  29. {mt5cli-1.1.3 → mt5cli-1.1.4}/docs/api/client.md +0 -0
  30. {mt5cli-1.1.3 → mt5cli-1.1.4}/docs/api/converters.md +0 -0
  31. {mt5cli-1.1.3 → mt5cli-1.1.4}/docs/api/exceptions.md +0 -0
  32. {mt5cli-1.1.3 → mt5cli-1.1.4}/docs/api/grafana.md +0 -0
  33. {mt5cli-1.1.3 → mt5cli-1.1.4}/docs/api/history.md +0 -0
  34. {mt5cli-1.1.3 → mt5cli-1.1.4}/docs/api/index.md +0 -0
  35. {mt5cli-1.1.3 → mt5cli-1.1.4}/docs/api/schemas.md +0 -0
  36. {mt5cli-1.1.3 → mt5cli-1.1.4}/docs/api/sdk.md +0 -0
  37. {mt5cli-1.1.3 → mt5cli-1.1.4}/docs/api/telemetry.md +0 -0
  38. {mt5cli-1.1.3 → mt5cli-1.1.4}/docs/api/trading.md +0 -0
  39. {mt5cli-1.1.3 → mt5cli-1.1.4}/docs/api/utils.md +0 -0
  40. {mt5cli-1.1.3 → mt5cli-1.1.4}/examples/grafana/README.md +0 -0
  41. {mt5cli-1.1.3 → mt5cli-1.1.4}/examples/grafana/compose.yml +0 -0
  42. {mt5cli-1.1.3 → mt5cli-1.1.4}/examples/grafana/dashboards/mt5cli-market.json +0 -0
  43. {mt5cli-1.1.3 → mt5cli-1.1.4}/examples/grafana/dashboards/mt5cli-overview.json +0 -0
  44. {mt5cli-1.1.3 → mt5cli-1.1.4}/examples/grafana/dashboards/mt5cli-trades.json +0 -0
  45. {mt5cli-1.1.3 → mt5cli-1.1.4}/examples/grafana/provisioning/dashboards/mt5cli.yml +0 -0
  46. {mt5cli-1.1.3 → mt5cli-1.1.4}/examples/grafana/provisioning/datasources/mt5cli-sqlite.yml +0 -0
  47. {mt5cli-1.1.3 → mt5cli-1.1.4}/mkdocs.yml +0 -0
  48. {mt5cli-1.1.3 → mt5cli-1.1.4}/mt5cli/__init__.py +0 -0
  49. {mt5cli-1.1.3 → mt5cli-1.1.4}/mt5cli/__main__.py +0 -0
  50. {mt5cli-1.1.3 → mt5cli-1.1.4}/mt5cli/client.py +0 -0
  51. {mt5cli-1.1.3 → mt5cli-1.1.4}/mt5cli/contract.py +0 -0
  52. {mt5cli-1.1.3 → mt5cli-1.1.4}/mt5cli/converters.py +0 -0
  53. {mt5cli-1.1.3 → mt5cli-1.1.4}/mt5cli/exceptions.py +0 -0
  54. {mt5cli-1.1.3 → mt5cli-1.1.4}/mt5cli/grafana.py +0 -0
  55. {mt5cli-1.1.3 → mt5cli-1.1.4}/mt5cli/history.py +0 -0
  56. {mt5cli-1.1.3 → mt5cli-1.1.4}/mt5cli/retry.py +0 -0
  57. {mt5cli-1.1.3 → mt5cli-1.1.4}/mt5cli/schemas.py +0 -0
  58. {mt5cli-1.1.3 → mt5cli-1.1.4}/mt5cli/sdk.py +0 -0
  59. {mt5cli-1.1.3 → mt5cli-1.1.4}/mt5cli/telemetry.py +0 -0
  60. {mt5cli-1.1.3 → mt5cli-1.1.4}/mt5cli/utils.py +0 -0
  61. {mt5cli-1.1.3 → mt5cli-1.1.4}/tests/__init__.py +0 -0
  62. {mt5cli-1.1.3 → mt5cli-1.1.4}/tests/conftest.py +0 -0
  63. {mt5cli-1.1.3 → mt5cli-1.1.4}/tests/test_contracts.py +0 -0
  64. {mt5cli-1.1.3 → mt5cli-1.1.4}/tests/test_examples.py +0 -0
  65. {mt5cli-1.1.3 → mt5cli-1.1.4}/tests/test_grafana.py +0 -0
  66. {mt5cli-1.1.3 → mt5cli-1.1.4}/tests/test_history.py +0 -0
  67. {mt5cli-1.1.3 → mt5cli-1.1.4}/tests/test_sdk.py +0 -0
  68. {mt5cli-1.1.3 → mt5cli-1.1.4}/tests/test_telemetry.py +0 -0
  69. {mt5cli-1.1.3 → mt5cli-1.1.4}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mt5cli
3
- Version: 1.1.3
3
+ Version: 1.1.4
4
4
  Summary: Generic MT5 data and execution infrastructure for Python applications
5
5
  Project-URL: Repository, https://github.com/dceoy/mt5cli.git
6
6
  Author-email: dceoy <dceoy@users.noreply.github.com>
@@ -18,7 +18,7 @@ Classifier: Topic :: Office/Business :: Financial :: Investment
18
18
  Requires-Python: <3.14,>=3.11
19
19
  Requires-Dist: click>=8.1.0
20
20
  Requires-Dist: pandas>=2.2.2
21
- Requires-Dist: pdmt5>=1.1.0
21
+ Requires-Dist: pdmt5>=1.1.1
22
22
  Requires-Dist: pydantic>=2.13.4
23
23
  Requires-Dist: typer>=0.15.0
24
24
  Provides-Extra: otel
@@ -189,37 +189,37 @@ python -m mt5cli -o account.csv account-info
189
189
 
190
190
  ## Commands
191
191
 
192
- | Command | Description |
193
- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
194
- | `rates-from` | Export rates from a start date |
195
- | `rates-from-pos` | Export rates from a start position |
196
- | `latest-rates` | Export latest rates from a start position |
197
- | `rates-range` | Export rates for a date range |
198
- | `ticks-from` | Export ticks from a start date |
199
- | `ticks-range` | Export ticks for a date range |
200
- | `ticks-recent` | Export ticks from a recent trailing window |
201
- | `account-info` | Export account information |
202
- | `terminal-info` | Export terminal information |
203
- | `version` | Export MetaTrader 5 version information |
204
- | `last-error` | Export the last error information |
205
- | `symbols` | Export symbol list |
206
- | `symbol-info` | Export symbol details |
207
- | `symbol-info-tick` | Export the last tick for a symbol |
208
- | `minimum-margins` | Export minimum-volume buy and sell margin requirements |
209
- | `market-book` | Export market depth (order book) |
210
- | `orders` | Export active orders |
211
- | `positions` | Export open positions |
212
- | `history-orders` | Export historical orders |
213
- | `history-deals` | Export historical deals |
214
- | `recent-history-deals` | Export historical deals from a recent trailing window |
215
- | `mt5-summary` | Export terminal/account status summary |
216
- | `order-check` | Check funds sufficiency for a trade request |
217
- | `order-send` | Send a raw trade request to the trade server (`--yes` required; expert path) |
218
- | `close-positions` | Close open positions by `--symbol` or `--ticket` (`--yes` required for live; `--dry-run` available; optional `--deviation` / `--comment` / `--magic`) |
219
- | `collect-history` | Collect rates, history-orders, and history-deals for one or more symbols into a single SQLite database (ticks opt-in via `--dataset ticks`) |
220
- | `history-gaps` | Export a SQLite-only one-row-per-gap report from managed rate compatibility views without connecting to MT5 |
221
- | `grafana-schema` | Create or refresh Grafana-ready views and indexes in an existing SQLite database (idempotent, no MT5 connection) |
222
- | `snapshot` | Snapshot current account, position, order, and terminal state into SQLite for live Grafana dashboards |
192
+ | Command | Description |
193
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
194
+ | `rates-from` | Export rates from a start date |
195
+ | `rates-from-pos` | Export rates from a start position |
196
+ | `latest-rates` | Export latest rates from a start position |
197
+ | `rates-range` | Export rates for a date range |
198
+ | `ticks-from` | Export ticks from a start date |
199
+ | `ticks-range` | Export ticks for a date range |
200
+ | `ticks-recent` | Export ticks from a recent trailing window |
201
+ | `account-info` | Export account information |
202
+ | `terminal-info` | Export terminal information |
203
+ | `version` | Export MetaTrader 5 version information |
204
+ | `last-error` | Export the last error information |
205
+ | `symbols` | Export symbol list |
206
+ | `symbol-info` | Export symbol details |
207
+ | `symbol-info-tick` | Export the last tick for a symbol |
208
+ | `minimum-margins` | Export minimum-volume buy and sell margin requirements |
209
+ | `market-book` | Export market depth (order book) |
210
+ | `orders` | Export active orders |
211
+ | `positions` | Export open positions |
212
+ | `history-orders` | Export historical orders |
213
+ | `history-deals` | Export historical deals |
214
+ | `recent-history-deals` | Export historical deals from a recent trailing window |
215
+ | `mt5-summary` | Export terminal/account status summary |
216
+ | `order-check` | Check funds sufficiency for a trade request |
217
+ | `order-send` | Send a raw trade request to the trade server (`--yes` required; expert path) |
218
+ | `close-positions` | Close open positions by `--symbol` or `--ticket` (`--yes` required for live; `--dry-run` available; optional `--filling-mode` / `--deviation` / `--comment` / `--magic`) |
219
+ | `collect-history` | Collect rates, history-orders, and history-deals for one or more symbols into a single SQLite database (ticks opt-in via `--dataset ticks`) |
220
+ | `history-gaps` | Export a SQLite-only one-row-per-gap report from managed rate compatibility views without connecting to MT5 |
221
+ | `grafana-schema` | Create or refresh Grafana-ready views and indexes in an existing SQLite database (idempotent, no MT5 connection) |
222
+ | `snapshot` | Snapshot current account, position, order, and terminal state into SQLite for live Grafana dashboards |
223
223
 
224
224
  Use `order-check` to validate a request payload before running `order-send --yes`.
225
225
  `close-positions` is the safer high-level alternative that builds correct close
@@ -158,37 +158,37 @@ python -m mt5cli -o account.csv account-info
158
158
 
159
159
  ## Commands
160
160
 
161
- | Command | Description |
162
- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
163
- | `rates-from` | Export rates from a start date |
164
- | `rates-from-pos` | Export rates from a start position |
165
- | `latest-rates` | Export latest rates from a start position |
166
- | `rates-range` | Export rates for a date range |
167
- | `ticks-from` | Export ticks from a start date |
168
- | `ticks-range` | Export ticks for a date range |
169
- | `ticks-recent` | Export ticks from a recent trailing window |
170
- | `account-info` | Export account information |
171
- | `terminal-info` | Export terminal information |
172
- | `version` | Export MetaTrader 5 version information |
173
- | `last-error` | Export the last error information |
174
- | `symbols` | Export symbol list |
175
- | `symbol-info` | Export symbol details |
176
- | `symbol-info-tick` | Export the last tick for a symbol |
177
- | `minimum-margins` | Export minimum-volume buy and sell margin requirements |
178
- | `market-book` | Export market depth (order book) |
179
- | `orders` | Export active orders |
180
- | `positions` | Export open positions |
181
- | `history-orders` | Export historical orders |
182
- | `history-deals` | Export historical deals |
183
- | `recent-history-deals` | Export historical deals from a recent trailing window |
184
- | `mt5-summary` | Export terminal/account status summary |
185
- | `order-check` | Check funds sufficiency for a trade request |
186
- | `order-send` | Send a raw trade request to the trade server (`--yes` required; expert path) |
187
- | `close-positions` | Close open positions by `--symbol` or `--ticket` (`--yes` required for live; `--dry-run` available; optional `--deviation` / `--comment` / `--magic`) |
188
- | `collect-history` | Collect rates, history-orders, and history-deals for one or more symbols into a single SQLite database (ticks opt-in via `--dataset ticks`) |
189
- | `history-gaps` | Export a SQLite-only one-row-per-gap report from managed rate compatibility views without connecting to MT5 |
190
- | `grafana-schema` | Create or refresh Grafana-ready views and indexes in an existing SQLite database (idempotent, no MT5 connection) |
191
- | `snapshot` | Snapshot current account, position, order, and terminal state into SQLite for live Grafana dashboards |
161
+ | Command | Description |
162
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
163
+ | `rates-from` | Export rates from a start date |
164
+ | `rates-from-pos` | Export rates from a start position |
165
+ | `latest-rates` | Export latest rates from a start position |
166
+ | `rates-range` | Export rates for a date range |
167
+ | `ticks-from` | Export ticks from a start date |
168
+ | `ticks-range` | Export ticks for a date range |
169
+ | `ticks-recent` | Export ticks from a recent trailing window |
170
+ | `account-info` | Export account information |
171
+ | `terminal-info` | Export terminal information |
172
+ | `version` | Export MetaTrader 5 version information |
173
+ | `last-error` | Export the last error information |
174
+ | `symbols` | Export symbol list |
175
+ | `symbol-info` | Export symbol details |
176
+ | `symbol-info-tick` | Export the last tick for a symbol |
177
+ | `minimum-margins` | Export minimum-volume buy and sell margin requirements |
178
+ | `market-book` | Export market depth (order book) |
179
+ | `orders` | Export active orders |
180
+ | `positions` | Export open positions |
181
+ | `history-orders` | Export historical orders |
182
+ | `history-deals` | Export historical deals |
183
+ | `recent-history-deals` | Export historical deals from a recent trailing window |
184
+ | `mt5-summary` | Export terminal/account status summary |
185
+ | `order-check` | Check funds sufficiency for a trade request |
186
+ | `order-send` | Send a raw trade request to the trade server (`--yes` required; expert path) |
187
+ | `close-positions` | Close open positions by `--symbol` or `--ticket` (`--yes` required for live; `--dry-run` available; optional `--filling-mode` / `--deviation` / `--comment` / `--magic`) |
188
+ | `collect-history` | Collect rates, history-orders, and history-deals for one or more symbols into a single SQLite database (ticks opt-in via `--dataset ticks`) |
189
+ | `history-gaps` | Export a SQLite-only one-row-per-gap report from managed rate compatibility views without connecting to MT5 |
190
+ | `grafana-schema` | Create or refresh Grafana-ready views and indexes in an existing SQLite database (idempotent, no MT5 connection) |
191
+ | `snapshot` | Snapshot current account, position, order, and terminal state into SQLite for live Grafana dashboards |
192
192
 
193
193
  Use `order-check` to validate a request payload before running `order-send --yes`.
194
194
  `close-positions` is the safer high-level alternative that builds correct close
@@ -234,8 +234,12 @@ that closes open positions by `--symbol` or `--ticket` using
234
234
  `close_open_positions()`. Both `order-send --yes` and `close-positions --yes`
235
235
  are live execution paths. `close-positions --dry-run` previews close orders
236
236
  without placing them and does not require `--yes`. `close-positions` also
237
- accepts optional `--deviation`, `--comment`, and `--magic`; `--magic` scopes
238
- the selected open positions fail-closed when position magic metadata is absent.
237
+ accepts optional `--filling-mode`, `--deviation`, `--comment`, and `--magic`;
238
+ `--magic` scopes the selected open positions fail-closed when position magic
239
+ metadata is absent. When `--filling-mode` (or the `order_filling_mode`
240
+ parameter of `close_open_positions()`) is omitted, the filling mode is
241
+ resolved per symbol with `resolve_broker_filling_mode()` so closes are not
242
+ rejected on brokers whose symbols do not support IOC.
239
243
 
240
244
  `history-gaps` reads an existing SQLite history database and exports one row
241
245
  per detected gap from managed rate compatibility views. It never initializes
@@ -166,10 +166,10 @@ mt5cli --login '${MT5_LOGIN}' --password '${MT5_PASSWORD}' --server '${MT5_SERVE
166
166
  These commands send requests to the live trade server and can place or close
167
167
  real trades. Both require `--yes` for live execution.
168
168
 
169
- | Command | Description |
170
- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
171
- | `order-send` | Send a **raw** trade request directly to MT5 (`--yes` required; expert path — no extra validation) |
172
- | `close-positions` | Close open positions by `--symbol` or `--ticket` (`--yes` required for live; `--dry-run` to preview; optional `--deviation` / `--comment` / `--magic`) |
169
+ | Command | Description |
170
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
171
+ | `order-send` | Send a **raw** trade request directly to MT5 (`--yes` required; expert path — no extra validation) |
172
+ | `close-positions` | Close open positions by `--symbol` or `--ticket` (`--yes` required for live; `--dry-run` to preview; optional `--filling-mode` / `--deviation` / `--comment` / `--magic`) |
173
173
 
174
174
  Use `order-check` (Trading State) to validate funds before running `order-send --yes`.
175
175
  `close-positions` is the safer high-level alternative that builds correct close
@@ -37,6 +37,8 @@ if TYPE_CHECKING:
37
37
 
38
38
  from pdmt5 import Mt5Config
39
39
 
40
+ from .trading import OrderFillingMode
41
+
40
42
  logger = logging.getLogger(__name__)
41
43
 
42
44
  # ---------------------------------------------------------------------------
@@ -727,6 +729,16 @@ def close_positions(
727
729
  help="Position ticket to close (repeat for multiple tickets).",
728
730
  ),
729
731
  ] = None,
732
+ filling_mode: Annotated[
733
+ str | None,
734
+ typer.Option(
735
+ "--filling-mode",
736
+ help=(
737
+ "Order filling mode for close requests (IOC, FOK, or RETURN)."
738
+ " Defaults to per-symbol broker resolution."
739
+ ),
740
+ ),
741
+ ] = None,
730
742
  deviation: Annotated[
731
743
  int | None,
732
744
  typer.Option(help="Optional slippage/deviation for each close request."),
@@ -762,11 +774,19 @@ def close_positions(
762
774
 
763
775
  Raises:
764
776
  typer.BadParameter: If neither ``--symbol`` nor ``--ticket`` is given,
765
- or if ``--yes`` is missing for a live (non-dry-run) run.
777
+ if ``--filling-mode`` is not IOC, FOK, or RETURN, or if ``--yes``
778
+ is missing for a live (non-dry-run) run.
766
779
  """
767
780
  if not symbol and not ticket:
768
781
  msg = "Provide at least one --symbol or --ticket to close positions."
769
782
  raise typer.BadParameter(msg)
783
+ order_filling_mode: OrderFillingMode | None = None
784
+ if filling_mode is not None:
785
+ normalized_filling_mode = filling_mode.upper()
786
+ if normalized_filling_mode not in {"IOC", "FOK", "RETURN"}:
787
+ msg = f"Unsupported filling mode: {filling_mode!r}."
788
+ raise typer.BadParameter(msg, param_hint="--filling-mode")
789
+ order_filling_mode = cast("OrderFillingMode", normalized_filling_mode)
770
790
  if not dry_run and not yes:
771
791
  msg = "Pass --yes to close live positions."
772
792
  raise typer.BadParameter(msg, param_hint="--yes")
@@ -777,6 +797,7 @@ def close_positions(
777
797
  client,
778
798
  symbols=list(symbol) if symbol else None,
779
799
  tickets=list(ticket) if ticket else None,
800
+ order_filling_mode=order_filling_mode,
780
801
  deviation=deviation,
781
802
  comment=comment,
782
803
  magic=magic,
@@ -767,6 +767,18 @@ def _supported_filling_modes(
767
767
  supported.add("FOK")
768
768
  if isinstance(ioc_flag, int) and filling_mode & ioc_flag:
769
769
  supported.add("IOC")
770
+ # MQL5 permits IOC/FOK for Request and Instant execution regardless of the
771
+ # SYMBOL_FILLING_MODE bitmask, which only governs Market/Exchange execution.
772
+ implicit_ioc_fok_modes = {
773
+ mode
774
+ for mode in (
775
+ getattr(client.mt5, "SYMBOL_TRADE_EXECUTION_REQUEST", None),
776
+ getattr(client.mt5, "SYMBOL_TRADE_EXECUTION_INSTANT", None),
777
+ )
778
+ if isinstance(mode, int)
779
+ }
780
+ if trade_exemode is not None and trade_exemode in implicit_ioc_fok_modes:
781
+ supported.update({"IOC", "FOK"})
770
782
  market_execution = getattr(client.mt5, "SYMBOL_TRADE_EXECUTION_MARKET", None)
771
783
  if trade_exemode is not None and not (
772
784
  isinstance(market_execution, int) and trade_exemode == market_execution
@@ -1548,6 +1560,7 @@ def close_open_positions(
1548
1560
  *,
1549
1561
  symbols: str | list[str] | None = None,
1550
1562
  tickets: list[int] | None = None,
1563
+ order_filling_mode: OrderFillingMode | None = None,
1551
1564
  deviation: int | None = None,
1552
1565
  comment: str | None = None,
1553
1566
  magic: int | None = None,
@@ -1555,6 +1568,10 @@ def close_open_positions(
1555
1568
  ) -> list[OrderExecutionResult]:
1556
1569
  """Close matching open positions.
1557
1570
 
1571
+ When ``order_filling_mode`` is ``None``, the filling mode is resolved per
1572
+ symbol with :func:`resolve_broker_filling_mode` so closes are not rejected
1573
+ on brokers whose symbols do not support IOC.
1574
+
1558
1575
  Returns:
1559
1576
  Normalized execution results for matching positions.
1560
1577
  """
@@ -1565,14 +1582,26 @@ def close_open_positions(
1565
1582
  magic=magic,
1566
1583
  )
1567
1584
  results: list[OrderExecutionResult] = []
1585
+ resolved_filling_modes: dict[str, OrderFillingMode] = {}
1568
1586
  for row in positions.to_dict("records"):
1569
1587
  pos_type = row["type"]
1570
1588
  side: OrderSide = "SELL" if pos_type == client.mt5.POSITION_TYPE_BUY else "BUY"
1589
+ symbol = str(row["symbol"])
1590
+ if order_filling_mode is None:
1591
+ if symbol not in resolved_filling_modes:
1592
+ resolved_filling_modes[symbol] = resolve_broker_filling_mode(
1593
+ client,
1594
+ symbol=symbol,
1595
+ )
1596
+ filling_mode = resolved_filling_modes[symbol]
1597
+ else:
1598
+ filling_mode = order_filling_mode
1571
1599
  result = place_market_order(
1572
1600
  client,
1573
- symbol=str(row["symbol"]),
1601
+ symbol=symbol,
1574
1602
  volume=float(row["volume"]),
1575
1603
  order_side=side,
1604
+ order_filling_mode=filling_mode,
1576
1605
  position=int(row["ticket"]),
1577
1606
  deviation=deviation,
1578
1607
  comment=comment,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mt5cli"
3
- version = "1.1.3"
3
+ version = "1.1.4"
4
4
  description = "Generic MT5 data and execution infrastructure for Python applications"
5
5
  authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
6
6
  maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
@@ -9,7 +9,7 @@ license-files = ["LICENSE"]
9
9
  readme = "README.md"
10
10
  requires-python = ">= 3.11, < 3.14"
11
11
  dependencies = [
12
- "pdmt5 >= 1.1.0",
12
+ "pdmt5 >= 1.1.1",
13
13
  "pandas >= 2.2.2",
14
14
  "pydantic >= 2.13.4",
15
15
  "click >= 8.1.0",
@@ -603,6 +603,15 @@ class TestHelpText:
603
603
  assert "--dry-run" in output
604
604
  assert "--yes" in output
605
605
 
606
+ def test_close_positions_help_mentions_filling_mode(self) -> None:
607
+ """close-positions help must document the --filling-mode option."""
608
+ result = runner.invoke(
609
+ app,
610
+ ["-o", "out.csv", "close-positions", "--help"],
611
+ )
612
+ assert result.exit_code == 0
613
+ assert "--filling-mode" in normalize_cli_output(result.output)
614
+
606
615
 
607
616
  # ---------------------------------------------------------------------------
608
617
  # close-positions command
@@ -798,6 +807,84 @@ class TestClosePositions:
798
807
  assert request["comment"] == "close-me"
799
808
  assert request["magic"] == 7
800
809
 
810
+ @pytest.mark.parametrize(
811
+ ("cli_value", "expected"),
812
+ [
813
+ pytest.param("ioc", 30, id="lowercase-ioc"),
814
+ pytest.param("IOC", 30, id="uppercase-ioc"),
815
+ pytest.param("fok", 31, id="lowercase-fok"),
816
+ pytest.param("FOK", 31, id="uppercase-fok"),
817
+ pytest.param("return", 32, id="lowercase-return"),
818
+ pytest.param("RETURN", 32, id="uppercase-return"),
819
+ pytest.param("Return", 32, id="mixed-case-return"),
820
+ ],
821
+ )
822
+ def test_close_positions_forwards_explicit_filling_mode(
823
+ self,
824
+ tmp_path: Path,
825
+ trading_client: MagicMock,
826
+ cli_value: str,
827
+ expected: int,
828
+ ) -> None:
829
+ """--filling-mode is normalized and forwarded without broker resolution."""
830
+ trading_client.mt5.ORDER_FILLING_FOK = 31
831
+ trading_client.mt5.ORDER_FILLING_RETURN = 32
832
+ output = tmp_path / "close.json"
833
+ result = runner.invoke(
834
+ app,
835
+ [
836
+ "-o",
837
+ str(output),
838
+ "close-positions",
839
+ "--symbol",
840
+ "JP225",
841
+ "--filling-mode",
842
+ cli_value,
843
+ "--dry-run",
844
+ ],
845
+ )
846
+ assert result.exit_code == 0, result.output
847
+ data = json.loads(output.read_text())
848
+ request = json.loads(data[0]["request"])
849
+ assert request["type_filling"] == expected
850
+ trading_client.symbol_info_as_dict.assert_not_called()
851
+
852
+ @pytest.mark.parametrize(
853
+ "cli_value",
854
+ ["BOGUS", "ioc ", " FOK", "GTC", ""],
855
+ ids=[
856
+ "unknown-word",
857
+ "trailing-space",
858
+ "leading-space",
859
+ "order-time-mode",
860
+ "empty",
861
+ ],
862
+ )
863
+ def test_close_positions_rejects_unknown_filling_mode(
864
+ self,
865
+ tmp_path: Path,
866
+ trading_client: MagicMock,
867
+ cli_value: str,
868
+ ) -> None:
869
+ """Unknown --filling-mode values fail closed before any close request."""
870
+ output = tmp_path / "close.json"
871
+ result = runner.invoke(
872
+ app,
873
+ [
874
+ "-o",
875
+ str(output),
876
+ "close-positions",
877
+ "--symbol",
878
+ "JP225",
879
+ "--filling-mode",
880
+ cli_value,
881
+ "--dry-run",
882
+ ],
883
+ )
884
+ assert result.exit_code != 0
885
+ assert "filling mode" in normalize_cli_output(result.output).lower()
886
+ trading_client.order_send.assert_not_called()
887
+
801
888
  def test_order_send_unchanged(
802
889
  self,
803
890
  tmp_path: Path,
@@ -73,6 +73,7 @@ def _mock_trade_client() -> MagicMock:
73
73
  client.mt5.ORDER_TIME_GTC = 40
74
74
  client.mt5.SYMBOL_TRADE_EXECUTION_MARKET = 3
75
75
  client.mt5.SYMBOL_TRADE_EXECUTION_REQUEST = 4
76
+ client.mt5.SYMBOL_TRADE_EXECUTION_INSTANT = 5
76
77
  client.mt5.TRADE_RETCODE_PLACED = 10008
77
78
  client.mt5.TRADE_RETCODE_DONE = 10009
78
79
  client.mt5.TRADE_RETCODE_DONE_PARTIAL = 10010
@@ -2434,6 +2435,49 @@ class TestVolumeAndExecution:
2434
2435
 
2435
2436
  assert result == "RETURN"
2436
2437
 
2438
+ @pytest.mark.parametrize(
2439
+ "execution_attr",
2440
+ ["SYMBOL_TRADE_EXECUTION_REQUEST", "SYMBOL_TRADE_EXECUTION_INSTANT"],
2441
+ ids=["request-execution", "instant-execution"],
2442
+ )
2443
+ @pytest.mark.parametrize(
2444
+ "filling_mode", [0, None], ids=["filling-mode-zero", "filling-mode-none"]
2445
+ )
2446
+ @pytest.mark.parametrize(
2447
+ ("preferred_modes", "expected"),
2448
+ [
2449
+ pytest.param(("IOC", "FOK"), "IOC", id="prefers-ioc"),
2450
+ pytest.param(("FOK", "IOC"), "FOK", id="prefers-fok"),
2451
+ ],
2452
+ )
2453
+ def test_resolve_broker_filling_mode_request_instant_execution_implies_ioc_fok(
2454
+ self,
2455
+ execution_attr: str,
2456
+ filling_mode: int | None,
2457
+ preferred_modes: tuple[str, str],
2458
+ expected: str,
2459
+ ) -> None:
2460
+ """Request/Instant execution supports IOC/FOK regardless of the mask.
2461
+
2462
+ MQL5 permits IOC and FOK for Request and Instant execution even when
2463
+ ``SYMBOL_FILLING_MODE`` carries no matching bits (that bitmask only
2464
+ governs Market/Exchange execution), so the resolver must honor
2465
+ whichever of IOC/FOK is preferred without requiring mask bits.
2466
+ """
2467
+ client = _mock_trade_client()
2468
+ client.symbol_info_as_dict.return_value = {
2469
+ "filling_mode": filling_mode,
2470
+ "trade_exemode": getattr(client.mt5, execution_attr),
2471
+ }
2472
+
2473
+ result = resolve_broker_filling_mode(
2474
+ client,
2475
+ symbol="EURUSD",
2476
+ preferred_modes=cast("Any", preferred_modes),
2477
+ )
2478
+
2479
+ assert result == expected
2480
+
2437
2481
  def test_resolve_broker_filling_mode_keeps_preferred_when_metadata_unparseable(
2438
2482
  self,
2439
2483
  caplog: pytest.LogCaptureFixture,
@@ -2545,6 +2589,114 @@ class TestVolumeAndExecution:
2545
2589
  assert result == []
2546
2590
  client.order_send.assert_not_called()
2547
2591
 
2592
+ def test_close_open_positions_resolves_filling_mode_per_symbol(self) -> None:
2593
+ """Default closes resolve the broker filling mode instead of assuming IOC."""
2594
+ client = _mock_trade_client()
2595
+ client.mt5.ORDER_FILLING_FOK = 31
2596
+ client.positions_get_as_df.return_value = pd.DataFrame(
2597
+ [
2598
+ {"ticket": 1, "symbol": "EURUSD", "type": 0, "volume": 0.1},
2599
+ {"ticket": 2, "symbol": "EURUSD", "type": 0, "volume": 0.2},
2600
+ ],
2601
+ )
2602
+ client.symbol_info_as_dict.return_value = {
2603
+ "filling_mode": client.mt5.SYMBOL_FILLING_FOK,
2604
+ "trade_exemode": client.mt5.SYMBOL_TRADE_EXECUTION_MARKET,
2605
+ }
2606
+ client.symbol_info_tick_as_dict.return_value = {"ask": 1.2, "bid": 1.1}
2607
+
2608
+ result = close_open_positions(client, symbols="EURUSD", dry_run=True)
2609
+
2610
+ assert [_request_from_result(r)["type_filling"] for r in result] == [31, 31]
2611
+ client.symbol_info_as_dict.assert_called_once()
2612
+
2613
+ def test_close_open_positions_resolves_filling_mode_per_distinct_symbol(
2614
+ self,
2615
+ ) -> None:
2616
+ """Each distinct symbol gets its own resolved mode with one lookup each."""
2617
+ client = _mock_trade_client()
2618
+ client.mt5.ORDER_FILLING_FOK = 31
2619
+ client.positions_get_as_df.return_value = pd.DataFrame(
2620
+ [
2621
+ {"ticket": 1, "symbol": "EURUSD", "type": 0, "volume": 0.1},
2622
+ {"ticket": 2, "symbol": "USDJPY", "type": 0, "volume": 0.2},
2623
+ {"ticket": 3, "symbol": "EURUSD", "type": 0, "volume": 0.3},
2624
+ ],
2625
+ )
2626
+ snapshots = {
2627
+ "EURUSD": {
2628
+ "filling_mode": client.mt5.SYMBOL_FILLING_FOK,
2629
+ "trade_exemode": client.mt5.SYMBOL_TRADE_EXECUTION_MARKET,
2630
+ },
2631
+ "USDJPY": {
2632
+ "filling_mode": (
2633
+ client.mt5.SYMBOL_FILLING_FOK | client.mt5.SYMBOL_FILLING_IOC
2634
+ ),
2635
+ "trade_exemode": client.mt5.SYMBOL_TRADE_EXECUTION_MARKET,
2636
+ },
2637
+ }
2638
+ client.symbol_info_as_dict.side_effect = (
2639
+ lambda symbol: snapshots[symbol] # pyright: ignore[reportUnknownLambdaType]
2640
+ )
2641
+ client.symbol_info_tick_as_dict.return_value = {"ask": 1.2, "bid": 1.1}
2642
+
2643
+ result = close_open_positions(client, dry_run=True)
2644
+
2645
+ # Order matches the source positions frame: EURUSD, USDJPY, EURUSD.
2646
+ type_fillings = [_request_from_result(r)["type_filling"] for r in result]
2647
+ assert [r["symbol"] for r in result] == ["EURUSD", "USDJPY", "EURUSD"]
2648
+ assert type_fillings == [31, 30, 31]
2649
+ # One symbol_info lookup per unique symbol, not per position.
2650
+ assert client.symbol_info_as_dict.call_count == 2
2651
+
2652
+ def test_close_open_positions_keeps_ioc_when_supported(self) -> None:
2653
+ """Symbols advertising IOC support keep the IOC close default."""
2654
+ client = _mock_trade_client()
2655
+ client.positions_get_as_df.return_value = pd.DataFrame(
2656
+ [{"ticket": 1, "symbol": "EURUSD", "type": 0, "volume": 0.1}],
2657
+ )
2658
+ client.symbol_info_as_dict.return_value = {
2659
+ "filling_mode": (
2660
+ client.mt5.SYMBOL_FILLING_FOK | client.mt5.SYMBOL_FILLING_IOC
2661
+ ),
2662
+ "trade_exemode": client.mt5.SYMBOL_TRADE_EXECUTION_MARKET,
2663
+ }
2664
+ client.symbol_info_tick_as_dict.return_value = {"ask": 1.2, "bid": 1.1}
2665
+
2666
+ result = close_open_positions(client, symbols="EURUSD", dry_run=True)
2667
+
2668
+ assert _request_from_result(result[0])["type_filling"] == 30
2669
+
2670
+ @pytest.mark.parametrize(
2671
+ ("order_filling_mode", "expected"),
2672
+ [
2673
+ pytest.param("IOC", 30, id="ioc"),
2674
+ pytest.param("FOK", 31, id="fok"),
2675
+ pytest.param("RETURN", 32, id="return"),
2676
+ ],
2677
+ )
2678
+ def test_close_open_positions_forwards_explicit_filling_mode(
2679
+ self, order_filling_mode: str, expected: int
2680
+ ) -> None:
2681
+ """An explicit order_filling_mode bypasses broker resolution."""
2682
+ client = _mock_trade_client()
2683
+ client.mt5.ORDER_FILLING_FOK = 31
2684
+ client.mt5.ORDER_FILLING_RETURN = 32
2685
+ client.positions_get_as_df.return_value = pd.DataFrame(
2686
+ [{"ticket": 1, "symbol": "EURUSD", "type": 0, "volume": 0.1}],
2687
+ )
2688
+ client.symbol_info_tick_as_dict.return_value = {"ask": 1.2, "bid": 1.1}
2689
+
2690
+ result = close_open_positions(
2691
+ client,
2692
+ symbols="EURUSD",
2693
+ order_filling_mode=cast("Any", order_filling_mode),
2694
+ dry_run=True,
2695
+ )
2696
+
2697
+ assert _request_from_result(result[0])["type_filling"] == expected
2698
+ client.symbol_info_as_dict.assert_not_called()
2699
+
2548
2700
  def test_filter_positions_magic_is_fail_closed_without_magic_column(self) -> None:
2549
2701
  """Test direct magic filtering fails closed when the DataFrame lacks magic."""
2550
2702
  positions = pd.DataFrame([{"ticket": 1, "symbol": "EURUSD"}])
@@ -499,7 +499,7 @@ wheels = [
499
499
 
500
500
  [[package]]
501
501
  name = "mt5cli"
502
- version = "1.1.3"
502
+ version = "1.1.4"
503
503
  source = { editable = "." }
504
504
  dependencies = [
505
505
  { name = "click" },
@@ -543,7 +543,7 @@ requires-dist = [
543
543
  { name = "opentelemetry-exporter-otlp-proto-http", marker = "extra == 'otel'" },
544
544
  { name = "opentelemetry-sdk", marker = "extra == 'otel'" },
545
545
  { name = "pandas", specifier = ">=2.2.2" },
546
- { name = "pdmt5", specifier = ">=1.1.0" },
546
+ { name = "pdmt5", specifier = ">=1.1.1" },
547
547
  { name = "pyarrow", marker = "extra == 'parquet'", specifier = ">=19.0.0" },
548
548
  { name = "pydantic", specifier = ">=2.13.4" },
549
549
  { name = "typer", specifier = ">=0.15.0" },
@@ -800,16 +800,16 @@ wheels = [
800
800
 
801
801
  [[package]]
802
802
  name = "pdmt5"
803
- version = "1.1.0"
803
+ version = "1.1.1"
804
804
  source = { registry = "https://pypi.org/simple" }
805
805
  dependencies = [
806
806
  { name = "metatrader5", marker = "sys_platform == 'win32'" },
807
807
  { name = "pandas" },
808
808
  { name = "pydantic" },
809
809
  ]
810
- sdist = { url = "https://files.pythonhosted.org/packages/35/7a/7caafb93b74ccaa60a1c844948698bed714e04aa3be292a05fd71864faa0/pdmt5-1.1.0.tar.gz", hash = "sha256:fcdab1924e001204ae776dd2afc1f150fe01e6944cba99dae72af23bc99137c6", size = 22160, upload-time = "2026-07-04T04:41:51.476Z" }
810
+ sdist = { url = "https://files.pythonhosted.org/packages/4d/9c/4c51e9b109440629762a5ca719c367798469bb590b8ce99aa0652c467549/pdmt5-1.1.1.tar.gz", hash = "sha256:2d8e4e9b87d237793f8534813b373545eadb5c0e1ced13ee8be8dc473c77190d", size = 23879, upload-time = "2026-07-07T15:50:19.604Z" }
811
811
  wheels = [
812
- { url = "https://files.pythonhosted.org/packages/6f/80/1062ea4e4c82de81d91ba8bb43b1a6d4903771010d87eca15d5d18d827df/pdmt5-1.1.0-py3-none-any.whl", hash = "sha256:603273065814824680d6ec8847139b3029c7dd04da7c25868ad7f4ede55a9acf", size = 20489, upload-time = "2026-07-04T04:41:49.999Z" },
812
+ { url = "https://files.pythonhosted.org/packages/8f/18/4221deef65dc320da9b6299e1914c36c1e81b29a5557f0c1083718b36545/pdmt5-1.1.1-py3-none-any.whl", hash = "sha256:9df16148ebec754c15ce3d1b02af82aaeec3c5cb1603a47258fa68c74f45b453", size = 22274, upload-time = "2026-07-07T15:50:18.489Z" },
813
813
  ]
814
814
 
815
815
  [[package]]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes