mt5cli 0.4.2__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.
- {mt5cli-0.4.2 → mt5cli-0.4.3}/PKG-INFO +6 -1
- {mt5cli-0.4.2 → mt5cli-0.4.3}/README.md +5 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/docs/api/history.md +35 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/docs/api/index.md +20 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/docs/index.md +24 -6
- {mt5cli-0.4.2 → mt5cli-0.4.3}/mt5cli/__init__.py +12 -1
- {mt5cli-0.4.2 → mt5cli-0.4.3}/mt5cli/cli.py +48 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/mt5cli/history.py +225 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/mt5cli/sdk.py +171 -2
- {mt5cli-0.4.2 → mt5cli-0.4.3}/mt5cli/utils.py +55 -10
- {mt5cli-0.4.2 → mt5cli-0.4.3}/pyproject.toml +1 -1
- {mt5cli-0.4.2 → mt5cli-0.4.3}/tests/test_cli.py +60 -1
- {mt5cli-0.4.2 → mt5cli-0.4.3}/tests/test_history.py +277 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/tests/test_sdk.py +173 -1
- {mt5cli-0.4.2 → mt5cli-0.4.3}/tests/test_utils.py +108 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/uv.lock +1 -1
- {mt5cli-0.4.2 → mt5cli-0.4.3}/.agents/skills/local-qa/SKILL.md +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/.agents/skills/local-qa/scripts/qa.sh +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/.agents/skills/mt5cli/SKILL.md +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/.claude/agents/codex.md +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/.claude/settings.json +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/.github/FUNDING.yml +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/.github/dependabot.yml +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/.github/renovate.json +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/.github/workflows/ci.yml +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/.github/workflows/claude.yml +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/.github/workflows/release.yml +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/.gitignore +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/AGENTS.md +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/CLAUDE.md +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/LICENSE +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/docs/api/cli.md +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/docs/api/sdk.md +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/docs/api/utils.md +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/mkdocs.yml +0 -0
- {mt5cli-0.4.2 → mt5cli-0.4.3}/mt5cli/__main__.py +0 -0
- {mt5cli-0.4.2 → 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.
|
|
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
|
|
|
@@ -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`.
|
|
@@ -65,12 +65,18 @@ from datetime import UTC, datetime
|
|
|
65
65
|
from pathlib import Path
|
|
66
66
|
|
|
67
67
|
from mt5cli import (
|
|
68
|
+
Dataset,
|
|
69
|
+
IfExists,
|
|
68
70
|
Mt5CliClient,
|
|
69
71
|
collect_history,
|
|
70
72
|
copy_rates_range,
|
|
71
73
|
detect_format,
|
|
72
74
|
export_dataframe,
|
|
75
|
+
export_dataframe_to_sqlite,
|
|
76
|
+
minimum_margins,
|
|
77
|
+
recent_ticks,
|
|
73
78
|
)
|
|
79
|
+
from mt5cli.history import resolve_rate_view_name
|
|
74
80
|
|
|
75
81
|
# Fetch rates programmatically
|
|
76
82
|
rates = copy_rates_range(
|
|
@@ -86,6 +92,20 @@ fmt = detect_format(Path("output.parquet")) # Returns "parquet"
|
|
|
86
92
|
# Export a DataFrame
|
|
87
93
|
export_dataframe(rates, Path("output.csv"), "csv")
|
|
88
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
|
+
|
|
89
109
|
# Collect history into SQLite
|
|
90
110
|
collect_history(
|
|
91
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
|
|
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
|
|
96
|
-
|
|
|
97
|
-
| `ticks-from`
|
|
98
|
-
| `ticks-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
|
|
@@ -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
|
|
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,
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
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()
|