mt5cli 1.4.2__tar.gz → 1.5.0__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.
- {mt5cli-1.4.2 → mt5cli-1.5.0}/.agents/skills/local-qa/scripts/qa.sh +2 -2
- {mt5cli-1.4.2 → mt5cli-1.5.0}/.github/workflows/claude.yml +2 -3
- {mt5cli-1.4.2 → mt5cli-1.5.0}/PKG-INFO +2 -2
- {mt5cli-1.4.2 → mt5cli-1.5.0}/docs/api/public-contract.md +3 -1
- mt5cli-1.5.0/mt5cli/lifecycle.py +87 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/sdk.py +1 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/pyproject.toml +1 -1
- mt5cli-1.5.0/tests/unit/test_lifecycle.py +145 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/uv.lock +1 -1
- mt5cli-1.4.2/AGENTS.md +0 -29
- mt5cli-1.4.2/CLAUDE.md +0 -1
- {mt5cli-1.4.2 → mt5cli-1.5.0}/.agents/skills/local-qa/SKILL.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/.agents/skills/mt5cli/SKILL.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/.claude/agents/codex.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/.claude/settings.json +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/.github/CODEOWNERS +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/.github/FUNDING.yml +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/.github/dependabot.yml +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/.github/renovate.json +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/.github/workflows/ci.yml +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/.github/workflows/release.yml +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/.gitignore +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/LICENSE +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/README.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/docs/api/cli.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/docs/api/client.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/docs/api/converters.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/docs/api/exceptions.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/docs/api/grafana.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/docs/api/history.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/docs/api/index.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/docs/api/marketdata.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/docs/api/observability.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/docs/api/schemas.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/docs/api/telemetry.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/docs/api/trading.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/docs/api/utils.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/docs/index.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/examples/grafana/README.md +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/examples/grafana/compose.yml +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/examples/grafana/dashboards/mt5cli-market.json +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/examples/grafana/dashboards/mt5cli-overview.json +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/examples/grafana/dashboards/mt5cli-trades.json +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/examples/grafana/provisioning/dashboards/mt5cli.yml +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/examples/grafana/provisioning/datasources/mt5cli-sqlite.yml +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mkdocs.yml +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/__init__.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/__main__.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/cli.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/client.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/contract.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/converters.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/exceptions.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/grafana.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/history.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/marketdata.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/observability.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/rates.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/retry.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/schemas.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/telemetry.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/trading.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/mt5cli/utils.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/cli/test_cli.bats +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/__init__.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/conftest.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_cli.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_client.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_contracts.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_docs_contract.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_examples.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_grafana.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_history.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_history_contract.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_history_edge_contract.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_marketdata.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_observability.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_packaging.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_rate_timestamp_contract.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_rates.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_rates_contract.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_stable_sdk_cleanup.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_telemetry.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_trading.py +0 -0
- {mt5cli-1.4.2 → mt5cli-1.5.0}/tests/unit/test_utils.py +0 -0
|
@@ -32,9 +32,9 @@ case "${OSTYPE}" in
|
|
|
32
32
|
| xargs -0 -t shellcheck
|
|
33
33
|
|
|
34
34
|
# GitHub Actions
|
|
35
|
-
zizmor --fix=safe .github/workflows
|
|
35
|
+
uvx zizmor --fix=safe .github/workflows
|
|
36
36
|
git ls-files -z -- '.github/workflows/*.yml' | xargs -0 -t actionlint
|
|
37
|
-
checkov --framework=all --output=github_failed_only --directory=.
|
|
37
|
+
uvx checkov --framework=all --output=github_failed_only --directory=.
|
|
38
38
|
;;
|
|
39
39
|
*)
|
|
40
40
|
echo 'GitHub Actions and shell script linting is only supported on Linux and macOS.'
|
|
@@ -27,8 +27,7 @@ jobs:
|
|
|
27
27
|
if: >
|
|
28
28
|
github.event_name == 'pull_request'
|
|
29
29
|
&& (! github.event.pull_request.draft)
|
|
30
|
-
&&
|
|
31
|
-
&& (! startsWith(github.head_ref, 'renovate/'))
|
|
30
|
+
&& github.event.pull_request.user.type != 'Bot'
|
|
32
31
|
permissions:
|
|
33
32
|
contents: read
|
|
34
33
|
pull-requests: write
|
|
@@ -46,7 +45,7 @@ jobs:
|
|
|
46
45
|
|| (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude'))
|
|
47
46
|
|| (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
|
48
47
|
permissions:
|
|
49
|
-
contents:
|
|
48
|
+
contents: write
|
|
50
49
|
pull-requests: write
|
|
51
50
|
issues: write
|
|
52
51
|
id-token: write
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: mt5cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
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>
|
|
@@ -39,6 +39,7 @@ operational capability still has exactly one owning module:
|
|
|
39
39
|
| Module | Owns |
|
|
40
40
|
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
41
41
|
| `mt5cli.client` | Connection lifecycle (`build_config`, `mt5_session`, `MT5Client`); the sole session factory |
|
|
42
|
+
| `mt5cli.lifecycle` | Account selection within an already-active persistent MT5 session (`switch_account`) |
|
|
42
43
|
| `mt5cli.marketdata` | Stateless one-off market-data reads and multi-account rate collection (`AccountSpec`, `collect_latest_rates_for_accounts`, etc.) |
|
|
43
44
|
| `mt5cli.history` | Legacy history collection, incremental updates, and SQLite storage (`collect_history`, `update_history`, `write_*_dataset`) |
|
|
44
45
|
| `mt5cli.rates` | Canonical rate persistence/loading and stable update wrappers (`load_rate_series_from_sqlite`, `update_history`, `ThrottledHistoryUpdater`) |
|
|
@@ -66,6 +67,7 @@ These names are exported from `mt5cli` and enumerated in
|
|
|
66
67
|
| `build_config` | Build the underlying MT5 connection config from connection fields; `login` accepts `int \| str \| None` — numeric strings are coerced to `int`, blank strings are treated as unset, and `${ENV_VAR}` / `$ENV_NAME` placeholders in string parameters are expanded when `allow_whole_dollar_env=True` |
|
|
67
68
|
| `substitute_mapping_values` | Generic selected-key environment substitution for downstream configuration adapters |
|
|
68
69
|
| `mt5_session` | Canonical context manager: initialize/login once, yield `MT5Client`, shut down once; a supplied `client=` remains caller-owned. |
|
|
70
|
+
| `switch_account` | Change the account selected by an already-active persistent `MT5Client` with MT5 `login()` only; verifies the requested login/server and does not reinitialize or shut down the terminal |
|
|
69
71
|
| `Mt5Config` | Canonical pdmt5 connection configuration type returned by `build_config` and accepted by managed-session wrappers |
|
|
70
72
|
| `AccountSpec` | Generic account group: symbols plus optional credentials |
|
|
71
73
|
| `resolve_account_spec`, `resolve_account_specs` | Merge overrides and expand `${ENV_VAR}` placeholders; opt-in `allow_whole_dollar_env` for bare `$NAME` |
|
|
@@ -374,4 +376,4 @@ their own adapter layer.
|
|
|
374
376
|
`tests/unit/test_contracts.py` asserts that every name in `STABLE_SDK_EXPORTS` is
|
|
375
377
|
importable from `mt5cli`, that all package-root exports are covered by the
|
|
376
378
|
stable set, and documents key closed-bar, SQLite loading, account-resolution,
|
|
377
|
-
and trading-session behaviors.
|
|
379
|
+
and trading-session behaviors.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Persistent MT5 session lifecycle helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
6
|
+
|
|
7
|
+
from pdmt5 import Mt5RuntimeError
|
|
8
|
+
|
|
9
|
+
from .exceptions import Mt5ConnectionError, normalize_mt5_exception
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pdmt5 import Mt5DataClient
|
|
13
|
+
|
|
14
|
+
from .client import MT5Client
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _account_matches(account: Any, *, login: int, server: str | None) -> bool: # noqa: ANN401
|
|
18
|
+
"""Return whether an MT5 account snapshot matches the requested account."""
|
|
19
|
+
if getattr(account, "login", None) != login:
|
|
20
|
+
return False
|
|
21
|
+
return server is None or getattr(account, "server", None) == server
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def switch_account(
|
|
25
|
+
client: MT5Client,
|
|
26
|
+
*,
|
|
27
|
+
login: int,
|
|
28
|
+
password: str | None = None,
|
|
29
|
+
server: str | None = None,
|
|
30
|
+
timeout: int | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Switch an active persistent MT5 session without reinitializing the terminal.
|
|
33
|
+
|
|
34
|
+
The configured account on ``client`` is intentionally left unchanged, but this
|
|
35
|
+
does not restore the prior account. For a self-owned client
|
|
36
|
+
(``with MT5Client(...) as client:``), exiting and re-entering reconnects using
|
|
37
|
+
the original configuration. For clients obtained via ``mt5_session()`` or
|
|
38
|
+
``from_connected_client()`` (the common case), exiting and re-entering ``client``
|
|
39
|
+
has no effect on the connection at all, so the switched account persists for the
|
|
40
|
+
life of the terminal session.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
client: An already-entered ``MT5Client`` with a persistent connection and
|
|
44
|
+
an already-active account.
|
|
45
|
+
login: Target MT5 account login.
|
|
46
|
+
password: Optional target account password.
|
|
47
|
+
server: Optional target trade server.
|
|
48
|
+
timeout: Optional MT5 login timeout in milliseconds.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
Mt5ConnectionError: If the client is not active, has no active account,
|
|
52
|
+
or the account switch fails.
|
|
53
|
+
"""
|
|
54
|
+
connected = cast("Mt5DataClient | None", getattr(client, "_client", None))
|
|
55
|
+
if connected is None or not getattr(connected, "_is_initialized", True):
|
|
56
|
+
# pdmt5 auto-initializes on the first call, so an externally-bound
|
|
57
|
+
# inactive client must be rejected here.
|
|
58
|
+
msg = "MT5 account switching requires an active persistent session."
|
|
59
|
+
raise Mt5ConnectionError(msg)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
account = connected.account_info()
|
|
63
|
+
if _account_matches(account, login=login, server=server):
|
|
64
|
+
return
|
|
65
|
+
login_succeeded = connected.login(
|
|
66
|
+
login=login,
|
|
67
|
+
password=password,
|
|
68
|
+
server=server,
|
|
69
|
+
timeout=timeout,
|
|
70
|
+
)
|
|
71
|
+
last_error = None if login_succeeded else connected.last_error()
|
|
72
|
+
active_account = connected.account_info() if login_succeeded else None
|
|
73
|
+
except Mt5RuntimeError as exc:
|
|
74
|
+
normalized = normalize_mt5_exception(exc)
|
|
75
|
+
raise normalized from exc
|
|
76
|
+
|
|
77
|
+
if not login_succeeded:
|
|
78
|
+
msg = f"MT5 account switch failed: {last_error}"
|
|
79
|
+
raise Mt5ConnectionError(msg)
|
|
80
|
+
if not _account_matches(active_account, login=login, server=server):
|
|
81
|
+
msg = (
|
|
82
|
+
"MT5 account switch did not activate the requested account: "
|
|
83
|
+
f"requested login={login}, server={server!r}, "
|
|
84
|
+
f"active login={getattr(active_account, 'login', None)!r}, "
|
|
85
|
+
f"server={getattr(active_account, 'server', None)!r}"
|
|
86
|
+
)
|
|
87
|
+
raise Mt5ConnectionError(msg)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mt5cli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.5.0"
|
|
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"}]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Tests for persistent MT5 session lifecycle helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import ModuleType, SimpleNamespace
|
|
6
|
+
from unittest.mock import MagicMock
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from pdmt5 import Mt5DataClient, Mt5RuntimeError
|
|
10
|
+
|
|
11
|
+
from mt5cli import MT5Client, Mt5ConnectionError, switch_account
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _connected_client(raw_client: MagicMock) -> MT5Client:
|
|
15
|
+
"""Return an MT5Client facade bound to a caller-owned active client."""
|
|
16
|
+
return MT5Client.from_connected_client(raw_client)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.parametrize(
|
|
20
|
+
("active_login", "active_server", "requested_server", "expect_login"),
|
|
21
|
+
[
|
|
22
|
+
pytest.param(222, "Demo", "Demo", False, id="same-login-same-server"),
|
|
23
|
+
pytest.param(222, "Demo", None, False, id="same-login-no-server-constraint"),
|
|
24
|
+
pytest.param(111, "Demo", "Demo", True, id="different-login"),
|
|
25
|
+
pytest.param(222, "Other", "Demo", True, id="same-login-different-server"),
|
|
26
|
+
],
|
|
27
|
+
)
|
|
28
|
+
def test_switch_account_pre_switch_match_decision(
|
|
29
|
+
active_login: int,
|
|
30
|
+
active_server: str | None,
|
|
31
|
+
requested_server: str | None,
|
|
32
|
+
expect_login: bool,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""The pre-switch account-match check decides whether login() runs."""
|
|
35
|
+
raw_client = MagicMock()
|
|
36
|
+
active_account = SimpleNamespace(login=active_login, server=active_server)
|
|
37
|
+
raw_client.account_info.side_effect = [
|
|
38
|
+
active_account,
|
|
39
|
+
SimpleNamespace(login=222, server=requested_server or active_server),
|
|
40
|
+
]
|
|
41
|
+
raw_client.login.return_value = True
|
|
42
|
+
client = _connected_client(raw_client)
|
|
43
|
+
|
|
44
|
+
switch_account(client, login=222, server=requested_server)
|
|
45
|
+
|
|
46
|
+
assert raw_client.login.called is expect_login
|
|
47
|
+
raw_client.shutdown.assert_not_called()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_switch_account_uses_login_without_reinitializing_terminal() -> None:
|
|
51
|
+
"""Changing accounts calls login only and verifies the resulting account."""
|
|
52
|
+
raw_client = MagicMock()
|
|
53
|
+
raw_client.account_info.side_effect = [
|
|
54
|
+
SimpleNamespace(login=111, server="Demo"),
|
|
55
|
+
SimpleNamespace(login=222, server="Demo"),
|
|
56
|
+
]
|
|
57
|
+
raw_client.login.return_value = True
|
|
58
|
+
client = _connected_client(raw_client)
|
|
59
|
+
|
|
60
|
+
switch_account(
|
|
61
|
+
client,
|
|
62
|
+
login=222,
|
|
63
|
+
password="secret",
|
|
64
|
+
server="Demo",
|
|
65
|
+
timeout=60_000,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
raw_client.login.assert_called_once_with(
|
|
69
|
+
login=222,
|
|
70
|
+
password="secret",
|
|
71
|
+
server="Demo",
|
|
72
|
+
timeout=60_000,
|
|
73
|
+
)
|
|
74
|
+
raw_client.initialize_and_login_mt5.assert_not_called()
|
|
75
|
+
raw_client.shutdown.assert_not_called()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_switch_account_requires_active_persistent_session() -> None:
|
|
79
|
+
"""Transient clients cannot switch an account without opening a session."""
|
|
80
|
+
client = MT5Client()
|
|
81
|
+
|
|
82
|
+
with pytest.raises(Mt5ConnectionError, match="active persistent session"):
|
|
83
|
+
switch_account(client, login=222)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_switch_account_rejects_inactive_externally_bound_client() -> None:
|
|
87
|
+
"""An uninitialized real client cannot trigger pdmt5's auto-initialization.
|
|
88
|
+
|
|
89
|
+
``MT5Client.from_connected_client()`` accepts any raw client object without
|
|
90
|
+
validating that it is already connected. pdmt5's ``account_info()``/``login()``
|
|
91
|
+
silently call ``initialize()`` on an uninitialized client, so this guards the
|
|
92
|
+
guarantee that ``switch_account`` never (re)initializes the terminal.
|
|
93
|
+
"""
|
|
94
|
+
mock_mt5 = ModuleType("mock_mt5")
|
|
95
|
+
mock_mt5.initialize = MagicMock() # type: ignore[attr-defined]
|
|
96
|
+
raw_client = Mt5DataClient(mt5=mock_mt5)
|
|
97
|
+
client = MT5Client.from_connected_client(raw_client)
|
|
98
|
+
|
|
99
|
+
with pytest.raises(Mt5ConnectionError, match="active persistent session"):
|
|
100
|
+
switch_account(client, login=222)
|
|
101
|
+
|
|
102
|
+
mock_mt5.initialize.assert_not_called() # type: ignore[attr-defined]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_switch_account_normalizes_login_failure() -> None:
|
|
106
|
+
"""A failed MT5 login becomes the stable connection exception."""
|
|
107
|
+
raw_client = MagicMock()
|
|
108
|
+
raw_client.account_info.return_value = SimpleNamespace(login=111, server="Demo")
|
|
109
|
+
raw_client.login.return_value = False
|
|
110
|
+
raw_client.last_error.return_value = (-6, "authorization failed")
|
|
111
|
+
client = _connected_client(raw_client)
|
|
112
|
+
|
|
113
|
+
with pytest.raises(Mt5ConnectionError, match="authorization failed"):
|
|
114
|
+
switch_account(client, login=222, server="Demo")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_switch_account_normalizes_runtime_error() -> None:
|
|
118
|
+
"""Underlying pdmt5 runtime errors use the stable connection exception.
|
|
119
|
+
|
|
120
|
+
This also covers the no-active-account case: pdmt5's
|
|
121
|
+
``Mt5DataClient.account_info()`` raises ``Mt5RuntimeError`` rather than
|
|
122
|
+
returning ``None`` when MT5 has no active account, so ``switch_account``
|
|
123
|
+
requires an already-active account and surfaces that failure the same way
|
|
124
|
+
as any other runtime error.
|
|
125
|
+
"""
|
|
126
|
+
raw_client = MagicMock()
|
|
127
|
+
raw_client.account_info.side_effect = Mt5RuntimeError("connection lost")
|
|
128
|
+
client = _connected_client(raw_client)
|
|
129
|
+
|
|
130
|
+
with pytest.raises(Mt5ConnectionError, match="connection lost"):
|
|
131
|
+
switch_account(client, login=222, server="Demo")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_switch_account_fails_if_requested_account_is_not_active_after_login() -> None:
|
|
135
|
+
"""A successful login return cannot bypass active-account verification."""
|
|
136
|
+
raw_client = MagicMock()
|
|
137
|
+
raw_client.account_info.side_effect = [
|
|
138
|
+
SimpleNamespace(login=111, server="Demo"),
|
|
139
|
+
SimpleNamespace(login=222, server="Other"),
|
|
140
|
+
]
|
|
141
|
+
raw_client.login.return_value = True
|
|
142
|
+
client = _connected_client(raw_client)
|
|
143
|
+
|
|
144
|
+
with pytest.raises(Mt5ConnectionError, match="did not activate"):
|
|
145
|
+
switch_account(client, login=222, server="Demo")
|
mt5cli-1.4.2/AGENTS.md
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# Repository Guidelines
|
|
2
|
-
|
|
3
|
-
## Build, Test, and Development Commands
|
|
4
|
-
|
|
5
|
-
Use `.agents/skills/local-qa/SKILL.md` for pre-handoff QA. It runs `.agents/skills/local-qa/scripts/qa.sh`, which formats, lints, type-checks, tests, formats Markdown, and checks GitHub workflows.
|
|
6
|
-
|
|
7
|
-
`bats --verbose-run ./tests/cli/` runs the Windows/MT5-oriented CLI tests against the installed `mt5cli` entry point. These require a Windows environment where `uv sync` can install MetaTrader5; state clearly when they were not run.
|
|
8
|
-
|
|
9
|
-
`skills/mt5cli/SKILL.md` documents the mt5cli agent skill.
|
|
10
|
-
|
|
11
|
-
## Coding Style & Naming Conventions
|
|
12
|
-
|
|
13
|
-
Preserve the package’s small, typed helper style rather than adding broad abstractions.
|
|
14
|
-
|
|
15
|
-
## Design Principles
|
|
16
|
-
|
|
17
|
-
Apply KISS, DRY, and YAGNI when changing code. Prefer the simplest implementation that satisfies the current CLI/API contract. Remove duplication when shared behavior is already proven by at least two concrete call sites, but avoid generic helpers for speculative reuse. Do not add configuration flags, extension hooks, or alternate backends until a real repository use case requires them.
|
|
18
|
-
|
|
19
|
-
## Testing Guidelines
|
|
20
|
-
|
|
21
|
-
Parametrize unit tests with `pytest.mark.parametrize` when the same behavior needs coverage across multiple inputs. Mock MT5/pdmt5 boundaries; do not require a live MetaTrader terminal in unit tests.
|
|
22
|
-
|
|
23
|
-
## Commit & Pull Request Guidelines
|
|
24
|
-
|
|
25
|
-
Recent history uses concise imperative commits, sometimes with conventional prefixes such as `feat:` or `chore:` and PR numbers appended by GitHub. Keep commits scoped to one logical change. Pull requests should describe behavior changes, note tests run, link related issues, and call out MT5/live-trading risk where relevant.
|
|
26
|
-
|
|
27
|
-
## Security & Configuration Tips
|
|
28
|
-
|
|
29
|
-
Never commit account credentials, broker passwords, exported private data, or local `.venv` contents. Treat `order_send` and CLI `order-send --yes` as live execution paths; gate examples and tests so they cannot place real trades accidentally.
|
mt5cli-1.4.2/CLAUDE.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
AGENTS.md
|
|
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
|
|
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
|