Returns decomposition metrics (l*_cfr / l*_fr / l*_rr)
ERM3 can sync nine optional daily metrics (l1_cfr … l3_rr, including incremental l*_fr) into the V3 pipeline: wide columns on security_history_latest when present, and historical slices served from consolidated Zarr via the public API (the legacy long-form security_history table is not used by this API). They describe actual simple returns from a returns-decomposition pipeline step, not hedge notionals and not variance fractions.
What the keys mean
| Wire key | Meaning |
|---|---|
l1_cfr | Combined factor return through L1 (market) |
l1_fr | Incremental L1 (market-only) factor return |
l1_rr | Residual return at L1 |
l2_cfr | Combined factor return through L2 (sector) |
l2_fr | Incremental L2 (sector) factor return on top of L1 |
l2_rr | Residual return at L2 |
l3_cfr | Combined factor return through L3 (subsector) |
l3_fr | Incremental L3 (subsector) factor return on top of L1+L2 |
l3_rr | Residual return at L3 |
lstar_rr | Residual return at the L* level the model dispatched to — see "Which residual?" below |
lstar_level | Which level Lstar picked: 1 = L1, 2 = L2, 3 = L3; null = no recommendation (both L2 and L3 ER missing) |
Units: daily simple returns as decimals (same convention as returns_gross), e.g. 0.01 ≈ 1%.
Naming: *_cfr = combined factor return; *_rr = residual return at that level.
Which residual should I use?
l3_rr is the residual after market + sector + subsector — fixed at L3 regardless of whether the subsector layer is statistically warranted. lstar_rr is the residual at the level the cascade actually picks for that name: L3 when the subsector ETF adds material explanatory power (subsector marginal ER ≥ 1%), L2 when only the sector does, L1 otherwise.
- Stat-arb / panel queries ("best residual per name"): use
lstar_rr— you get a comparable signal across the universe, withlstar_leveltelling you which hedge depth was assumed. - Fixed-depth attribution ("show me what's left after subsector"): use
l3_rr. - Custom threshold: hit
GET /api/lstar?threshold=…—lstar_rrin MetricsV3 is materialized at the canonical 1% threshold; SDK callers wanting a different θ should round-trip through/api/lstar.
For names with weak subsector signal (lstar_level = 1 or 2), l3_rr overstates the cleanness of the residual because it subtracts a layer the model wouldn't have prescribed.
Not the same as ER or hedge ratios
l*_res_er(and other*_erfields) are explained risk — variance shares in [0, 1] from hedge-weight regressions.l*_mkt_hr,l*_sec_hr,l*_sub_hrare hedge ratios (dollars of ETF per 1 of stock).- The
l*_cfr/l*_rrseries come fromds_erm3_returns_*(returns decomposition), not fromds_erm3_hedge_weights.
Do not confuse l3_rr (residual return for one day) with informal “residual risk” language around l3_residual_er (idiosyncratic variance share). See SEMANTIC_ALIASES.md for the full wire ↔ SDK name table.
Where they appear in the API
GET /api/metrics/{ticker}— optional fields undermetricswhen populated on the latest row.- Daily history —
GET /api/ticker-returnsand data-plane history routes return Zarr-backed series;_metadata.data_source/_metadata.rangedescribe provenance on JSON (see Response metadata). security_history_latest— optional wide columns when the ERM3 sync has materialized them (supabase/migrations/in this repo).
Sync progress uses erm3_sync_state_v3 with table_name = security_history_returns_decomp.
Populating data (ERM3 sync)
This repository does not ship the ERM3 sync CLI. In the ERM3 repo, V3 sync supports the returns-decomposition dataset (often included in dataset set all) and a ticker filter:
- CLI:
--returns-decomp-tickerswith values such asmag7,all, or a comma-separated ticker list. - Python:
run_sync(..., returns_decomp_tickers="mag7" | "all" | "A,B,C").
See the propagation note ERM3_V3_RETURNS_DECOMP_SUPABASE_PROPAGATION.md for zarr variables, metric_key table, and checklist.