What (One-liner)
TVP-KFAVAR v2 macro regime detection: sklearn PCA extracts the first factor → statsmodels Markov Switching AR(1) with switching variance → 3-state labels. Auto-fallback to v1 rolling statistics on convergence failure. The primary source of the regime field for macro_snapshot_v1 / macro_factor_v1.
How to use
Consumed indirectly on Canvas (macro_snapshot_v1 aggregates the regime field). Direct Python:
from services.mfle.tvp_kfavar import detect_regime, detect_regime_from_yfinance, detect_regime_from_fred
# From a DataFrame
r = detect_regime(df, window=60, k_regimes=3)
# From yfinance defaults ['SPY', 'TLT', 'GLD', 'QQQ']
r = detect_regime_from_yfinance(symbols=None, window=60)
# From FRED (INDPRO + UNRATE)
r = detect_regime_from_fred(window=12)Core formulas
if len(df) < window:
return {regime: expansion, confidence: 0.5, method: 'insufficient_data'}
# Step 1: PCA factor extraction
factor_1, var_explained = PCA(n_components=1).fit_transform(standardize(df.pct_change()))
# Step 2: Markov Switching AR(1)
res = MarkovAutoregression(factor_1, k_regimes=3, order=1,
switching_ar=False, switching_variance=True).fit()
if res fails: res = same but k=2 fallback
if still fails: return _rolling_fallback(...)
# Step 3: Smoothed probs → current regime
current_probs = res.smoothed_marginal_probabilities[-1]
current_state = argmax(current_probs)
# Step 4: Label states
# k=2: higher mean → expansion
# k=3: highest variance → high_volatility; of the rest, higher mean → expansion
current_regime = label_map[current_state]
confidence = clip(current_probs[current_state], 0.1, 0.99)
return {regime, confidence, factors: {factor_1, regime_probs, transition_matrix, volatility}, method='markov_switching'}Assumptions & applicability
Assumptions: consistent DataFrame frequency + ≥ window observations + single-factor PCA adequacy + statsmodels MS-AR convergence + no switching-AR requirement.
Fits: the Canvas macro regime field, institution-decision context, cross-asset narrative overlays.
Does not fit: intraday, NBER-official replication, the full FAVAR, structural-break analysis, series shorter than window.
Input / Output contract
detect_regime(df, window?=60, vol_threshold?=1.5, growth_threshold?=0.0, k_regimes?=3) → {regime, confidence, factors (content differs by method), timestamp, method}.
Wrappers:
detect_regime_from_yfinance(symbols?=['SPY','TLT','GLD','QQQ'], window?=60)— 6mo 1d hardcoded fetchdetect_regime_from_fred(window?=12)— INDPRO + UNRATE only
Known limitations
- Single-EM-fit init sensitivity: different inits may reorder regimes
- k=3 → 2 auto-fallback: label_map dimensions change; transition_matrix size differs
- Single-factor PCA: a simplification of Bernanke FAVAR, not the full multi-factor VAR
- switching_variance=True / switching_ar=False / order=1 hardcoded: not configurable
- from_yfinance 6mo 1d hardcoded: period cannot be overridden
- from_fred INDPRO+UNRATE only (no FEDFUNDS, unlike macro_cli_v1)
- factors differ entirely between method='markov_switching' and 'rolling_fallback': clients must branch
- statsmodels DataFrame→ndarray shim inherited from the markov_regime fix (2026-04-20)
- insufficient_data defaults to EXPANSION: callers must read the method field to tell the difference
References
Hamilton (1989) Markov Switching AR + Bernanke-Boivin-Eliasz (2005) FAVAR + Primiceri (2005) TVP-VAR + Kim-Nelson (1999) State-Space Models with Regime Switching. Full citations in the frontmatter.
Golden Test
tests/golden/fixtures/tier3/tvp_kfavar_regime/ — captured_snapshot with monkey-patched upstreams, 1e-6 tolerance, 13 tests:
Direct detect_regime (4 scenarios)
trending_up— 6-ticker drift → Markov convergesregime_shift— 80/20 low→high vol → labels split across 3 statestoo_short— 3 obs → method='insufficient_data'borderline_data— 1 ticker, 70 obs → primary or fallback depending on convergence
Wrappers (4 scenarios)
yfinance_wrapper_ok— monkey-patched MultiIndex download → full pipelineyfinance_wrapper_empty— empty → expansion + confidence 0.5fred_wrapper_unavailable— fred_available=False → note 'FRED_API_KEY not set'fred_wrapper_fetch_none— fetch None → note 'FRED fetch failed'
Contract tests
regime_enum_contract— the 3 enum values onlymethod_enum_contract— the 3 enum values onlyconfidence_clipping_primary— [0.1, 0.99]determinism_primary_path— 5 reruns identicalregime_probs_sum_close_to_one— valid distribution
Changelog
- 2.0.0 (2026-04-21) — First Active, pins the v2 PCA + Markov Switching pipeline (Tier 3 batch 4)
- 1.0.0 (2026-03-xx) — Historical v1 rolling stats (retained as the fallback path)

