Data pipeline¶
Canonical schema¶
Every source is normalised to one long-format schema before entering the rest of the pipeline:
| Column | Type | Constraint |
|---|---|---|
timestamp |
datetime | sorted per symbol |
symbol |
string | non-empty |
open, high, low, close, adjusted_close |
float | finite, strictly positive |
volume |
float | finite, non-negative |
Timezone-aware timestamps are converted to UTC. Timezone-naive timestamps are assumed to already represent UTC and are not shifted.
quantlab.data.base.ensure_canonical_schema enforces the column set;
quantlab.data.validator.DataValidator checks the constraints and produces a
DataQualityReport (duplicates, missing values, invalid prices, OHLC
consistency, coverage gaps).
Instruments¶
DataConfig.instruments is a list of InstrumentConfig entries
(symbol/source/calendar), each fully explicit — no global source or
calendar for the whole experiment. A portfolio can mix sources and
calendars, e.g. US equities from Yahoo (XNYS) alongside crypto from
Binance (24/7), at daily frequency; see
Limitations for what that support does and does not
cover (rebalancing, windowed-feature dilution, intraday). ExperimentConfig.benchmark is itself an InstrumentConfig;
if its symbol duplicates a tradable instrument it must match that instrument's
source/calendar exactly and is never re-downloaded, and it never contaminates
the tradable universe's own timeline (an external 24/7 benchmark cannot inject
synthetic weekend bars into an all-equities portfolio).
Sources¶
- Yahoo Finance (
quantlab.data.yahoo.YahooFinanceDataSource) — Adjusted close is retained when Yahoo supplies it; a warning is logged when raw close must be used instead. - Binance (
quantlab.data.binance.BinanceDataSource) — crypto OHLCV, handles the 1000-candle pagination limit and 429 rate-limit backoff. No corporate actions, soadjusted_close == close. Alwayscalendar: "24/7". - CSV (
source: csvon an instrument) — readsdata/raw/<SYMBOL>.csv, already in canonical schema. Used for fully offline experiments and tests; neutral with respect to frequency compatibility (its real frequency is only known after reading the file, checked byDataValidatorpost-load).
Verified closures vs. missing data¶
A verified closure — a date that is a non-session day on an instrument's own
calendar (quantlab.data.calendar.is_session_day) — is distinguished from
genuinely missing data. quantlab.data.closures.insert_verified_closure_bars
fills a closed instrument with a flat synthetic bar (open/high/low/close and
adjusted_close each carried forward independently from the last known value,
volume forced to zero) whenever another instrument in the same tradable
universe has a real bar that day, so return is exactly zero and the instrument
is excluded from that day's rebalancing (see
Backtesting for the tradability-aware execution
side). This is a no-op for a single-calendar experiment and for non-daily
frequencies. A gap that is not a calendar closure (e.g. a genuinely missing
trading day) is governed entirely by missing_value_policy, as below — but,
unlike a closure, it is never assumed to have zero return; the policy decides
whether it's rejected, dropped, or filled.
Cleaning¶
The missing-value policy is always explicit, from the config, and applies at two levels — a missing value inside an existing row, and a (date, symbol) combination with no row at all for a real trading session are structurally different problems, handled by different code with the same policy:
DataCleaner(quantlab.data.cleaner) handles a missing value inside a row that exists.DataLoader.load()separately applies the identical policy to a genuine gap — a real session, on a symbol's own calendar, with no row whatsoever (quantlab.data.loader._apply_missing_value_policy_to_genuine_gaps), scoped to the tradable universe only (an external benchmark's own gaps are its own concern). Left ungoverned, such a gap would silently produce an incomplete panel — caught only later, confusingly, by the backtest engine's "asset return missing while held" error.
Both levels honour the same four policies:
drop— remove rows with any missing price; for a genuine gap, remove the entire date from the tradable universe (every symbol, not just the one missing), so the resulting panel stays dense.forward_fill— carry prices forward within each symbol, never backward, for at mostdata.forward_fill_limitconsecutive bars (default: one). For a genuine gap, a filled row is synthetic and flat (open/high/low/close and adjusted_close carried forward independently, volume forced to zero — the same shape as a verified-closure bar, but this is not one: it still counts toward the fill limit and is not exempt from rebalancing). Any unresolved or newly OHLC-inconsistent row, or a date where the fill limit is exceeded, is dropped the same way as underdrop.raise— fail if any canonical field is missing, including volume, timestamp, or symbol; for a genuine gap, fail at load time naming the affected (date, symbol) pairs, rather than later inside the engine.none— leave gaps for the caller to handle, at both levels.
Storage¶
ParquetStorage caches each (source, symbol, frequency) as one Parquet file
under data/cache/ and reuses cache entries that pass coverage and internal-gap
checks. Remote gaps or forced refreshes are downloaded and merged.
DataLoader.load() is the entry point used by the CLI and dashboard:
download-or-cache → discard bars that have not settled → slice to the requested
range → inspect raw defects → clean → validate the final frame → insert
verified-closure bars → apply missing_value_policy to any remaining genuine
gap → trim to the tradable universe's common start/end coverage. Slicing
before forward filling prevents extra history in a wider cache from changing
the first requested observation.
Universes¶
quantlab.data.universe.Universe provides convenience constructors
(from_symbols, from_csv, crypto_major, us_sector_etfs,
liquid_multi_asset_etfs). These reflect the current composition of an
index or sector set — they do not reconstruct historical membership, so
backtests over them carry a survivorship-bias caveat (see
Limitations).