This commit is contained in:
@@ -1,12 +1,28 @@
|
|||||||
# Public API Contract
|
# Public API Contract
|
||||||
|
|
||||||
mt5cli is the generic MT5 data and execution infrastructure layer for downstream
|
mt5cli is the canonical operational trading SDK and CLI/batch layer over pdmt5.
|
||||||
Python applications. The intended dependency direction is:
|
The intended dependency direction is:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
downstream app -> mt5cli -> pdmt5 -> MetaTrader 5
|
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
|
Downstream packages should import from the package root (`from mt5cli import
|
||||||
...`) and use the public tier sets in `mt5cli.contract` to distinguish API
|
...`) and use the public tier sets in `mt5cli.contract` to distinguish API
|
||||||
stability. CLI commands mirror the same behavior but are not importable Python
|
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;
|
and returned as `status="failed"` with normalized `request` / `response` details;
|
||||||
`dry_run=True` never calls `ensure_symbol_selected()` or `order_send()`.
|
`dry_run=True` never calls `ensure_symbol_selected()` or `order_send()`.
|
||||||
|
|
||||||
### Errors and MT5 type re-exports
|
### Errors
|
||||||
|
|
||||||
| Symbol | Role |
|
| Symbol | Role |
|
||||||
| ------------------------------------------------------------------------------------ | ----------------------------------------------- |
|
| ------------------------------------------------------------------------------------ | -------------------------------------------- |
|
||||||
| `Mt5CliError`, `Mt5ConnectionError`, `Mt5OperationError`, `Mt5SchemaError` | Stable mt5cli exception types |
|
| `Mt5CliError`, `Mt5ConnectionError`, `Mt5OperationError`, `Mt5SchemaError` | Stable mt5cli exception types |
|
||||||
| `normalize_mt5_exception`, `call_with_normalized_errors`, `is_recoverable_mt5_error` | Error normalization and retry classification |
|
| `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 |
|
|
||||||
|
|
||||||
## Secondary public exports
|
## Secondary public exports
|
||||||
|
|
||||||
@@ -176,7 +191,7 @@ the MT5 SDK helper.
|
|||||||
| Export helpers | `detect_format`, `export_dataframe`, `export_dataframe_to_sqlite` |
|
| Export helpers | `detect_format`, `export_dataframe`, `export_dataframe_to_sqlite` |
|
||||||
| Symbol parsing | `normalize_symbol`, `normalize_symbols` |
|
| Symbol parsing | `normalize_symbol`, `normalize_symbols` |
|
||||||
| Time parsing | `ensure_utc`, `parse_date_range`, `parse_datetime`, `recent_window` |
|
| 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` |
|
| Trading data shapes | `POSITION_COLUMNS` |
|
||||||
|
|
||||||
## CLI commands
|
## CLI commands
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ strategy responsibilities.
|
|||||||
|
|
||||||
from importlib.metadata import version
|
from importlib.metadata import version
|
||||||
|
|
||||||
from pdmt5 import Mt5Config, Mt5RuntimeError, Mt5TradingClient, Mt5TradingError
|
|
||||||
|
|
||||||
from .client import MT5Client, build_config, mt5_session
|
from .client import MT5Client, build_config, mt5_session
|
||||||
from .contract import (
|
from .contract import (
|
||||||
PUBLIC_EXPORT_TIERS,
|
PUBLIC_EXPORT_TIERS,
|
||||||
@@ -152,8 +150,6 @@ from .trading import (
|
|||||||
update_trailing_stop_loss_for_open_positions,
|
update_trailing_stop_loss_for_open_positions,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
TICK_FLAG_MAP,
|
|
||||||
TIMEFRAME_MAP,
|
|
||||||
parse_datetime,
|
parse_datetime,
|
||||||
parse_tick_flags,
|
parse_tick_flags,
|
||||||
parse_timeframe,
|
parse_timeframe,
|
||||||
@@ -169,8 +165,6 @@ __all__ = [
|
|||||||
"REQUIRED_COLUMNS",
|
"REQUIRED_COLUMNS",
|
||||||
"SECONDARY_PUBLIC_EXPORTS",
|
"SECONDARY_PUBLIC_EXPORTS",
|
||||||
"STABLE_SDK_EXPORTS",
|
"STABLE_SDK_EXPORTS",
|
||||||
"TICK_FLAG_MAP",
|
|
||||||
"TIMEFRAME_MAP",
|
|
||||||
"TIME_COLUMNS",
|
"TIME_COLUMNS",
|
||||||
"AccountSpec",
|
"AccountSpec",
|
||||||
"DataKind",
|
"DataKind",
|
||||||
@@ -180,13 +174,9 @@ __all__ = [
|
|||||||
"MT5Client",
|
"MT5Client",
|
||||||
"MarginVolume",
|
"MarginVolume",
|
||||||
"Mt5CliError",
|
"Mt5CliError",
|
||||||
"Mt5Config",
|
|
||||||
"Mt5ConnectionError",
|
"Mt5ConnectionError",
|
||||||
"Mt5OperationError",
|
"Mt5OperationError",
|
||||||
"Mt5RuntimeError",
|
|
||||||
"Mt5SchemaError",
|
"Mt5SchemaError",
|
||||||
"Mt5TradingClient",
|
|
||||||
"Mt5TradingError",
|
|
||||||
"OrderExecutionResult",
|
"OrderExecutionResult",
|
||||||
"OrderFillingMode",
|
"OrderFillingMode",
|
||||||
"OrderLimits",
|
"OrderLimits",
|
||||||
|
|||||||
@@ -6,13 +6,9 @@ STABLE_SDK_EXPORTS: frozenset[str] = frozenset({
|
|||||||
"AccountSpec",
|
"AccountSpec",
|
||||||
"MT5Client",
|
"MT5Client",
|
||||||
"Mt5CliError",
|
"Mt5CliError",
|
||||||
"Mt5Config",
|
|
||||||
"Mt5ConnectionError",
|
"Mt5ConnectionError",
|
||||||
"Mt5OperationError",
|
"Mt5OperationError",
|
||||||
"Mt5RuntimeError",
|
|
||||||
"Mt5SchemaError",
|
"Mt5SchemaError",
|
||||||
"Mt5TradingClient",
|
|
||||||
"Mt5TradingError",
|
|
||||||
"OrderFillingMode",
|
"OrderFillingMode",
|
||||||
"OrderSide",
|
"OrderSide",
|
||||||
"OrderTimeMode",
|
"OrderTimeMode",
|
||||||
@@ -93,8 +89,6 @@ SECONDARY_PUBLIC_EXPORTS: frozenset[str] = frozenset({
|
|||||||
"KNOWN_MT5_TIME_COLUMNS",
|
"KNOWN_MT5_TIME_COLUMNS",
|
||||||
"POSITION_COLUMNS",
|
"POSITION_COLUMNS",
|
||||||
"REQUIRED_COLUMNS",
|
"REQUIRED_COLUMNS",
|
||||||
"TICK_FLAG_MAP",
|
|
||||||
"TIMEFRAME_MAP",
|
|
||||||
"TIME_COLUMNS",
|
"TIME_COLUMNS",
|
||||||
"account_info",
|
"account_info",
|
||||||
"collect_latest_rates",
|
"collect_latest_rates",
|
||||||
|
|||||||
+4
-6
@@ -10,7 +10,8 @@ from pathlib import Path
|
|||||||
from typing import TYPE_CHECKING, Any, TypeGuard
|
from typing import TYPE_CHECKING, Any, TypeGuard
|
||||||
|
|
||||||
import click
|
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_copy_ticks as _parse_copy_ticks
|
||||||
from pdmt5 import parse_timeframe as _parse_timeframe
|
from pdmt5 import parse_timeframe as _parse_timeframe
|
||||||
|
|
||||||
@@ -23,14 +24,11 @@ if TYPE_CHECKING:
|
|||||||
# Constants
|
# 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(
|
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(
|
_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] = {
|
_FORMAT_EXTENSIONS: dict[str, str] = {
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ license-files = ["LICENSE"]
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">= 3.11, < 3.14"
|
requires-python = ">= 3.11, < 3.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pdmt5>=0.3.0",
|
"pdmt5>=1.0.0",
|
||||||
"click >= 8.1.0",
|
"click >= 8.1.0",
|
||||||
"typer >= 0.15.0",
|
"typer >= 0.15.0",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -835,6 +835,28 @@ class TestStableSdkContract:
|
|||||||
assert "close" in result.columns
|
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
|
# Packaging metadata
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ from pytest_mock import MockerFixture # noqa: TC002
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pdmt5 import TIMEFRAME_MAP
|
||||||
|
|
||||||
from mt5cli import history
|
from mt5cli import history
|
||||||
from mt5cli.history import (
|
from mt5cli.history import (
|
||||||
DEFAULT_HISTORY_TIMEFRAMES,
|
DEFAULT_HISTORY_TIMEFRAMES,
|
||||||
@@ -58,7 +60,7 @@ from mt5cli.history import (
|
|||||||
write_rates_dataset,
|
write_rates_dataset,
|
||||||
write_streamed_frame,
|
write_streamed_frame,
|
||||||
)
|
)
|
||||||
from mt5cli.utils import TIMEFRAME_MAP, Dataset, IfExists
|
from mt5cli.utils import Dataset, IfExists
|
||||||
|
|
||||||
|
|
||||||
class TestResolveRateViewName:
|
class TestResolveRateViewName:
|
||||||
|
|||||||
+8
-12
@@ -11,15 +11,15 @@ from typing import TYPE_CHECKING
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import mt5cli.utils
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from mt5cli.utils import (
|
from mt5cli.utils import (
|
||||||
DATETIME_TYPE,
|
DATETIME_TYPE,
|
||||||
REQUEST_TYPE,
|
REQUEST_TYPE,
|
||||||
TICK_FLAG_MAP,
|
|
||||||
TICK_FLAGS_TYPE,
|
TICK_FLAGS_TYPE,
|
||||||
TIMEFRAME_MAP,
|
|
||||||
TIMEFRAME_TYPE,
|
TIMEFRAME_TYPE,
|
||||||
Dataset,
|
Dataset,
|
||||||
IfExists,
|
IfExists,
|
||||||
@@ -373,17 +373,13 @@ class TestParseRequest:
|
|||||||
class TestConstants:
|
class TestConstants:
|
||||||
"""Tests for module constants."""
|
"""Tests for module constants."""
|
||||||
|
|
||||||
def test_timeframe_map_has_expected_keys(self) -> None:
|
def test_timeframe_map_is_private_in_utils(self) -> None:
|
||||||
"""Test that TIMEFRAME_MAP contains standard timeframes."""
|
"""TIMEFRAME_MAP is a private implementation detail; not a public attribute."""
|
||||||
for key in ("M1", "M5", "M15", "M30", "H1", "H4", "D1", "W1", "MN1"):
|
assert not hasattr(mt5cli.utils, "TIMEFRAME_MAP")
|
||||||
assert key in TIMEFRAME_MAP
|
|
||||||
|
|
||||||
def test_tick_flag_map_has_expected_keys(self) -> None:
|
def test_tick_flag_map_absent_from_utils(self) -> None:
|
||||||
"""Test that TICK_FLAG_MAP contains standard flags with MT5 values."""
|
"""TICK_FLAG_MAP is not exposed by mt5cli.utils."""
|
||||||
assert {"ALL", "INFO", "TRADE"} <= set(TICK_FLAG_MAP)
|
assert not hasattr(mt5cli.utils, "TICK_FLAG_MAP")
|
||||||
assert TICK_FLAG_MAP["ALL"] == -1
|
|
||||||
assert TICK_FLAG_MAP["INFO"] == 1
|
|
||||||
assert TICK_FLAG_MAP["TRADE"] == 2
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("dataset", "expected"),
|
("dataset", "expected"),
|
||||||
|
|||||||
@@ -518,7 +518,7 @@ dev = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "click", specifier = ">=8.1.0" },
|
{ 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 = "pyarrow", marker = "extra == 'parquet'", specifier = ">=19.0.0" },
|
||||||
{ name = "typer", specifier = ">=0.15.0" },
|
{ name = "typer", specifier = ">=0.15.0" },
|
||||||
]
|
]
|
||||||
@@ -691,16 +691,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdmt5"
|
name = "pdmt5"
|
||||||
version = "0.3.0"
|
version = "1.0.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "metatrader5", marker = "sys_platform == 'win32'" },
|
{ name = "metatrader5", marker = "sys_platform == 'win32'" },
|
||||||
{ name = "pandas" },
|
{ name = "pandas" },
|
||||||
{ name = "pydantic" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user