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
download-*Nine acquisition stages, one per source, each recording a vintage and writing a checkpoint.reconcile-permno-tickerResolve identifiers across the price, signal and factor panels.build-universe, build-panelAssemble the point-in-time universe and the annual signal panel.preprocessCross-sectionally rank and impute signals under the declared schema.train-brtRun the recursive annual fits;train-linearandtrain-nnsupply the comparators.form-portfoliosForm deciles and broadcast annual predictions to monthly portfolio returns.evaluateCompute factor-adjusted performance and the sub-period breakdowns.run-ablations, decompose-gapRun the registered ablations and attribute the replication gap.render-report, validate-production-runRender the note, then gate it.
Reproducibility and validation
- Every stage validates its output against
src/schemas.pybefore 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_implementedorblocked, instead of returning an empty result that looks like a finding. validate-production-runfails 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.