mt5cli 0.5.1__tar.gz → 0.5.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.5.1 → mt5cli-0.5.3}/PKG-INFO +5 -2
- {mt5cli-0.5.1 → mt5cli-0.5.3}/README.md +4 -1
- {mt5cli-0.5.1 → mt5cli-0.5.3}/docs/api/history.md +14 -2
- mt5cli-0.5.3/docs/api/sdk.md +83 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/mt5cli/__init__.py +12 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/mt5cli/history.py +89 -9
- {mt5cli-0.5.1 → mt5cli-0.5.3}/mt5cli/sdk.py +323 -2
- {mt5cli-0.5.1 → mt5cli-0.5.3}/pyproject.toml +1 -1
- {mt5cli-0.5.1 → mt5cli-0.5.3}/tests/test_history.py +187 -3
- {mt5cli-0.5.1 → mt5cli-0.5.3}/tests/test_sdk.py +312 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/uv.lock +1 -1
- mt5cli-0.5.1/docs/api/sdk.md +0 -3
- {mt5cli-0.5.1 → mt5cli-0.5.3}/.agents/skills/local-qa/SKILL.md +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/.agents/skills/local-qa/scripts/qa.sh +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/.agents/skills/mt5cli/SKILL.md +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/.claude/agents/codex.md +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/.claude/settings.json +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/.github/FUNDING.yml +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/.github/dependabot.yml +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/.github/renovate.json +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/.github/workflows/ci.yml +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/.github/workflows/claude.yml +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/.github/workflows/release.yml +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/.gitignore +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/AGENTS.md +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/CLAUDE.md +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/LICENSE +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/docs/api/cli.md +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/docs/api/index.md +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/docs/api/utils.md +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/docs/index.md +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/mkdocs.yml +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/mt5cli/__main__.py +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/mt5cli/cli.py +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/mt5cli/utils.py +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/tests/__init__.py +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/tests/test_cli.py +0 -0
- {mt5cli-0.5.1 → mt5cli-0.5.3}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mt5cli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.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>
|
|
@@ -160,7 +160,10 @@ update_history_with_config(
|
|
|
160
160
|
- **Rate view resolution**: use `resolve_rate_view_name()` / `resolve_rate_view_names()` to map symbols and granularities to existing SQLite compatibility views without creating databases. Both accept `None` (or a missing path) and return deterministic default names unless `require_existing=True`.
|
|
161
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.
|
|
162
162
|
- **Multi-series rate loading**: use `build_rate_targets()` to build neutral `RateTarget(symbol, timeframe)` pairs, `resolve_rate_tables()` to map them to table/view names (pass `require_existing=True` for strict resolution), and `load_rate_series_from_sqlite()` to load them into a mapping keyed by `(symbol, integer timeframe)`. The loader requires existing managed views unless `explicit_tables` is supplied, and rejects duplicate `(symbol, timeframe)` targets.
|
|
163
|
-
- **Multi-account latest rates**: use `collect_latest_rates_for_accounts()` with `AccountSpec` to read the latest bars for several account groups, merged into a `(symbol, integer timeframe)` mapping.
|
|
163
|
+
- **Multi-account latest rates**: use `collect_latest_rates_for_accounts()` with `AccountSpec` to read the latest bars for several account groups, merged into a `(symbol, integer timeframe)` mapping. For long-running pollers, `collect_latest_rates_for_accounts_with_retries()` adds bounded exponential backoff that retries only `pdmt5.Mt5TradingError` / `pdmt5.Mt5RuntimeError` and re-raises once `retry_count` is exhausted.
|
|
164
|
+
- **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.
|
|
165
|
+
- **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` and let the caller decide logging.
|
|
166
|
+
- **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.
|
|
164
167
|
- **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 `Mt5CliClient` that shuts down on exit.
|
|
165
168
|
- **SQLite export helpers**: use `export_dataframe_to_sqlite()` for append mode, optional index export, and post-write deduplication by key columns.
|
|
166
169
|
- **Recent ticks and margins**: `recent_ticks()` and `minimum_margins()` SDK helpers (and matching CLI commands) cover common downstream read-only queries.
|
|
@@ -136,7 +136,10 @@ update_history_with_config(
|
|
|
136
136
|
- **Rate view resolution**: use `resolve_rate_view_name()` / `resolve_rate_view_names()` to map symbols and granularities to existing SQLite compatibility views without creating databases. Both accept `None` (or a missing path) and return deterministic default names unless `require_existing=True`.
|
|
137
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.
|
|
138
138
|
- **Multi-series rate loading**: use `build_rate_targets()` to build neutral `RateTarget(symbol, timeframe)` pairs, `resolve_rate_tables()` to map them to table/view names (pass `require_existing=True` for strict resolution), and `load_rate_series_from_sqlite()` to load them into a mapping keyed by `(symbol, integer timeframe)`. The loader requires existing managed views unless `explicit_tables` is supplied, and rejects duplicate `(symbol, timeframe)` targets.
|
|
139
|
-
- **Multi-account latest rates**: use `collect_latest_rates_for_accounts()` with `AccountSpec` to read the latest bars for several account groups, merged into a `(symbol, integer timeframe)` mapping.
|
|
139
|
+
- **Multi-account latest rates**: use `collect_latest_rates_for_accounts()` with `AccountSpec` to read the latest bars for several account groups, merged into a `(symbol, integer timeframe)` mapping. For long-running pollers, `collect_latest_rates_for_accounts_with_retries()` adds bounded exponential backoff that retries only `pdmt5.Mt5TradingError` / `pdmt5.Mt5RuntimeError` and re-raises once `retry_count` is exhausted.
|
|
140
|
+
- **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.
|
|
141
|
+
- **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` and let the caller decide logging.
|
|
142
|
+
- **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.
|
|
140
143
|
- **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 `Mt5CliClient` that shuts down on exit.
|
|
141
144
|
- **SQLite export helpers**: use `export_dataframe_to_sqlite()` for append mode, optional index export, and post-write deduplication by key columns.
|
|
142
145
|
- **Recent ticks and margins**: `recent_ticks()` and `minimum_margins()` SDK helpers (and matching CLI commands) cover common downstream read-only queries.
|
|
@@ -133,8 +133,8 @@ The `update_history` SDK path uses the same base tables and optional
|
|
|
133
133
|
### Rate view resolution
|
|
134
134
|
|
|
135
135
|
Downstream tools can resolve mt5cli-managed compatibility view names from an
|
|
136
|
-
existing SQLite history database without creating files or guessing
|
|
137
|
-
|
|
136
|
+
existing SQLite history database without creating files or guessing naming
|
|
137
|
+
schemes:
|
|
138
138
|
|
|
139
139
|
```python
|
|
140
140
|
from pathlib import Path
|
|
@@ -215,3 +215,15 @@ frame = series["EURUSD", 1] # keyed by (symbol, integer timeframe)
|
|
|
215
215
|
requires existing managed `rate_*` compatibility views and raises
|
|
216
216
|
`ValueError` when they are missing. Duplicate `(symbol, timeframe)` targets
|
|
217
217
|
are rejected.
|
|
218
|
+
- `load_rate_series_by_granularity()` is a thin wrapper that builds the targets,
|
|
219
|
+
loads the series, and rekeys the result by granularity name to avoid
|
|
220
|
+
converting integer timeframes downstream:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from mt5cli import load_rate_series_by_granularity
|
|
224
|
+
|
|
225
|
+
series = load_rate_series_by_granularity(
|
|
226
|
+
"history.db", ["EURUSD"], ["M1", "H1"], count=1000
|
|
227
|
+
)
|
|
228
|
+
frame = series["EURUSD", "M1"] # keyed by (symbol | None, granularity_name)
|
|
229
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# SDK Module
|
|
2
|
+
|
|
3
|
+
::: mt5cli.sdk
|
|
4
|
+
|
|
5
|
+
## Resilient multi-account orchestration
|
|
6
|
+
|
|
7
|
+
The SDK ships strategy-agnostic helpers for building long-running collectors on
|
|
8
|
+
top of the read-only client. None of them depend on a particular trading
|
|
9
|
+
application.
|
|
10
|
+
|
|
11
|
+
### Retrying transient rate collection
|
|
12
|
+
|
|
13
|
+
`collect_latest_rates_for_accounts_with_retries()` wraps
|
|
14
|
+
`collect_latest_rates_for_accounts()` with bounded exponential backoff. Only
|
|
15
|
+
`pdmt5.Mt5TradingError` and `pdmt5.Mt5RuntimeError` are retried; the final
|
|
16
|
+
failure is re-raised once `retry_count` is exhausted.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from mt5cli import AccountSpec, collect_latest_rates_for_accounts_with_retries
|
|
20
|
+
|
|
21
|
+
accounts = [AccountSpec(symbols=["EURUSD"], login=12345)]
|
|
22
|
+
rates = collect_latest_rates_for_accounts_with_retries(
|
|
23
|
+
accounts,
|
|
24
|
+
["M1", "H1"],
|
|
25
|
+
count=500,
|
|
26
|
+
retry_count=3,
|
|
27
|
+
backoff_base=2, # sleeps 2s, 4s, 8s between attempts
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Resolving credentials and `${ENV_VAR}` placeholders
|
|
32
|
+
|
|
33
|
+
`resolve_account_spec()` / `resolve_account_specs()` merge explicit override
|
|
34
|
+
values over `AccountSpec` fields and expand `${ENV_VAR}` placeholders, keeping
|
|
35
|
+
secrets out of plan/config files. A missing environment variable raises
|
|
36
|
+
`ValueError`.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import os
|
|
40
|
+
|
|
41
|
+
from mt5cli import AccountSpec, resolve_account_specs
|
|
42
|
+
|
|
43
|
+
os.environ["MT5_LOGIN"] = "12345"
|
|
44
|
+
os.environ["MT5_PASSWORD"] = "secret"
|
|
45
|
+
accounts = [
|
|
46
|
+
AccountSpec(symbols=["EURUSD"], login="${MT5_LOGIN}", password="${MT5_PASSWORD}")
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
resolved = resolve_account_specs(accounts, server="Broker-Demo")
|
|
50
|
+
# resolved[0].login == "12345", resolved[0].server == "Broker-Demo"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Throttled incremental history updates
|
|
54
|
+
|
|
55
|
+
`ThrottledHistoryUpdater` wraps `update_history()` with a minimum interval
|
|
56
|
+
between successful runs (using a monotonic clock), so an application loop can
|
|
57
|
+
call it every iteration without over-fetching.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from pdmt5 import Mt5Config, Mt5DataClient
|
|
61
|
+
|
|
62
|
+
from mt5cli import Dataset, ThrottledHistoryUpdater
|
|
63
|
+
|
|
64
|
+
updater = ThrottledHistoryUpdater(
|
|
65
|
+
output="history.db",
|
|
66
|
+
datasets={Dataset.rates},
|
|
67
|
+
timeframes=["M1"],
|
|
68
|
+
interval_seconds=60, # <= 0 updates on every call
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
client = Mt5DataClient(config=Mt5Config(login=12345))
|
|
72
|
+
client.initialize_and_login_mt5()
|
|
73
|
+
try:
|
|
74
|
+
while True:
|
|
75
|
+
updater.update(client, ["EURUSD", "GBPUSD"]) # no-op until 60s elapse
|
|
76
|
+
# ... do other work; break when shutting down ...
|
|
77
|
+
finally:
|
|
78
|
+
client.shutdown()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
By default `Mt5TradingError`, `Mt5RuntimeError`, and `sqlite3.Error` propagate so
|
|
82
|
+
the caller controls logging; pass `suppress_errors=True` to swallow them and
|
|
83
|
+
return `False` without advancing the throttle.
|
|
@@ -8,6 +8,7 @@ from .history import (
|
|
|
8
8
|
build_rate_view_name,
|
|
9
9
|
load_rate_data,
|
|
10
10
|
load_rate_data_from_connection,
|
|
11
|
+
load_rate_series_by_granularity,
|
|
11
12
|
load_rate_series_from_sqlite,
|
|
12
13
|
resolve_history_datasets,
|
|
13
14
|
resolve_history_tick_flags,
|
|
@@ -19,11 +20,13 @@ from .history import (
|
|
|
19
20
|
from .sdk import (
|
|
20
21
|
AccountSpec,
|
|
21
22
|
Mt5CliClient,
|
|
23
|
+
ThrottledHistoryUpdater,
|
|
22
24
|
account_info,
|
|
23
25
|
build_config,
|
|
24
26
|
collect_history,
|
|
25
27
|
collect_latest_rates,
|
|
26
28
|
collect_latest_rates_for_accounts,
|
|
29
|
+
collect_latest_rates_for_accounts_with_retries,
|
|
27
30
|
copy_rates_from,
|
|
28
31
|
copy_rates_from_pos,
|
|
29
32
|
copy_rates_range,
|
|
@@ -42,6 +45,9 @@ from .sdk import (
|
|
|
42
45
|
positions,
|
|
43
46
|
recent_history_deals,
|
|
44
47
|
recent_ticks,
|
|
48
|
+
resolve_account_spec,
|
|
49
|
+
resolve_account_specs,
|
|
50
|
+
substitute_env_placeholders,
|
|
45
51
|
symbol_info,
|
|
46
52
|
symbol_info_tick,
|
|
47
53
|
symbols,
|
|
@@ -75,6 +81,7 @@ __all__ = [
|
|
|
75
81
|
"IfExists",
|
|
76
82
|
"Mt5CliClient",
|
|
77
83
|
"RateTarget",
|
|
84
|
+
"ThrottledHistoryUpdater",
|
|
78
85
|
"account_info",
|
|
79
86
|
"build_config",
|
|
80
87
|
"build_rate_targets",
|
|
@@ -82,6 +89,7 @@ __all__ = [
|
|
|
82
89
|
"collect_history",
|
|
83
90
|
"collect_latest_rates",
|
|
84
91
|
"collect_latest_rates_for_accounts",
|
|
92
|
+
"collect_latest_rates_for_accounts_with_retries",
|
|
85
93
|
"copy_rates_from",
|
|
86
94
|
"copy_rates_from_pos",
|
|
87
95
|
"copy_rates_range",
|
|
@@ -96,6 +104,7 @@ __all__ = [
|
|
|
96
104
|
"latest_rates",
|
|
97
105
|
"load_rate_data",
|
|
98
106
|
"load_rate_data_from_connection",
|
|
107
|
+
"load_rate_series_by_granularity",
|
|
99
108
|
"load_rate_series_from_sqlite",
|
|
100
109
|
"market_book",
|
|
101
110
|
"minimum_margins",
|
|
@@ -110,12 +119,15 @@ __all__ = [
|
|
|
110
119
|
"positions",
|
|
111
120
|
"recent_history_deals",
|
|
112
121
|
"recent_ticks",
|
|
122
|
+
"resolve_account_spec",
|
|
123
|
+
"resolve_account_specs",
|
|
113
124
|
"resolve_history_datasets",
|
|
114
125
|
"resolve_history_tick_flags",
|
|
115
126
|
"resolve_history_timeframes",
|
|
116
127
|
"resolve_rate_tables",
|
|
117
128
|
"resolve_rate_view_name",
|
|
118
129
|
"resolve_rate_view_names",
|
|
130
|
+
"substitute_env_placeholders",
|
|
119
131
|
"symbol_info",
|
|
120
132
|
"symbol_info_tick",
|
|
121
133
|
"symbols",
|
|
@@ -21,7 +21,7 @@ from .utils import (
|
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
if TYPE_CHECKING:
|
|
24
|
-
from collections.abc import Callable, Sequence
|
|
24
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
25
25
|
|
|
26
26
|
from pdmt5 import Mt5DataClient
|
|
27
27
|
|
|
@@ -706,6 +706,55 @@ def load_rate_series_from_sqlite(
|
|
|
706
706
|
conn.close()
|
|
707
707
|
|
|
708
708
|
|
|
709
|
+
def load_rate_series_by_granularity(
|
|
710
|
+
conn_or_path: SqliteConnOrPath,
|
|
711
|
+
symbols: Sequence[str],
|
|
712
|
+
granularities: Sequence[int | str],
|
|
713
|
+
count: int,
|
|
714
|
+
*,
|
|
715
|
+
explicit_tables: Sequence[str] | None = None,
|
|
716
|
+
allow_missing_symbol: bool = False,
|
|
717
|
+
) -> dict[tuple[str | None, str], pd.DataFrame]:
|
|
718
|
+
"""Load rate series keyed by symbol and string granularity name.
|
|
719
|
+
|
|
720
|
+
Builds targets with :func:`build_rate_targets` and loads them with
|
|
721
|
+
:func:`load_rate_series_from_sqlite`, then rekeys the result by granularity
|
|
722
|
+
name (for example ``M1``) instead of the integer timeframe to reduce
|
|
723
|
+
downstream boilerplate.
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
conn_or_path: SQLite database path or open connection.
|
|
727
|
+
symbols: MT5 symbol names. May be empty when ``allow_missing_symbol``.
|
|
728
|
+
granularities: MT5 timeframes as integers or names (for example ``M1``).
|
|
729
|
+
count: Number of most recent rows to load per series.
|
|
730
|
+
explicit_tables: Optional explicit table or view names matching the
|
|
731
|
+
built targets in row-major order. Required when symbols are omitted.
|
|
732
|
+
allow_missing_symbol: When True and ``symbols`` is empty, build targets
|
|
733
|
+
with ``symbol=None`` for each granularity instead of raising.
|
|
734
|
+
|
|
735
|
+
Returns:
|
|
736
|
+
Mapping keyed by ``(symbol | None, granularity_name)`` to each rate
|
|
737
|
+
DataFrame. Propagates ``ValueError`` (via :func:`build_rate_targets` and
|
|
738
|
+
:func:`load_rate_series_from_sqlite`) when inputs are empty or invalid,
|
|
739
|
+
table resolution fails, or duplicate targets are present.
|
|
740
|
+
"""
|
|
741
|
+
targets = build_rate_targets(
|
|
742
|
+
symbols,
|
|
743
|
+
granularities,
|
|
744
|
+
allow_missing_symbol=allow_missing_symbol,
|
|
745
|
+
)
|
|
746
|
+
series = load_rate_series_from_sqlite(
|
|
747
|
+
conn_or_path,
|
|
748
|
+
targets,
|
|
749
|
+
count,
|
|
750
|
+
explicit_tables=explicit_tables,
|
|
751
|
+
)
|
|
752
|
+
return {
|
|
753
|
+
(symbol, resolve_granularity_name(timeframe)): frame
|
|
754
|
+
for (symbol, timeframe), frame in series.items()
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
|
|
709
758
|
def get_table_columns(conn: sqlite3.Connection, table: str) -> set[str]:
|
|
710
759
|
"""Return existing SQLite columns for a table."""
|
|
711
760
|
quoted_table = quote_sqlite_identifier(table)
|
|
@@ -989,7 +1038,20 @@ def drop_duplicates_in_table(
|
|
|
989
1038
|
)
|
|
990
1039
|
|
|
991
1040
|
|
|
992
|
-
|
|
1041
|
+
@dataclass(frozen=True)
|
|
1042
|
+
class DedupScope:
|
|
1043
|
+
"""Scoped deduplication predicate and the columns it references.
|
|
1044
|
+
|
|
1045
|
+
Attributes:
|
|
1046
|
+
where: SQL predicate appended to the duplicate-removal query.
|
|
1047
|
+
params: Parameters bound to the scope predicate.
|
|
1048
|
+
required_columns: Columns that must be present in the written table for
|
|
1049
|
+
the scope to run.
|
|
1050
|
+
"""
|
|
1051
|
+
|
|
1052
|
+
where: str
|
|
1053
|
+
params: tuple[object, ...]
|
|
1054
|
+
required_columns: frozenset[str]
|
|
993
1055
|
|
|
994
1056
|
|
|
995
1057
|
def _record_dedup_scope(
|
|
@@ -997,17 +1059,25 @@ def _record_dedup_scope(
|
|
|
997
1059
|
dataset: Dataset,
|
|
998
1060
|
scope_where: str,
|
|
999
1061
|
scope_params: tuple[object, ...],
|
|
1062
|
+
required_columns: frozenset[str],
|
|
1000
1063
|
) -> None:
|
|
1001
|
-
dedup_scopes.setdefault(dataset, []).append(
|
|
1064
|
+
dedup_scopes.setdefault(dataset, []).append(
|
|
1065
|
+
DedupScope(scope_where, scope_params, required_columns),
|
|
1066
|
+
)
|
|
1002
1067
|
|
|
1003
1068
|
|
|
1004
1069
|
def deduplicate_history_tables(
|
|
1005
1070
|
conn: sqlite3.Connection,
|
|
1006
1071
|
written_columns: dict[Dataset, set[str]],
|
|
1007
1072
|
written_tables: set[Dataset],
|
|
1008
|
-
dedup_scopes:
|
|
1073
|
+
dedup_scopes: Mapping[Dataset, Sequence[DedupScope]] | None = None,
|
|
1009
1074
|
) -> None:
|
|
1010
|
-
"""Deduplicate appended history tables by stable identifiers.
|
|
1075
|
+
"""Deduplicate appended history tables by stable identifiers.
|
|
1076
|
+
|
|
1077
|
+
Scopes whose required columns are not present in the written table are
|
|
1078
|
+
skipped. If all scopes for a dataset are skipped, the table receives one
|
|
1079
|
+
unscoped deduplication pass instead.
|
|
1080
|
+
"""
|
|
1011
1081
|
cursor = conn.cursor()
|
|
1012
1082
|
for dataset in written_tables:
|
|
1013
1083
|
columns = written_columns.get(dataset, set())
|
|
@@ -1026,16 +1096,19 @@ def deduplicate_history_tables(
|
|
|
1026
1096
|
table,
|
|
1027
1097
|
)
|
|
1028
1098
|
continue
|
|
1029
|
-
|
|
1099
|
+
raw_scopes: Sequence[DedupScope] = (
|
|
1100
|
+
dedup_scopes.get(dataset, ()) if dedup_scopes else ()
|
|
1101
|
+
)
|
|
1102
|
+
scopes = [scope for scope in raw_scopes if scope.required_columns <= columns]
|
|
1030
1103
|
if scopes:
|
|
1031
|
-
for
|
|
1104
|
+
for scope in scopes:
|
|
1032
1105
|
drop_duplicates_in_table(
|
|
1033
1106
|
cursor,
|
|
1034
1107
|
table,
|
|
1035
1108
|
list(keys),
|
|
1036
1109
|
keep="last",
|
|
1037
|
-
scope_where=
|
|
1038
|
-
scope_params=
|
|
1110
|
+
scope_where=scope.where,
|
|
1111
|
+
scope_params=scope.params,
|
|
1039
1112
|
)
|
|
1040
1113
|
continue
|
|
1041
1114
|
drop_duplicates_in_table(cursor, table, list(keys), keep="last")
|
|
@@ -1402,6 +1475,7 @@ def _write_incremental_rates(
|
|
|
1402
1475
|
Dataset.rates,
|
|
1403
1476
|
"symbol = ? AND timeframe = ? AND time >= ?",
|
|
1404
1477
|
(symbol, timeframe, start_date),
|
|
1478
|
+
frozenset({"symbol", "timeframe", "time"}),
|
|
1405
1479
|
)
|
|
1406
1480
|
|
|
1407
1481
|
|
|
@@ -1440,6 +1514,7 @@ def _write_incremental_ticks(
|
|
|
1440
1514
|
Dataset.ticks,
|
|
1441
1515
|
"symbol = ? AND time >= ?",
|
|
1442
1516
|
(symbol, start_date),
|
|
1517
|
+
frozenset({"symbol", "time"}),
|
|
1443
1518
|
)
|
|
1444
1519
|
|
|
1445
1520
|
|
|
@@ -1478,6 +1553,7 @@ def _write_incremental_history_orders(
|
|
|
1478
1553
|
Dataset.history_orders,
|
|
1479
1554
|
"symbol = ? AND time >= ?",
|
|
1480
1555
|
(symbol, start_date),
|
|
1556
|
+
frozenset({"symbol", "time"}),
|
|
1481
1557
|
)
|
|
1482
1558
|
|
|
1483
1559
|
|
|
@@ -1531,6 +1607,7 @@ def _write_incremental_history_deals(
|
|
|
1531
1607
|
Dataset.history_deals,
|
|
1532
1608
|
"symbol = ? AND time >= ?",
|
|
1533
1609
|
(symbol, start_by_symbol[symbol, None]),
|
|
1610
|
+
frozenset({"symbol", "time"}),
|
|
1534
1611
|
)
|
|
1535
1612
|
if "type" in columns:
|
|
1536
1613
|
_record_dedup_scope(
|
|
@@ -1538,6 +1615,7 @@ def _write_incremental_history_deals(
|
|
|
1538
1615
|
Dataset.history_deals,
|
|
1539
1616
|
f"type NOT IN {_TRADE_DEAL_TYPES_SQL} AND time >= ?",
|
|
1540
1617
|
(account_event_start,),
|
|
1618
|
+
frozenset({"type", "time"}),
|
|
1541
1619
|
)
|
|
1542
1620
|
if "type" not in columns and "symbol" in columns:
|
|
1543
1621
|
_record_dedup_scope(
|
|
@@ -1545,6 +1623,7 @@ def _write_incremental_history_deals(
|
|
|
1545
1623
|
Dataset.history_deals,
|
|
1546
1624
|
"(symbol IS NULL OR symbol = '') AND time >= ?",
|
|
1547
1625
|
(account_event_start,),
|
|
1626
|
+
frozenset({"symbol", "time"}),
|
|
1548
1627
|
)
|
|
1549
1628
|
return
|
|
1550
1629
|
start_by_symbol = load_incremental_start_datetimes(
|
|
@@ -1572,6 +1651,7 @@ def _write_incremental_history_deals(
|
|
|
1572
1651
|
Dataset.history_deals,
|
|
1573
1652
|
"symbol = ? AND time >= ?",
|
|
1574
1653
|
(symbol, start_date),
|
|
1654
|
+
frozenset({"symbol", "time"}),
|
|
1575
1655
|
)
|
|
1576
1656
|
|
|
1577
1657
|
|