Module 3 — Research Stack & Where Your Pipeline Fits
Part I · Foundations (Fast Track) · Priority ⭐ Recommended · Status: Draft v0.1 Prerequisites: Module 0, Module 2
Overview
Strategy code is easy; research infrastructure is what separates a pile of notebooks from a repeatable edge factory. This module defines the reference anatomy of a professional quant research workflow — the stages, the contracts between them, and the reproducibility habits that make results trustworthy months later.
Then it does something most curricula skip: it audits your actual pipeline against that anatomy. You run m1-trading-model, which already implements large parts of this reference design (content-addressed cache DAG, pre-registered ledger, atomic model bundles). The goal here is not admiration — it’s producing a written gap list that feeds Modules 17–19.
The Reference Research Pipeline
Figure: the anatomy of a professional research stack. Every arrow is a contract; every box owes its neighbors a stable schema. The dashed feedback loop is where most retail stacks are missing limbs.
flowchart TD
ingest[(1 Data ingestion<br/>point-in-time)]
features[2 Feature engineering<br/>causal transforms only]
ingest --> features
features --> label[3 Labeling<br/>triple-barrier etc.]
label --> train[4 Train / validate<br/>purged walk-forward]
train --> evaluate[5 Evaluation harness<br/>costs, tearsheets, ablations]
evaluate --> registry[6 Model registry<br/>hashed artifacts]
registry --> deploy[7 Deployment<br/>live execution]
deploy -.->|"drift signals,<br/>live-vs-backtest gaps"| monitor[8 Monitoring]
monitor -.->|"retrain / retire triggers"| train
classDef data fill:#e8f0fe,stroke:#4a86e8
classDef process fill:#f3f3f3,stroke:#888
classDef ok fill:#e6f4ea,stroke:#2e7d32
classDef risk fill:#fde8e8,stroke:#c0392b
class ingest data
class features,label,train,evaluate,monitor process
class registry ok
class deploy riskHow to read this:
- Stages 1–5 form the research half (fast iteration); 6–8 form the operations half (money at risk). The registry is the airlock between them: nothing reaches deployment except through a hashed, documented artifact.
- The red node is deliberate: deployment is where mistakes become expensive, so entry into it must be gated and boring.
- The feedback loop (monitoring → training) is the difference between a strategy that decays quietly and one you notice decaying (Module 19).
Stage contracts
| Stage | Consumes | Produces | Key invariant |
|---|---|---|---|
| Ingestion | Raw feeds | Canonical bars/ticks | Point-in-time; no restatement |
| Features | Canonical bars | Feature frames | Causal: uses ≤ bar-close info only |
| Labeling | Candidates + prices | Supervised targets | Path-dependent truth (barriers), not fixed-horizon shortcuts |
| Train/validate | Features + labels | OOS predictions | Purged folds; holdout never ranks trials |
| Evaluation | Predictions | Net-of-cost economics | Same cost model as live will face |
| Registry | Approved artifacts | Hashed bundle | Immutable; provenance mandatory |
| Deployment | Bundle | Orders + telemetry | Bit-faithful feature parity with training |
Meaning: the invariants are the architecture. Schemas can evolve; the invariants (causality, purge discipline, parity, immutability) may not be violated even once without invalidating every downstream result.
Two Ways to Simulate Time
Research code simulates markets in one of two styles. Knowing which you’re in — and what each silently assumes — prevents an entire genre of bugs.
Vectorized treats history as one big array: signals computed column-wise, positions implied by shifts. Fast, ideal for screening hundreds of variants.
import pandas as pd
df = pd.DataFrame({"close": [10, 10.4, 10.2, 10.9, 11.2, 10.8]})
# Signal known at close of bar t -> tradable at bar t+1. Shift FIRST.
zscore = (df["close"] - df["close"].rolling(20).mean()) / df["close"].rolling(20).std()
position = np.sign(zscore).shift(1).fillna(0.0) # <- the guard
bar_pnl = position * df["close"].diff() # vectorized P&L
⚠️ Pitfall: The
.shift(1)is the entire honesty of the snippet above. A vectorized signal without it “decides” using the close and profits off that same close — look-ahead bias so common it has its own name in every backtesting post ever written.
Event-driven walks bar-by-bar (or tick-by-tick) through a queue: orders, fills, latency, partial executions modeled explicitly. Slower, but it’s where realistic fills and portfolio state live.
| Aspect | Vectorized | Event-driven |
|---|---|---|
| Speed | 100–1000× faster | Slow |
| Fill realism | Implicit assumptions | Explicit order lifecycle |
| Best for | Screening, parameter sweeps | Final validation, execution logic |
| Failure mode | Silent look-ahead/fill fantasy | Bugs in state machinery |
Engines you’ll meet in this curriculum:
| Engine | Style | Note |
|---|---|---|
vectorbt |
Vectorized | Parameter sweeps at absurd speed (Module 7 labs) |
backtesting.py / backtrader |
Event-driven | Approachable single-asset validation |
nautilus_trader |
Event-driven | Institutional-grade event core |
pysystemtrade |
Full-stack | Carver’s production system, open source |
| MT5 Strategy Tester | Tick-level event-driven | Your native ground truth; weak analytics vs Python tear sheets |
Module 5 reconciles all of them on one system — including where the Strategy Tester disagrees with your Python stack, and why those disagreements are the lesson.
Reproducibility Engineering
A result you cannot re-run six months later never happened. Four habits make research reproducible:
1. Config-as-code
Every experiment parameter lives in a file, not in notebook-cell history:
import tomllib
from dataclasses import dataclass
@dataclass(frozen=True)
class ExperimentConfig:
symbol: str
timeframe: str
barrier_pt_atr: float
barrier_sl_atr: float
max_hold_bars: int
fold_scheme: str # e.g. "purged_kfold_v1"
with open("experiments/R042.toml", "rb") as fh:
cfg = ExperimentConfig(**tomllib.load(fh)["experiment"])
2. Seed everything
def seed_everything(seed: int) -> None:
import random
random.seed(seed)
np.random.seed(seed)
# torch.manual_seed(seed) # when neural nets join (Module 20)
SEED = 42 # one constant, referenced everywhere
seed_everything(SEED)
3. Snapshot the data
Record exactly which data window produced a result — ideally by content hash, not by date string:
import hashlib, json
def fingerprint(frame_bytes: bytes) -> str:
"""Short content hash identifying a dataset snapshot."""
return hashlib.sha256(frame_bytes).hexdigest()[:12]
print(json.dumps({
"dataset_sha": fingerprint(open("data/xauusd_m5.parquet", "rb").read()),
"rows": 1_482_113,
"window": ["2017-01-01", "2026-05-13"],
}))
4. Register outcomes, not vibes
Append an evaluation record next to every pre-registered card (Module 0’s loop):
record = {
"id": "R042",
"status": "completed",
"config_sha": fingerprint(cfg.__repr__().encode()),
"oos_net_sharpe": 1.31,
"n_oos_trades": 214,
"verdict": "promote-to-paper", # -> reject / hold
}
with open("experiment_ledger.jsonl", "a") as f:
f.write(json.dumps(record) + "\n")
Notebook vs Script Boundary
House rule, stated once: notebooks explore; scripts ship.
- A notebook may be messy, stateful, and half-wrong — that’s what exploration is.
- The moment a finding graduates into the pipeline (a feature, a filter, a validation step), it becomes an imported function with tests. Copy-pasting notebook cells into production modules is how silent divergence begins.
- Promotion ritual: extract function → add unit test → delete the cell → import the module in the notebook. The notebook then proves the shipped code reproduces the exploratory finding.
This mirrors how your production repo already works (src/lgbm_pipeline modules with pytest coverage; notebooks as private scratch) — here it’s codified so research repos converge on the same shape.
Audit: Mapping Your Pipeline to the Anatomy
🔧 For your pipeline:
m1-trading-modelalready implements this reference design further than most professional shops:
Anatomy stage Your implementation Verdict Point-in-time ingestion Phase 1 + exclusive as_ofboundary✅ Causal features Phase 2 + content-addressed cache DAG ( polars_v15)✅ exemplary Honest labels meta_triple_barrier.pyas sole supervised path✅ Purged validation Nested walk-forward + untouched holdout, v12 tuning ✅ Evaluation harness Phase 5 engines + canonical diagnostics ✅ Registry Atomic hashed final_model_bundle_v2✅ Deployment src/livesupervisor (journal-authoritative)✅ hardened Monitoring feedback Schema-v6 snapshots exist; drift consumers absent ❌ gap Verified gaps carried into later modules: drift monitors & parity harness (M24), combinatorial/PBO statistics (M19), model zoo beyond XGBoost (M17), sequence models (M20), session-granular cost curves (M22).
That table is the template for your own audit. Most retail pipelines fail at stages 1 (survivorship, restated fundamentals), 4 (shuffled CV), and 8 (no monitoring at all); yours fails only at 8 — which is precisely why Module 19’s drift work and Module 24’s parity harness sit on the critical path of your study plan.
Testing It Honestly
An infrastructure audit checklist — answer per stage, in writing:
- Ingestion: Can I reproduce any past result’s exact input window? Who else could have modified it?
- Features: Is there a test asserting no feature uses information after bar close?
- Labeling: Could two researchers, given the config, generate byte-identical labels?
- Train/validate: Does any selection decision touch the holdout? Show me the ledger.
- Evaluation: Is the cost model identical to (or conservative vs) live fills?
- Registry: Can I prove which data/config/code produced the deployed model?
- Deployment: If I feed the same historical bar to training code and live code, do outputs match bit-for-bit?
- Monitoring: What alarm fires when live diverges from expectation — and who hears it?
Any “don’t know” is a finding. Findings go straight into the project below.
Hands-On Project
Deliverable: docs/research/pipeline_audit.md — your stack scored against the reference anatomy, plus a ranked gap backlog.
Tasks:
- Build the eight-row stage table (anatomy stage → your implementation → verdict ✅/⚠️/❌) for your own research setup, not just the production repo. Research-side gaps usually outnumber production ones.
- Answer the eight audit questions above with evidence (file paths, test names, ledger entries) — not intentions.
- Write a top-5 gap backlog, each item linked to the curriculum module that closes it and a rough effort guess.
- Run the promotion ritual once: take any useful notebook-only function you have, ship it behind a test, and record the before/after.
Acceptance criteria:
- Table covers all eight stages with evidence links, not adjectives.
- At least three findings marked ⚠️ or worse, each with a named module owner.
- Gap backlog ordered by leverage (what unblocks the most downstream study).
- One function demonstrably promoted from notebook to tested script.
Key Takeaways
- Research infrastructure is eight stages with contracts; the invariants (causality, purge discipline, parity, immutability) matter more than the tools.
- Vectorized simulation is for screening and carries a standing look-ahead debt; event-driven is for verdicts. Know which regime you’re in every time you write
.shift(1)— or fail to. - Reproducibility is four cheap habits: configs in files, seeds everywhere, datasets fingerprinted, outcomes registered. None costs more than minutes; all pay for months.
- Notebooks explore, scripts ship; the promotion ritual keeps the two from drifting apart.
- Your production stack already embodies most of this anatomy — the honest audit shows monitoring and research-side statistics as the open fronts, which is exactly where Modules 19, 24, and 17 aim.
References
- Marcos López de Prado — Advances in Financial Machine Learning, ch. 1–2 (research factories, backtest hygiene), ch. 7 (ensemble/registry thinking)
- Stefan Jansen — Machine Learning for Algorithmic Trading, 2nd ed., ch. 1–2 (pipeline anatomy, data hygiene)
- Robert Carver — pysystemtrade (open-source full-stack reference; read
system/layout against this module’s anatomy) - paperswithbacktest — awesome-systematic-trading (engine survey context)
- vectorbt — documentation (the vectorized philosophy, taken to its extreme)
- Next in sequence: Part II begins — Module 4 — Market Data Engineering