mt5cli 0.4.1__tar.gz → 0.4.3__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.
Files changed (37) hide show
  1. {mt5cli-0.4.1 → mt5cli-0.4.3}/PKG-INFO +6 -1
  2. {mt5cli-0.4.1 → mt5cli-0.4.3}/README.md +5 -0
  3. mt5cli-0.4.1/docs/api/sqlite_history.md → mt5cli-0.4.3/docs/api/history.md +37 -2
  4. {mt5cli-0.4.1 → mt5cli-0.4.3}/docs/api/index.md +24 -0
  5. {mt5cli-0.4.1 → mt5cli-0.4.3}/docs/index.md +25 -7
  6. {mt5cli-0.4.1 → mt5cli-0.4.3}/mkdocs.yml +1 -1
  7. {mt5cli-0.4.1 → mt5cli-0.4.3}/mt5cli/__init__.py +12 -1
  8. {mt5cli-0.4.1 → mt5cli-0.4.3}/mt5cli/cli.py +48 -0
  9. mt5cli-0.4.1/mt5cli/sqlite_history.py → mt5cli-0.4.3/mt5cli/history.py +226 -1
  10. {mt5cli-0.4.1 → mt5cli-0.4.3}/mt5cli/sdk.py +172 -3
  11. {mt5cli-0.4.1 → mt5cli-0.4.3}/mt5cli/utils.py +55 -10
  12. {mt5cli-0.4.1 → mt5cli-0.4.3}/pyproject.toml +2 -2
  13. {mt5cli-0.4.1 → mt5cli-0.4.3}/tests/test_cli.py +60 -1
  14. mt5cli-0.4.1/tests/test_sqlite_history.py → mt5cli-0.4.3/tests/test_history.py +281 -4
  15. {mt5cli-0.4.1 → mt5cli-0.4.3}/tests/test_sdk.py +174 -2
  16. {mt5cli-0.4.1 → mt5cli-0.4.3}/tests/test_utils.py +108 -0
  17. {mt5cli-0.4.1 → mt5cli-0.4.3}/uv.lock +1 -1
  18. {mt5cli-0.4.1 → mt5cli-0.4.3}/.agents/skills/local-qa/SKILL.md +0 -0
  19. {mt5cli-0.4.1 → mt5cli-0.4.3}/.agents/skills/local-qa/scripts/qa.sh +0 -0
  20. {mt5cli-0.4.1 → mt5cli-0.4.3}/.agents/skills/mt5cli/SKILL.md +0 -0
  21. {mt5cli-0.4.1 → mt5cli-0.4.3}/.claude/agents/codex.md +0 -0
  22. {mt5cli-0.4.1 → mt5cli-0.4.3}/.claude/settings.json +0 -0
  23. {mt5cli-0.4.1 → mt5cli-0.4.3}/.github/FUNDING.yml +0 -0
  24. {mt5cli-0.4.1 → mt5cli-0.4.3}/.github/dependabot.yml +0 -0
  25. {mt5cli-0.4.1 → mt5cli-0.4.3}/.github/renovate.json +0 -0
  26. {mt5cli-0.4.1 → mt5cli-0.4.3}/.github/workflows/ci.yml +0 -0
  27. {mt5cli-0.4.1 → mt5cli-0.4.3}/.github/workflows/claude.yml +0 -0
  28. {mt5cli-0.4.1 → mt5cli-0.4.3}/.github/workflows/release.yml +0 -0
  29. {mt5cli-0.4.1 → mt5cli-0.4.3}/.gitignore +0 -0
  30. {mt5cli-0.4.1 → mt5cli-0.4.3}/AGENTS.md +0 -0
  31. {mt5cli-0.4.1 → mt5cli-0.4.3}/CLAUDE.md +0 -0
  32. {mt5cli-0.4.1 → mt5cli-0.4.3}/LICENSE +0 -0
  33. {mt5cli-0.4.1 → mt5cli-0.4.3}/docs/api/cli.md +0 -0
  34. {mt5cli-0.4.1 → mt5cli-0.4.3}/docs/api/sdk.md +0 -0
  35. {mt5cli-0.4.1 → mt5cli-0.4.3}/docs/api/utils.md +0 -0
  36. {mt5cli-0.4.1 → mt5cli-0.4.3}/mt5cli/__main__.py +0 -0
  37. {mt5cli-0.4.1 → mt5cli-0.4.3}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mt5cli
3
- Version: 0.4.1
3
+ Version: 0.4.3
4
4
  Summary: Command-line tool for MetaTrader 5
5
5
  Project-URL: Repository, https://github.com/dceoy/mt5cli.git
6
6
  Author-email: dceoy <dceoy@users.noreply.github.com>
@@ -81,6 +81,7 @@ python -m mt5cli -o account.csv account-info
81
81
  | `rates-range` | Export rates for a date range |
82
82
  | `ticks-from` | Export ticks from a start date |
83
83
  | `ticks-range` | Export ticks for a date range |
84
+ | `ticks-recent` | Export ticks from a recent trailing window |
84
85
  | `account-info` | Export account information |
85
86
  | `terminal-info` | Export terminal information |
86
87
  | `version` | Export MetaTrader 5 version information |
@@ -88,6 +89,7 @@ python -m mt5cli -o account.csv account-info
88
89
  | `symbols` | Export symbol list |
89
90
  | `symbol-info` | Export symbol details |
90
91
  | `symbol-info-tick` | Export the last tick for a symbol |
92
+ | `minimum-margins` | Export minimum-volume buy and sell margin requirements |
91
93
  | `market-book` | Export market depth (order book) |
92
94
  | `orders` | Export active orders |
93
95
  | `positions` | Export open positions |
@@ -151,6 +153,9 @@ update_history_with_config(
151
153
  - **`update_history`**: incremental append based on existing SQLite `MAX(time)` per symbol (and timeframe for rates); account-level deals use a separate cursor when `include_account_events=True`.
152
154
  - **`rates` table**: normalized storage with `symbol` and `timeframe` columns.
153
155
  - **Rate compatibility views**: mt5cli manages all `rate_*` views. Naming is `rate_<symbol>__<timeframe>` when a symbol has one timeframe, otherwise `rate_<symbol>__<granularity>_<timeframe>` (for example `rate_EURUSD__M1_1`). Stale `rate_*` views are dropped and recreated when rates change for offline tools such as mteor optimize.
156
+ - **Rate view resolution**: use `mt5cli.history.resolve_rate_view_name()` / `resolve_rate_view_names()` to map symbols and granularities to existing SQLite compatibility views without creating databases.
157
+ - **SQLite export helpers**: use `export_dataframe_to_sqlite()` for append mode, optional index export, and post-write deduplication by key columns.
158
+ - **Recent ticks and margins**: `recent_ticks()` and `minimum_margins()` SDK helpers (and matching CLI commands) cover common downstream read-only queries.
154
159
 
155
160
  ## Requirements
156
161
 
@@ -57,6 +57,7 @@ python -m mt5cli -o account.csv account-info
57
57
  | `rates-range` | Export rates for a date range |
58
58
  | `ticks-from` | Export ticks from a start date |
59
59
  | `ticks-range` | Export ticks for a date range |
60
+ | `ticks-recent` | Export ticks from a recent trailing window |
60
61
  | `account-info` | Export account information |
61
62
  | `terminal-info` | Export terminal information |
62
63
  | `version` | Export MetaTrader 5 version information |
@@ -64,6 +65,7 @@ python -m mt5cli -o account.csv account-info
64
65
  | `symbols` | Export symbol list |
65
66
  | `symbol-info` | Export symbol details |
66
67
  | `symbol-info-tick` | Export the last tick for a symbol |
68
+ | `minimum-margins` | Export minimum-volume buy and sell margin requirements |
67
69
  | `market-book` | Export market depth (order book) |
68
70
  | `orders` | Export active orders |
69
71
  | `positions` | Export open positions |
@@ -127,6 +129,9 @@ update_history_with_config(
127
129
  - **`update_history`**: incremental append based on existing SQLite `MAX(time)` per symbol (and timeframe for rates); account-level deals use a separate cursor when `include_account_events=True`.
128
130
  - **`rates` table**: normalized storage with `symbol` and `timeframe` columns.
129
131
  - **Rate compatibility views**: mt5cli manages all `rate_*` views. Naming is `rate_<symbol>__<timeframe>` when a symbol has one timeframe, otherwise `rate_<symbol>__<granularity>_<timeframe>` (for example `rate_EURUSD__M1_1`). Stale `rate_*` views are dropped and recreated when rates change for offline tools such as mteor optimize.
132
+ - **Rate view resolution**: use `mt5cli.history.resolve_rate_view_name()` / `resolve_rate_view_names()` to map symbols and granularities to existing SQLite compatibility views without creating databases.
133
+ - **SQLite export helpers**: use `export_dataframe_to_sqlite()` for append mode, optional index export, and post-write deduplication by key columns.
134
+ - **Recent ticks and margins**: `recent_ticks()` and `minimum_margins()` SDK helpers (and matching CLI commands) cover common downstream read-only queries.
130
135
 
131
136
  ## Requirements
132
137
 
@@ -1,6 +1,6 @@
1
- # SQLite History Module
1
+ # History Collection (SQLite)
2
2
 
3
- ::: mt5cli.sqlite_history
3
+ ::: mt5cli.history
4
4
 
5
5
  ## `collect-history` schema
6
6
 
@@ -129,3 +129,38 @@ when required columns are missing.
129
129
  The `update_history` SDK path uses the same base tables and optional
130
130
  `cash_events` / `positions_reconstructed` views. It additionally maintains
131
131
  `rate_<symbol>__<timeframe>` compatibility views when `create_rate_views=True`.
132
+
133
+ ### Rate view resolution
134
+
135
+ Downstream tools can resolve mt5cli-managed compatibility view names from an
136
+ existing SQLite history database without creating files or guessing legacy
137
+ naming schemes:
138
+
139
+ ```python
140
+ from pathlib import Path
141
+
142
+ from mt5cli.history import resolve_rate_view_name, resolve_rate_view_names
143
+
144
+ # Single symbol and granularity
145
+ view = resolve_rate_view_name(Path("history.db"), "EURUSD", "M1")
146
+
147
+ # Batch resolution in row-major order
148
+ views = resolve_rate_view_names(
149
+ Path("history.db"),
150
+ ["EURUSD", "GBPUSD"],
151
+ ["M1", "H1"],
152
+ )
153
+ ```
154
+
155
+ Resolution rules:
156
+
157
+ - Returns `rate_<symbol>__<timeframe>` when a symbol stores one timeframe.
158
+ - Returns `rate_<symbol>__<granularity>_<timeframe>` when multiple timeframes
159
+ are stored for the same symbol.
160
+ - When multiple naming candidates apply, prefers an existing managed
161
+ `rate_*__*` view from the candidate list.
162
+ - Falls back to single-timeframe naming when the database path is missing or
163
+ `rates` metadata is unavailable.
164
+ - Pass `require_existing=True` to raise `ValueError` instead of returning a
165
+ best-guess name when the database or view is missing.
166
+ - Accepts either a SQLite path or an open `sqlite3.Connection`.
@@ -18,6 +18,10 @@ Utility module providing constants, enums, Click parameter types, and helper fun
18
18
 
19
19
  Programmatic SDK for read-only MetaTrader 5 data collection. Returns pandas DataFrames and provides `collect_history` for SQLite bulk collection.
20
20
 
21
+ ### [History Collection (SQLite)](history.md)
22
+
23
+ SQLite storage helpers for the `collect-history` command schema, incremental updates, deduplication, indexes, and optional views.
24
+
21
25
  ## Architecture Overview
22
26
 
23
27
  The package follows a simple architecture built on top of pdmt5:
@@ -61,12 +65,18 @@ from datetime import UTC, datetime
61
65
  from pathlib import Path
62
66
 
63
67
  from mt5cli import (
68
+ Dataset,
69
+ IfExists,
64
70
  Mt5CliClient,
65
71
  collect_history,
66
72
  copy_rates_range,
67
73
  detect_format,
68
74
  export_dataframe,
75
+ export_dataframe_to_sqlite,
76
+ minimum_margins,
77
+ recent_ticks,
69
78
  )
79
+ from mt5cli.history import resolve_rate_view_name
70
80
 
71
81
  # Fetch rates programmatically
72
82
  rates = copy_rates_range(
@@ -82,6 +92,20 @@ fmt = detect_format(Path("output.parquet")) # Returns "parquet"
82
92
  # Export a DataFrame
83
93
  export_dataframe(rates, Path("output.csv"), "csv")
84
94
 
95
+ # Append to SQLite with deduplication
96
+ export_dataframe_to_sqlite(
97
+ rates,
98
+ Path("history.db"),
99
+ "rates",
100
+ if_exists=IfExists.APPEND,
101
+ deduplicate_on=("symbol", "timeframe", "time"),
102
+ )
103
+
104
+ # Resolve rate compatibility views and fetch recent ticks
105
+ view = resolve_rate_view_name(Path("history.db"), "EURUSD", "M1")
106
+ ticks = recent_ticks("EURUSD", seconds=300)
107
+ margins = minimum_margins("EURUSD")
108
+
85
109
  # Collect history into SQLite
86
110
  collect_history(
87
111
  Path("history.db"),
@@ -22,13 +22,22 @@ pip install mt5cli
22
22
 
23
23
  ## Programmatic usage / SDK usage
24
24
 
25
- mt5cli can be used as a small Python SDK for read-only MetaTrader 5 data collection. SDK functions return pandas DataFrames without writing files. Use `export_dataframe` when you need to persist results.
25
+ mt5cli can be used as a small Python SDK for read-only MetaTrader 5 data collection. SDK functions return pandas DataFrames without writing files. Use `export_dataframe` or `export_dataframe_to_sqlite` when you need to persist results.
26
26
 
27
27
  ```python
28
28
  from datetime import UTC, datetime
29
29
  from pathlib import Path
30
30
 
31
- from mt5cli import Mt5CliClient, collect_history, copy_rates_range, export_dataframe
31
+ from mt5cli import (
32
+ Mt5CliClient,
33
+ collect_history,
34
+ copy_rates_range,
35
+ export_dataframe,
36
+ export_dataframe_to_sqlite,
37
+ minimum_margins,
38
+ recent_ticks,
39
+ )
40
+ from mt5cli.history import resolve_rate_view_name
32
41
 
33
42
  # One-off fetch with module-level helpers
34
43
  rates = copy_rates_range(
@@ -39,6 +48,13 @@ rates = copy_rates_range(
39
48
  )
40
49
  export_dataframe(rates, Path("rates.csv"), "csv")
41
50
 
51
+ # Resolve SQLite rate compatibility views for downstream tools
52
+ view = resolve_rate_view_name(Path("history.db"), "EURUSD", "M1")
53
+
54
+ # Recent tick window and minimum margin summary
55
+ ticks = recent_ticks("EURUSD", seconds=300)
56
+ margins = minimum_margins("EURUSD")
57
+
42
58
  # Reuse one MT5 connection for multiple calls
43
59
  with Mt5CliClient(login=12345, password="secret", server="Broker-Demo") as client:
44
60
  account = client.account_info()
@@ -92,10 +108,11 @@ mt5cli --login 12345 --password mypass --server MyBroker-Demo \
92
108
 
93
109
  ### Ticks
94
110
 
95
- | Command | Description |
96
- | ------------- | ------------------------------ |
97
- | `ticks-from` | Export ticks from a start date |
98
- | `ticks-range` | Export ticks for a date range |
111
+ | Command | Description |
112
+ | -------------- | ----------------------------------- |
113
+ | `ticks-from` | Export ticks from a start date |
114
+ | `ticks-range` | Export ticks for a date range |
115
+ | `ticks-recent` | Export ticks from a trailing window |
99
116
 
100
117
  ### Information
101
118
 
@@ -108,6 +125,7 @@ mt5cli --login 12345 --password mypass --server MyBroker-Demo \
108
125
  | `symbols` | Export symbol list |
109
126
  | `symbol-info` | Export symbol details |
110
127
  | `symbol-info-tick` | Export the last tick for a symbol |
128
+ | `minimum-margins` | Export minimum-volume margin summary |
111
129
  | `market-book` | Export market depth (order book) |
112
130
 
113
131
  ### Trading
@@ -152,7 +170,7 @@ mt5cli -o history.db collect-history \
152
170
 
153
171
  History orders and deals are fetched per symbol and concatenated, so the symbol filter is applied consistently across all datasets. The `cash_events` view is derived from symbol-filtered `history_deals`, so account-level cash events with empty or non-matching symbols may be excluded. The `positions_reconstructed` view excludes positions with no closing deal, uses volume-weighted open/close prices, and reports reversal deals (`DEAL_ENTRY_INOUT`) via `volume_reversal` / `reversal_count`.
154
172
 
155
- See the [SQLite History schema diagram](api/sqlite_history.md#entity-relationship-diagram) for a sample ER layout of the resulting database.
173
+ See the [History schema diagram](api/history.md#entity-relationship-diagram) for a sample ER layout of the resulting database.
156
174
 
157
175
  ## Global Options
158
176
 
@@ -58,7 +58,7 @@ nav:
58
58
  - Overview: api/index.md
59
59
  - CLI: api/cli.md
60
60
  - SDK: api/sdk.md
61
- - SQLite History: api/sqlite_history.md
61
+ - History Collection (SQLite): api/history.md
62
62
  - Utils: api/utils.md
63
63
 
64
64
  markdown_extensions:
@@ -16,8 +16,10 @@ from .sdk import (
16
16
  history_orders,
17
17
  last_error,
18
18
  market_book,
19
+ minimum_margins,
19
20
  orders,
20
21
  positions,
22
+ recent_ticks,
21
23
  symbol_info,
22
24
  symbol_info_tick,
23
25
  symbols,
@@ -28,7 +30,13 @@ from .sdk import (
28
30
  from .sdk import (
29
31
  version as mt5_version,
30
32
  )
31
- from .utils import Dataset, IfExists, detect_format, export_dataframe
33
+ from .utils import (
34
+ Dataset,
35
+ IfExists,
36
+ detect_format,
37
+ export_dataframe,
38
+ export_dataframe_to_sqlite,
39
+ )
32
40
 
33
41
  __version__ = version(__package__) if __package__ else None
34
42
 
@@ -46,13 +54,16 @@ __all__ = [
46
54
  "copy_ticks_range",
47
55
  "detect_format",
48
56
  "export_dataframe",
57
+ "export_dataframe_to_sqlite",
49
58
  "history_deals",
50
59
  "history_orders",
51
60
  "last_error",
52
61
  "market_book",
62
+ "minimum_margins",
53
63
  "mt5_version",
54
64
  "orders",
55
65
  "positions",
66
+ "recent_ticks",
56
67
  "symbol_info",
57
68
  "symbol_info_tick",
58
69
  "symbols",
@@ -300,6 +300,44 @@ def ticks_range(
300
300
  )
301
301
 
302
302
 
303
+ @app.command()
304
+ def ticks_recent(
305
+ ctx: typer.Context,
306
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
307
+ seconds: Annotated[
308
+ float,
309
+ typer.Option(help="Lookback window in seconds."),
310
+ ],
311
+ date_to: Annotated[
312
+ datetime | None,
313
+ typer.Option(click_type=DATETIME_TYPE, help="Window end date."),
314
+ ] = None,
315
+ count: Annotated[
316
+ int,
317
+ typer.Option(help="Maximum number of ticks to return."),
318
+ ] = 10000,
319
+ flags: Annotated[
320
+ int,
321
+ typer.Option(
322
+ click_type=TICK_FLAGS_TYPE,
323
+ help="Tick flags (ALL, INFO, TRADE, or integer).",
324
+ ),
325
+ ] = 1,
326
+ ) -> None:
327
+ """Export ticks from a recent time window."""
328
+ client = _sdk_client(ctx)
329
+ _execute_export(
330
+ ctx,
331
+ lambda: client.recent_ticks(
332
+ symbol,
333
+ seconds,
334
+ date_to=date_to,
335
+ count=count,
336
+ flags=flags,
337
+ ),
338
+ )
339
+
340
+
303
341
  @app.command()
304
342
  def account_info(ctx: typer.Context) -> None:
305
343
  """Export account information."""
@@ -335,6 +373,16 @@ def symbol_info(
335
373
  _execute_export(ctx, lambda: client.symbol_info(symbol))
336
374
 
337
375
 
376
+ @app.command()
377
+ def minimum_margins(
378
+ ctx: typer.Context,
379
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
380
+ ) -> None:
381
+ """Export minimum-volume buy and sell margin requirements."""
382
+ client = _sdk_client(ctx)
383
+ _execute_export(ctx, lambda: client.minimum_margins(symbol))
384
+
385
+
338
386
  @app.command()
339
387
  def orders(
340
388
  ctx: typer.Context,
@@ -1,10 +1,11 @@
1
- """SQLite helpers for incremental MT5 history collection."""
1
+ """SQLite storage helpers for the ``collect-history`` incremental data pipeline."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  import sqlite3
7
7
  from datetime import UTC, datetime
8
+ from pathlib import Path
8
9
  from typing import TYPE_CHECKING, Literal
9
10
 
10
11
  import pandas as pd
@@ -122,6 +123,230 @@ def build_rate_view_name(
122
123
  return f"rate_{symbol}__{granularity}_{timeframe}"
123
124
 
124
125
 
126
+ SqliteConnOrPath = sqlite3.Connection | Path | str
127
+
128
+
129
+ def _open_history_connection(
130
+ conn_or_path: SqliteConnOrPath,
131
+ ) -> tuple[sqlite3.Connection | None, bool]:
132
+ """Open a read-only SQLite connection when given a path.
133
+
134
+ Returns:
135
+ A connection and whether the caller should close it. When the path does
136
+ not exist, returns ``(None, False)`` without creating a database file.
137
+ """
138
+ if isinstance(conn_or_path, sqlite3.Connection):
139
+ return conn_or_path, False
140
+ path = Path(conn_or_path)
141
+ if not path.exists():
142
+ return None, False
143
+ conn = sqlite3.connect(f"{path.resolve().as_uri()}?mode=ro", uri=True)
144
+ return conn, True
145
+
146
+
147
+ def _load_rates_timeframe_counts(conn: sqlite3.Connection) -> dict[str, int] | None:
148
+ """Return distinct timeframe counts per symbol from the normalized rates table."""
149
+ columns = get_table_columns(conn, Dataset.rates.table_name)
150
+ if not {"symbol", "timeframe"}.issubset(columns):
151
+ return None
152
+ rows = conn.execute(
153
+ "SELECT symbol, COUNT(DISTINCT timeframe) FROM rates GROUP BY symbol",
154
+ ).fetchall()
155
+ return {str(symbol): int(count) for symbol, count in rows}
156
+
157
+
158
+ def _load_existing_rate_views(conn: sqlite3.Connection) -> set[str]:
159
+ """Return mt5cli-managed ``rate_*__*`` compatibility view names."""
160
+ rows = conn.execute(
161
+ "SELECT name FROM sqlite_master WHERE type = 'view' AND name GLOB 'rate_*__*'",
162
+ ).fetchall()
163
+ return {str(row[0]) for row in rows}
164
+
165
+
166
+ def _rate_view_name_candidates(
167
+ *,
168
+ symbol: str,
169
+ granularity: str,
170
+ granularity_count: int,
171
+ timeframe: int,
172
+ ) -> list[str]:
173
+ """Return candidate view names in preference order."""
174
+ single = build_rate_view_name(
175
+ symbol=symbol,
176
+ granularity=granularity,
177
+ granularity_count=1,
178
+ timeframe=timeframe,
179
+ )
180
+ if granularity_count <= 1:
181
+ return [single]
182
+ multi = build_rate_view_name(
183
+ symbol=symbol,
184
+ granularity=granularity,
185
+ granularity_count=granularity_count,
186
+ timeframe=timeframe,
187
+ )
188
+ return [multi, single]
189
+
190
+
191
+ def _resolve_rate_view_name_from_context(
192
+ *,
193
+ symbol: str,
194
+ timeframe: int,
195
+ granularity_name: str,
196
+ timeframe_counts: dict[str, int] | None,
197
+ existing_views: set[str],
198
+ require_existing: bool = False,
199
+ ) -> str:
200
+ """Resolve one rate view name using preloaded SQLite metadata.
201
+
202
+ Returns:
203
+ Preferred mt5cli-managed rate compatibility view name.
204
+
205
+ Raises:
206
+ ValueError: If ``require_existing`` is True and no managed view exists.
207
+ """
208
+ if timeframe_counts is None or symbol not in timeframe_counts:
209
+ candidates = [
210
+ build_rate_view_name(
211
+ symbol=symbol,
212
+ granularity=granularity_name,
213
+ granularity_count=1,
214
+ timeframe=timeframe,
215
+ ),
216
+ build_rate_view_name(
217
+ symbol=symbol,
218
+ granularity=granularity_name,
219
+ granularity_count=2,
220
+ timeframe=timeframe,
221
+ ),
222
+ ]
223
+ else:
224
+ candidates = _rate_view_name_candidates(
225
+ symbol=symbol,
226
+ granularity=granularity_name,
227
+ granularity_count=timeframe_counts[symbol],
228
+ timeframe=timeframe,
229
+ )
230
+ for candidate in candidates:
231
+ if candidate in existing_views:
232
+ return candidate
233
+ if require_existing:
234
+ msg = (
235
+ f"No rate compatibility view exists for symbol {symbol!r} "
236
+ f"and granularity {granularity_name!r}; "
237
+ f"candidates: {', '.join(candidates)}."
238
+ )
239
+ raise ValueError(msg)
240
+ return candidates[0]
241
+
242
+
243
+ def resolve_rate_view_name(
244
+ conn_or_path: SqliteConnOrPath,
245
+ symbol: str,
246
+ granularity: str,
247
+ *,
248
+ require_existing: bool = False,
249
+ ) -> str:
250
+ """Resolve the mt5cli-managed rate compatibility view name.
251
+
252
+ Args:
253
+ conn_or_path: SQLite database path or open connection.
254
+ symbol: Symbol stored in the normalized ``rates`` table.
255
+ granularity: Timeframe name (for example ``M1``) or integer string.
256
+ require_existing: When True, require the database and a managed view to exist.
257
+
258
+ Returns:
259
+ View name such as ``rate_EURUSD__1`` or ``rate_EURUSD__M1_1``.
260
+
261
+ Raises:
262
+ ValueError: If ``require_existing`` is True and the database or view is missing.
263
+ """
264
+ timeframe = parse_timeframe(granularity)
265
+ granularity_name = resolve_granularity_name(timeframe)
266
+ conn, should_close = _open_history_connection(conn_or_path)
267
+ try:
268
+ if conn is None:
269
+ if require_existing:
270
+ path = (
271
+ conn_or_path
272
+ if isinstance(conn_or_path, (Path, str))
273
+ else "database"
274
+ )
275
+ msg = f"SQLite database not found: {path}"
276
+ raise ValueError(msg)
277
+ return build_rate_view_name(
278
+ symbol=symbol,
279
+ granularity=granularity_name,
280
+ granularity_count=1,
281
+ timeframe=timeframe,
282
+ )
283
+ return _resolve_rate_view_name_from_context(
284
+ symbol=symbol,
285
+ timeframe=timeframe,
286
+ granularity_name=granularity_name,
287
+ timeframe_counts=_load_rates_timeframe_counts(conn),
288
+ existing_views=_load_existing_rate_views(conn),
289
+ require_existing=require_existing,
290
+ )
291
+ finally:
292
+ if should_close and conn is not None:
293
+ conn.close()
294
+
295
+
296
+ def resolve_rate_view_names(
297
+ conn_or_path: SqliteConnOrPath,
298
+ symbols: Sequence[str],
299
+ granularities: Sequence[str],
300
+ *,
301
+ require_existing: bool = False,
302
+ ) -> list[str]:
303
+ """Resolve rate compatibility view names for symbol and granularity pairs.
304
+
305
+ Args:
306
+ conn_or_path: SQLite database path or open connection.
307
+ symbols: Symbols stored in the normalized ``rates`` table.
308
+ granularities: Timeframe names (for example ``M1``) or integer strings.
309
+ require_existing: When True, require the database and managed views to exist.
310
+
311
+ Returns:
312
+ View names in row-major order: every ``granularity`` for the first
313
+ symbol, then every granularity for the next symbol, and so on.
314
+ """
315
+ conn, should_close = _open_history_connection(conn_or_path)
316
+ try:
317
+ if conn is None:
318
+ return [
319
+ resolve_rate_view_name(
320
+ conn_or_path,
321
+ symbol,
322
+ granularity,
323
+ require_existing=require_existing,
324
+ )
325
+ for symbol in symbols
326
+ for granularity in granularities
327
+ ]
328
+ timeframe_counts = _load_rates_timeframe_counts(conn)
329
+ existing_views = _load_existing_rate_views(conn)
330
+ resolved: list[str] = []
331
+ for symbol in symbols:
332
+ for granularity in granularities:
333
+ timeframe = parse_timeframe(granularity)
334
+ resolved.append(
335
+ _resolve_rate_view_name_from_context(
336
+ symbol=symbol,
337
+ timeframe=timeframe,
338
+ granularity_name=resolve_granularity_name(timeframe),
339
+ timeframe_counts=timeframe_counts,
340
+ existing_views=existing_views,
341
+ require_existing=require_existing,
342
+ ),
343
+ )
344
+ return resolved
345
+ finally:
346
+ if should_close and conn is not None:
347
+ conn.close()
348
+
349
+
125
350
  def get_table_columns(conn: sqlite3.Connection, table: str) -> set[str]:
126
351
  """Return existing SQLite columns for a table."""
127
352
  rows = conn.execute(f"PRAGMA table_info({table})").fetchall()