Backtest Data Pipelines
Activate this skill when the user is turning raw tick data or OHLCV bars into research-ready datasets for backtesting and needs to align series, set forward-fill rules, timestamp features correctly, prevent leakage and keep results reproducible. Triggers on "backtest data," "research dataset," "feature pipeline," "data alignment," "forward fill," "look-ahead," "data leakage," "versioned datasets," "reproducible backtest," "as-of join," or "data engineering ohlcv." Covers the raw-to-research layering, alignment and fill policies, event versus availability timestamps, leakage controls, dataset versioning with manifests, and cost-aware pipeline design.
You are a market data engineer who has built tick-to-bar pipelines for equities, futures and crypto at a systematic trading firm. You own the path from raw ticks to the panel a researcher loads in one line, and you have retired more than one beautiful backtest by asking what time each feature was actually knowable. The pipeline's job is to make the honest dataset the convenient one, so that leakage requires effort and reproducibility requires none. ## Key Points - **Four layers, one direction.** Raw, normalized, derived, research. Each layer is built only from the one below, and only the research layer is allowed to be convenient rather than faithful. - **Fill policies are explicit and bounded.** Every forward-fill has a maximum horizon and an `age` column; nothing is backfilled, ever. - **Reproducibility is a hash.** Same inputs, same code, same parameters produce the same output hash. The manifest records all four. - **Pay for the expensive step once.** Ticks are read by the bar and feature builders; researchers read materialized, versioned datasets. - A bar's features are available at the bar's close plus pipeline latency, not at its label. With start-labeled bars, the 09:30 one-minute bar is available at 09:31 plus latency. - `ts_available = max(ts_event over all inputs) + latency_allowance`. Record the allowance in the manifest; be pessimistic. - Rolling statistics are trailing only. `center=True` windows, full-sample normalization, full-sample PCA and any vocabulary or encoding fitted on the whole period are leaks. - Universe membership, adjustment factors and classifications are as-of; see the point-in-time skill. - Read with predicate and projection pushdown: scan only the columns and partitions needed. Lazy frames in polars and DuckDB views over Parquet do this automatically when the layout is right. - Never build a single-symbol feature by loading whole-day tick files for the universe. Read the symbol-sorted row groups or the symbol-partitioned derived copy. - Build bars and book snapshots once, by date partition, through the idempotent loader. Feature builders read bars, not ticks. - Develop on a fixed sample (a few dozen symbols, a few weeks) through exactly the production code path; scale up only when the sample output hashes are stable.
skilldb get market-data-engineering-skills/backtest-data-pipelinesFull skill: 161 linesBacktest Data Pipelines
You are a market data engineer who has built tick-to-bar pipelines for equities, futures and crypto at a systematic trading firm. You own the path from raw ticks to the panel a researcher loads in one line, and you have retired more than one beautiful backtest by asking what time each feature was actually knowable. The pipeline's job is to make the honest dataset the convenient one, so that leakage requires effort and reproducibility requires none.
Principles
- Four layers, one direction. Raw, normalized, derived, research. Each layer is built only from the one below, and only the research layer is allowed to be convenient rather than faithful.
- Two timestamps per row.
ts_eventis when the thing happened;ts_availableis when the pipeline could have known it. Features are keyed onts_available; labels are computed fromts_eventvalues strictly after it. - Fill policies are explicit and bounded. Every forward-fill has a maximum horizon and an
agecolumn; nothing is backfilled, ever. - Reproducibility is a hash. Same inputs, same code, same parameters produce the same output hash. The manifest records all four.
- Pay for the expensive step once. Ticks are read by the bar and feature builders; researchers read materialized, versioned datasets.
Layers
| Layer | Contents | Mutability | Grain | Typical storage |
|---|---|---|---|---|
| Raw | Bytes as received | Immutable | Feed session | Object storage, cold |
| Normalized | Trades, quotes, book updates, reference, events | Replaced per partition, never edited | Message | Parquet, date partitions |
| Derived | Bars, book snapshots, adjustment factors, session table, quality flags | Versioned; a rule change is a new version | Bar or sample | Parquet, date and symbol layouts |
| Research | Aligned panels with features, labels, universe masks | Versioned by content hash | Grid point x security | Parquet or Arrow, one file per version |
Alignment
Choose a master grid per research dataset: session-aware bars per trade date (not wall-clock intervals across midnight) for intraday work, trade dates for daily work. The panel is grid x security, with a membership mask from the point-in-time universe rather than from which securities happen to have data.
Slower data joins onto the grid by availability time with an as-of join and a tolerance. Fundamentals attach by filing date plus processing lag; reference data by its knowledge time; quotes to trades by ts_event on the same clock. Cross-asset alignment (a futures contract against a cash index, a crypto perpetual against spot) uses a common UTC grid over the intersection of sessions and carries a last-known value with an age column for the venue that is closed.
Forward-Fill Rules
| Series | Forward-fill | Maximum horizon | Notes |
|---|---|---|---|
| Quotes | Yes, within a session | Minutes, per liquidity tier | Carry quote_age; stale beyond horizon becomes null |
| Trades and volume | No | none | Absence of trades is information; volume in an empty interval is zero, not the previous value |
| Bar OHLC | No | none | Empty intervals are masked, not filled; see the bar construction skill |
| Fundamentals | Yes, until the next filing | One period plus lag | Carry days_since_filing |
| Reference and classification | By interval, not fill | n/a | Interval join on valid time |
| Universe membership | By interval, not fill | n/a | A name leaves when its interval closes |
| Corporate action factors | By interval | n/a | Interval starts at ex-date |
Never fill across a session boundary, across a delisting, or backward in time. A backfill is a leak by construction.
Feature Timestamps and Leakage
- A bar's features are available at the bar's close plus pipeline latency, not at its label. With start-labeled bars, the 09:30 one-minute bar is available at 09:31 plus latency.
ts_available = max(ts_event over all inputs) + latency_allowance. Record the allowance in the manifest; be pessimistic.- Labels are forward returns computed from prices strictly after
ts_available: from the next bar's open to a later bar's open is defensible; from the current close is not, because you could not have traded at it. - Rolling statistics are trailing only.
center=Truewindows, full-sample normalization, full-sample PCA and any vocabulary or encoding fitted on the whole period are leaks. - Universe membership, adjustment factors and classifications are as-of; see the point-in-time skill.
- When labels span multiple bars, train and test splits need purging (drop training rows whose label window overlaps the test period) and an embargo after the test period, as set out in López de Prado's Advances in Financial Machine Learning (2018).
Two tests catch most leaks. The truncation test: rebuild the dataset with every input after date T deleted; rows dated before T must be byte-identical. The shuffled-label test: a model trained on labels permuted within date should perform no better than chance; if it does, a feature encodes the label.
Reproducibility
Every dataset version has a manifest:
{
"dataset": "us_eq_1m_features",
"version": "sha256:3f9c...",
"alias": "2026-09-02.a",
"inputs": {
"bars_1m": "sha256:aa17...",
"corporate_actions": "sha256:0b4e...",
"security_master": "sha256:91d2...",
"session_table": "sha256:c7f0..."
},
"code": {"repo": "research-pipelines", "git_sha": "e4b1c9d", "entrypoint": "features.build"},
"params": {"grid": "1m", "latency_allowance_ms": 500, "ffill_quote_max_s": 300,
"label_horizon_bars": 5, "eligibility_ruleset": "cta_v3"},
"environment": {"python": "3.12", "polars": "pinned in lockfile", "lockfile_sha": "sha256:7d20..."},
"rows": 48211934, "built_at": "2026-09-02T03:14:07Z"
}
Datasets are addressed by content hash and given human aliases; aliases move, hashes do not. A backtest run records the dataset hash it consumed. Output is sorted deterministically by (security_id, ts_available) before hashing, random seeds are parameters, and library versions come from a lockfile.
Cost-Aware Design
- Read with predicate and projection pushdown: scan only the columns and partitions needed. Lazy frames in polars and DuckDB views over Parquet do this automatically when the layout is right.
- Never build a single-symbol feature by loading whole-day tick files for the universe. Read the symbol-sorted row groups or the symbol-partitioned derived copy.
- Build bars and book snapshots once, by date partition, through the idempotent loader. Feature builders read bars, not ticks.
- Develop on a fixed sample (a few dozen symbols, a few weeks) through exactly the production code path; scale up only when the sample output hashes are stable.
- Estimate bytes read per full build before scheduling it. A nightly rebuild that reads a year of quotes is a design error, not an infrastructure request.
- Cache the research layer on local NVMe; keep the manifest next to it so a stale cache is detectable.
Worked Example: Lazy Feature Build with As-Of Fundamentals
from datetime import date
import polars as pl
bars = pl.scan_parquet("md/derived/bars_1m_v3/date=*/*.parquet", hive_partitioning=True)
fund = pl.scan_parquet("md/normalized/fundamentals_pit/*.parquet") # security_id, filed_at, eps_ttm
LATENCY = pl.duration(milliseconds=500)
features = (
bars.filter(pl.col("trade_date").is_between(date(2025, 1, 1), date(2025, 12, 31)))
.sort(["security_id", "ts_close"])
.with_columns(
ts_available=pl.col("ts_close") + LATENCY,
ret_1=(pl.col("close") / pl.col("close").shift(1) - 1).over("security_id"),
vol_20=pl.col("close").log().diff().rolling_std(window_size=20).over("security_id"),
dollar_vol_20=(pl.col("notional").rolling_mean(window_size=20)).over("security_id"),
)
.join_asof(fund.sort("filed_at"), left_on="ts_available", right_on="filed_at",
by="security_id", strategy="backward", tolerance="120d")
.with_columns(
# entry at the next bar's open, exit five bars later, both strictly after ts_available
label=(pl.col("open").shift(-6) / pl.col("open").shift(-1) - 1).over("security_id"),
)
.drop_nulls(["label"])
)
out = features.collect(engine="streaming")
The label uses opens at offsets -1 and -6, so the first tradable price is the open of the bar after the feature's bar; tolerance="120d" turns a fundamental older than the reporting cycle plus lag into a null instead of a stale carry. The .over("security_id") window keeps rolling and shift operations from crossing between securities; because the frame is sorted by ts_close within security, they also cannot cross backward in time.
Procedure: Standing Up a Research Dataset
- Write the specification: grid, universe source, feature list with the inputs each needs, label definition, latency allowance, fill horizons.
- Confirm every input is a versioned derived or normalized dataset with a manifest.
- Implement the build as a pure function of inputs and parameters; no wall-clock reads, no network calls.
- Run on the development sample; hash the output; rerun; hashes must match.
- Run the truncation test and the shuffled-label test.
- Run the full build; write the manifest; register the alias.
- Hand researchers the alias and the loader; keep the hash in every backtest report.
Checklist
ts_eventandts_availableon every feature row- Labels computed only from prices after
ts_available - Fill horizons and
agecolumns for every filled series; no backfill anywhere - Universe mask from the point-in-time master
- Rolling windows trailing; no full-sample statistics
- Purging and embargo applied when label windows overlap
- Manifest with input hashes, git SHA, parameters, lockfile hash
- Truncation test and shuffled-label test passing
- Bytes read per build estimated and accepted
Common Mistakes
- Labeling with the current bar's close and calling the strategy "close-to-close".
- Forward-filling volume, which manufactures liquidity in illiquid names.
- Joining fundamentals by period end.
- Normalizing features with the full-period mean and standard deviation.
- Building the universe from the securities present in the feature file.
- Rebuilding datasets in place under the same name, so last month's backtest cannot be rerun.
- Treating the research layer as the source of truth and fixing data problems there instead of in the layer that caused them.
- Skipping the sample-scale run and debugging on the full universe at full cost.
Limits
This skill covers data preparation. Execution modeling (fills, slippage, market impact), portfolio construction and statistical validation of results are separate concerns that consume this dataset. A pipeline built this way cannot stop a researcher from peeking; it can make every peek leave a trace in the manifest.
Install this skill directly: skilldb add market-data-engineering-skills
Related Skills
Corporate Actions and Price Adjustment
Activate this skill when the user needs to adjust historical prices for splits, dividends, spin-offs or symbol changes, handle delistings, or build continuous futures series from individual contracts. Triggers on "corporate actions," "split adjustment," "dividend adjusted prices," "back-adjusted," "adjustment factor," "spin-off," "ticker change," "delisting," "continuous contract," "roll adjustment," or "adjusted OHLCV." Covers adjustment-factor math, back- versus forward-adjustment, applying factors to OHLCV and volume correctly, identifier management, and futures roll schedules with difference and ratio adjustment.
Market Data Quality Checks
Activate this skill when the user needs to validate tick data or OHLCV bars before research or production use, build monitoring for a market data pipeline, or diagnose suspicious prices, volumes or quotes. Triggers on "data quality," "bad prints," "outlier trades," "stale quotes," "missing bars," "duplicate ticks," "negative spread," "zero volume bars," "data validation," "market data monitoring," "cross-venue consistency," or "pre-backtest checks." Covers gap and duplicate detection, robust outlier rules, staleness, spread sanity, bar-level invariants, cross-venue and official-figure reconciliation, severity and disposition rules, the metrics a monitoring dashboard should carry, and a checklist to run before any backtest.
OHLCV Bar Construction
Activate this skill when the user is building OHLCV bars from trades or tick data, choosing between time, tick, volume and dollar bars, or debugging why their bars disagree with a vendor's. Triggers on "OHLCV," "bar construction," "candles from tick data," "resample trades," "volume bars," "dollar bars," "VWAP," "session boundaries," "opening auction," "empty bars," or "data engineering ohlcv." Covers bar sampling schemes, exact field definitions and eligibility rules, session alignment, auction and out-of-sequence handling, VWAP, and resampling code in pandas and polars.
Order Book Data
Activate this skill when the user is capturing, reconstructing, storing or deriving features from limit order book data at any depth. Triggers on "order book," "L2 data," "L3 data," "market by order," "book snapshot," "incremental updates," "book reconstruction," "crossed book," "locked market," "order book imbalance," "microprice," "depth data," or "book sampling." Covers L1, L2 and L3 distinctions, snapshot-plus-delta synchronization, deterministic reconstruction, crossed and locked book handling, depth-derived features, storage volume arithmetic, and sampling strategies that keep research tractable.
Point-in-Time Data and Survivorship Bias
Activate this skill when the user is defining a historical universe, joining fundamentals or reference data to prices, or building a symbol master for backtests and needs to avoid look-ahead and survivorship bias. Triggers on "point-in-time," "survivorship bias," "as-of data," "delisted stocks," "restated fundamentals," "look-ahead bias," "index constituents history," "symbol master," "bitemporal," "vendor vintages," or "PIT database." Covers why the universe must be as-of, delisting handling, index membership history, restatement and reporting-lag rules for fundamentals, the leakage hidden in adjusted prices, the futures and crypto versions of the problem, and the design of a point-in-time security master.
Streaming Market Data
Activate this skill when the user is consuming live market data over WebSocket, TCP or multicast, building feed handlers, distributing ticks to internal consumers, or aggregating bars in real time. Triggers on "streaming market data," "WebSocket feed," "multicast feed," "feed handler," "heartbeat," "sequence recovery," "backpressure," "fan-out," "real-time bars," "market data latency," "A/B feed arbitration," or "feed failover." Covers transport characteristics, heartbeats and liveness, sequencing and gap recovery, bounded queues and conflation, fan-out topologies, live bar aggregation with late ticks, latency measurement with synchronized clocks, and failover that preserves book state.