Software companion

Real-Time Machine Learning in the Cross-Section: A Public-Data Reproduction

Overview

Reproducing a published cross-sectional result from public data means the interesting quantity is not the headline number but the distance between it and the original, and where that distance comes from. The pipeline is therefore built so that every stage is attributable. Canonical columns are declared in src/schemas.py and validated before any parquet, CSV or JSON is written, and each non-dry-run stage appends to run_manifest.json with the effective configuration hash, source vintages, per-stage row counts, runtime, warnings and artifact paths.

The protocol follows the target paper: annual prediction from July of year t to June of year t+1, recursive annual refits, annual rebalancing, and a boosted-tree grid of exactly fifteen fits per recursive window. Twenty-eight ablations are registered as configuration files rather than written as one-off scripts, which is what makes the gap decomposition a measurement instead of a narrative.

Implementation

Core libraries
LightGBM, torch, scikit-learn, statsmodels, pandas, pyarrow, pydantic, Jinja2, openassetpricing
Replication scope
The target paper’s 207-anomaly Chen–Zimmermann sub-analysis over 1987–2019 out-of-sample, not the full signal universe
Protocol
Annual July-to-June prediction, recursive annual refits, fifteen boosted-tree fits per window before refit
Data sources
OSAP anomaly signals, yfinance monthly prices, Ken French factors, Hou–Xue–Zhang Q-factors, Chen–Velikov effective spreads, FRED macro, Wurgler sentiment
Configuration
33 YAML files: default, headline, smoke, public and local baselines plus 28 registered ablations
Tests
46 modules, including a fixture integration pipeline and a golden report test

Components

src/data/
One downloader per source — OSAP signals, prices, French and Q factors, Chen–Velikov spreads, Wurgler sentiment, Pástor–Stambaugh liquidity, FRED macro — each recording its vintage
src/data/reconcile.py
Security-identifier reconciliation between the price, signal and factor panels
src/data/panel.py, preprocess.py
Panel assembly and the cross-sectional rank transform to [−1, +1] with post-ranking imputation to zero
src/models/brt.py, linear.py, nn.py
The boosted-tree model and the linear and neural comparators behind one wrapper interface
src/analysis/protocol.py, train.py
The recursive annual training driver and the window protocol
src/analysis/portfolio.py, costs.py
Decile formation, long-short construction, and the effective-spread cost treatment
src/analysis/eval.py, subperiods.py
Factor-adjusted evaluation and the sub-period breakdowns
src/analysis/decompose.py, attribution_map.py
The gap decomposition attributing the replication difference to identified causes
src/analysis/ablation_defs.py, ablation_runner.py
The registered ablation set, its runner, and the selection cache
src/reporting/validation.py
The publication gate behind validate-production-run
src/utils/manifest.py, snapshot.py
The run manifest and input snapshot hashing

Stages

  1. download-* Nine acquisition stages, one per source, each recording a vintage and writing a checkpoint.
  2. reconcile-permno-ticker Resolve identifiers across the price, signal and factor panels.
  3. build-universe, build-panel Assemble the point-in-time universe and the annual signal panel.
  4. preprocess Cross-sectionally rank and impute signals under the declared schema.
  5. train-brt Run the recursive annual fits; train-linear and train-nn supply the comparators.
  6. form-portfolios Form deciles and broadcast annual predictions to monthly portfolio returns.
  7. evaluate Compute factor-adjusted performance and the sub-period breakdowns.
  8. run-ablations, decompose-gap Run the registered ablations and attribute the replication gap.
  9. render-report, validate-production-run Render the note, then gate it.

Reproducibility and validation

  • Every stage validates its output against src/schemas.py before writing, so a broken column contract fails at the stage that caused it rather than three stages later.
  • Commands that are not yet implemented fail closed with a manifest entry marked not_implemented or blocked, instead of returning an empty result that looks like a finding.
  • validate-production-run fails closed on smoke and fixture paths, missing artifacts, placeholder report text, bad snapshot hashes, and on the forbidden full-universe tag that would misstate the replication scope.
  • The smoke configuration is a labelled 30-year, 100-security target with synthetic data and an injected regressor; it exercises the annual window protocol and is explicitly not a result.
  • Known data limits are handled rather than ignored: the Chen–Velikov spread panel ends in 2017, so post-2017 cost reporting falls back to a declared fixed-basis-point sensitivity grid.

Availability

Not publicly released. Three of the signals the headline uses require subscription-backed access, so a public-only subset cannot reproduce the pinned run. The repository already separates that gated headline path from a public-data track, and a release would have to make the boundary explicit to be useful.

Companion research

A real-time machine-learning pipeline reproduced on public data: the headline Sharpe survives six-factor adjustment and collapses under value weighting.

Read the research report