mt5cli 1.1.4__tar.gz → 1.1.6__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-1.1.4 → mt5cli-1.1.6}/AGENTS.md +1 -1
- {mt5cli-1.1.4 → mt5cli-1.1.6}/PKG-INFO +2 -2
- {mt5cli-1.1.4 → mt5cli-1.1.6}/docs/api/history.md +32 -1
- {mt5cli-1.1.4 → mt5cli-1.1.6}/docs/api/public-contract.md +1 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/docs/api/sdk.md +2 -2
- {mt5cli-1.1.4 → mt5cli-1.1.6}/docs/api/trading.md +40 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/docs/index.md +14 -14
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mt5cli/__init__.py +2 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mt5cli/cli.py +20 -6
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mt5cli/contract.py +1 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mt5cli/converters.py +1 -1
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mt5cli/history.py +117 -11
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mt5cli/schemas.py +15 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mt5cli/sdk.py +27 -7
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mt5cli/trading.py +96 -1
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mt5cli/utils.py +1 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/pyproject.toml +2 -2
- {mt5cli-1.1.4 → mt5cli-1.1.6}/tests/test_cli.py +86 -0
- mt5cli-1.1.6/tests/test_client.py +44 -0
- mt5cli-1.1.6/tests/test_contracts.py +505 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/tests/test_examples.py +7 -11
- {mt5cli-1.1.4 → mt5cli-1.1.6}/tests/test_grafana.py +0 -13
- {mt5cli-1.1.4 → mt5cli-1.1.6}/tests/test_history.py +265 -4
- mt5cli-1.1.6/tests/test_packaging.py +36 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/tests/test_sdk.py +101 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/tests/test_telemetry.py +9 -27
- {mt5cli-1.1.4 → mt5cli-1.1.6}/tests/test_trading.py +192 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/tests/test_utils.py +1 -7
- {mt5cli-1.1.4 → mt5cli-1.1.6}/uv.lock +6 -6
- mt5cli-1.1.4/tests/test_contracts.py +0 -867
- {mt5cli-1.1.4 → mt5cli-1.1.6}/.agents/skills/local-qa/SKILL.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/.agents/skills/local-qa/scripts/qa.sh +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/.agents/skills/mt5cli/SKILL.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/.agents/skills/pr-feedback-triage/SKILL.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/.claude/agents/codex.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/.claude/settings.json +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/.github/FUNDING.yml +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/.github/dependabot.yml +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/.github/renovate.json +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/.github/workflows/ci.yml +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/.github/workflows/claude.yml +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/.github/workflows/opencode.yml +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/.github/workflows/release.yml +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/.gitignore +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/CLAUDE.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/LICENSE +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/README.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/docs/api/cli.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/docs/api/client.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/docs/api/converters.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/docs/api/exceptions.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/docs/api/grafana.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/docs/api/index.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/docs/api/schemas.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/docs/api/telemetry.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/docs/api/utils.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/examples/grafana/README.md +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/examples/grafana/compose.yml +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/examples/grafana/dashboards/mt5cli-market.json +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/examples/grafana/dashboards/mt5cli-overview.json +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/examples/grafana/dashboards/mt5cli-trades.json +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/examples/grafana/provisioning/dashboards/mt5cli.yml +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/examples/grafana/provisioning/datasources/mt5cli-sqlite.yml +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mkdocs.yml +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mt5cli/__main__.py +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mt5cli/client.py +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mt5cli/exceptions.py +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mt5cli/grafana.py +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mt5cli/retry.py +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/mt5cli/telemetry.py +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/tests/__init__.py +0 -0
- {mt5cli-1.1.4 → mt5cli-1.1.6}/tests/conftest.py +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Project Structure & Module Organization
|
|
4
4
|
|
|
5
|
-
`mt5cli/` contains the package source. Important modules include `cli.py` for the Typer command-line app, `client.py` and `sdk.py` for public MT5 client/session APIs, `history.py` for SQLite history collection, `
|
|
5
|
+
`mt5cli/` contains the package source. Important modules include `cli.py` for the Typer command-line app, `client.py` and `sdk.py` for public MT5 client/session APIs, `history.py` for SQLite history collection, `trading.py` for trading helpers, `grafana.py` for Grafana schema support, `telemetry.py` for observability, `utils.py` and `converters.py` for export behavior, and `schemas.py` for normalized dataset contracts. `tests/` holds pytest coverage for CLI behavior, SDK contracts, trading helpers, history, and utilities. `docs/` and `mkdocs.yml` define the MkDocs site and API reference. `skills/mt5cli/SKILL.md` documents the mt5cli agent skill.
|
|
6
6
|
|
|
7
7
|
## Build, Test, and Development Commands
|
|
8
8
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mt5cli
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.6
|
|
4
4
|
Summary: Generic MT5 data and execution infrastructure for Python applications
|
|
5
5
|
Project-URL: Repository, https://github.com/dceoy/mt5cli.git
|
|
6
6
|
Author-email: dceoy <dceoy@users.noreply.github.com>
|
|
@@ -18,7 +18,7 @@ Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
|
18
18
|
Requires-Python: <3.14,>=3.11
|
|
19
19
|
Requires-Dist: click>=8.1.0
|
|
20
20
|
Requires-Dist: pandas>=2.2.2
|
|
21
|
-
Requires-Dist: pdmt5>=1.
|
|
21
|
+
Requires-Dist: pdmt5>=1.2.0
|
|
22
22
|
Requires-Dist: pydantic>=2.13.4
|
|
23
23
|
Requires-Dist: typer>=0.15.0
|
|
24
24
|
Provides-Extra: otel
|
|
@@ -20,6 +20,21 @@ append using dataset-specific keys (for example `ticket` on history tables, or
|
|
|
20
20
|
Optional views are created when `--with-views` is set and the `history-deals` dataset
|
|
21
21
|
was written.
|
|
22
22
|
|
|
23
|
+
`ticks` and `symbols` are opt-in datasets (pass `--dataset ticks` /
|
|
24
|
+
`--dataset symbols`); they are excluded from the default `rates`,
|
|
25
|
+
`history-orders`, `history-deals` selection. Unlike the other datasets,
|
|
26
|
+
`symbols` is not a time-windowed history table: each collection or update
|
|
27
|
+
writes one row per requested symbol, timestamped with the collection's
|
|
28
|
+
`date_to` (one-shot `collect-history`) or the update's end time (incremental
|
|
29
|
+
`update_history`). It snapshots broker-reported symbol metadata (`point`,
|
|
30
|
+
`digits`, `trade_contract_size`, `volume_min`, `volume_max`, `volume_step`,
|
|
31
|
+
`trade_tick_size`, `trade_tick_value`, `currency_profit`) so downstream
|
|
32
|
+
consumers can convert `rates.spread` (points) into a relative spread without a
|
|
33
|
+
live terminal connection. Metadata is only valid for the account/broker that
|
|
34
|
+
produced the snapshot. When a symbol's `point` is missing or zero, the row is
|
|
35
|
+
still written with NULL metadata and a warning is logged — a bad symbol never
|
|
36
|
+
aborts the rest of the sync.
|
|
37
|
+
|
|
23
38
|
### Entity-relationship diagram
|
|
24
39
|
|
|
25
40
|
Sample layout for a full collection with `--with-views`:
|
|
@@ -79,6 +94,20 @@ erDiagram
|
|
|
79
94
|
REAL fee
|
|
80
95
|
}
|
|
81
96
|
|
|
97
|
+
symbols {
|
|
98
|
+
TEXT symbol "dedup key"
|
|
99
|
+
TEXT time "dedup key, snapshot moment"
|
|
100
|
+
REAL point
|
|
101
|
+
INTEGER digits
|
|
102
|
+
REAL trade_contract_size
|
|
103
|
+
REAL volume_min
|
|
104
|
+
REAL volume_max
|
|
105
|
+
REAL volume_step
|
|
106
|
+
REAL trade_tick_size
|
|
107
|
+
REAL trade_tick_value
|
|
108
|
+
TEXT currency_profit
|
|
109
|
+
}
|
|
110
|
+
|
|
82
111
|
cash_events {
|
|
83
112
|
INTEGER ticket
|
|
84
113
|
TEXT symbol
|
|
@@ -106,6 +135,7 @@ erDiagram
|
|
|
106
135
|
rates ||--o{ history_deals : "symbol (logical)"
|
|
107
136
|
ticks ||--o{ history_deals : "symbol (logical)"
|
|
108
137
|
history_orders ||--o{ history_deals : "order ~ ticket (logical)"
|
|
138
|
+
symbols ||--o{ rates : "symbol (logical)"
|
|
109
139
|
history_deals ||--|| cash_events : "VIEW: type NOT IN (0,1)"
|
|
110
140
|
history_deals ||--o{ positions_reconstructed : "VIEW: GROUP BY position_id"
|
|
111
141
|
```
|
|
@@ -118,6 +148,7 @@ erDiagram
|
|
|
118
148
|
| `ticks` | table | `copy_ticks_range` | Indexed on `(symbol, time)` when columns exist. |
|
|
119
149
|
| `history_orders` | table | `history_orders_get` | Fetched per `--symbol`, then concatenated. |
|
|
120
150
|
| `history_deals` | table | `history_deals_get` | Fetched per `--symbol`, then concatenated. Indexed on `(position_id, symbol)` when present. |
|
|
151
|
+
| `symbols` | table | `symbol_info` | Opt-in. One row per symbol per collection/update, snapshotted at `date_to` / update end. |
|
|
121
152
|
| `cash_events` | view | `history_deals` | Non-trade deal types (deposits, balance ops, etc.). Requires `type` column. |
|
|
122
153
|
| `positions_reconstructed` | view | `history_deals` | One row per closed `position_id`; volume-weighted prices and reversal stats. |
|
|
123
154
|
|
|
@@ -240,7 +271,7 @@ frame = series["EURUSD", 1] # keyed by (symbol, integer timeframe)
|
|
|
240
271
|
`require_existing` is ignored.
|
|
241
272
|
- `load_rate_series_from_sqlite()` returns a mapping keyed by
|
|
242
273
|
`(symbol, integer timeframe)`. Unless `explicit_tables` is supplied, it
|
|
243
|
-
requires existing managed `rate_*` compatibility views and raises
|
|
274
|
+
requires existing managed `rate_*__*` compatibility views and raises
|
|
244
275
|
`ValueError` when they are missing. Duplicate `(symbol, timeframe)` targets
|
|
245
276
|
are rejected.
|
|
246
277
|
- `load_rate_series_by_granularity()` is a thin wrapper that builds the targets,
|
|
@@ -88,6 +88,7 @@ strategy entries, exits, Kelly sizing, or signal logic.
|
|
|
88
88
|
| ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
|
|
89
89
|
| `get_account_snapshot`, `get_symbol_snapshot`, `get_tick_snapshot`, `get_positions_frame` | Normalized account/symbol/tick/position views |
|
|
90
90
|
| `extract_tick_price` | Positive finite bid/ask extraction from tick mappings |
|
|
91
|
+
| `estimate_server_clock_offset_seconds` | Broker server clock offset from true UTC, via the latest tick |
|
|
91
92
|
| `detect_position_side` | Net long / short / flat from open positions |
|
|
92
93
|
| `calculate_spread_ratio` | Relative bid-ask spread |
|
|
93
94
|
| `calculate_margin_and_volume`, `calculate_volume_by_margin`, `calculate_new_position_margin_ratio` | Margin budget and volume sizing |
|
|
@@ -160,8 +160,8 @@ updater = ThrottledHistoryUpdater(
|
|
|
160
160
|
)
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
-
By default recoverable errors (`
|
|
164
|
-
`
|
|
163
|
+
By default recoverable errors (`Mt5RuntimeError`, `sqlite3.Error`,
|
|
164
|
+
`ValueError`, `OSError`, and MT5 client capability
|
|
165
165
|
`AttributeError` / `TypeError` for history API methods) propagate so the caller
|
|
166
166
|
controls logging; pass `suppress_errors=True` to swallow them and return
|
|
167
167
|
`False` without advancing the throttle. Other `AttributeError` / `TypeError`
|
|
@@ -236,6 +236,45 @@ or `None` result from the underlying client is normalized to an empty DataFrame.
|
|
|
236
236
|
Downstream packages own all strategy-specific transformations. mt5cli does not
|
|
237
237
|
provide entry-deal classification, Kelly sizing, or any betting-specific helpers.
|
|
238
238
|
|
|
239
|
+
## Broker server clock offset
|
|
240
|
+
|
|
241
|
+
MT5 tick, bar, and deal timestamps are epoch values labeled in the broker
|
|
242
|
+
server's wall clock, which is commonly UTC+2 or UTC+3 rather than true UTC.
|
|
243
|
+
Any code that mixes those timestamps with true UTC (freshness checks,
|
|
244
|
+
trailing history windows) silently inherits the broker's offset as a bias
|
|
245
|
+
unless it is measured and applied explicitly.
|
|
246
|
+
|
|
247
|
+
`estimate_server_clock_offset_seconds()` reads the latest tick for a symbol
|
|
248
|
+
and returns the broker's clock offset from true UTC, rounded to the nearest
|
|
249
|
+
half hour, or `None` when no valid tick time is available. Pass the result to
|
|
250
|
+
`fetch_recent_history_deals_for_trading_client()` via
|
|
251
|
+
`server_clock_offset_seconds` to shift the trailing window so it covers the
|
|
252
|
+
true most-recent deals:
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from mt5cli import (
|
|
256
|
+
create_trading_client,
|
|
257
|
+
estimate_server_clock_offset_seconds,
|
|
258
|
+
fetch_recent_history_deals_for_trading_client,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
client = create_trading_client(login=12345, server="Broker-Demo")
|
|
262
|
+
try:
|
|
263
|
+
offset = estimate_server_clock_offset_seconds(client, "JP225")
|
|
264
|
+
deals_df = fetch_recent_history_deals_for_trading_client(
|
|
265
|
+
client,
|
|
266
|
+
symbol="JP225",
|
|
267
|
+
hours=24,
|
|
268
|
+
server_clock_offset_seconds=offset,
|
|
269
|
+
)
|
|
270
|
+
finally:
|
|
271
|
+
client.shutdown()
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
No offset is applied automatically anywhere; omitting
|
|
275
|
+
`server_clock_offset_seconds` keeps the trailing window anchored to true UTC
|
|
276
|
+
(current behavior, byte-identical to prior releases).
|
|
277
|
+
|
|
239
278
|
## Migration from application-local helpers
|
|
240
279
|
|
|
241
280
|
| Application-local concern | mt5cli replacement |
|
|
@@ -247,6 +286,7 @@ provide entry-deal classification, Kelly sizing, or any betting-specific helpers
|
|
|
247
286
|
| Local order or position margin estimation | `estimate_order_margin()`, `calculate_positions_margin()` |
|
|
248
287
|
| Local closed-bar fetch from a trading session | `fetch_latest_closed_rates_for_trading_client()`, `fetch_latest_closed_rates_indexed()` |
|
|
249
288
|
| Local recent deal history fetch from a trading session | `fetch_recent_history_deals_for_trading_client()` |
|
|
289
|
+
| Local broker server clock offset measurement | `estimate_server_clock_offset_seconds()` |
|
|
250
290
|
| Local SL/TP price derivation | `determine_order_limits()` |
|
|
251
291
|
| Throttled SQLite history loop with ad-hoc error handling | `ThrottledHistoryUpdater(suppress_errors=True)` |
|
|
252
292
|
|
|
@@ -178,10 +178,10 @@ applications should prefer dedicated closing helpers or their own risk controls.
|
|
|
178
178
|
|
|
179
179
|
### Bulk Collection
|
|
180
180
|
|
|
181
|
-
| Command | Description
|
|
182
|
-
| ----------------- |
|
|
183
|
-
| `collect-history` | Collect rates, history-orders, and history-deals (ticks opt-in via `--dataset ticks`) for one or more symbols into a single SQLite database (optional cash-event/position views) |
|
|
184
|
-
| `history-gaps` | Export a SQLite-only one-row-per-gap report from managed rate compatibility views without connecting to MT5
|
|
181
|
+
| Command | Description |
|
|
182
|
+
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
183
|
+
| `collect-history` | Collect rates, history-orders, and history-deals (ticks and symbol metadata opt-in via `--dataset ticks` / `--dataset symbols`) for one or more symbols into a single SQLite database (optional cash-event/position views) |
|
|
184
|
+
| `history-gaps` | Export a SQLite-only one-row-per-gap report from managed rate compatibility views without connecting to MT5 |
|
|
185
185
|
|
|
186
186
|
```bash
|
|
187
187
|
mt5cli -o history.db collect-history \
|
|
@@ -193,16 +193,16 @@ mt5cli -o history.db collect-history \
|
|
|
193
193
|
|
|
194
194
|
`collect-history` options:
|
|
195
195
|
|
|
196
|
-
| Option | Default | Description
|
|
197
|
-
| -------------- | ------------------------------------ |
|
|
198
|
-
| `--symbol/-s` | _required_ | Symbol to collect (repeat for multiple).
|
|
199
|
-
| `--date-from` | _required_ | Start date in ISO 8601.
|
|
200
|
-
| `--date-to` | _required_ | End date in ISO 8601.
|
|
201
|
-
| `--dataset` | rates, history-orders, history-deals | Repeatable: `rates`, `ticks`, `history-orders`, `history-deals`. Ticks are opt-in: pass `--dataset ticks` to include them. |
|
|
202
|
-
| `--timeframe` | `M1` | Rates timeframe; recorded in a `timeframe` column on the `rates` table.
|
|
203
|
-
| `--flags` | `ALL` | Tick copy flags forwarded to `copy_ticks_range`.
|
|
204
|
-
| `--if-exists` | `fail` | `append`, `replace`, or `fail` when a target table already exists.
|
|
205
|
-
| `--with-views` | off | Add `cash_events` and `positions_reconstructed` views (requires the `history-deals` dataset).
|
|
196
|
+
| Option | Default | Description |
|
|
197
|
+
| -------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
198
|
+
| `--symbol/-s` | _required_ | Symbol to collect (repeat for multiple). |
|
|
199
|
+
| `--date-from` | _required_ | Start date in ISO 8601. |
|
|
200
|
+
| `--date-to` | _required_ | End date in ISO 8601. |
|
|
201
|
+
| `--dataset` | rates, history-orders, history-deals | Repeatable: `rates`, `ticks`, `history-orders`, `history-deals`, `symbols`. Ticks and symbol metadata are opt-in: pass `--dataset ticks` / `--dataset symbols` to include them. |
|
|
202
|
+
| `--timeframe` | `M1` | Rates timeframe; recorded in a `timeframe` column on the `rates` table. |
|
|
203
|
+
| `--flags` | `ALL` | Tick copy flags forwarded to `copy_ticks_range`. |
|
|
204
|
+
| `--if-exists` | `fail` | `append`, `replace`, or `fail` when a target table already exists. |
|
|
205
|
+
| `--with-views` | off | Add `cash_events` and `positions_reconstructed` views (requires the `history-deals` dataset). |
|
|
206
206
|
|
|
207
207
|
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`.
|
|
208
208
|
|
|
@@ -66,6 +66,7 @@ from .trading import (
|
|
|
66
66
|
determine_order_limits,
|
|
67
67
|
ensure_symbol_selected,
|
|
68
68
|
estimate_order_margin,
|
|
69
|
+
estimate_server_clock_offset_seconds,
|
|
69
70
|
extract_tick_price,
|
|
70
71
|
fetch_latest_closed_rates_for_trading_client,
|
|
71
72
|
fetch_latest_closed_rates_indexed,
|
|
@@ -127,6 +128,7 @@ __all__ = [
|
|
|
127
128
|
"drop_forming_rate_bar",
|
|
128
129
|
"ensure_symbol_selected",
|
|
129
130
|
"estimate_order_margin",
|
|
131
|
+
"estimate_server_clock_offset_seconds",
|
|
130
132
|
"extract_tick_price",
|
|
131
133
|
"fetch_latest_closed_rates",
|
|
132
134
|
"fetch_latest_closed_rates_for_trading_client",
|
|
@@ -6,7 +6,7 @@ import json
|
|
|
6
6
|
import logging
|
|
7
7
|
import os
|
|
8
8
|
import re
|
|
9
|
-
import
|
|
9
|
+
from contextlib import closing
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from datetime import datetime # noqa: TC003
|
|
12
12
|
from pathlib import Path # noqa: TC003
|
|
@@ -17,7 +17,11 @@ import typer
|
|
|
17
17
|
|
|
18
18
|
from . import sdk
|
|
19
19
|
from .client import MT5Client
|
|
20
|
-
from .history import
|
|
20
|
+
from .history import (
|
|
21
|
+
open_existing_sqlite_database,
|
|
22
|
+
report_rate_gaps,
|
|
23
|
+
resolve_granularity_name,
|
|
24
|
+
)
|
|
21
25
|
from .trading import OrderExecutionResult, close_open_positions, create_trading_client
|
|
22
26
|
from .utils import (
|
|
23
27
|
DATETIME_TYPE,
|
|
@@ -33,6 +37,7 @@ from .utils import (
|
|
|
33
37
|
)
|
|
34
38
|
|
|
35
39
|
if TYPE_CHECKING:
|
|
40
|
+
import sqlite3
|
|
36
41
|
from collections.abc import Callable
|
|
37
42
|
|
|
38
43
|
from pdmt5 import Mt5Config
|
|
@@ -838,10 +843,15 @@ def history_gaps(
|
|
|
838
843
|
"""Export SQLite rate gaps without connecting to MT5.
|
|
839
844
|
|
|
840
845
|
Raises:
|
|
841
|
-
typer.BadParameter: If
|
|
842
|
-
|
|
846
|
+
typer.BadParameter: If the source database does not exist, if no
|
|
847
|
+
compatible rate view is available and no explicit table is
|
|
848
|
+
provided, or if granularity inference fails.
|
|
843
849
|
"""
|
|
844
|
-
|
|
850
|
+
try:
|
|
851
|
+
conn, _ = open_existing_sqlite_database(sqlite3_path)
|
|
852
|
+
except ValueError as exc:
|
|
853
|
+
raise typer.BadParameter(str(exc), param_hint="--sqlite3") from exc
|
|
854
|
+
with closing(conn):
|
|
845
855
|
tables = list(table) if table else _default_gap_tables(conn)
|
|
846
856
|
if not tables:
|
|
847
857
|
msg = (
|
|
@@ -902,7 +912,8 @@ def collect_history(
|
|
|
902
912
|
help=(
|
|
903
913
|
"Dataset to include (repeat for multiple)."
|
|
904
914
|
" Defaults to rates, history-orders, history-deals."
|
|
905
|
-
" Ticks are opt-in:
|
|
915
|
+
" Ticks and symbols metadata are opt-in:"
|
|
916
|
+
" pass --dataset ticks or --dataset symbols to include them."
|
|
906
917
|
),
|
|
907
918
|
),
|
|
908
919
|
] = None,
|
|
@@ -943,6 +954,9 @@ def collect_history(
|
|
|
943
954
|
Tables written depend on ``--dataset``: ``rates``, ``history_orders``,
|
|
944
955
|
``history_deals`` by default. ``ticks`` are opt-in: pass
|
|
945
956
|
``--dataset ticks`` to include them (tick data grows the database quickly).
|
|
957
|
+
``symbols`` is also opt-in: pass ``--dataset symbols`` to snapshot
|
|
958
|
+
per-symbol metadata (point, digits, contract size, volume limits) at
|
|
959
|
+
``date_to``, one row per symbol per collection.
|
|
946
960
|
History datasets are fetched per symbol and concatenated. Rates rows carry
|
|
947
961
|
the requested ``timeframe`` so appended runs at different timeframes remain
|
|
948
962
|
distinguishable.
|
|
@@ -44,6 +44,7 @@ STABLE_SDK_EXPORTS: frozenset[str] = frozenset({
|
|
|
44
44
|
"drop_forming_rate_bar",
|
|
45
45
|
"ensure_symbol_selected",
|
|
46
46
|
"estimate_order_margin",
|
|
47
|
+
"estimate_server_clock_offset_seconds",
|
|
47
48
|
"extract_tick_price",
|
|
48
49
|
"fetch_latest_closed_rates",
|
|
49
50
|
"fetch_latest_closed_rates_for_trading_client",
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from datetime import UTC, datetime, timedelta
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
-
from pdmt5 import get_timeframe_name as _get_timeframe_name
|
|
8
|
+
from pdmt5.constants import get_timeframe_name as _get_timeframe_name
|
|
9
9
|
|
|
10
10
|
from .utils import parse_datetime, parse_tick_flags, parse_timeframe
|
|
11
11
|
|
|
@@ -5,15 +5,17 @@ from __future__ import annotations
|
|
|
5
5
|
import logging
|
|
6
6
|
import re
|
|
7
7
|
import sqlite3
|
|
8
|
+
from collections.abc import Mapping
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
from datetime import UTC, datetime, timedelta
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import TYPE_CHECKING, Literal, SupportsInt, cast, overload
|
|
12
13
|
|
|
13
14
|
import pandas as pd
|
|
14
|
-
from pdmt5 import
|
|
15
|
+
from pdmt5 import Mt5RuntimeError
|
|
16
|
+
from pdmt5.constants import get_timeframe_name as _get_timeframe_name
|
|
15
17
|
|
|
16
|
-
from .schemas import DEDUP_KEYS, DataKind, ensure_utc_columns
|
|
18
|
+
from .schemas import DEDUP_KEYS, REQUIRED_COLUMNS, DataKind, ensure_utc_columns
|
|
17
19
|
from .utils import (
|
|
18
20
|
TIMEFRAME_NAMES,
|
|
19
21
|
Dataset,
|
|
@@ -24,7 +26,7 @@ from .utils import (
|
|
|
24
26
|
)
|
|
25
27
|
|
|
26
28
|
if TYPE_CHECKING:
|
|
27
|
-
from collections.abc import Callable,
|
|
29
|
+
from collections.abc import Callable, Sequence
|
|
28
30
|
|
|
29
31
|
from pdmt5 import Mt5DataClient
|
|
30
32
|
|
|
@@ -49,8 +51,18 @@ _HISTORY_DEDUP_KEYS: dict[Dataset, tuple[tuple[str, ...], ...]] = {
|
|
|
49
51
|
Dataset.ticks: DEDUP_KEYS[DataKind.ticks],
|
|
50
52
|
Dataset.history_orders: DEDUP_KEYS[DataKind.history_orders],
|
|
51
53
|
Dataset.history_deals: DEDUP_KEYS[DataKind.history_deals],
|
|
54
|
+
Dataset.symbols: DEDUP_KEYS[DataKind.symbols],
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
_SYMBOLS_SNAPSHOT_FIELDS: tuple[str, ...] = tuple(
|
|
58
|
+
sorted(REQUIRED_COLUMNS[DataKind.symbols] - {"symbol", "time"}),
|
|
59
|
+
)
|
|
60
|
+
_SYMBOLS_FLOAT_FIELDS: tuple[str, ...] = tuple(
|
|
61
|
+
field
|
|
62
|
+
for field in _SYMBOLS_SNAPSHOT_FIELDS
|
|
63
|
+
if field not in {"currency_profit", "digits"}
|
|
64
|
+
)
|
|
65
|
+
|
|
54
66
|
_TRADE_DEAL_TYPES: tuple[int, int] = (0, 1)
|
|
55
67
|
_TRADE_DEAL_TYPES_SQL = f"({', '.join(str(value) for value in _TRADE_DEAL_TYPES)})"
|
|
56
68
|
|
|
@@ -91,7 +103,8 @@ def resolve_history_datasets(datasets: set[Dataset] | None) -> set[Dataset]:
|
|
|
91
103
|
Returns:
|
|
92
104
|
``DEFAULT_HISTORY_DATASETS`` (rates, history-orders, history-deals)
|
|
93
105
|
when ``datasets`` is None, otherwise the configured selection (which
|
|
94
|
-
may be empty or explicitly include ``Dataset.ticks``
|
|
106
|
+
may be empty or explicitly include ``Dataset.ticks`` /
|
|
107
|
+
``Dataset.symbols``).
|
|
95
108
|
"""
|
|
96
109
|
if datasets is None:
|
|
97
110
|
return set(DEFAULT_HISTORY_DATASETS)
|
|
@@ -222,7 +235,7 @@ def _open_history_connection(
|
|
|
222
235
|
return conn, True
|
|
223
236
|
|
|
224
237
|
|
|
225
|
-
def
|
|
238
|
+
def open_existing_sqlite_database(
|
|
226
239
|
conn_or_path: SqliteConnOrPath,
|
|
227
240
|
) -> tuple[sqlite3.Connection, bool]:
|
|
228
241
|
"""Open a read-only SQLite database or reuse an existing connection.
|
|
@@ -490,7 +503,7 @@ def load_rate_data(
|
|
|
490
503
|
DataFrame indexed by ascending ``time``.
|
|
491
504
|
|
|
492
505
|
"""
|
|
493
|
-
conn, should_close =
|
|
506
|
+
conn, should_close = open_existing_sqlite_database(conn_or_path)
|
|
494
507
|
try:
|
|
495
508
|
return load_rate_data_from_connection(conn, table, count=count)
|
|
496
509
|
finally:
|
|
@@ -899,8 +912,8 @@ def load_rate_series_from_sqlite(
|
|
|
899
912
|
be unique. Omit when loading a single explicit ``table``.
|
|
900
913
|
count: Optional number of most recent rows to load per series.
|
|
901
914
|
explicit_tables: Optional explicit table or view names matching targets.
|
|
902
|
-
When omitted, managed ``rate_*`` compatibility views must
|
|
903
|
-
exist in the database.
|
|
915
|
+
When omitted, managed ``rate_*__*`` compatibility views must
|
|
916
|
+
already exist in the database.
|
|
904
917
|
table: Optional single table or view name to load directly.
|
|
905
918
|
|
|
906
919
|
Returns:
|
|
@@ -943,7 +956,7 @@ def load_rate_series_from_sqlite(
|
|
|
943
956
|
if explicit_tables is not None
|
|
944
957
|
else None
|
|
945
958
|
)
|
|
946
|
-
conn, should_close =
|
|
959
|
+
conn, should_close = open_existing_sqlite_database(conn_or_path)
|
|
947
960
|
try:
|
|
948
961
|
resolved_tables = tables or resolve_rate_tables(
|
|
949
962
|
conn,
|
|
@@ -1639,9 +1652,9 @@ def create_positions_reconstructed_view(
|
|
|
1639
1652
|
|
|
1640
1653
|
|
|
1641
1654
|
def drop_rate_compatibility_views(conn: sqlite3.Connection) -> None:
|
|
1642
|
-
"""Drop all mt5cli-managed ``rate_*`` compatibility views."""
|
|
1655
|
+
"""Drop all mt5cli-managed ``rate_*__*`` compatibility views."""
|
|
1643
1656
|
rows = conn.execute(
|
|
1644
|
-
"SELECT name FROM sqlite_master WHERE type = 'view' AND name GLOB 'rate_*'",
|
|
1657
|
+
"SELECT name FROM sqlite_master WHERE type = 'view' AND name GLOB 'rate_*__*'",
|
|
1645
1658
|
).fetchall()
|
|
1646
1659
|
for (view_name,) in rows:
|
|
1647
1660
|
quoted_view_name = quote_sqlite_identifier(str(view_name))
|
|
@@ -1801,6 +1814,62 @@ def write_ticks_dataset(
|
|
|
1801
1814
|
)
|
|
1802
1815
|
|
|
1803
1816
|
|
|
1817
|
+
def write_symbols_dataset(
|
|
1818
|
+
conn: sqlite3.Connection,
|
|
1819
|
+
client: Mt5DataClient,
|
|
1820
|
+
symbols: Sequence[str],
|
|
1821
|
+
snapshot_time: datetime,
|
|
1822
|
+
if_exists: IfExists,
|
|
1823
|
+
written_columns: dict[Dataset, set[str]],
|
|
1824
|
+
) -> bool:
|
|
1825
|
+
"""Snapshot per-symbol metadata (point, digits, contract size, volume limits).
|
|
1826
|
+
|
|
1827
|
+
Broker-reported symbol metadata is only valid for the account/broker that
|
|
1828
|
+
produced the snapshot. When a symbol's metadata cannot be retrieved, or
|
|
1829
|
+
when its ``point`` is missing or zero (the terminal has no valid metadata
|
|
1830
|
+
for it), a row is still persisted with NULL metadata and a warning is
|
|
1831
|
+
logged; the sync is never aborted for a single bad symbol.
|
|
1832
|
+
|
|
1833
|
+
Returns:
|
|
1834
|
+
True if the symbols table was written.
|
|
1835
|
+
"""
|
|
1836
|
+
|
|
1837
|
+
def _fetch_symbol_frame(sym: str) -> pd.DataFrame:
|
|
1838
|
+
try:
|
|
1839
|
+
info = client.symbol_info_as_dict(symbol=sym)
|
|
1840
|
+
except Mt5RuntimeError:
|
|
1841
|
+
info = None
|
|
1842
|
+
if not isinstance(info, Mapping):
|
|
1843
|
+
logger.warning(
|
|
1844
|
+
"Symbol %r metadata could not be retrieved; persisting NULL metadata.",
|
|
1845
|
+
sym,
|
|
1846
|
+
)
|
|
1847
|
+
info = {}
|
|
1848
|
+
if info.get("point"):
|
|
1849
|
+
metadata = {field: info.get(field) for field in _SYMBOLS_SNAPSHOT_FIELDS}
|
|
1850
|
+
else:
|
|
1851
|
+
if info:
|
|
1852
|
+
logger.warning(
|
|
1853
|
+
"Symbol %r has missing or zero point; persisting NULL metadata.",
|
|
1854
|
+
sym,
|
|
1855
|
+
)
|
|
1856
|
+
metadata = dict.fromkeys(_SYMBOLS_SNAPSHOT_FIELDS)
|
|
1857
|
+
frame = pd.DataFrame([{"symbol": sym, "time": snapshot_time, **metadata}])
|
|
1858
|
+
for field in _SYMBOLS_FLOAT_FIELDS:
|
|
1859
|
+
frame[field] = frame[field].astype("float64")
|
|
1860
|
+
frame["digits"] = frame["digits"].astype("Int64")
|
|
1861
|
+
return frame
|
|
1862
|
+
|
|
1863
|
+
return _stream_symbol_frames(
|
|
1864
|
+
conn,
|
|
1865
|
+
symbols,
|
|
1866
|
+
Dataset.symbols,
|
|
1867
|
+
if_exists,
|
|
1868
|
+
written_columns,
|
|
1869
|
+
_fetch_symbol_frame,
|
|
1870
|
+
)
|
|
1871
|
+
|
|
1872
|
+
|
|
1804
1873
|
def write_history_dataset(
|
|
1805
1874
|
conn: sqlite3.Connection,
|
|
1806
1875
|
fetch: Callable[..., pd.DataFrame],
|
|
@@ -1935,6 +2004,25 @@ def _write_incremental_ticks(
|
|
|
1935
2004
|
)
|
|
1936
2005
|
|
|
1937
2006
|
|
|
2007
|
+
def _write_incremental_symbols(
|
|
2008
|
+
conn: sqlite3.Connection,
|
|
2009
|
+
client: Mt5DataClient,
|
|
2010
|
+
symbols: Sequence[str],
|
|
2011
|
+
end_date: datetime,
|
|
2012
|
+
written_columns: dict[Dataset, set[str]],
|
|
2013
|
+
written_tables: set[Dataset],
|
|
2014
|
+
) -> None:
|
|
2015
|
+
if write_symbols_dataset(
|
|
2016
|
+
conn,
|
|
2017
|
+
client,
|
|
2018
|
+
symbols,
|
|
2019
|
+
end_date,
|
|
2020
|
+
IfExists.APPEND,
|
|
2021
|
+
written_columns,
|
|
2022
|
+
):
|
|
2023
|
+
written_tables.add(Dataset.symbols)
|
|
2024
|
+
|
|
2025
|
+
|
|
1938
2026
|
def _write_incremental_history_orders(
|
|
1939
2027
|
conn: sqlite3.Connection,
|
|
1940
2028
|
client: Mt5DataClient,
|
|
@@ -2182,6 +2270,15 @@ def write_incremental_datasets( # noqa: PLR0913
|
|
|
2182
2270
|
dedup_scopes,
|
|
2183
2271
|
include_account_events=include_account_events,
|
|
2184
2272
|
)
|
|
2273
|
+
if Dataset.symbols in selected_datasets:
|
|
2274
|
+
_write_incremental_symbols(
|
|
2275
|
+
conn,
|
|
2276
|
+
client,
|
|
2277
|
+
symbols,
|
|
2278
|
+
end_date,
|
|
2279
|
+
written_columns,
|
|
2280
|
+
written_tables,
|
|
2281
|
+
)
|
|
2185
2282
|
_finalize_incremental_writes(
|
|
2186
2283
|
conn,
|
|
2187
2284
|
selected_datasets,
|
|
@@ -2259,4 +2356,13 @@ def write_collected_datasets(
|
|
|
2259
2356
|
include_account_events=False,
|
|
2260
2357
|
):
|
|
2261
2358
|
written_tables.add(Dataset.history_deals)
|
|
2359
|
+
if Dataset.symbols in datasets and write_symbols_dataset(
|
|
2360
|
+
conn,
|
|
2361
|
+
client,
|
|
2362
|
+
symbols,
|
|
2363
|
+
date_to,
|
|
2364
|
+
if_exists,
|
|
2365
|
+
written_columns,
|
|
2366
|
+
):
|
|
2367
|
+
written_tables.add(Dataset.symbols)
|
|
2262
2368
|
return written_tables, written_columns
|
|
@@ -46,6 +46,7 @@ class DataKind(StrEnum):
|
|
|
46
46
|
positions = "positions"
|
|
47
47
|
history_orders = "history_orders"
|
|
48
48
|
history_deals = "history_deals"
|
|
49
|
+
symbols = "symbols"
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
REQUIRED_COLUMNS: dict[DataKind, frozenset[str]] = {
|
|
@@ -108,6 +109,19 @@ REQUIRED_COLUMNS: dict[DataKind, frozenset[str]] = {
|
|
|
108
109
|
"price",
|
|
109
110
|
"profit",
|
|
110
111
|
}),
|
|
112
|
+
DataKind.symbols: frozenset({
|
|
113
|
+
"symbol",
|
|
114
|
+
"time",
|
|
115
|
+
"point",
|
|
116
|
+
"digits",
|
|
117
|
+
"trade_contract_size",
|
|
118
|
+
"volume_min",
|
|
119
|
+
"volume_max",
|
|
120
|
+
"volume_step",
|
|
121
|
+
"trade_tick_size",
|
|
122
|
+
"trade_tick_value",
|
|
123
|
+
"currency_profit",
|
|
124
|
+
}),
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
_OPTIONAL_TIME_COLUMNS_BY_KIND: dict[DataKind, frozenset[str]] = {
|
|
@@ -135,6 +149,7 @@ DEDUP_KEYS: dict[DataKind, tuple[tuple[str, ...], ...]] = {
|
|
|
135
149
|
DataKind.ticks: (("symbol", "time_msc"), ("symbol", "time")),
|
|
136
150
|
DataKind.history_orders: (("ticket",), ("symbol", "time", "type")),
|
|
137
151
|
DataKind.history_deals: (("ticket",), ("symbol", "time", "type", "entry")),
|
|
152
|
+
DataKind.symbols: (("symbol", "time"),),
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
|