Data zones: raw → clean → view (design)#
Design note for the WRDS-scale data lifecycle in
numeraire-dataset. Written before the implementation (W3-1). The three-zone split makes preprocessing reproducible and pinnable: a numeraire result’sdata_vintagestring must trace back, unambiguously, to exactly the bytes and the exact transform recipe that produced its inputs.
Why three zones#
Preprocessing is part of the method. A Sharpe or an alpha is only reproducible if the cleaning — delisting adjustments, share-code filters, CCM link windows, the accounting-to-return lag — is pinned as tightly as the model. So the pipeline is split into three zones with hard boundaries:
raw/ immutable download cache, keyed by (source, vintage) — bytes as pulled, never edited
clean/ deterministic transforms of raw → tidy PIT tables — a recipe hash pins the transform
view/ numeraire TimeSeriesView / CrossSectionView builders — lazy numeraire import, no new state
Arrows point one way: view reads clean, clean reads raw, raw reads the outside world.
Nothing downstream mutates an upstream zone.
raw — immutable, pull-stamped#
One directory per
(source, vintage), e.g.raw/crsp_msf/2024-12/.vintageis the pull’s identity: a WRDS query date, a FRED-MD month, a Ken French release. Content is the bytes exactly as pulled (parquet/csv), never edited in place — a re-pull with different content is a new vintage directory, not an overwrite.Each pull writes a sibling
_meta.json:{source, vintage, pulled_at, query_hash, pit_status, row_count, content_digest}.pit_statusrecords whether the pull is itself point-in-time (a dated vintage) or a latest-snapshot convenience pull (revised, not PIT) — so a downstream golden can refuse a non-PIT source.The cache directory is user-configurable and defaults OUTSIDE the repo (
$NUMERAIRE_DATA→ platform cache dir, perpaths.data_home). Licensed data (CRSP/Compustat) lives only here, on the user’s machine behind their own WRDS credentials — never in any git history.
clean — deterministic, recipe-hashed#
A
cleantable is produced by one or more steps: pureframe(s) → framefunctions with all parameters explicitly declared (no hidden globals, no ambient config). A step is deterministic: same inputs + same params → same output, bit-for-bit.Every step’s identity is
(name, version, params). Its recipe hash is the SHA-256 of the canonical JSON of{name, version, params, inputs: [<recipe hash of each input>]}. The hash therefore chains: a clean table’s recipe hash transitively pins every upstream transform and every raw vintage that fed it. Bumpversionwhen a step’s logic changes in a way that should invalidate downstream caches.Steps register in an open registry (the same pattern as numeraire’s evaluator registry —
register_step/get_step/available_steps, a module-global dict,KeyErroron a duplicate name unlessoverwrite). No second registration style is invented. A@step(...)decorator is sugar overregister_step.
view — numeraire builders, lazy#
viewbuilders turn acleantable into a numeraireTimeSeriesView/CrossSectionView. They importnumerairelazily (assources.to_timeseries_viewalready does), so the clean + raw zones stay numeraire-free and installable without it.viewadds no persisted state.
data.lock.json#
A single lock file records, for each built clean (and pinned raw) artifact, exactly what it is:
{
"version": 1,
"artifacts": {
"crsp_monthly_clean": {
"kind": "clean",
"step": "crsp_monthly_clean",
"step_version": 1,
"recipe_hash": "sha256:9f3c…",
"content_digest": "sha256:1a2b…",
"inputs": ["crsp_msf@2024-12", "crsp_msenames@2024-12"],
"rows": 4193021,
"built_at": "2026-07-05T…"
},
"crsp_msf@2024-12": {
"kind": "raw", "source": "crsp_msf", "vintage": "2024-12",
"content_digest": "sha256:…", "pit_status": "vintage"
}
}
}
recipe_hashpins the transform;content_digestpins the actual output bytes (so a corrupted or partial build is detectable independently of the recipe).data_vintagederivation (the contract with numeraire). Thedata_vintagestring a numeraire result carries for a tableTis derived directly fromT’s lock entry:data_vintage = f"{name}@{recipe_hash[:12]}"(e.g.crsp_monthly_clean@9f3c1d2e4a5b). Given a result’sdata_vintage, the lock entry — and thus the full recipe + raw vintages — is recoverable by name and hash prefix. No provenance is stored in two places that could disagree.
WRDS goldens ride the WRDS-CRED tier#
The reproductions that need CRSP/Compustat (W3-2 GKX-lite, W3-3 ALZ empirical, W3-4 HXZ) register
their targets as numeraire.golden.GoldenCase(tier=WRDS_CRED, available=<creds+cache present>).
That mechanism (built in F3) makes them skip in CI and run verbatim where the user’s WRDS
credentials and raw cache are present — one code path, no forked assertions, no licensed bytes in
the repo. Live WRDS tests carry @pytest.mark.wrds and are deselected by default.
Boundaries restated (red lines)#
WRDS credentials go through env / the tidyfinance WRDS config — never committed, never in code.
The raw cache is user-configurable and outside the repo by default.
No licensed data (CRSP/Compustat/JKP returns) in any tracked file or git history — ever.