Preparing data for backtests: How we have designed DataKitchen
·Vectopian · algolab, datakitchen, backtesting, data
Most backtests fail before the strategy does. They fail in the series.
Unadjusted prices jump on split mornings and look like crashes. Sparse sessions invent silence — or continuity — that never traded. You fill at the close of a coarse bar and call it execution, while the path inside the minute would have wrecked you. One vendor’s feed shows a spike another never saw. Corporate-action files disagree. The Walk-Forward smiles anyway, because the engine believed every bar it was handed.
I have lost count of how many “edges” evaporated the moment I asked a simpler question: was this market behavior, or was it the data?
We pull from three vendors on purpose. None of them is gospel. Cross-checking for anomalies — jumps that only one feed believes, dividend or split mismatches, odd gaps, bars that fail basic OHLC sanity — is not optional hygiene. When sources disagree, that disagreement is a finding. Papering over it is how you ship fiction.
That is the mess we designed around.
AlgoLab is our research stack — strategies, indicators, walk-forwards, Monte Carlo, the lab bench where we ask questions once the numbers are trustworthy.
DataKitchen is the part of AlgoLab that sits in front of that bench. It lands vendor dumps, normalizes corporate actions, resamples into research products, validates them, and publishes stores a backtest can pin. Not glamorous. Just the kitchen between a raw download and anything I am willing to call research.
Here is how we built and run it.
The pipeline
1. Ingest — park the raw files somewhere sane
Everything lands under one shared data home, split by provider. Each download becomes a job folder: the market files plus an authoritative metadata.json. Folder names are for humans. Sampling logic never parses ticker, schema, or date range from the name alone.
I have been bitten by pretty folder names that did not match the query window inside. We stopped trusting names. Metadata wins.
2. Discover and select — decide what this run is about
Before we resample anything, we lock the universe: which jobs, which symbols, which UTC window. Sometimes that is an explicit input path. Sometimes discovery. Sometimes a selection file.
If you skip this and “just resample whatever is on disk,” you will mix histories and wonder why the Walk-Forward will not reproduce. We treat selection as part of the research claim.
3. Adjust — splits and dividends are data, not footnotes
We build adjustment books from vendor corporate-action feeds and anything we have curated ourselves. Normalize to our JSON. Merge across sources when they disagree, and look at the discrepancies — that is often where the anomaly is.
Without this step, momentum and vol fire on corporate actions. You get morning “crashes” that never traded. We pin an adjustment mode on the output (raw, split_adjusted, dividend_adjusted, split_dividend_adjusted) so a result always answers: which prices did you use?
Most of our research sits on split_adjusted. Raw is for when we want to see the scars.
4. Resample — this is the kitchen
Native bars (often fine-grained OHLCV) get aggregated to the intervals we actually trade and study — minutes, hours, days. Factors apply here when the mode asks for them.
Where a feed simply omits zero-volume intraday bars, we gap-fill on purpose: forward-filled OHLC, volume zero. I would rather see an explicit dead minute than let the backtester invent continuity by skipping holes.
Two products leave the kitchen:
| Layer | What we use it for |
|---|---|
| Signal | OHLCV for decisions and indicators |
| Execution | Quotes for fills — synthesized on liquid names; CBBO when OHLCV is thin or missing |
I care about that split. Computing signals off closes and pretending those closes were fills is how you invent edge. Indicators stay out of the kitchen on purpose — AlgoLab’s engine computes them later — so the same cleaned store can feed more than one strategy without another resample.
5. Publish — the output folder tree
Cleaned products land under a DataKitchen output root. The path is the claim. Read it left to right:
| |
What each segment means:
| Segment | What it answers |
|---|---|
| adjustment | Which corporate-action reality? raw, split_adjusted, dividend_adjusted, or split_dividend_adjusted. We pin one mode as the research root. |
| assetClass | What kind of instrument? equity, etf, index, forex, future, crypto. ETFs are first-class — not buried under equity. |
| market | Where does it live? Country for stocks/ETFs/indexes, exchange for futures, venue for FX/crypto. Stops AAA in one country colliding with AAA in another. |
| TICKER | Uppercase hub for that symbol. Options nest under the underlying here, not as a parallel top-level class. |
| signal / ohlcv / interval | Decision bars. Intervals we write: 1m, 5m, 1h, 1d. No indicators in these files — AlgoLab computes those later. |
| execution / quotes / variant | Fill surface. Bid/ask for the engine. Default on liquid names: synthesized. Illiquid / untraded: often cbbo. |
| manifest.json | Provenance and counts for that product. If I cannot defend the manifest, I do not defend the backtest. |
| year= / month= | Hive partitions so we can pull only the months a study needs. |
Vendor identity is not a folder. It lives in the manifest. Concurrent research jobs pin one adjustment mode and prune what they do not need.
Fixed-point prices, UTC timestamps. If I cannot say which job and which mode produced a bar, I do not trust the bar.
Quotes and slippage — how we actually fill
Signal tells the strategy when. Execution tells the engine at what price. We refuse to fill at the signal close and call it done.
How we source quotes
Proper quote history — real bid/ask tapes — is often expensive, and we do not need it for every name. So we match the fill surface to the instrument:
Highly liquid stocks and ETFs — where OHLCV is dense and trustworthy — we lean on the kitchen’s default synthesized quotes under execution/quotes/synthesized: a bid/ask surface derived from the bar data so every liquid symbol still has something fillable without buying a full quote tape for the whole universe. That is the path behind the 1-minute slippage stance below.
Illiquid names, or stretches with no (or useless) OHLCV — thin books, barely traded symbols, gaps where a bar series simply is not there — synthesized-from-OHLCV is the wrong tool. For those we use richer quote constructions when we have them. CBBO (consolidated best bid/offer) is the main one: normalized bid/ask under execution/quotes/cbbo, including periods that did not trade and would never show up as a clean OHLCV bar. Same row schema as synthesized; AlgoLab just points at a different variant folder. Market-by-order style variants can sit beside CBBO the same way when we need them.
Slippage models are not baked into the kitchen files — the engine applies them on top of whichever quote variant we pinned.
How we account for slippage with 1-minute bars
On the liquid path, we keep 1-minute Signal bars (signal/ohlcv/1m) for a reason. A daily or hourly close hides the path price took inside the bar. The 1-minute high, low, and range are where fills actually live when you are late, aggressive, or just unlucky.
In those backtests we:
- Take the decision from Signal (prior bar — no peeking into the bar you are filling on).
- Join the Execution quotes for that time (usually synthesized on liquid names).
- Stress the fill using what 1-minute bars show about intra-bar movement — the range inside that minute is the honest envelope for “I got filled worse than mid.” We also run explicit slippage scenarios (tight / base / stress, in basis points per side) so a result has to survive more than one cost assumption.
- Charge commissions the same way across studies so comparisons stay fair.
On the illiquid / CBBO path, the quote tape is the fill surface — including untraded moments OHLCV never recorded. We still do not fill at a fantasy close; we just stop pretending a sparse bar series was a liquid book.
If a strategy only “works” when you fill at the close of a coarse bar and ignore the 1-minute path (or ignore a thin book entirely), it does not work. The kitchen gives us both surfaces so AlgoLab can be strict about that.
6. Validate — fail here, not in the equity curve
summarize for a sanity pass. validate for the mean parts: manifest vs rows, bad OHLC, unordered bars, intra-session gaps. Overnight is not a “missing bar” in our book — sessions have edges. We lean hard on the 1m (and 5m / 1h) series here: that is where silent holes show up.
This is also where vendor cross-checks bite: a series that only one of the three feeds believes, or a corporate-action event the others never saw, gets flagged before it becomes “alpha.”
If validate complains, we fix the kitchen run. We do not “nudge” the strategy until the store is quiet.
7. Backtest — only after the kitchen is done
AlgoLab loads a pinned mode root, computes indicators in-process, joins quotes for fills (with the 1-minute-aware slippage stance above), then full sample, walk-forward, Monte Carlo, optimize — whatever the question is.
DataKitchen’s job ends when the store is trustworthy. The lab’s job starts there. Mixing those jobs is how cleaning decisions hide inside “alpha.”
What I actually get from cleaning
- Prices that do not jump on splits unless I asked for raw.
- Decisions and fills on different surfaces — with 1-minute bars keeping slippage honest.
- Gaps that are policy, not accidents.
- Three-vendor cross-checks so one bad feed does not own the narrative.
- A folder tree that states the claim: mode, asset class, market, ticker, signal vs execution.
- A paper trail —
metadata.jsonand manifests — so a pretty result can still be challenged. - An honest label on the adjustment mode, because
rawandsplit_adjustedare different research claims.
If a backtest looks too good and we skipped the kitchen, my first guess is not genius. It is data.