Skip to content

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:

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 fetch
  • detect_regime_from_fred(window?=12) — INDPRO + UNRATE only

Known limitations

  1. Single-EM-fit init sensitivity: different inits may reorder regimes
  2. k=3 → 2 auto-fallback: label_map dimensions change; transition_matrix size differs
  3. Single-factor PCA: a simplification of Bernanke FAVAR, not the full multi-factor VAR
  4. switching_variance=True / switching_ar=False / order=1 hardcoded: not configurable
  5. from_yfinance 6mo 1d hardcoded: period cannot be overridden
  6. from_fred INDPRO+UNRATE only (no FEDFUNDS, unlike macro_cli_v1)
  7. factors differ entirely between method='markov_switching' and 'rolling_fallback': clients must branch
  8. statsmodels DataFrame→ndarray shim inherited from the markov_regime fix (2026-04-20)
  9. 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 converges
  • regime_shift — 80/20 low→high vol → labels split across 3 states
  • too_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 pipeline
  • yfinance_wrapper_empty — empty → expansion + confidence 0.5
  • fred_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 only
  • method_enum_contract — the 3 enum values only
  • confidence_clipping_primary — [0.1, 0.99]
  • determinism_primary_path — 5 reruns identical
  • regime_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)

Verifiable intelligence for the decisions that demand scrutiny.