Drop Mt5TradingError support; require pdmt5>=1.0.4 (#101)

* chore: upgrade pdmt5 to v1.0.4

pdmt5 1.0.4 removes Mt5TradingClient/Mt5TradingError entirely and wraps
Mt5Config.password in pydantic SecretStr. Drop the now-dead conditional
Mt5TradingError handling in mt5cli.exceptions/sdk/retry (mt5cli already
type-checks trading clients against its own protocol, so no functional
change), and unwrap SecretStr when forwarding a base config's password to
per-account configs. Update tests and docs accordingly.

* chore: declare pydantic as a direct runtime dependency

mt5cli.sdk imports SecretStr directly from pydantic, so pin it explicitly
instead of relying on pdmt5's transitive dependency.

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Daichi Narushima
2026-07-04 01:30:32 +09:00
committed by GitHub
parent efc0de230a
commit c2cf0656dd
10 changed files with 173 additions and 173 deletions
+3 -5
View File
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
import pandas as pd
import pytest
from pdmt5 import Mt5RuntimeError, Mt5TradingError
from pdmt5 import Mt5RuntimeError
from pytest_mock import MockerFixture # noqa: TC002
import mt5cli
@@ -26,7 +26,6 @@ from mt5cli import (
MT5Client,
Mt5CliError,
Mt5ConnectionError,
Mt5OperationError,
Mt5SchemaError,
OrderExecutionResult,
OrderLimits,
@@ -247,7 +246,7 @@ def test_granularity_name_maps_timeframe_alias() -> None:
@pytest.mark.parametrize(
"exc",
[Mt5RuntimeError("init failed"), Mt5TradingError("trade failed")],
[Mt5RuntimeError("init failed")],
)
def test_is_recoverable_mt5_error(exc: Exception) -> None:
"""Recoverable MT5 errors are classified consistently."""
@@ -258,12 +257,11 @@ def test_is_recoverable_mt5_error(exc: Exception) -> None:
("exc", "expected_type"),
[
(Mt5RuntimeError("x"), Mt5ConnectionError),
(Mt5TradingError("x"), Mt5OperationError),
],
)
def test_normalize_mt5_exception_maps_types(
exc: Exception,
expected_type: type[Mt5ConnectionError | Mt5OperationError],
expected_type: type[Mt5ConnectionError],
) -> None:
"""MT5 exceptions map to stable mt5cli types."""
assert isinstance(normalize_mt5_exception(exc), expected_type)
+12 -5
View File
@@ -10,7 +10,8 @@ from unittest.mock import MagicMock, call
import pandas as pd
import pytest
from pdmt5 import Mt5RuntimeError, Mt5TradingError
from pdmt5 import Mt5RuntimeError
from pydantic import SecretStr
from pytest_mock import MockerFixture # noqa: TC002
if TYPE_CHECKING:
@@ -1459,7 +1460,9 @@ class TestCollectLatestRatesForAccounts:
return mock_client
mocker.patch("mt5cli.sdk.Mt5DataClient", side_effect=_record_config)
base = build_config(login=999, server="Base-Server", timeout=5000)
base = build_config(
login=999, server="Base-Server", timeout=5000, password="base-pass"
)
accounts = [
AccountSpec(symbols=["EURUSD"], login="", server="Acct-Server"),
]
@@ -1471,6 +1474,8 @@ class TestCollectLatestRatesForAccounts:
assert config.login == 999
assert config.server == "Acct-Server"
assert config.timeout == 5000
assert config.password is not None
assert config.password.get_secret_value() == "base-pass" # type: ignore[union-attr]
@pytest.mark.parametrize(
("accounts", "timeframes", "count", "match"),
@@ -1551,7 +1556,7 @@ class TestCollectLatestRatesForAccountsWithRetries:
wrapped = mocker.patch(
"mt5cli.sdk.collect_latest_rates_for_accounts",
side_effect=[
Mt5TradingError("boom"),
Mt5RuntimeError("boom"),
Mt5RuntimeError("boom"),
expected,
],
@@ -2125,7 +2130,10 @@ class TestBuildConfigWholeDollarEnv:
config = build_config(**{field: f"${env_var}"}, allow_whole_dollar_env=True) # type: ignore[arg-type]
assert getattr(config, field) == env_value
actual = getattr(config, field)
if isinstance(actual, SecretStr):
actual = actual.get_secret_value()
assert actual == env_value
def test_build_config_leaves_dollar_literal_by_default(
self,
@@ -2228,7 +2236,6 @@ class TestThrottledHistoryUpdater:
"error",
[
Mt5RuntimeError("boom"),
Mt5TradingError("trade failed"),
sqlite3.OperationalError("locked"),
ValueError("invalid symbols"),
OSError("disk full"),
+21 -20
View File
@@ -12,7 +12,7 @@ import pandas as pd
import pytest
from numpy import float64 as np_float64
from numpy import int64 as np_int64
from pdmt5 import Mt5RuntimeError, Mt5TradingClient, Mt5TradingError
from pdmt5 import Mt5RuntimeError
from pytest_mock import MockerFixture # noqa: TC002
from mt5cli.exceptions import Mt5OperationError
@@ -23,6 +23,7 @@ from mt5cli.trading import (
OrderLimits,
OrderSide,
ProjectionMode,
_Mt5ClientProtocol, # type: ignore[reportPrivateUsage]
calculate_account_projected_margin_ratio,
calculate_margin_and_volume,
calculate_new_position_margin_ratio,
@@ -426,7 +427,7 @@ class TestDetermineOrderLimits:
field: str,
bad_value: object,
) -> None:
"""Test invalid entry tick values raise Mt5TradingError."""
"""Test invalid entry tick values raise Mt5OperationError."""
tick: dict[str, object] = {"ask": 1.1, "bid": 1.0}
tick[field] = bad_value
client = MagicMock()
@@ -452,7 +453,7 @@ class TestDetermineOrderLimits:
kwarg: str,
match: str,
) -> None:
"""Test protective levels inside trade_stops_level raise Mt5TradingError."""
"""Test protective levels inside trade_stops_level raise Mt5OperationError."""
client = MagicMock()
client.symbol_info_tick_as_dict.return_value = {"ask": ask, "bid": bid}
client.symbol_info_as_dict.return_value = {
@@ -570,7 +571,7 @@ class TestDetermineOrderLimits:
client.symbol_select.assert_not_called()
def test_raises_when_symbol_selection_fails(self) -> None:
"""Test failed symbol selection raises Mt5TradingError."""
"""Test failed symbol selection raises Mt5OperationError."""
client = MagicMock()
client.symbol_info_as_dict.return_value = {"visible": False}
client.symbol_select.return_value = False
@@ -580,7 +581,7 @@ class TestDetermineOrderLimits:
ensure_symbol_selected(client, "EURUSD")
def test_raises_when_symbol_select_is_unavailable(self) -> None:
"""Test missing symbol_select raises Mt5TradingError."""
"""Test missing symbol_select raises Mt5OperationError."""
client = MagicMock()
client.symbol_info_as_dict.return_value = {"visible": False}
del client.symbol_select
@@ -643,7 +644,7 @@ class TestCreateTradingClient:
assert result is mock_client
config = trading_client.call_args.kwargs["config"]
assert config.login == 12345
assert config.password == ("test" + "-pass")
assert config.password.get_secret_value() == ("test" + "-pass")
assert config.server == "Demo"
assert config.path == "/opt/terminal64.exe"
assert trading_client.call_args.kwargs["retry_count"] == 2
@@ -694,7 +695,7 @@ class TestSnapshotsAndState:
def account_info(self) -> SimpleNamespace:
return SimpleNamespace(login=7, currency="JPY")
result = get_account_snapshot(cast("Mt5TradingClient", ObjectClient()))
result = get_account_snapshot(cast("_Mt5ClientProtocol", ObjectClient()))
assert result["login"] == 7
assert result["currency"] == "JPY"
@@ -704,7 +705,7 @@ class TestSnapshotsAndState:
client = object()
with pytest.raises(AttributeError, match="account_info"):
get_account_snapshot(cast("Mt5TradingClient", client))
get_account_snapshot(cast("_Mt5ClientProtocol", client))
def test_symbol_and_tick_snapshots_fill_symbol(self) -> None:
"""Test symbol and tick snapshots expose stable fields."""
@@ -779,7 +780,7 @@ class TestSnapshotsAndState:
field: str,
bad_value: object,
) -> None:
"""Test invalid bid/ask values raise Mt5TradingError."""
"""Test invalid bid/ask values raise Mt5OperationError."""
tick: dict[str, object] = {"bid": 100.0, "ask": 100.0}
tick[field] = bad_value
client = MagicMock()
@@ -911,7 +912,7 @@ class TestEstimateOrderMargin:
ids=["zero", "nan", "inf"],
)
def test_rejects_invalid_volume(self, volume: float) -> None:
"""Test non-positive or non-finite volume raises Mt5TradingError."""
"""Test non-positive or non-finite volume raises Mt5OperationError."""
client = _mock_trade_client()
with pytest.raises(Mt5OperationError, match="positive finite number"):
@@ -942,7 +943,7 @@ class TestEstimateOrderMargin:
ids=["zero", "inf", "none", "string"],
)
def test_rejects_invalid_margin_result(self, margin_value: object) -> None:
"""Test invalid margin estimates raise Mt5TradingError."""
"""Test invalid margin estimates raise Mt5OperationError."""
client = _mock_trade_client()
client.symbol_info_tick_as_dict.return_value = {"ask": 1.1010, "bid": 1.1000}
client.order_calc_margin.return_value = margin_value
@@ -1059,7 +1060,7 @@ class TestCalculatePositionsMargin:
assert client.order_calc_margin.call_count == expected_margin_calls
def test_propagates_invalid_tick_or_margin_errors(self) -> None:
"""Test invalid tick or margin data raises Mt5TradingError."""
"""Test invalid tick or margin data raises Mt5OperationError."""
client = _mock_trade_client()
client.positions_get_as_df.return_value = pd.DataFrame(
[{"symbol": "EURUSD", "type": 0, "volume": 0.1}],
@@ -1321,7 +1322,7 @@ class TestVolumeAndExecution:
return 10.0
result = calculate_margin_and_volume(
cast("Mt5TradingClient", ClientWithoutNative()),
cast("_Mt5ClientProtocol", ClientWithoutNative()),
"EURUSD",
unit_margin_ratio=0.5,
preserved_margin_ratio=0.0,
@@ -1795,7 +1796,7 @@ class TestVolumeAndExecution:
client.account_info_as_dict.return_value = {"equity": 1000.0}
mocker.patch(
"mt5cli.trading.calculate_positions_margin",
side_effect=[Mt5TradingError("bad tick"), 30.0],
side_effect=[Mt5OperationError("bad tick"), 30.0],
)
result = calculate_symbol_group_margin_ratio(
@@ -3112,7 +3113,7 @@ class TestFetchLatestClosedRatesForTradingClient:
client.copy_rates_from_pos_as_df.assert_not_called()
def test_raises_when_trading_client_cannot_fetch_rates(self) -> None:
"""Test missing rate-fetch methods raise Mt5TradingError."""
"""Test missing rate-fetch methods raise Mt5OperationError."""
client = MagicMock(spec=[])
with pytest.raises(Mt5OperationError, match="cannot fetch rate data"):
@@ -3544,7 +3545,7 @@ class TestCalculatePositionsMarginBySymbol:
client = _mock_trade_client()
mocker.patch(
"mt5cli.trading.calculate_positions_margin",
side_effect=[Mt5TradingError("tick unavailable"), 30.0],
side_effect=[Mt5OperationError("tick unavailable"), 30.0],
)
with caplog.at_level(logging.WARNING, logger="mt5cli.trading"):
@@ -3561,7 +3562,7 @@ class TestCalculatePositionsMarginBySymbol:
@pytest.mark.parametrize(
"exc",
[
Mt5TradingError("trading error"),
Mt5OperationError("trading error"),
Mt5RuntimeError("runtime error"),
AttributeError("missing attr"),
],
@@ -3586,7 +3587,7 @@ class TestCalculatePositionsMarginBySymbol:
client = _mock_trade_client()
mocker.patch(
"mt5cli.trading.calculate_positions_margin",
side_effect=[Mt5TradingError("err1"), Mt5TradingError("err2")],
side_effect=[Mt5OperationError("err1"), Mt5OperationError("err2")],
)
result = calculate_positions_margin_by_symbol(
@@ -3641,7 +3642,7 @@ class TestCalculatePositionsMarginSafe:
client = _mock_trade_client()
mocker.patch(
"mt5cli.trading.calculate_positions_margin",
side_effect=[Mt5TradingError("tick unavailable"), 30.0],
side_effect=[Mt5OperationError("tick unavailable"), 30.0],
)
total = calculate_positions_margin_safe(client, symbols=["EURUSD", "GBPUSD"])
@@ -3653,7 +3654,7 @@ class TestCalculatePositionsMarginSafe:
client = _mock_trade_client()
mocker.patch(
"mt5cli.trading.calculate_positions_margin",
side_effect=[Mt5TradingError("err1"), Mt5RuntimeError("err2")],
side_effect=[Mt5OperationError("err1"), Mt5RuntimeError("err2")],
)
total = calculate_positions_margin_safe(client, symbols=["EURUSD", "GBPUSD"])