diff --git a/docs/api/public-contract.md b/docs/api/public-contract.md index 1922abf..3b9fe67 100644 --- a/docs/api/public-contract.md +++ b/docs/api/public-contract.md @@ -1,12 +1,28 @@ # Public API Contract -mt5cli is the generic MT5 data and execution infrastructure layer for downstream -Python applications. The intended dependency direction is: +mt5cli is the canonical operational trading SDK and CLI/batch layer over pdmt5. +The intended dependency direction is: ```text downstream app -> mt5cli -> pdmt5 -> MetaTrader 5 ``` +## Responsibility boundary + +| Layer | Owns | +| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **pdmt5** | MT5 core wrapper; DataFrame/dict conversion; canonical MT5 constants and parsers; direct low-level order primitives | +| **mt5cli** | CLI/batch workflows; SQLite history collection; normalized datasets; closed-bar helpers; small downstream operational SDK; generic broker-facing margin/volume/order orchestration | +| **downstream** | Strategy logic; signals; risk policy; backtesting; optimization; YAML/application semantics | + +Downstream code should import raw pdmt5 types and constants (such as +`Mt5Config`, `Mt5TradingClient`, `Mt5RuntimeError`, `Mt5TradingError`, +`TIMEFRAME_MAP`, `COPY_TICKS_MAP`) directly from `pdmt5` when needed. +mt5cli does not serve as a pass-through compatibility namespace for pdmt5. + +Note: the former `mt5cli` re-export `TICK_FLAG_MAP` corresponds to `COPY_TICKS_MAP` +in pdmt5 — the name changed, it was not simply moved. + Downstream packages should import from the package root (`from mt5cli import ...`) and use the public tier sets in `mt5cli.contract` to distinguish API stability. CLI commands mirror the same behavior but are not importable Python @@ -133,13 +149,12 @@ sending requests. Failed, malformed, or unknown broker retcodes are fail-closed and returned as `status="failed"` with normalized `request` / `response` details; `dry_run=True` never calls `ensure_symbol_selected()` or `order_send()`. -### Errors and MT5 type re-exports +### Errors -| Symbol | Role | -| ------------------------------------------------------------------------------------ | ----------------------------------------------- | -| `Mt5CliError`, `Mt5ConnectionError`, `Mt5OperationError`, `Mt5SchemaError` | Stable mt5cli exception types | -| `normalize_mt5_exception`, `call_with_normalized_errors`, `is_recoverable_mt5_error` | Error normalization and retry classification | -| `Mt5Config`, `Mt5RuntimeError`, `Mt5TradingClient`, `Mt5TradingError` | Re-exported pdmt5 types for adapter convenience | +| Symbol | Role | +| ------------------------------------------------------------------------------------ | -------------------------------------------- | +| `Mt5CliError`, `Mt5ConnectionError`, `Mt5OperationError`, `Mt5SchemaError` | Stable mt5cli exception types | +| `normalize_mt5_exception`, `call_with_normalized_errors`, `is_recoverable_mt5_error` | Error normalization and retry classification | ## Secondary public exports @@ -176,7 +191,7 @@ the MT5 SDK helper. | Export helpers | `detect_format`, `export_dataframe`, `export_dataframe_to_sqlite` | | Symbol parsing | `normalize_symbol`, `normalize_symbols` | | Time parsing | `ensure_utc`, `parse_date_range`, `parse_datetime`, `recent_window` | -| MT5 parsing maps | `granularity_name`, `parse_tick_flags`, `parse_timeframe`, `TICK_FLAG_MAP`, `TIMEFRAME_MAP` | +| MT5 parsing maps | `granularity_name`, `parse_tick_flags`, `parse_timeframe` | | Trading data shapes | `POSITION_COLUMNS` | ## CLI commands diff --git a/mt5cli/__init__.py b/mt5cli/__init__.py index 70829a7..535588c 100644 --- a/mt5cli/__init__.py +++ b/mt5cli/__init__.py @@ -8,8 +8,6 @@ strategy responsibilities. from importlib.metadata import version -from pdmt5 import Mt5Config, Mt5RuntimeError, Mt5TradingClient, Mt5TradingError - from .client import MT5Client, build_config, mt5_session from .contract import ( PUBLIC_EXPORT_TIERS, @@ -152,8 +150,6 @@ from .trading import ( update_trailing_stop_loss_for_open_positions, ) from .utils import ( - TICK_FLAG_MAP, - TIMEFRAME_MAP, parse_datetime, parse_tick_flags, parse_timeframe, @@ -169,8 +165,6 @@ __all__ = [ "REQUIRED_COLUMNS", "SECONDARY_PUBLIC_EXPORTS", "STABLE_SDK_EXPORTS", - "TICK_FLAG_MAP", - "TIMEFRAME_MAP", "TIME_COLUMNS", "AccountSpec", "DataKind", @@ -180,13 +174,9 @@ __all__ = [ "MT5Client", "MarginVolume", "Mt5CliError", - "Mt5Config", "Mt5ConnectionError", "Mt5OperationError", - "Mt5RuntimeError", "Mt5SchemaError", - "Mt5TradingClient", - "Mt5TradingError", "OrderExecutionResult", "OrderFillingMode", "OrderLimits", diff --git a/mt5cli/contract.py b/mt5cli/contract.py index 447e23d..83d903d 100644 --- a/mt5cli/contract.py +++ b/mt5cli/contract.py @@ -6,13 +6,9 @@ STABLE_SDK_EXPORTS: frozenset[str] = frozenset({ "AccountSpec", "MT5Client", "Mt5CliError", - "Mt5Config", "Mt5ConnectionError", "Mt5OperationError", - "Mt5RuntimeError", "Mt5SchemaError", - "Mt5TradingClient", - "Mt5TradingError", "OrderFillingMode", "OrderSide", "OrderTimeMode", @@ -93,8 +89,6 @@ SECONDARY_PUBLIC_EXPORTS: frozenset[str] = frozenset({ "KNOWN_MT5_TIME_COLUMNS", "POSITION_COLUMNS", "REQUIRED_COLUMNS", - "TICK_FLAG_MAP", - "TIMEFRAME_MAP", "TIME_COLUMNS", "account_info", "collect_latest_rates", diff --git a/mt5cli/utils.py b/mt5cli/utils.py index cf60e7e..f43fe5e 100644 --- a/mt5cli/utils.py +++ b/mt5cli/utils.py @@ -10,7 +10,8 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, TypeGuard import click -from pdmt5 import COPY_TICKS_MAP, TIMEFRAME_MAP +from pdmt5 import COPY_TICKS_MAP as _COPY_TICKS_MAP +from pdmt5 import TIMEFRAME_MAP as _TIMEFRAME_MAP from pdmt5 import parse_copy_ticks as _parse_copy_ticks from pdmt5 import parse_timeframe as _parse_timeframe @@ -23,14 +24,11 @@ if TYPE_CHECKING: # Constants # --------------------------------------------------------------------------- -# Backward-compatible snapshot; prefer ``COPY_TICKS_MAP`` from pdmt5 directly. -TICK_FLAG_MAP: dict[str, int] = dict(COPY_TICKS_MAP) - TIMEFRAME_NAMES: tuple[str, ...] = tuple( - name for name in TIMEFRAME_MAP if not name.startswith("TIMEFRAME_") + name for name in _TIMEFRAME_MAP if not name.startswith("TIMEFRAME_") ) _TICK_FLAG_NAMES: tuple[str, ...] = tuple( - name for name in COPY_TICKS_MAP if not name.startswith("COPY_TICKS_") + name for name in _COPY_TICKS_MAP if not name.startswith("COPY_TICKS_") ) _FORMAT_EXTENSIONS: dict[str, str] = { diff --git a/pyproject.toml b/pyproject.toml index 1f85b56..62d43c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ license-files = ["LICENSE"] readme = "README.md" requires-python = ">= 3.11, < 3.14" dependencies = [ - "pdmt5>=0.3.0", + "pdmt5>=1.0.0", "click >= 8.1.0", "typer >= 0.15.0", ] diff --git a/tests/test_contracts.py b/tests/test_contracts.py index f09a729..1394f2f 100644 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -835,6 +835,28 @@ class TestStableSdkContract: assert "close" in result.columns +@pytest.mark.parametrize( + "name", + [ + "Mt5Config", + "Mt5RuntimeError", + "Mt5TradingClient", + "Mt5TradingError", + "TICK_FLAG_MAP", + "TIMEFRAME_MAP", + ], +) +def test_pdmt5_pass_through_names_removed_from_public_contract(name: str) -> None: + """Removed pdmt5 pass-through names are not part of the public contract.""" + assert name not in STABLE_SDK_EXPORTS, ( + f"{name!r} should not be in STABLE_SDK_EXPORTS" + ) + assert name not in SECONDARY_PUBLIC_EXPORTS, ( + f"{name!r} should not be in SECONDARY_PUBLIC_EXPORTS" + ) + assert name not in mt5cli.__all__, f"{name!r} should not be in mt5cli.__all__" + + # --------------------------------------------------------------------------- # Packaging metadata # --------------------------------------------------------------------------- diff --git a/tests/test_history.py b/tests/test_history.py index 3041d67..10ba1fb 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -15,6 +15,8 @@ from pytest_mock import MockerFixture # noqa: TC002 if TYPE_CHECKING: from pathlib import Path +from pdmt5 import TIMEFRAME_MAP + from mt5cli import history from mt5cli.history import ( DEFAULT_HISTORY_TIMEFRAMES, @@ -58,7 +60,7 @@ from mt5cli.history import ( write_rates_dataset, write_streamed_frame, ) -from mt5cli.utils import TIMEFRAME_MAP, Dataset, IfExists +from mt5cli.utils import Dataset, IfExists class TestResolveRateViewName: diff --git a/tests/test_utils.py b/tests/test_utils.py index 3b2968f..415335a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -11,15 +11,15 @@ from typing import TYPE_CHECKING import pandas as pd import pytest +import mt5cli.utils + if TYPE_CHECKING: from pathlib import Path from mt5cli.utils import ( DATETIME_TYPE, REQUEST_TYPE, - TICK_FLAG_MAP, TICK_FLAGS_TYPE, - TIMEFRAME_MAP, TIMEFRAME_TYPE, Dataset, IfExists, @@ -373,17 +373,13 @@ class TestParseRequest: class TestConstants: """Tests for module constants.""" - def test_timeframe_map_has_expected_keys(self) -> None: - """Test that TIMEFRAME_MAP contains standard timeframes.""" - for key in ("M1", "M5", "M15", "M30", "H1", "H4", "D1", "W1", "MN1"): - assert key in TIMEFRAME_MAP + def test_timeframe_map_is_private_in_utils(self) -> None: + """TIMEFRAME_MAP is a private implementation detail; not a public attribute.""" + assert not hasattr(mt5cli.utils, "TIMEFRAME_MAP") - def test_tick_flag_map_has_expected_keys(self) -> None: - """Test that TICK_FLAG_MAP contains standard flags with MT5 values.""" - assert {"ALL", "INFO", "TRADE"} <= set(TICK_FLAG_MAP) - assert TICK_FLAG_MAP["ALL"] == -1 - assert TICK_FLAG_MAP["INFO"] == 1 - assert TICK_FLAG_MAP["TRADE"] == 2 + def test_tick_flag_map_absent_from_utils(self) -> None: + """TICK_FLAG_MAP is not exposed by mt5cli.utils.""" + assert not hasattr(mt5cli.utils, "TICK_FLAG_MAP") @pytest.mark.parametrize( ("dataset", "expected"), diff --git a/uv.lock b/uv.lock index 7fd391b..6000eec 100644 --- a/uv.lock +++ b/uv.lock @@ -518,7 +518,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.1.0" }, - { name = "pdmt5", specifier = ">=0.3.0" }, + { name = "pdmt5", specifier = ">=1.0.0" }, { name = "pyarrow", marker = "extra == 'parquet'", specifier = ">=19.0.0" }, { name = "typer", specifier = ">=0.15.0" }, ] @@ -691,16 +691,16 @@ wheels = [ [[package]] name = "pdmt5" -version = "0.3.0" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "metatrader5", marker = "sys_platform == 'win32'" }, { name = "pandas" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/cc/c8fa3a01e0e34178fec8527992f7bb8eda5881477ce23aaacaa9b2ef7bec/pdmt5-0.3.0.tar.gz", hash = "sha256:bb612d5c2695eafac9b2a7b74756e13bd383d7e5517bd90c9a2efa92492c484c", size = 215100, upload-time = "2026-06-11T13:26:46.976Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/6d/b51d2d0ec4636e914210be03a7da20e5078bc8cd7a351edd13bc30b7d2b1/pdmt5-1.0.0.tar.gz", hash = "sha256:ba53a1a5db41fdf4c022ef55353f5a4f6884f625c9310de2b0ddb20457956716", size = 123155, upload-time = "2026-06-25T23:41:50.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/03/b12cc4c9db983d971c9172b3765161b6d91136d0624e6718a04dd815e7a1/pdmt5-0.3.0-py3-none-any.whl", hash = "sha256:5388b406cc583202600cfe22c9d781679b1d931b1ed5a2b5dcf37c566149b49f", size = 26250, upload-time = "2026-06-11T13:26:45.689Z" }, + { url = "https://files.pythonhosted.org/packages/5e/38/27b712c572d8146efddf571a0e4e7b6d2314a335eb0f47dec1f499ab2189/pdmt5-1.0.0-py3-none-any.whl", hash = "sha256:f969c17902f9ffcbf56d7ccf9285aab39ececd676fcca50c094abcf9d728d517", size = 23992, upload-time = "2026-06-25T23:41:49.067Z" }, ] [[package]]