Factor correlation (macro + style)

RiskModels stores daily factor total returns in Supabase macro_factors: one row per factor_key and trading date (teo), with return_gross and optional metadata. Two sleeves share the table and the correlation endpoints:

  • Macro sleeve (10 keys) — rates, credit, commodities, FX, volatility, crypto. Backed by ds_macro_factor.zarr.
  • Style sleeve (8 keys) — momentum, quality, low vol, value, growth, size, dividend, moat. Mirrored from ds_etf.zarr into the same table, with metadata.category = "style" so callers can distinguish the two.

All 18 canonical keys are valid anywhere a factor_key is accepted.

Macro sleeve (10 keys)

KeyUnderlyingTypical meaning
inflationTIPUS TIPS — inflation expectations proxy
term_spreadVGITIntermediate Treasury — long-end / slope proxy (ust10y2y legacy alias)
short_ratesBIL1–3mo Treasury bills — short-rate proxy
creditHYGHigh-yield corporate — credit spread proxy
oilUSOWTI crude daily return
goldGLDGold daily return
usdUUPUS Dollar (DXY) daily return (dxy legacy alias)
volatilityVXXVIX short-term futures — captures roll cost
bitcoinBITOBitcoin futures ETF (btc alias)
vix_spotFRED VIXCLSPure spot VIX (vix legacy alias)

volatility and vix_spot are different factors — VXX captures futures roll dynamics and term structure; VIXCLS is the pure spot index. Keep both if you care about regime detection vs cost of carrying vol.

Style sleeve (8 keys)

KeyUnderlyingHistoryInterpretation
momentumMTUM2013-04–MSCI USA Momentum
qualityQUAL2013-07–MSCI USA Quality
low_volUSMV2011-10–MSCI USA Min Vol (minvol alias)
valueVLUE2013-04–MSCI USA Value
growthIWF2000-05–Russell 1000 Growth
sizeIWM2000-05–Russell 2000 small-cap (small_cap alias)
dividendSCHD2011-10–Schwab US Dividend Equity (div, yield aliases)
moatMOAT2012-04–VanEck Wide Moat

MSCI factor ETFs only go back to 2011–2013; pre-launch windows return null correlations. IWF/IWM extend to 2000-05 for deeper growth/size history.

Why style factors pair naturally with ERM3 residuals

The ERM3 cascade (L1 market → L2 sector → L3 subsector) produces a residual that is orthogonal by construction to SPY, the sector ETF, and the subsector ETF. When you correlate that residual against a raw style ETF (MTUM, QUAL, …), the market and sector components embedded in the style ETF contribute zero to the correlation. What remains is the pure-style tilt sitting inside your idiosyncratic residual — the part of your "alpha" that actually comes from systematic style exposure you did not consciously size for.

Use return_type=l3_residual with style factors to read this cleanly. gross works too but conflates market / sector / subsector exposure back in.

Common names aliases are normalized server-side: mtummomentum, qualquality, usmv / minvollow_vol, vluevalue, iwfgrowth, iwm / small_capsize, schd / divdividend, moatmoat.

Correlation vs a stock (ERM3)

Use these when you want Pearson or Spearman correlation between a stock return series and one or more factor series:

  • POST /api/correlation — single ticker or batch (array of tickers).
  • GET /api/metrics/{ticker}/correlation — same math; pass factor keys as a comma-separated factors (or factor) query parameter.

return_type selects the stock return series (not the factor series):

ValueMeaning
grossStock gross daily return
l1Residual vs market only (uses L1 hedge ratio and SPY)
l2Residual vs market + sector ETFs
l3_residualResidual after L3 hedge replication (market + sector + subsector ETFs) — recommended for style factors

Correlations are return correlations in roughly [-1, 1] — they are not hedge notionals (l3_market_hr) or variance shares (l3_residual_er). See SEMANTIC_ALIASES.md for full semantics and JSON Schema links.

The implementation requires at least ~30 overlapping paired days per factor; otherwise that factor's entry is null. A null is not a sign error — it means insufficient overlap (including pre-launch windows for short-history MSCI style factors).

Example: style DNA of a residual

from riskmodels import RiskModelsClient

client = RiskModelsClient.from_env()

style = client.get_factor_correlation_single(
    "NVDA",
    factors=["momentum", "quality", "low_vol", "value",
             "growth", "size", "dividend", "moat"],
    return_type="l3_residual",
    window_days=504,       # ~2 years
)
print(style["correlations"])
# e.g. {"momentum": 0.41, "quality": 0.10, "low_vol": -0.19, "value": -0.31,
#       "growth": 0.28, "size": -0.06, "dividend": -0.12, "moat": 0.08}

A PM reads this as: "My NVDA residual — the part I thought was pure idiosyncratic alpha — is materially positive momentum and negative value. If I am value-styled, I am carrying anti-value exposure inside my residual that I did not consciously size."

Raw factor time series (no ticker)

GET /api/macro-factors returns long-format rows: factor_key, teo, return_gross (and optional metadata when non-empty), for a date range you choose. No equity ticker is required. Both sleeves are returned when no filter is passed.

Query parameters:

  • factors or factor — comma-separated keys (optional; default all 18 canonical keys).
  • start, end — inclusive YYYY-MM-DD (optional; defaults: end = today UTC, start = five calendar years before end). Maximum span: 20 years.

OAuth scope: macro-factor-series. Billing is per request (see OpenAPI / pricing).

Python SDK (requires scope on your key):

from riskmodels import RiskModelsClient

client = RiskModelsClient.from_env()

# Macro series only
macro = client.get_macro_factor_series(
    factors=["bitcoin", "vix_spot", "oil"],
    start="2023-01-01",
    end="2023-12-31",
    as_dataframe=True,
)

# Style series only
style = client.get_macro_factor_series(
    factors=["momentum", "quality", "low_vol", "value"],
    start="2023-01-01",
    end="2023-12-31",
    as_dataframe=True,
)

CLI:

riskmodels macro-factors --factors momentum,quality,low_vol --start 2023-01-01 --end 2023-12-31

JSON Schema for the success body: /schemas/macro-factors-series-v1.json (also under MCP schema-paths).

Related

Factor correlation (macro + style) | RiskModels API