Skip to main content

🏛️ RegimeLab

Institutional-grade market regime detection, systemic risk telemetry, and walk-forward asset allocation engine in Python.

PyPI version Python 3.12+ License: MIT Type Checked with mypy Code style: ruff


⚡ Why RegimeLab?

Most open-source regime detection scripts fit a Hidden Markov Model (HMM) on the entire in-sample dataset and claim predictive power. In production quantitative finance, this fails due to three fatal flaws:

  1. Look-Ahead Bias: Training filters without strict point-in-time (asof) truncation leaks future distribution moments into past states.
  2. Label Switching: HMM/GMM state indices are mathematically interchangeable between refits. Without canonical sorting, "State 0" randomly alternates between Bull and Bear across rolling windows.
  3. Calendar Desynchronization: Multi-asset cross-sections suffer from holiday mismatches, halted assets, and survivorship bias.

RegimeLab solves these operational hurdles, providing a turnkey, causal quantitative engine for systematic asset allocation and macro risk monitoring.


📊 Feature Matrix: Raw Tooling vs. RegimeLab

Challenge Raw hmmlearn / statsmodels RegimeLab Framework
State Labeling Unordered integer states (permutes on refit) Deterministic Canonical Sorting — stress score over emission means by default for HMM/Hamilton (configurable via ranking_method / stress_signs); Sharpe and volatility ordering still available
Temporal Clock In-sample full-sample fitting (Look-ahead) Strict Point-in-Time (asof) cursor & expanding-window walk-forward
Systemic Risk None Kritzman Absorption Ratio (PCA), VIX Term Spread & Sector Breadth
Execution Reality Pure theoretical classification Walk-Forward Backtester with transaction costs (bps) and confidence floors
Data Ingestion Expects clean 2D NumPy array Multi-Asset PIT Alignment, staleness budgets, synthetic & Parquet providers
Reporting Matplotlib static plot Interactive Plotly HTML reports + Textual TUI Terminal Dashboard

🆕 What's new in 0.4.0

  • simulate_next_open / NextOpenResult: an opt-in position ledger with opening fills, actual unit drift, self-financing costs, observation masks and publication ordering.
  • The legacy overlay keeps valid-input outputs but now rejects unavailable prices needed for a held return or benchmark.
  • Warning migration: replace consumers of non_session_asof with asof_not_in_panel. This means absent panel coverage, not a confirmed market closure. Check an exchange calendar for publication eligibility; is_stale=False alone does not establish freshness.
  • Selected-restart HMM convergence metadata and opt-in initial-equity drawdowns are corrected without changing legacy metric defaults.

See the execution contract and CHANGELOG for conventions and compatibility.

🆕 What's new in 0.3.0

  • Typed regime statistics: LabelAssignment.to_statistics() returns RegimeStatistics per regime; TelemetryPayload.state_statistics and each WalkForwardFold carry them (statistics_for(regime) looks one up).
  • simulate_weights: the execution engine behind run_backtest is now public — backtest any hand-built weight schedule under the same signal-lag, rebalancing and cost assumptions.
  • Leading low-confidence sessions sit in cash: sessions before the first confident decision now earn the risk-free rate instead of a hard zero.
  • Single data path: the pipeline fetches through load_aligned_panel — cache in data.cache_dir within data.cache_ttl_hours, anchored on data.calendar_anchor; force_refresh=True bypasses the cache.
  • Breaking for type consumers: BacktestResult.regimes and regime_metrics are now optional (None when no regime path was supplied).

See the CHANGELOG for the full list.

🆕 What's new in 0.2.0

  • Causal walk-forward: each fold labels only its own interval (r_k, r_{k+1}] with a model trained through r_k — no look-ahead, no overlapping labels.
  • Stress labelling: RankingMethod.STRESS (default for HMM/Hamilton) orders states by signed emission means; the positive-drift guardrail no longer applies.
  • Official volatility sources: ^VIX/^VIX3M read from CBOE, then FRED, with the configured provider as fallback (volatility_source).
  • Data-quality telemetry: TelemetryPayload reports session_date, observation_date, is_stale and explicit warnings for stale or missing inputs.
  • Signal vs. investable universes: DataSettings.correlation_assets decouples the correlation features from risk_assets, which now only drives the backtest.
  • Live signal = backtest signal: run_single_asof defaults to refit_policy="walkforward", reproducing the walk-forward posterior for the requested date.

See the CHANGELOG for the full list.

🚀 Quickstart

1. Installation

# Install the latest version available on PyPI
pip install regimelab

# Or install with interactive TUI support
pip install "regimelab[tui]"

Tagged wheels and source archives are also available on GitHub Releases. GitHub and PyPI are published separately; if a version is not on PyPI yet, download its wheel and install it with pip install path/to/wheel.whl.

2. Python API Usage

Current Market Regime Detection (3 lines)

from regimelab import Settings
from regimelab.pipeline import run_single_asof

# Run point-in-time regime inference for any historical or current date.
# refit_policy="walkforward" (the default) reproduces the exact posterior the
# walk-forward backtest produced for that date — the live signal is the
# track record. Pass refit_policy="daily" to refit through the as-of date.
settings = Settings(data={"provider": "yfinance"})
payload = run_single_asof(settings, asof="2024-12-31")

print(f"Detected Regime: {payload.regime.value}")
print(f"Confidence: {payload.probabilities.confidence:.2%}")
print(f"Target Allocation: {payload.target_weights.weights if payload.target_weights else {}}")

# Data quality is explicit: is_stale is True when the posterior was computed
# on an earlier session, and warnings explain why.
print(f"Computed on: {payload.observation_date} (last session {payload.session_date})")
print(f"Stale: {payload.is_stale}")
for warning in payload.warnings:
    print(f"  warning: {warning}")

Extract Causal Systemic Risk Features

from regimelab import Settings
from regimelab.data.fetcher import load_aligned_panel
from regimelab.features import build_feature_matrix

settings = Settings(data={"provider": "yfinance"})
panel = load_aligned_panel(settings)
features = build_feature_matrix(panel, settings)

# Inspect causal feature matrix
print(features[["absorption_ratio", "absorption_delta", "vix_term_spread", "breadth"]].tail())

Walk-Forward Backtesting Engine

from regimelab import Settings
from regimelab.pipeline import run_pipeline

settings = Settings(
    data={"provider": "synthetic"}, # Fully offline, reproducible dataset
    model={"classifier": "hmm", "n_states": 4},
    backtest={"transaction_cost_bps": 5.0, "confidence_floor": 0.5},
)

result = run_pipeline(settings, command="backtest_run")
metrics = result.backtest.metrics

print(f"Strategy CAGR: {metrics.cagr:.2%}")
print(f"Sharpe Ratio:  {metrics.sharpe:.2f}")
print(f"Max Drawdown:  {metrics.max_drawdown:.2%}")

Backtest your own weights

Any target-weight schedule — not only the regime map — can be simulated under the same execution assumptions (signal lag, event-driven rebalancing, costs):

from regimelab.backtest import simulate_weights
from regimelab.data.fetcher import load_aligned_panel

prices = load_aligned_panel(settings).close()
# A decision on day t is held from t + signal_lag_days until the next one;
# CASH is the residual (here 0 — SPY/TLT are fully invested).
targets = pd.DataFrame({"SPY": 0.6, "TLT": 0.4}, index=prices.index[::21])
result = simulate_weights(prices, targets, settings)
print(f"60/40 Sharpe: {result.metrics.sharpe:.2f}")

This is the legacy close-only overlay: it retains its integer-lag, constant-weight and cash-inclusive-turnover conventions. It is not an opening fill simulator. Its numerical outputs for valid inputs remain unchanged for compatibility.

Next-open position ledger (0.4.0)

simulate_next_open retains actual adjusted units between instructions, pays costs on risky buys/sells (not cash transfers), and requires a caller-supplied exchange schedule. It is included in 0.4.0, not in 0.3.0.

This fully offline example buys at 103 after the previous close of 100. It earns only the subsequent move from 103 to 104, not the overnight gap:

import pandas as pd
from regimelab import Settings
from regimelab.backtest import simulate_next_open

dates = pd.date_range("2024-01-02", periods=3)
closes = pd.DataFrame({"SPY": [100.0, 104.0, 104.0]}, index=dates)
opens = pd.DataFrame({"SPY": [100.0, 103.0, 104.0]}, index=dates)
sessions = pd.DataFrame({
    "market_open": (dates + pd.Timedelta(hours=9, minutes=30)).tz_localize("America/New_York"),
    "market_close": (dates + pd.Timedelta(hours=16)).tz_localize("America/New_York"),
}, index=dates)
settings = Settings(
    _env_file=None,
    data={"risk_assets": ("SPY",)},
    backtest={"regime_weights": {}, "initial_capital": 1000.0,
              "transaction_cost_bps": 0.0, "risk_free_rate": 0.0},
)
targets = pd.DataFrame({"SPY": [1.0]}, index=dates[:1])
result = simulate_next_open(
    closes, targets, settings, open_prices=opens, sessions=sessions
)
print(result.net_returns.iloc[0])  # 1 / 103, approximately 0.00970874
print(result.to_frame())          # units, cash, fees, timing and opening/closing weights

Without published_at, availability at the decision close is an explicit research assumption. Supply actual timezone-aware publication timestamps for an observed record. Instructions at/after an opening cannot receive an earlier fill; an older delayed observation cannot override a newer one. Use genuinely observed, consistently adjusted OHLC and the optional open_observed / close_observed provenance masks. The library does not infer freshness from a finite carried price or guarantee subscriber auction fills. See the execution specification for cash accrual, self-financing costs, benchmark treatment and compatibility boundaries.

3. CLI & Terminal Dashboard

RegimeLab ships with a powerful Typer CLI:

# 1. Run full walk-forward pipeline and generate interactive HTML report
regimelab run --report market_report.html

# 2. Inspect point-in-time telemetry for a specific date (JSON output)
regimelab asof 2023-10-15

# 3. Launch the full interactive Textual Terminal Dashboard
regimelab tui

Additional CLI commands include regimelab report OUTPUT for direct HTML generation and regimelab asof YYYY-MM-DD --output telemetry.json for persisted JSON payloads.


🧠 Core Methodology & Architecture

                       ┌──────────────────────────────────────────────┐
                       │     Data Layer (PIT Alignment & Caching)     │
                       └──────────────────────┬───────────────────────┘
                                              │
                       ┌──────────────────────▼───────────────────────┐
                       │   Causal Features (PCA Absorption, Spread)   │
                       └──────────────────────┬───────────────────────┘
                                              │
                       ┌──────────────────────▼───────────────────────┐
                       │ Models: Hamilton / HMM / GMM + Anti-Switch   │
                       └──────────────────────┬───────────────────────┘
                                              │
               ┌──────────────────────────────┴──────────────────────────────┐
               ▼                                                             ▼
  ┌─────────────────────────┐                                   ┌─────────────────────────┐
  │   Walk-Forward Engine   │                                   │  Telemetry, HTML & TUI  │
  │  (Dynamic Allocations)  │                                   │ (Interactive Artifacts) │
  └─────────────────────────┘                                   └─────────────────────────┘

1. Canonical State Labeling (regimelab.models.labeling)

To eliminate label switching, RegimeLab fits the underlying statistical model (Gaussian HMM, Hamilton Markov Switching, or GMM) and evaluates the conditional distribution parameters of each state. HMM and Hamilton sort states by a stress score — the mean signed standardised emission mean over features with a known stress sign (stress_signs makes the sign table configurable) — while the GMM control orders by conditional volatility. Sharpe ($\frac{\mu}{\sigma}$) ordering remains available via ranking_method. States are then mapped deterministically to:

  • BULL_TREND (High return, low volatility)
  • NEUTRAL_TRANSITION (Moderate return, mean-reverting)
  • HIGH_VOL_BEAR (Negative drift, elevated variance)
  • RISK_OFF (Severe drawdown regime)

2. Kritzman Absorption Ratio (regimelab.features.absorption)

Quantifies market fragility via Principal Component Analysis (PCA) over rolling multi-asset return covariance matrices:

$$ \text{Absorption Ratio} = \frac{\sum_{i=1}^{k} \sigma^2_{PC_i}}{\sum_{j=1}^{N} \sigma^2_j} $$

A rapid spike in the absorption ratio ($\Delta \text{AR} > 1.5$) indicates tightening cross-asset coupling, signaling systemic vulnerability prior to market crashes.


⚙️ Configuration (regimelab.toml)

Customize execution parameters via regimelab.toml, environment variables (REGIMELAB_DATA__PROVIDER=yfinance), or Python kwargs:

[data]
provider = "yfinance"          # "yfinance", "synthetic", or "parquet"
start = "2005-01-01"
benchmark = "SPY"
calendar_anchor = "SPY"

[model]
classifier = "hmm"             # "hmm", "gmm", or "hamilton"
n_states = 4
covariance_type = "diag"
min_train_observations = 756

[backtest]
transaction_cost_bps = 5.0
confidence_floor = 0.5
refit_frequency_days = 63

🧪 Testing & Formal Verification

RegimeLab is built with property-based testing (hypothesis) to mathematically guarantee absence of look-ahead leakage:

# Run test suite with causality property tests
uv run pytest -q

# Run strict mypy type checking
uv run mypy src/regimelab

# Lint with ruff
uv run ruff check .

📄 License

MIT License. Developed for quantitative researchers, portfolio managers, and systematic trading engineers.

Metadata

Release files for regimelab 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for regimelab 0.4.0
File Size Uploaded Provenance
regimelab-0.4.0.tar.gz 328.7 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for regimelab 0.4.0
File Interpreter ABI Platform
regimelab-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 467.6 kB

Release files / regimelab-0.4.0.tar.gz

Download URL regimelab-0.4.0.tar.gz
Size 328.7 kB
Tags Source
SHA-256 checksum
How to use checksums
f11981b1ee584bbc2806542fcf7e5fb59b8f94e38579c777d8da9bf3fe8598f6
BLAKE2b-256 checksum
How to use checksums
1fd1b94764d8e40469f87509cccc9f5da44ad98d113eecef2413da6ddee05e2b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / regimelab-0.4.0-py3-none-any.whl

Download URL regimelab-0.4.0-py3-none-any.whl
Size 138.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f53297b71a3cb29dab682214147daa02377f09739d201dde9602c88469e0f4e0
BLAKE2b-256 checksum
How to use checksums
4935015d20b862fd7d1b3929fe7c626defb6955ce4c8e3c238d1da937904968b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for regimelab 0.4.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page