2026-06-09 02:40:25 +09:00
|
|
|
# History Collection (SQLite)
|
2026-06-09 01:28:22 +09:00
|
|
|
|
2026-06-09 02:40:25 +09:00
|
|
|
::: mt5cli.history
|
2026-06-09 01:28:22 +09:00
|
|
|
|
|
|
|
|
## `collect-history` schema
|
|
|
|
|
|
|
|
|
|
The `collect-history` command (and the matching `collect_history` SDK function) writes
|
|
|
|
|
selected MT5 datasets into one SQLite database. Each dataset becomes a table; column
|
|
|
|
|
names and types mirror the pdmt5 DataFrame schema for that export, with two additions:
|
|
|
|
|
|
|
|
|
|
- `symbol` is prepended on every table.
|
|
|
|
|
- `timeframe` is prepended on `rates` so appended runs at different bar sizes stay
|
|
|
|
|
distinguishable.
|
|
|
|
|
|
|
|
|
|
SQLite does not declare foreign keys. Rows are linked logically by `symbol`, time
|
|
|
|
|
windows, and (for deals) `position_id` / `order`. Duplicate rows are removed on
|
|
|
|
|
append using dataset-specific keys (for example `ticket` on history tables, or
|
|
|
|
|
`(symbol, timeframe, time)` on rates).
|
|
|
|
|
|
|
|
|
|
Optional views are created when `--with-views` is set and the `history-deals` dataset
|
|
|
|
|
was written.
|
|
|
|
|
|
|
|
|
|
### Entity-relationship diagram
|
|
|
|
|
|
|
|
|
|
Sample layout for a full collection with `--with-views`:
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
erDiagram
|
|
|
|
|
rates {
|
|
|
|
|
TEXT symbol "dedup key"
|
|
|
|
|
INTEGER timeframe "dedup key"
|
|
|
|
|
TEXT time "dedup key"
|
|
|
|
|
REAL open
|
|
|
|
|
REAL high
|
|
|
|
|
REAL low
|
|
|
|
|
REAL close
|
|
|
|
|
INTEGER tick_volume
|
|
|
|
|
INTEGER spread
|
|
|
|
|
INTEGER real_volume
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ticks {
|
|
|
|
|
TEXT symbol "dedup key"
|
|
|
|
|
TEXT time "dedup key"
|
|
|
|
|
INTEGER time_msc "dedup key (preferred)"
|
|
|
|
|
REAL bid
|
|
|
|
|
REAL ask
|
|
|
|
|
REAL last
|
|
|
|
|
INTEGER volume
|
|
|
|
|
INTEGER flags
|
|
|
|
|
REAL volume_real
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
history_orders {
|
|
|
|
|
INTEGER ticket "dedup key"
|
|
|
|
|
TEXT symbol
|
|
|
|
|
TEXT time
|
|
|
|
|
INTEGER type
|
|
|
|
|
INTEGER state
|
|
|
|
|
REAL volume_initial
|
|
|
|
|
REAL price_open
|
|
|
|
|
REAL price_current
|
|
|
|
|
INTEGER magic
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
history_deals {
|
|
|
|
|
INTEGER ticket "dedup key"
|
|
|
|
|
INTEGER order
|
|
|
|
|
INTEGER position_id "groups position view"
|
|
|
|
|
TEXT symbol
|
|
|
|
|
TEXT time
|
|
|
|
|
INTEGER type "0/1 trade, else cash event"
|
|
|
|
|
INTEGER entry "0 IN, 1 OUT, 2 INOUT, 3 OUT_BY"
|
|
|
|
|
REAL volume
|
|
|
|
|
REAL price
|
|
|
|
|
REAL profit
|
|
|
|
|
REAL commission
|
|
|
|
|
REAL swap
|
|
|
|
|
REAL fee
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cash_events {
|
|
|
|
|
INTEGER ticket
|
|
|
|
|
TEXT symbol
|
|
|
|
|
TEXT time
|
|
|
|
|
INTEGER type
|
|
|
|
|
REAL profit
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
positions_reconstructed {
|
|
|
|
|
INTEGER position_id
|
|
|
|
|
TEXT symbol
|
|
|
|
|
TEXT open_time
|
|
|
|
|
TEXT close_time
|
|
|
|
|
INTEGER direction
|
|
|
|
|
REAL volume_open
|
|
|
|
|
REAL volume_close
|
|
|
|
|
REAL volume_reversal
|
|
|
|
|
REAL open_price
|
|
|
|
|
REAL close_price
|
|
|
|
|
REAL total_profit
|
|
|
|
|
INTEGER reversal_count
|
|
|
|
|
INTEGER deals_count
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rates ||--o{ history_deals : "symbol (logical)"
|
|
|
|
|
ticks ||--o{ history_deals : "symbol (logical)"
|
|
|
|
|
history_orders ||--o{ history_deals : "order ~ ticket (logical)"
|
|
|
|
|
history_deals ||--|| cash_events : "VIEW: type NOT IN (0,1)"
|
|
|
|
|
history_deals ||--o{ positions_reconstructed : "VIEW: GROUP BY position_id"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Tables and views
|
|
|
|
|
|
|
|
|
|
| Object | Kind | Source | Notes |
|
|
|
|
|
| ------------------------- | ----- | -------------------- | ------------------------------------------------------------------------------------------- |
|
|
|
|
|
| `rates` | table | `copy_rates_range` | Indexed on `(symbol, timeframe, time)` when columns exist. |
|
|
|
|
|
| `ticks` | table | `copy_ticks_range` | Indexed on `(symbol, time)` when columns exist. |
|
|
|
|
|
| `history_orders` | table | `history_orders_get` | Fetched per `--symbol`, then concatenated. |
|
|
|
|
|
| `history_deals` | table | `history_deals_get` | Fetched per `--symbol`, then concatenated. Indexed on `(position_id, symbol)` when present. |
|
|
|
|
|
| `cash_events` | view | `history_deals` | Non-trade deal types (deposits, balance ops, etc.). Requires `type` column. |
|
|
|
|
|
| `positions_reconstructed` | view | `history_deals` | One row per closed `position_id`; volume-weighted prices and reversal stats. |
|
|
|
|
|
|
|
|
|
|
Column sets can vary with terminal and pdmt5 version. Views are skipped with a warning
|
|
|
|
|
when required columns are missing.
|
|
|
|
|
|
|
|
|
|
### Incremental collection
|
|
|
|
|
|
|
|
|
|
The `update_history` SDK path uses the same base tables and optional
|
|
|
|
|
`cash_events` / `positions_reconstructed` views. It additionally maintains
|
|
|
|
|
`rate_<symbol>__<timeframe>` compatibility views when `create_rate_views=True`.
|
2026-06-09 03:29:03 +09:00
|
|
|
|
|
|
|
|
### Rate view resolution
|
|
|
|
|
|
|
|
|
|
Downstream tools can resolve mt5cli-managed compatibility view names from an
|
2026-06-09 23:27:54 +09:00
|
|
|
existing SQLite history database without creating files or guessing naming
|
|
|
|
|
schemes:
|
2026-06-09 03:29:03 +09:00
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from mt5cli.history import resolve_rate_view_name, resolve_rate_view_names
|
|
|
|
|
|
|
|
|
|
# Single symbol and granularity
|
|
|
|
|
view = resolve_rate_view_name(Path("history.db"), "EURUSD", "M1")
|
|
|
|
|
|
|
|
|
|
# Batch resolution in row-major order
|
|
|
|
|
views = resolve_rate_view_names(
|
|
|
|
|
Path("history.db"),
|
|
|
|
|
["EURUSD", "GBPUSD"],
|
|
|
|
|
["M1", "H1"],
|
|
|
|
|
)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Resolution rules:
|
|
|
|
|
|
|
|
|
|
- Returns `rate_<symbol>__<timeframe>` when a symbol stores one timeframe.
|
|
|
|
|
- Returns `rate_<symbol>__<granularity>_<timeframe>` when multiple timeframes
|
|
|
|
|
are stored for the same symbol.
|
|
|
|
|
- When multiple naming candidates apply, prefers an existing managed
|
|
|
|
|
`rate_*__*` view from the candidate list.
|
|
|
|
|
- Falls back to single-timeframe naming when the database path is missing or
|
|
|
|
|
`rates` metadata is unavailable.
|
|
|
|
|
- Pass `require_existing=True` to raise `ValueError` instead of returning a
|
|
|
|
|
best-guess name when the database or view is missing.
|
|
|
|
|
- Accepts either a SQLite path or an open `sqlite3.Connection`.
|
2026-06-09 11:27:29 +09:00
|
|
|
|
|
|
|
|
### Rate data loading
|
|
|
|
|
|
2026-06-15 02:47:05 +09:00
|
|
|
The canonical normalized rate table is `rates`; compatibility views are named
|
|
|
|
|
with `rate_<symbol>__<timeframe>` for single-timeframe symbols or
|
|
|
|
|
`rate_<symbol>__<granularity>_<timeframe>` when a symbol has multiple stored
|
|
|
|
|
timeframes. `resolve_rate_table_name()` returns `rates`, while
|
|
|
|
|
`resolve_rate_view_name()` returns the per-symbol compatibility view name.
|
|
|
|
|
|
|
|
|
|
Use `load_rate_data()` or `load_rate_series_from_sqlite(..., table=...)` to load
|
|
|
|
|
a single table or view from a SQLite path. Use
|
|
|
|
|
`load_rate_series_by_granularity()` to load multiple instrument/granularity
|
|
|
|
|
targets without hard-coding view names:
|
2026-06-09 11:27:29 +09:00
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
2026-06-15 02:47:05 +09:00
|
|
|
from mt5cli import (
|
|
|
|
|
load_rate_series_by_granularity,
|
|
|
|
|
load_rate_series_from_sqlite,
|
2026-06-26 18:23:30 +09:00
|
|
|
)
|
|
|
|
|
from mt5cli.history import (
|
|
|
|
|
load_rate_data,
|
2026-06-15 02:47:05 +09:00
|
|
|
resolve_rate_table_name,
|
2026-06-26 18:23:30 +09:00
|
|
|
resolve_rate_view_name,
|
2026-06-15 02:47:05 +09:00
|
|
|
)
|
2026-06-09 11:27:29 +09:00
|
|
|
|
|
|
|
|
view = resolve_rate_view_name(Path("history.db"), "EURUSD", "M1", require_existing=True)
|
|
|
|
|
rates = load_rate_data(Path("history.db"), view, count=1000)
|
2026-06-15 02:47:05 +09:00
|
|
|
same_rates = load_rate_series_from_sqlite(Path("history.db"), table=view, count=1000)
|
|
|
|
|
|
|
|
|
|
table = resolve_rate_table_name("EURUSD", "M1") # "rates"
|
|
|
|
|
series = load_rate_series_by_granularity(
|
|
|
|
|
Path("history.db"),
|
|
|
|
|
symbols=["EURUSD", "GBPUSD"],
|
|
|
|
|
granularities=["M1", "H1"],
|
|
|
|
|
count=500,
|
|
|
|
|
)
|
2026-06-09 11:27:29 +09:00
|
|
|
```
|
|
|
|
|
|
2026-06-15 02:47:05 +09:00
|
|
|
`count` returns the latest rows while preserving chronological order. Missing
|
|
|
|
|
tables/views and mismatched `explicit_tables` lengths raise `ValueError` with
|
|
|
|
|
the requested database target in the message.
|
|
|
|
|
|
2026-06-09 11:27:29 +09:00
|
|
|
The loader accepts close-based OHLC rate data or tick-like bid/ask data. It
|
|
|
|
|
validates that `time` exists, parses timestamps with pandas, and returns a
|
|
|
|
|
DataFrame indexed by ascending `DatetimeIndex` named `time`.
|
2026-06-09 15:37:24 +09:00
|
|
|
|
|
|
|
|
### Multi-series rate loading
|
|
|
|
|
|
|
|
|
|
For loading many rate series at once, build neutral `RateTarget` pairs and load
|
|
|
|
|
them from SQLite in one call. View names are resolved via the same
|
|
|
|
|
compatibility-view rules, or you can pass `explicit_tables` to bypass resolution:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from mt5cli import build_rate_targets, load_rate_series_from_sqlite
|
|
|
|
|
|
|
|
|
|
targets = build_rate_targets(["EURUSD", "GBPUSD"], ["M1", "H1"])
|
|
|
|
|
series = load_rate_series_from_sqlite(Path("history.db"), targets, count=1000)
|
|
|
|
|
frame = series["EURUSD", 1] # keyed by (symbol, integer timeframe)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- `build_rate_targets()` returns `RateTarget(symbol, timeframe)` pairs in
|
|
|
|
|
row-major order, normalizing timeframe names such as `"M1"` to their integer
|
|
|
|
|
values; set `allow_missing_symbol=True` to address series solely by
|
|
|
|
|
`explicit_tables` (targets carry `symbol=None`).
|
|
|
|
|
- `resolve_rate_tables()` maps targets to table or view names and validates that
|
|
|
|
|
any `explicit_tables` count matches the target count. Pass
|
|
|
|
|
`require_existing=True` to raise `ValueError` instead of returning a
|
|
|
|
|
best-guess name when the database or managed view is missing. When
|
|
|
|
|
`explicit_tables` is provided, names are returned as-is and
|
|
|
|
|
`require_existing` is ignored.
|
|
|
|
|
- `load_rate_series_from_sqlite()` returns a mapping keyed by
|
|
|
|
|
`(symbol, integer timeframe)`. Unless `explicit_tables` is supplied, it
|
|
|
|
|
requires existing managed `rate_*` compatibility views and raises
|
|
|
|
|
`ValueError` when they are missing. Duplicate `(symbol, timeframe)` targets
|
|
|
|
|
are rejected.
|
2026-06-10 00:15:07 +09:00
|
|
|
- `load_rate_series_by_granularity()` is a thin wrapper that builds the targets,
|
|
|
|
|
loads the series, and rekeys the result by granularity name to avoid
|
|
|
|
|
converting integer timeframes downstream:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from mt5cli import load_rate_series_by_granularity
|
|
|
|
|
|
|
|
|
|
series = load_rate_series_by_granularity(
|
|
|
|
|
"history.db", ["EURUSD"], ["M1", "H1"], count=1000
|
|
|
|
|
)
|
|
|
|
|
frame = series["EURUSD", "M1"] # keyed by (symbol | None, granularity_name)
|
|
|
|
|
```
|