mt5cli 0.4.3__tar.gz → 0.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.
Files changed (37) hide show
  1. {mt5cli-0.4.3 → mt5cli-0.5.0}/PKG-INFO +30 -25
  2. {mt5cli-0.4.3 → mt5cli-0.5.0}/README.md +29 -24
  3. {mt5cli-0.4.3 → mt5cli-0.5.0}/docs/api/history.md +19 -0
  4. {mt5cli-0.4.3 → mt5cli-0.5.0}/docs/index.md +20 -9
  5. {mt5cli-0.4.3 → mt5cli-0.5.0}/mt5cli/__init__.py +13 -0
  6. {mt5cli-0.4.3 → mt5cli-0.5.0}/mt5cli/cli.py +56 -0
  7. {mt5cli-0.4.3 → mt5cli-0.5.0}/mt5cli/history.py +138 -2
  8. {mt5cli-0.4.3 → mt5cli-0.5.0}/mt5cli/sdk.py +233 -4
  9. {mt5cli-0.4.3 → mt5cli-0.5.0}/pyproject.toml +1 -1
  10. {mt5cli-0.4.3 → mt5cli-0.5.0}/tests/test_cli.py +113 -0
  11. {mt5cli-0.4.3 → mt5cli-0.5.0}/tests/test_history.py +143 -0
  12. {mt5cli-0.4.3 → mt5cli-0.5.0}/tests/test_sdk.py +270 -2
  13. {mt5cli-0.4.3 → mt5cli-0.5.0}/uv.lock +1 -1
  14. {mt5cli-0.4.3 → mt5cli-0.5.0}/.agents/skills/local-qa/SKILL.md +0 -0
  15. {mt5cli-0.4.3 → mt5cli-0.5.0}/.agents/skills/local-qa/scripts/qa.sh +0 -0
  16. {mt5cli-0.4.3 → mt5cli-0.5.0}/.agents/skills/mt5cli/SKILL.md +0 -0
  17. {mt5cli-0.4.3 → mt5cli-0.5.0}/.claude/agents/codex.md +0 -0
  18. {mt5cli-0.4.3 → mt5cli-0.5.0}/.claude/settings.json +0 -0
  19. {mt5cli-0.4.3 → mt5cli-0.5.0}/.github/FUNDING.yml +0 -0
  20. {mt5cli-0.4.3 → mt5cli-0.5.0}/.github/dependabot.yml +0 -0
  21. {mt5cli-0.4.3 → mt5cli-0.5.0}/.github/renovate.json +0 -0
  22. {mt5cli-0.4.3 → mt5cli-0.5.0}/.github/workflows/ci.yml +0 -0
  23. {mt5cli-0.4.3 → mt5cli-0.5.0}/.github/workflows/claude.yml +0 -0
  24. {mt5cli-0.4.3 → mt5cli-0.5.0}/.github/workflows/release.yml +0 -0
  25. {mt5cli-0.4.3 → mt5cli-0.5.0}/.gitignore +0 -0
  26. {mt5cli-0.4.3 → mt5cli-0.5.0}/AGENTS.md +0 -0
  27. {mt5cli-0.4.3 → mt5cli-0.5.0}/CLAUDE.md +0 -0
  28. {mt5cli-0.4.3 → mt5cli-0.5.0}/LICENSE +0 -0
  29. {mt5cli-0.4.3 → mt5cli-0.5.0}/docs/api/cli.md +0 -0
  30. {mt5cli-0.4.3 → mt5cli-0.5.0}/docs/api/index.md +0 -0
  31. {mt5cli-0.4.3 → mt5cli-0.5.0}/docs/api/sdk.md +0 -0
  32. {mt5cli-0.4.3 → mt5cli-0.5.0}/docs/api/utils.md +0 -0
  33. {mt5cli-0.4.3 → mt5cli-0.5.0}/mkdocs.yml +0 -0
  34. {mt5cli-0.4.3 → mt5cli-0.5.0}/mt5cli/__main__.py +0 -0
  35. {mt5cli-0.4.3 → mt5cli-0.5.0}/mt5cli/utils.py +0 -0
  36. {mt5cli-0.4.3 → mt5cli-0.5.0}/tests/__init__.py +0 -0
  37. {mt5cli-0.4.3 → mt5cli-0.5.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mt5cli
3
- Version: 0.4.3
3
+ Version: 0.5.0
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>
@@ -37,6 +37,7 @@ Built on top of [pdmt5](https://github.com/dceoy/pdmt5), a pandas-based data han
37
37
  - **Comprehensive data access**: Rates, ticks, account info, symbols, orders, positions, and trading history
38
38
  - **Flexible timeframes**: Named timeframes (M1, H1, D1, etc.) and numeric values
39
39
  - **Connection management**: Optional credentials, server, and timeout configuration
40
+ - **SQLite rate loading**: Load mt5cli-managed rate tables/views for offline workflows
40
41
 
41
42
  ## Installation
42
43
 
@@ -74,30 +75,33 @@ python -m mt5cli -o account.csv account-info
74
75
 
75
76
  ## Commands
76
77
 
77
- | Command | Description |
78
- | ------------------ | ------------------------------------------------------------------------------------------------------------ |
79
- | `rates-from` | Export rates from a start date |
80
- | `rates-from-pos` | Export rates from a start position |
81
- | `rates-range` | Export rates for a date range |
82
- | `ticks-from` | Export ticks from a start date |
83
- | `ticks-range` | Export ticks for a date range |
84
- | `ticks-recent` | Export ticks from a recent trailing window |
85
- | `account-info` | Export account information |
86
- | `terminal-info` | Export terminal information |
87
- | `version` | Export MetaTrader 5 version information |
88
- | `last-error` | Export the last error information |
89
- | `symbols` | Export symbol list |
90
- | `symbol-info` | Export symbol details |
91
- | `symbol-info-tick` | Export the last tick for a symbol |
92
- | `minimum-margins` | Export minimum-volume buy and sell margin requirements |
93
- | `market-book` | Export market depth (order book) |
94
- | `orders` | Export active orders |
95
- | `positions` | Export open positions |
96
- | `history-orders` | Export historical orders |
97
- | `history-deals` | Export historical deals |
98
- | `order-check` | Check funds sufficiency for a trade request |
99
- | `order-send` | Send a trade request to the trade server (`--yes` required) |
100
- | `collect-history` | Bundle rates, ticks, history-orders, and history-deals for one or more symbols into a single SQLite database |
78
+ | Command | Description |
79
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------ |
80
+ | `rates-from` | Export rates from a start date |
81
+ | `rates-from-pos` | Export rates from a start position |
82
+ | `latest-rates` | Export latest rates from a start position |
83
+ | `rates-range` | Export rates for a date range |
84
+ | `ticks-from` | Export ticks from a start date |
85
+ | `ticks-range` | Export ticks for a date range |
86
+ | `ticks-recent` | Export ticks from a recent trailing window |
87
+ | `account-info` | Export account information |
88
+ | `terminal-info` | Export terminal information |
89
+ | `version` | Export MetaTrader 5 version information |
90
+ | `last-error` | Export the last error information |
91
+ | `symbols` | Export symbol list |
92
+ | `symbol-info` | Export symbol details |
93
+ | `symbol-info-tick` | Export the last tick for a symbol |
94
+ | `minimum-margins` | Export minimum-volume buy and sell margin requirements |
95
+ | `market-book` | Export market depth (order book) |
96
+ | `orders` | Export active orders |
97
+ | `positions` | Export open positions |
98
+ | `history-orders` | Export historical orders |
99
+ | `history-deals` | Export historical deals |
100
+ | `recent-history-deals` | Export historical deals from a recent trailing window |
101
+ | `mt5-summary` | Export terminal/account status summary |
102
+ | `order-check` | Check funds sufficiency for a trade request |
103
+ | `order-send` | Send a trade request to the trade server (`--yes` required) |
104
+ | `collect-history` | Bundle rates, ticks, history-orders, and history-deals for one or more symbols into a single SQLite database |
101
105
 
102
106
  Use `order-check` to validate a request payload before running `order-send --yes`.
103
107
 
@@ -154,6 +158,7 @@ update_history_with_config(
154
158
  - **`rates` table**: normalized storage with `symbol` and `timeframe` columns.
155
159
  - **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
160
  - **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.
161
+ - **Rate view loading**: use `load_rate_data()` / `load_rate_data_from_connection()` to load a SQLite rate table or view into a `DatetimeIndex` DataFrame.
157
162
  - **SQLite export helpers**: use `export_dataframe_to_sqlite()` for append mode, optional index export, and post-write deduplication by key columns.
158
163
  - **Recent ticks and margins**: `recent_ticks()` and `minimum_margins()` SDK helpers (and matching CLI commands) cover common downstream read-only queries.
159
164
 
@@ -13,6 +13,7 @@ Built on top of [pdmt5](https://github.com/dceoy/pdmt5), a pandas-based data han
13
13
  - **Comprehensive data access**: Rates, ticks, account info, symbols, orders, positions, and trading history
14
14
  - **Flexible timeframes**: Named timeframes (M1, H1, D1, etc.) and numeric values
15
15
  - **Connection management**: Optional credentials, server, and timeout configuration
16
+ - **SQLite rate loading**: Load mt5cli-managed rate tables/views for offline workflows
16
17
 
17
18
  ## Installation
18
19
 
@@ -50,30 +51,33 @@ python -m mt5cli -o account.csv account-info
50
51
 
51
52
  ## Commands
52
53
 
53
- | Command | Description |
54
- | ------------------ | ------------------------------------------------------------------------------------------------------------ |
55
- | `rates-from` | Export rates from a start date |
56
- | `rates-from-pos` | Export rates from a start position |
57
- | `rates-range` | Export rates for a date range |
58
- | `ticks-from` | Export ticks from a start date |
59
- | `ticks-range` | Export ticks for a date range |
60
- | `ticks-recent` | Export ticks from a recent trailing window |
61
- | `account-info` | Export account information |
62
- | `terminal-info` | Export terminal information |
63
- | `version` | Export MetaTrader 5 version information |
64
- | `last-error` | Export the last error information |
65
- | `symbols` | Export symbol list |
66
- | `symbol-info` | Export symbol details |
67
- | `symbol-info-tick` | Export the last tick for a symbol |
68
- | `minimum-margins` | Export minimum-volume buy and sell margin requirements |
69
- | `market-book` | Export market depth (order book) |
70
- | `orders` | Export active orders |
71
- | `positions` | Export open positions |
72
- | `history-orders` | Export historical orders |
73
- | `history-deals` | Export historical deals |
74
- | `order-check` | Check funds sufficiency for a trade request |
75
- | `order-send` | Send a trade request to the trade server (`--yes` required) |
76
- | `collect-history` | Bundle rates, ticks, history-orders, and history-deals for one or more symbols into a single SQLite database |
54
+ | Command | Description |
55
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------ |
56
+ | `rates-from` | Export rates from a start date |
57
+ | `rates-from-pos` | Export rates from a start position |
58
+ | `latest-rates` | Export latest rates from a start position |
59
+ | `rates-range` | Export rates for a date range |
60
+ | `ticks-from` | Export ticks from a start date |
61
+ | `ticks-range` | Export ticks for a date range |
62
+ | `ticks-recent` | Export ticks from a recent trailing window |
63
+ | `account-info` | Export account information |
64
+ | `terminal-info` | Export terminal information |
65
+ | `version` | Export MetaTrader 5 version information |
66
+ | `last-error` | Export the last error information |
67
+ | `symbols` | Export symbol list |
68
+ | `symbol-info` | Export symbol details |
69
+ | `symbol-info-tick` | Export the last tick for a symbol |
70
+ | `minimum-margins` | Export minimum-volume buy and sell margin requirements |
71
+ | `market-book` | Export market depth (order book) |
72
+ | `orders` | Export active orders |
73
+ | `positions` | Export open positions |
74
+ | `history-orders` | Export historical orders |
75
+ | `history-deals` | Export historical deals |
76
+ | `recent-history-deals` | Export historical deals from a recent trailing window |
77
+ | `mt5-summary` | Export terminal/account status summary |
78
+ | `order-check` | Check funds sufficiency for a trade request |
79
+ | `order-send` | Send a trade request to the trade server (`--yes` required) |
80
+ | `collect-history` | Bundle rates, ticks, history-orders, and history-deals for one or more symbols into a single SQLite database |
77
81
 
78
82
  Use `order-check` to validate a request payload before running `order-send --yes`.
79
83
 
@@ -130,6 +134,7 @@ update_history_with_config(
130
134
  - **`rates` table**: normalized storage with `symbol` and `timeframe` columns.
131
135
  - **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
136
  - **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.
137
+ - **Rate view loading**: use `load_rate_data()` / `load_rate_data_from_connection()` to load a SQLite rate table or view into a `DatetimeIndex` DataFrame.
133
138
  - **SQLite export helpers**: use `export_dataframe_to_sqlite()` for append mode, optional index export, and post-write deduplication by key columns.
134
139
  - **Recent ticks and margins**: `recent_ticks()` and `minimum_margins()` SDK helpers (and matching CLI commands) cover common downstream read-only queries.
135
140
 
@@ -164,3 +164,22 @@ Resolution rules:
164
164
  - Pass `require_existing=True` to raise `ValueError` instead of returning a
165
165
  best-guess name when the database or view is missing.
166
166
  - Accepts either a SQLite path or an open `sqlite3.Connection`.
167
+
168
+ ### Rate data loading
169
+
170
+ Use `load_rate_data()` to load a table or view from a SQLite path, or
171
+ `load_rate_data_from_connection()` when you already have a connection:
172
+
173
+ ```python
174
+ from pathlib import Path
175
+
176
+ from mt5cli import load_rate_data
177
+ from mt5cli.history import resolve_rate_view_name
178
+
179
+ view = resolve_rate_view_name(Path("history.db"), "EURUSD", "M1", require_existing=True)
180
+ rates = load_rate_data(Path("history.db"), view, count=1000)
181
+ ```
182
+
183
+ The loader accepts close-based OHLC rate data or tick-like bid/ask data. It
184
+ validates that `time` exists, parses timestamps with pandas, and returns a
185
+ DataFrame indexed by ascending `DatetimeIndex` named `time`.
@@ -13,6 +13,7 @@ mt5cli is a CLI application that exports MetaTrader 5 trading data to multiple f
13
13
  - **Comprehensive data access**: Rates, ticks, account info, symbols, orders, positions, and trading history
14
14
  - **Flexible timeframes**: Named timeframes (M1, H1, D1, etc.) and numeric values
15
15
  - **Connection management**: Optional credentials, server, and timeout configuration
16
+ - **SQLite rate loading**: Load mt5cli-managed rate tables/views for offline workflows
16
17
 
17
18
  ## Installation
18
19
 
@@ -34,6 +35,7 @@ from mt5cli import (
34
35
  copy_rates_range,
35
36
  export_dataframe,
36
37
  export_dataframe_to_sqlite,
38
+ load_rate_data,
37
39
  minimum_margins,
38
40
  recent_ticks,
39
41
  )
@@ -49,7 +51,8 @@ rates = copy_rates_range(
49
51
  export_dataframe(rates, Path("rates.csv"), "csv")
50
52
 
51
53
  # Resolve SQLite rate compatibility views for downstream tools
52
- view = resolve_rate_view_name(Path("history.db"), "EURUSD", "M1")
54
+ view = resolve_rate_view_name(Path("history.db"), "EURUSD", "M1", require_existing=True)
55
+ offline_rates = load_rate_data(Path("history.db"), view, count=1000)
53
56
 
54
57
  # Recent tick window and minimum margin summary
55
58
  ticks = recent_ticks("EURUSD", seconds=300)
@@ -59,6 +62,9 @@ margins = minimum_margins("EURUSD")
59
62
  with Mt5CliClient(login=12345, password="secret", server="Broker-Demo") as client:
60
63
  account = client.account_info()
61
64
  positions = client.positions()
65
+ latest = client.latest_rates("EURUSD", "M1", count=100)
66
+ summary = client.mt5_summary()
67
+ summary_table = client.mt5_summary_as_df()
62
68
 
63
69
  # Bulk SQLite collection (same behavior as the collect-history CLI command)
64
70
  collect_history(
@@ -74,6 +80,8 @@ collect_history(
74
80
 
75
81
  Timeframes, tick flags, and ISO 8601 date strings are accepted wherever noted in the SDK API.
76
82
 
83
+ `Mt5CliClient.mt5_summary()` returns the SDK structured form as plain nested Python values. Use `Mt5CliClient.mt5_summary_as_df()` when you need a one-row DataFrame for export. The `mt5-summary` CLI command uses this tabular form, so nested terminal/account fields are JSON-encoded strings that are safe for CSV, JSON, Parquet, and SQLite output.
84
+
77
85
  ## Quick Start
78
86
 
79
87
  ```bash
@@ -104,6 +112,7 @@ mt5cli --login 12345 --password mypass --server MyBroker-Demo \
104
112
  | ---------------- | ---------------------------------- |
105
113
  | `rates-from` | Export rates from a start date |
106
114
  | `rates-from-pos` | Export rates from a start position |
115
+ | `latest-rates` | Export latest rates |
107
116
  | `rates-range` | Export rates for a date range |
108
117
 
109
118
  ### Ticks
@@ -130,14 +139,16 @@ mt5cli --login 12345 --password mypass --server MyBroker-Demo \
130
139
 
131
140
  ### Trading
132
141
 
133
- | Command | Description |
134
- | ---------------- | ----------------------------------------------------------- |
135
- | `orders` | Export active orders |
136
- | `positions` | Export open positions |
137
- | `history-orders` | Export historical orders |
138
- | `history-deals` | Export historical deals |
139
- | `order-check` | Check funds sufficiency for a trade request |
140
- | `order-send` | Send a trade request to the trade server (`--yes` required) |
142
+ | Command | Description |
143
+ | ---------------------- | ----------------------------------------------------------- |
144
+ | `orders` | Export active orders |
145
+ | `positions` | Export open positions |
146
+ | `history-orders` | Export historical orders |
147
+ | `history-deals` | Export historical deals |
148
+ | `recent-history-deals` | Export historical deals from a trailing window |
149
+ | `mt5-summary` | Export terminal/account status summary |
150
+ | `order-check` | Check funds sufficiency for a trade request |
151
+ | `order-send` | Send a trade request to the trade server (`--yes` required) |
141
152
 
142
153
  Use `order-check` to validate a request payload before running `order-send --yes`.
143
154
 
@@ -2,11 +2,13 @@
2
2
 
3
3
  from importlib.metadata import version
4
4
 
5
+ from .history import load_rate_data, load_rate_data_from_connection
5
6
  from .sdk import (
6
7
  Mt5CliClient,
7
8
  account_info,
8
9
  build_config,
9
10
  collect_history,
11
+ collect_latest_rates,
10
12
  copy_rates_from,
11
13
  copy_rates_from_pos,
12
14
  copy_rates_range,
@@ -15,10 +17,14 @@ from .sdk import (
15
17
  history_deals,
16
18
  history_orders,
17
19
  last_error,
20
+ latest_rates,
18
21
  market_book,
19
22
  minimum_margins,
23
+ mt5_summary,
24
+ mt5_summary_as_df,
20
25
  orders,
21
26
  positions,
27
+ recent_history_deals,
22
28
  recent_ticks,
23
29
  symbol_info,
24
30
  symbol_info_tick,
@@ -47,6 +53,7 @@ __all__ = [
47
53
  "account_info",
48
54
  "build_config",
49
55
  "collect_history",
56
+ "collect_latest_rates",
50
57
  "copy_rates_from",
51
58
  "copy_rates_from_pos",
52
59
  "copy_rates_range",
@@ -58,11 +65,17 @@ __all__ = [
58
65
  "history_deals",
59
66
  "history_orders",
60
67
  "last_error",
68
+ "latest_rates",
69
+ "load_rate_data",
70
+ "load_rate_data_from_connection",
61
71
  "market_book",
62
72
  "minimum_margins",
73
+ "mt5_summary",
74
+ "mt5_summary_as_df",
63
75
  "mt5_version",
64
76
  "orders",
65
77
  "positions",
78
+ "recent_history_deals",
66
79
  "recent_ticks",
67
80
  "symbol_info",
68
81
  "symbol_info_tick",
@@ -222,6 +222,31 @@ def rates_from_pos(
222
222
  )
223
223
 
224
224
 
225
+ @app.command()
226
+ def latest_rates(
227
+ ctx: typer.Context,
228
+ symbol: Annotated[str, typer.Option(help="Symbol name.")],
229
+ timeframe: Annotated[
230
+ int,
231
+ typer.Option(
232
+ click_type=TIMEFRAME_TYPE,
233
+ help="Timeframe.",
234
+ ),
235
+ ],
236
+ count: Annotated[int, typer.Option(help="Number of records.")],
237
+ start_pos: Annotated[
238
+ int,
239
+ typer.Option(help="Start position (0 = current bar)."),
240
+ ] = 0,
241
+ ) -> None:
242
+ """Export latest rates from a start position."""
243
+ client = _sdk_client(ctx)
244
+ _execute_export(
245
+ ctx,
246
+ lambda: client.latest_rates(symbol, timeframe, count, start_pos=start_pos),
247
+ )
248
+
249
+
225
250
  @app.command()
226
251
  def rates_range(
227
252
  ctx: typer.Context,
@@ -475,6 +500,37 @@ def history_deals(
475
500
  )
476
501
 
477
502
 
503
+ @app.command()
504
+ def recent_history_deals(
505
+ ctx: typer.Context,
506
+ hours: Annotated[float, typer.Option(help="Lookback window in hours.")],
507
+ date_to: Annotated[
508
+ datetime | None,
509
+ typer.Option(click_type=DATETIME_TYPE, help="Window end date."),
510
+ ] = None,
511
+ group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
512
+ symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
513
+ ) -> None:
514
+ """Export historical deals from a recent trailing window."""
515
+ client = _sdk_client(ctx)
516
+ _execute_export(
517
+ ctx,
518
+ lambda: client.recent_history_deals(
519
+ hours,
520
+ date_to=date_to,
521
+ group=group,
522
+ symbol=symbol,
523
+ ),
524
+ )
525
+
526
+
527
+ @app.command()
528
+ def mt5_summary(ctx: typer.Context) -> None:
529
+ """Export a compact terminal/account status summary."""
530
+ client = _sdk_client(ctx)
531
+ _execute_export(ctx, client.mt5_summary_as_df)
532
+
533
+
478
534
  @app.command()
479
535
  def version(ctx: typer.Context) -> None:
480
536
  """Export MetaTrader5 version information."""
@@ -6,7 +6,7 @@ import logging
6
6
  import sqlite3
7
7
  from datetime import UTC, datetime
8
8
  from pathlib import Path
9
- from typing import TYPE_CHECKING, Literal
9
+ from typing import TYPE_CHECKING, Literal, cast
10
10
 
11
11
  import pandas as pd
12
12
 
@@ -126,6 +126,14 @@ def build_rate_view_name(
126
126
  SqliteConnOrPath = sqlite3.Connection | Path | str
127
127
 
128
128
 
129
+ def _require_non_empty_identifier(identifier: str, kind: str) -> str:
130
+ value = identifier.strip()
131
+ if not value:
132
+ msg = f"SQLite {kind} name must not be empty."
133
+ raise ValueError(msg)
134
+ return value
135
+
136
+
129
137
  def _open_history_connection(
130
138
  conn_or_path: SqliteConnOrPath,
131
139
  ) -> tuple[sqlite3.Connection | None, bool]:
@@ -144,6 +152,133 @@ def _open_history_connection(
144
152
  return conn, True
145
153
 
146
154
 
155
+ def _open_existing_sqlite_database(
156
+ conn_or_path: SqliteConnOrPath,
157
+ ) -> tuple[sqlite3.Connection, bool]:
158
+ """Open a read-only SQLite database or reuse an existing connection.
159
+
160
+ Returns:
161
+ Tuple of connection and whether the caller should close it.
162
+
163
+ Raises:
164
+ ValueError: If the database path does not exist or is not a file.
165
+ """
166
+ if isinstance(conn_or_path, sqlite3.Connection):
167
+ return conn_or_path, False
168
+ path = Path(conn_or_path)
169
+ if not path.exists():
170
+ msg = f"SQLite database not found: {path}"
171
+ raise ValueError(msg)
172
+ if not path.is_file():
173
+ msg = f"SQLite database path is not a file: {path}"
174
+ raise ValueError(msg)
175
+ conn = sqlite3.connect(f"{path.resolve().as_uri()}?mode=ro", uri=True)
176
+ return conn, True
177
+
178
+
179
+ def _validate_rate_load_request(table: str, count: int | None) -> str:
180
+ table_name = _require_non_empty_identifier(table, "table or view")
181
+ if count is not None and count <= 0:
182
+ msg = "count must be positive when provided."
183
+ raise ValueError(msg)
184
+ return table_name
185
+
186
+
187
+ def _ensure_rate_columns(columns: set[str], table: str) -> None:
188
+ if not columns:
189
+ msg = f"SQLite table or view not found: {table}"
190
+ raise ValueError(msg)
191
+ if "time" not in columns:
192
+ msg = f"SQLite table or view {table!r} must include a time column."
193
+ raise ValueError(msg)
194
+ if "close" not in columns and not {"ask", "bid"}.issubset(columns):
195
+ msg = (
196
+ f"SQLite table or view {table!r} must include close, "
197
+ "or both ask and bid columns."
198
+ )
199
+ raise ValueError(msg)
200
+
201
+
202
+ def _parse_rate_time_index(frame: pd.DataFrame, table: str) -> pd.DataFrame:
203
+ parsed = frame["time"].map(parse_sqlite_timestamp)
204
+ if parsed.isna().any():
205
+ msg = f"SQLite table or view {table!r} contains unparsable time values."
206
+ raise ValueError(msg)
207
+ result = frame.drop(columns=["time"])
208
+ result.index = pd.DatetimeIndex(parsed, name="time")
209
+ return result.sort_index(kind="stable")
210
+
211
+
212
+ def load_rate_data_from_connection(
213
+ connection: sqlite3.Connection,
214
+ table: str,
215
+ count: int | None = None,
216
+ ) -> pd.DataFrame:
217
+ """Load rate-like data from a SQLite table or view.
218
+
219
+ Args:
220
+ connection: Open SQLite connection.
221
+ table: Source table or view name.
222
+ count: Optional number of most recent rows to load.
223
+
224
+ Returns:
225
+ DataFrame indexed by ascending ``time``.
226
+
227
+ Raises:
228
+ ValueError: If inputs, schema, timestamps are invalid, or the table
229
+ or view contains no rows.
230
+ """
231
+ table_name = _validate_rate_load_request(table, count)
232
+ columns = get_table_columns(connection, table_name)
233
+ _ensure_rate_columns(columns, table_name)
234
+ quoted_table = quote_sqlite_identifier(table_name)
235
+ if count is None:
236
+ frame = cast(
237
+ "pd.DataFrame",
238
+ pd.read_sql_query( # type: ignore[reportUnknownMemberType]
239
+ f"SELECT * FROM {quoted_table} ORDER BY time ASC", # noqa: S608
240
+ connection,
241
+ ),
242
+ )
243
+ else:
244
+ frame = cast(
245
+ "pd.DataFrame",
246
+ pd.read_sql_query( # type: ignore[reportUnknownMemberType]
247
+ f"SELECT * FROM {quoted_table} ORDER BY time DESC LIMIT ?", # noqa: S608
248
+ connection,
249
+ params=(count,),
250
+ ),
251
+ )
252
+ if frame.empty:
253
+ msg = f"SQLite table or view {table_name!r} contains no rows."
254
+ raise ValueError(msg)
255
+ return _parse_rate_time_index(frame, table_name)
256
+
257
+
258
+ def load_rate_data(
259
+ conn_or_path: SqliteConnOrPath,
260
+ table: str,
261
+ count: int | None = None,
262
+ ) -> pd.DataFrame:
263
+ """Load rate-like data from a SQLite database path or connection.
264
+
265
+ Args:
266
+ conn_or_path: SQLite database path or open connection.
267
+ table: Source table or view name.
268
+ count: Optional number of most recent rows to load.
269
+
270
+ Returns:
271
+ DataFrame indexed by ascending ``time``.
272
+
273
+ """
274
+ conn, should_close = _open_existing_sqlite_database(conn_or_path)
275
+ try:
276
+ return load_rate_data_from_connection(conn, table, count=count)
277
+ finally:
278
+ if should_close:
279
+ conn.close()
280
+
281
+
147
282
  def _load_rates_timeframe_counts(conn: sqlite3.Connection) -> dict[str, int] | None:
148
283
  """Return distinct timeframe counts per symbol from the normalized rates table."""
149
284
  columns = get_table_columns(conn, Dataset.rates.table_name)
@@ -349,7 +484,8 @@ def resolve_rate_view_names(
349
484
 
350
485
  def get_table_columns(conn: sqlite3.Connection, table: str) -> set[str]:
351
486
  """Return existing SQLite columns for a table."""
352
- rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
487
+ quoted_table = quote_sqlite_identifier(table)
488
+ rows = conn.execute(f"PRAGMA table_info({quoted_table})").fetchall()
353
489
  return {str(row[1]) for row in rows}
354
490
 
355
491