mt5cli 0.8.1__tar.gz → 0.8.2__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-0.8.1 → mt5cli-0.8.2}/PKG-INFO +2 -2
- {mt5cli-0.8.1 → mt5cli-0.8.2}/README.md +1 -1
- {mt5cli-0.8.1 → mt5cli-0.8.2}/docs/api/public-contract.md +10 -10
- {mt5cli-0.8.1 → mt5cli-0.8.2}/docs/api/sdk.md +22 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/mt5cli/sdk.py +23 -5
- {mt5cli-0.8.1 → mt5cli-0.8.2}/pyproject.toml +1 -1
- {mt5cli-0.8.1 → mt5cli-0.8.2}/tests/test_sdk.py +151 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/uv.lock +1 -1
- {mt5cli-0.8.1 → mt5cli-0.8.2}/.agents/skills/local-qa/SKILL.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/.agents/skills/local-qa/scripts/qa.sh +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/.agents/skills/mt5cli/SKILL.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/.agents/skills/pr-feedback-triage/SKILL.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/.claude/agents/codex.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/.claude/settings.json +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/.github/FUNDING.yml +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/.github/dependabot.yml +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/.github/renovate.json +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/.github/workflows/ci.yml +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/.github/workflows/claude.yml +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/.github/workflows/release.yml +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/.gitignore +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/AGENTS.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/CLAUDE.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/LICENSE +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/docs/api/cli.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/docs/api/client.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/docs/api/converters.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/docs/api/exceptions.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/docs/api/history.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/docs/api/index.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/docs/api/schemas.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/docs/api/storage.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/docs/api/trading.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/docs/api/utils.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/docs/index.md +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/mkdocs.yml +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/mt5cli/__init__.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/mt5cli/__main__.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/mt5cli/cli.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/mt5cli/client.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/mt5cli/contract.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/mt5cli/converters.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/mt5cli/exceptions.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/mt5cli/history.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/mt5cli/retry.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/mt5cli/schemas.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/mt5cli/storage.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/mt5cli/trading.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/mt5cli/utils.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/tests/__init__.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/tests/conftest.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/tests/test_cli.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/tests/test_contracts.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/tests/test_history.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/tests/test_trading.py +0 -0
- {mt5cli-0.8.1 → mt5cli-0.8.2}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mt5cli
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
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>
|
|
@@ -273,7 +273,7 @@ eurusd_m1 = rates["EURUSD", "M1"] # closed bars only
|
|
|
273
273
|
```
|
|
274
274
|
|
|
275
275
|
- **Credential resolution**: use `resolve_account_spec()` / `resolve_account_specs()` to merge explicit override values over `AccountSpec` fields and expand `${ENV_VAR}` placeholders (via `substitute_env_placeholders()`), raising `ValueError` for missing variables. This keeps secrets out of plan/config files without coupling to any strategy code.
|
|
276
|
-
- **Throttled history updates**: use `ThrottledHistoryUpdater` to wrap `update_history()` with a minimum `interval_seconds` between successful runs (monotonic clock). Call `should_update()` / `update(client, symbols)` from an application loop; errors propagate by default, or pass `suppress_errors=True` to swallow recoverable `Mt5*Error`, `sqlite3.Error`, `ValueError`, `OSError`, and MT5 client capability errors for history API methods without advancing the throttle (other `AttributeError` / `TypeError` values always propagate).
|
|
276
|
+
- **Throttled history updates**: use `ThrottledHistoryUpdater` to wrap `update_history()` with a minimum `interval_seconds` between successful runs (monotonic clock). Call `should_update()` / `update(client, symbols)` from an application loop; errors propagate by default, or pass `suppress_errors=True` to swallow recoverable `Mt5*Error`, `sqlite3.Error`, `ValueError`, `OSError`, and MT5 client capability errors for history API methods without advancing the throttle (other `AttributeError` / `TypeError` values always propagate). Pass `update_backend` to inject a custom history update callable (same keyword arguments as `update_history`) instead of monkey-patching `mt5cli.sdk.update_history`.
|
|
277
277
|
- **Trading session helpers**: use `mt5_trading_session()` for a trading-capable `pdmt5.Mt5TradingClient` that initializes/logs in via `Mt5Config.path` and always shuts down safely. Pair with `detect_position_side()`, `calculate_margin_and_volume()`, and `determine_order_limits()` for generic position and sizing utilities. The read-only `mt5_session()` / `Mt5CliClient` SDK is unchanged.
|
|
278
278
|
- **Granularity-keyed rate loading**: `load_rate_series_by_granularity()` builds targets with `build_rate_targets()`, loads them with `load_rate_series_from_sqlite()`, and returns a mapping keyed by `(symbol | None, granularity_name)` such as `("EURUSD", "M1")` to reduce downstream boilerplate.
|
|
279
279
|
- **MT5 session helper**: use the `mt5_session()` context manager to attach to (or, when `Mt5Config.path` is set, launch) an MT5 terminal, log in, and yield a connected `MT5Client` that shuts down on exit.
|
|
@@ -249,7 +249,7 @@ eurusd_m1 = rates["EURUSD", "M1"] # closed bars only
|
|
|
249
249
|
```
|
|
250
250
|
|
|
251
251
|
- **Credential resolution**: use `resolve_account_spec()` / `resolve_account_specs()` to merge explicit override values over `AccountSpec` fields and expand `${ENV_VAR}` placeholders (via `substitute_env_placeholders()`), raising `ValueError` for missing variables. This keeps secrets out of plan/config files without coupling to any strategy code.
|
|
252
|
-
- **Throttled history updates**: use `ThrottledHistoryUpdater` to wrap `update_history()` with a minimum `interval_seconds` between successful runs (monotonic clock). Call `should_update()` / `update(client, symbols)` from an application loop; errors propagate by default, or pass `suppress_errors=True` to swallow recoverable `Mt5*Error`, `sqlite3.Error`, `ValueError`, `OSError`, and MT5 client capability errors for history API methods without advancing the throttle (other `AttributeError` / `TypeError` values always propagate).
|
|
252
|
+
- **Throttled history updates**: use `ThrottledHistoryUpdater` to wrap `update_history()` with a minimum `interval_seconds` between successful runs (monotonic clock). Call `should_update()` / `update(client, symbols)` from an application loop; errors propagate by default, or pass `suppress_errors=True` to swallow recoverable `Mt5*Error`, `sqlite3.Error`, `ValueError`, `OSError`, and MT5 client capability errors for history API methods without advancing the throttle (other `AttributeError` / `TypeError` values always propagate). Pass `update_backend` to inject a custom history update callable (same keyword arguments as `update_history`) instead of monkey-patching `mt5cli.sdk.update_history`.
|
|
253
253
|
- **Trading session helpers**: use `mt5_trading_session()` for a trading-capable `pdmt5.Mt5TradingClient` that initializes/logs in via `Mt5Config.path` and always shuts down safely. Pair with `detect_position_side()`, `calculate_margin_and_volume()`, and `determine_order_limits()` for generic position and sizing utilities. The read-only `mt5_session()` / `Mt5CliClient` SDK is unchanged.
|
|
254
254
|
- **Granularity-keyed rate loading**: `load_rate_series_by_granularity()` builds targets with `build_rate_targets()`, loads them with `load_rate_series_from_sqlite()`, and returns a mapping keyed by `(symbol | None, granularity_name)` such as `("EURUSD", "M1")` to reduce downstream boilerplate.
|
|
255
255
|
- **MT5 session helper**: use the `mt5_session()` context manager to attach to (or, when `Mt5Config.path` is set, launch) an MT5 terminal, log in, and yield a connected `MT5Client` that shuts down on exit.
|
|
@@ -67,16 +67,16 @@ timestamp normalization in downstream apps.
|
|
|
67
67
|
|
|
68
68
|
### SQLite history collection and rate loading
|
|
69
69
|
|
|
70
|
-
| Symbol | Role
|
|
71
|
-
| ----------------------------------------------------------------------------------------------------------------------------- |
|
|
72
|
-
| `collect_history` | One-shot date-range export into SQLite
|
|
73
|
-
| `update_history`, `update_history_with_config` | Incremental append from `MAX(time)` cursors
|
|
74
|
-
| `ThrottledHistoryUpdater` | Minimum interval between successful incremental updates
|
|
75
|
-
| `resolve_history_datasets`, `resolve_history_timeframes`, `resolve_history_tick_flags` | History pipeline configuration
|
|
76
|
-
| `build_rate_view_name`, `resolve_rate_table_name`, `resolve_rate_view_name`, `resolve_rate_view_names`, `resolve_rate_tables` | Map symbols/timeframes to mt5cli-managed table or view names
|
|
77
|
-
| `RateTarget`, `build_rate_targets` | Neutral `(symbol, timeframe)` series descriptors
|
|
78
|
-
| `load_rate_data`, `load_rate_data_from_connection` | Load one table/view into a time-indexed DataFrame
|
|
79
|
-
| `load_rate_series_from_sqlite`, `load_rate_series_by_granularity` | Load one or many series; fail clearly when managed views are missing
|
|
70
|
+
| Symbol | Role |
|
|
71
|
+
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
72
|
+
| `collect_history` | One-shot date-range export into SQLite |
|
|
73
|
+
| `update_history`, `update_history_with_config` | Incremental append from `MAX(time)` cursors |
|
|
74
|
+
| `ThrottledHistoryUpdater` | Minimum interval between successful incremental updates; optional `update_backend` injection |
|
|
75
|
+
| `resolve_history_datasets`, `resolve_history_timeframes`, `resolve_history_tick_flags` | History pipeline configuration |
|
|
76
|
+
| `build_rate_view_name`, `resolve_rate_table_name`, `resolve_rate_view_name`, `resolve_rate_view_names`, `resolve_rate_tables` | Map symbols/timeframes to mt5cli-managed table or view names |
|
|
77
|
+
| `RateTarget`, `build_rate_targets` | Neutral `(symbol, timeframe)` series descriptors |
|
|
78
|
+
| `load_rate_data`, `load_rate_data_from_connection` | Load one table/view into a time-indexed DataFrame |
|
|
79
|
+
| `load_rate_series_from_sqlite`, `load_rate_series_by_granularity` | Load one or many series; fail clearly when managed views are missing |
|
|
80
80
|
|
|
81
81
|
Pass `require_existing=True` to rate view resolution helpers when downstream
|
|
82
82
|
code must fail instead of receiving a best-guess view name. Multi-series loaders
|
|
@@ -113,6 +113,28 @@ finally:
|
|
|
113
113
|
client.shutdown()
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
Pass `update_backend` to substitute the default `update_history` implementation
|
|
117
|
+
without monkey-patching `mt5cli.sdk.update_history`. The callable receives the
|
|
118
|
+
same keyword arguments as `update_history` (`client`, `output`, `symbols`,
|
|
119
|
+
`datasets`, `timeframes`, `flags`, `lookback_hours`, `with_views`,
|
|
120
|
+
`include_account_events`). The resolved backend is stored on
|
|
121
|
+
`updater.update_backend` for inspection or subclassing.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from mt5cli import ThrottledHistoryUpdater, update_history
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def app_update_history(**kwargs) -> None:
|
|
128
|
+
update_history(**kwargs) # or delegate to application-specific logic
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
updater = ThrottledHistoryUpdater(
|
|
132
|
+
output="history.db",
|
|
133
|
+
interval_seconds=60,
|
|
134
|
+
update_backend=app_update_history,
|
|
135
|
+
)
|
|
136
|
+
```
|
|
137
|
+
|
|
116
138
|
By default recoverable errors (`Mt5TradingError`, `Mt5RuntimeError`,
|
|
117
139
|
`sqlite3.Error`, `ValueError`, `OSError`, and MT5 client capability
|
|
118
140
|
`AttributeError` / `TypeError` for history API methods) propagate so the caller
|
|
@@ -42,6 +42,8 @@ from .utils import coerce_login as _coerce_login
|
|
|
42
42
|
if TYPE_CHECKING:
|
|
43
43
|
from collections.abc import Callable, Iterator, Sequence
|
|
44
44
|
|
|
45
|
+
UpdateHistoryBackend = Callable[..., None]
|
|
46
|
+
|
|
45
47
|
T = TypeVar("T")
|
|
46
48
|
|
|
47
49
|
logger = logging.getLogger(__name__)
|
|
@@ -1044,10 +1046,16 @@ def update_history_with_config( # noqa: PLR0913
|
|
|
1044
1046
|
class ThrottledHistoryUpdater:
|
|
1045
1047
|
"""Throttled incremental SQLite history updater for long-running apps.
|
|
1046
1048
|
|
|
1047
|
-
Wraps :func:`update_history`
|
|
1048
|
-
updates, so a tight application loop can call
|
|
1049
|
-
iteration without re-fetching MT5 history more often
|
|
1050
|
-
uses a monotonic clock, so it is unaffected by
|
|
1049
|
+
Wraps :func:`update_history` (or a custom ``update_backend``) with a minimum
|
|
1050
|
+
interval between successful updates, so a tight application loop can call
|
|
1051
|
+
:meth:`update` every iteration without re-fetching MT5 history more often
|
|
1052
|
+
than desired. Timing uses a monotonic clock, so it is unaffected by
|
|
1053
|
+
wall-clock changes.
|
|
1054
|
+
|
|
1055
|
+
Downstream applications may pass ``update_backend`` to substitute the
|
|
1056
|
+
default :func:`update_history` implementation—for example to add
|
|
1057
|
+
application-specific logging, metrics, or test doubles—without monkey-
|
|
1058
|
+
patching ``mt5cli.sdk.update_history``.
|
|
1051
1059
|
"""
|
|
1052
1060
|
|
|
1053
1061
|
def __init__(
|
|
@@ -1062,6 +1070,7 @@ class ThrottledHistoryUpdater:
|
|
|
1062
1070
|
include_account_events: bool = True,
|
|
1063
1071
|
interval_seconds: float = 0.0,
|
|
1064
1072
|
suppress_errors: bool = False,
|
|
1073
|
+
update_backend: UpdateHistoryBackend | None = None,
|
|
1065
1074
|
) -> None:
|
|
1066
1075
|
"""Initialize the throttled updater.
|
|
1067
1076
|
|
|
@@ -1085,6 +1094,12 @@ class ThrottledHistoryUpdater:
|
|
|
1085
1094
|
the throttle. Other ``AttributeError`` / ``TypeError`` values
|
|
1086
1095
|
always propagate. When False (default), recoverable errors
|
|
1087
1096
|
propagate so callers control logging.
|
|
1097
|
+
update_backend: Callable invoked instead of :func:`update_history`
|
|
1098
|
+
when :meth:`update` runs. Receives the same keyword arguments as
|
|
1099
|
+
:func:`update_history` (``client``, ``output``, ``symbols``,
|
|
1100
|
+
``datasets``, ``timeframes``, ``flags``, ``lookback_hours``,
|
|
1101
|
+
``with_views``, ``include_account_events``). Defaults to
|
|
1102
|
+
:func:`update_history`.
|
|
1088
1103
|
"""
|
|
1089
1104
|
self.output = output
|
|
1090
1105
|
self.datasets = datasets
|
|
@@ -1095,6 +1110,9 @@ class ThrottledHistoryUpdater:
|
|
|
1095
1110
|
self.include_account_events = include_account_events
|
|
1096
1111
|
self.interval_seconds = interval_seconds
|
|
1097
1112
|
self.suppress_errors = suppress_errors
|
|
1113
|
+
self.update_backend = (
|
|
1114
|
+
update_history if update_backend is None else update_backend
|
|
1115
|
+
)
|
|
1098
1116
|
self._last_update_monotonic: float | None = None
|
|
1099
1117
|
|
|
1100
1118
|
@property
|
|
@@ -1145,7 +1163,7 @@ class ThrottledHistoryUpdater:
|
|
|
1145
1163
|
lookback_hours=self.lookback_hours,
|
|
1146
1164
|
date_to=None,
|
|
1147
1165
|
)
|
|
1148
|
-
|
|
1166
|
+
self.update_backend(
|
|
1149
1167
|
client=client,
|
|
1150
1168
|
output=self.output,
|
|
1151
1169
|
symbols=symbols,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mt5cli"
|
|
3
|
-
version = "0.8.
|
|
3
|
+
version = "0.8.2"
|
|
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"}]
|
|
@@ -2100,3 +2100,154 @@ class TestThrottledHistoryUpdater:
|
|
|
2100
2100
|
assert updater.update(MagicMock(), []) is False
|
|
2101
2101
|
update.assert_not_called()
|
|
2102
2102
|
assert updater.last_update_monotonic is None
|
|
2103
|
+
|
|
2104
|
+
def test_default_update_backend_is_update_history(self) -> None:
|
|
2105
|
+
"""Test the default backend resolves to update_history."""
|
|
2106
|
+
updater = ThrottledHistoryUpdater(output="history.db")
|
|
2107
|
+
assert updater.update_backend is update_history
|
|
2108
|
+
|
|
2109
|
+
def test_falsy_callable_update_backend_is_preserved(
|
|
2110
|
+
self,
|
|
2111
|
+
mocker: MockerFixture,
|
|
2112
|
+
) -> None:
|
|
2113
|
+
"""Test only None selects the default backend, not falsy callables."""
|
|
2114
|
+
|
|
2115
|
+
class FalsyCallable:
|
|
2116
|
+
def __init__(self) -> None:
|
|
2117
|
+
self.calls: list[dict[str, object]] = []
|
|
2118
|
+
|
|
2119
|
+
def __bool__(self) -> bool:
|
|
2120
|
+
return False
|
|
2121
|
+
|
|
2122
|
+
def __call__(self, **kwargs: object) -> None:
|
|
2123
|
+
self.calls.append(kwargs)
|
|
2124
|
+
|
|
2125
|
+
falsy_backend = FalsyCallable()
|
|
2126
|
+
default_backend = mocker.patch("mt5cli.sdk.update_history")
|
|
2127
|
+
updater = ThrottledHistoryUpdater(
|
|
2128
|
+
output="history.db",
|
|
2129
|
+
update_backend=falsy_backend,
|
|
2130
|
+
)
|
|
2131
|
+
|
|
2132
|
+
assert updater.update_backend is falsy_backend
|
|
2133
|
+
client = MagicMock()
|
|
2134
|
+
assert updater.update(client, ["EURUSD"]) is True
|
|
2135
|
+
assert len(falsy_backend.calls) == 1
|
|
2136
|
+
assert falsy_backend.calls[0]["client"] is client
|
|
2137
|
+
assert falsy_backend.calls[0]["symbols"] == ["EURUSD"]
|
|
2138
|
+
default_backend.assert_not_called()
|
|
2139
|
+
|
|
2140
|
+
def test_custom_update_backend_receives_expected_kwargs(
|
|
2141
|
+
self,
|
|
2142
|
+
mocker: MockerFixture,
|
|
2143
|
+
) -> None:
|
|
2144
|
+
"""Test a custom backend receives update_history keyword arguments."""
|
|
2145
|
+
backend = mocker.Mock()
|
|
2146
|
+
client = MagicMock()
|
|
2147
|
+
updater = ThrottledHistoryUpdater(
|
|
2148
|
+
output="history.db",
|
|
2149
|
+
datasets={Dataset.rates},
|
|
2150
|
+
timeframes=["M1", "H1"],
|
|
2151
|
+
flags="INFO",
|
|
2152
|
+
lookback_hours=12.0,
|
|
2153
|
+
with_views=True,
|
|
2154
|
+
include_account_events=False,
|
|
2155
|
+
update_backend=backend,
|
|
2156
|
+
)
|
|
2157
|
+
|
|
2158
|
+
updater.update(client, ["EURUSD", "GBPUSD"])
|
|
2159
|
+
|
|
2160
|
+
backend.assert_called_once_with(
|
|
2161
|
+
client=client,
|
|
2162
|
+
output="history.db",
|
|
2163
|
+
symbols=["EURUSD", "GBPUSD"],
|
|
2164
|
+
datasets={Dataset.rates},
|
|
2165
|
+
timeframes=["M1", "H1"],
|
|
2166
|
+
flags="INFO",
|
|
2167
|
+
lookback_hours=12.0,
|
|
2168
|
+
with_views=True,
|
|
2169
|
+
include_account_events=False,
|
|
2170
|
+
)
|
|
2171
|
+
|
|
2172
|
+
def test_throttled_calls_do_not_invoke_custom_backend(
|
|
2173
|
+
self,
|
|
2174
|
+
mocker: MockerFixture,
|
|
2175
|
+
) -> None:
|
|
2176
|
+
"""Test throttled update cycles skip the injected backend."""
|
|
2177
|
+
backend = mocker.Mock()
|
|
2178
|
+
monotonic = mocker.patch("mt5cli.sdk.time.monotonic")
|
|
2179
|
+
monotonic.side_effect = [100.0, 105.0, 200.0, 200.0]
|
|
2180
|
+
client = MagicMock()
|
|
2181
|
+
updater = ThrottledHistoryUpdater(
|
|
2182
|
+
output="history.db",
|
|
2183
|
+
interval_seconds=60,
|
|
2184
|
+
update_backend=backend,
|
|
2185
|
+
)
|
|
2186
|
+
|
|
2187
|
+
assert updater.update(client, ["EURUSD"]) is True
|
|
2188
|
+
assert updater.update(client, ["EURUSD"]) is False
|
|
2189
|
+
assert updater.update(client, ["EURUSD"]) is True
|
|
2190
|
+
assert backend.call_count == 2
|
|
2191
|
+
|
|
2192
|
+
def test_successful_custom_backend_advances_throttle(
|
|
2193
|
+
self,
|
|
2194
|
+
mocker: MockerFixture,
|
|
2195
|
+
) -> None:
|
|
2196
|
+
"""Test a successful custom backend updates _last_update_monotonic."""
|
|
2197
|
+
backend = mocker.Mock()
|
|
2198
|
+
monotonic = mocker.patch("mt5cli.sdk.time.monotonic", return_value=42.0)
|
|
2199
|
+
updater = ThrottledHistoryUpdater(
|
|
2200
|
+
output="history.db",
|
|
2201
|
+
update_backend=backend,
|
|
2202
|
+
)
|
|
2203
|
+
|
|
2204
|
+
assert updater.update(MagicMock(), ["EURUSD"]) is True
|
|
2205
|
+
assert updater.last_update_monotonic is monotonic.return_value
|
|
2206
|
+
monotonic.assert_called_once()
|
|
2207
|
+
|
|
2208
|
+
def test_failed_custom_backend_does_not_advance_throttle(
|
|
2209
|
+
self,
|
|
2210
|
+
mocker: MockerFixture,
|
|
2211
|
+
) -> None:
|
|
2212
|
+
"""Test a failing custom backend leaves _last_update_monotonic unchanged."""
|
|
2213
|
+
backend = mocker.Mock(side_effect=Mt5RuntimeError("boom"))
|
|
2214
|
+
updater = ThrottledHistoryUpdater(
|
|
2215
|
+
output="history.db",
|
|
2216
|
+
update_backend=backend,
|
|
2217
|
+
)
|
|
2218
|
+
|
|
2219
|
+
with pytest.raises(Mt5RuntimeError, match="boom"):
|
|
2220
|
+
updater.update(MagicMock(), ["EURUSD"])
|
|
2221
|
+
|
|
2222
|
+
assert updater.last_update_monotonic is None
|
|
2223
|
+
|
|
2224
|
+
def test_custom_backend_suppresses_recoverable_errors_when_requested(
|
|
2225
|
+
self,
|
|
2226
|
+
mocker: MockerFixture,
|
|
2227
|
+
) -> None:
|
|
2228
|
+
"""Test suppress_errors swallows recoverable custom backend errors."""
|
|
2229
|
+
backend = mocker.Mock(side_effect=Mt5RuntimeError("boom"))
|
|
2230
|
+
updater = ThrottledHistoryUpdater(
|
|
2231
|
+
output="history.db",
|
|
2232
|
+
suppress_errors=True,
|
|
2233
|
+
update_backend=backend,
|
|
2234
|
+
)
|
|
2235
|
+
|
|
2236
|
+
assert updater.update(MagicMock(), ["EURUSD"]) is False
|
|
2237
|
+
assert updater.last_update_monotonic is None
|
|
2238
|
+
|
|
2239
|
+
def test_custom_backend_propagates_errors_when_not_suppressed(
|
|
2240
|
+
self,
|
|
2241
|
+
mocker: MockerFixture,
|
|
2242
|
+
) -> None:
|
|
2243
|
+
"""Test recoverable custom backend errors propagate by default."""
|
|
2244
|
+
backend = mocker.Mock(side_effect=Mt5RuntimeError("boom"))
|
|
2245
|
+
updater = ThrottledHistoryUpdater(
|
|
2246
|
+
output="history.db",
|
|
2247
|
+
update_backend=backend,
|
|
2248
|
+
)
|
|
2249
|
+
|
|
2250
|
+
with pytest.raises(Mt5RuntimeError, match="boom"):
|
|
2251
|
+
updater.update(MagicMock(), ["EURUSD"])
|
|
2252
|
+
|
|
2253
|
+
assert updater.last_update_monotonic is None
|
|
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
|