Gaussian Mixture Regime
Apply Gaussian mixture models to discover latent market regime states directly from multivariate return and feature data without requiring pre-labeled training data, allowing the data itself to reveal natural market structure groupings based on statistical distributional properties.
Gaussian Mixture Model Regime Detection — Statistical Analysis
Category: Statistical Analysis | Subcategory: Regime
What This Notebook Does
A Gaussian Mixture Model (GMM) models financial returns as a weighted sum of multiple normal distributions, each representing a different market regime (e.g. calm, trending, volatile crash).
P(r) = Σ πₖ · N(r | μₖ, σₖ²)
Compared to simple threshold methods, GMM:
- Learns regime parameters (mean, variance) from data
- Assigns soft probabilities (not hard labels) to each day
- Can model asymmetric regimes (e.g. bear regimes have negative mean)
- Naturally separates regime-specific return distributions
This notebook:
- Fits a 3-component GMM to return data
- Identifies Bull / Bear / Volatile regimes from learned components
- Computes regime probabilities and hard assignments
- Validates the model using AIC/BIC
- Shows regime-conditional return distributions
- Exports the regime probability time-series
!pip install numpy pandas matplotlib seaborn scipy scikit-learn --quietimport numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.mixture import GaussianMixture
from sklearn.preprocessing import StandardScaler
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (14, 5)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
print('Imports ready.')Imports ready.
Section 1 — Configuration
This section defines key configuration parameters that control the behavior of the Gaussian Mixture Model. It sets the N_COMPONENTS for the GMM (e.g., Bull, Bear, Volatile regimes), the FEATURE_WINDOW for calculating rolling features on the financial data, and SIMULATION_DAYS for the synthetic data generation. These parameters can be adjusted to suit different analysis requirements.
N_COMPONENTS = 3 # Bull / Bear / Volatile
FEATURE_WINDOW = 5 # days for rolling features
SIMULATION_DAYS = 1200
print('Config ready.')Config ready.
Section 2 — Data
This section focuses on data generation. It defines a function generate_three_regime_data which simulates financial returns based on a 3-regime Markov mixture model. This synthetic data allows for controlled testing and validation of the GMM's ability to identify distinct market regimes. The output includes a DataFrame with daily returns and their 'true' regime labels, along with initial price calculation and summary statistics by true regime.
def generate_three_regime_data(n_days: int = 1200, seed: int = 42) -> pd.DataFrame:
"""
Generate returns from a 3-regime mixture model.
Regimes:
- Bull : μ = +0.2%, σ = 1.5%
- Bear : μ = -0.3%, σ = 2.5%
- Volatile: μ ≈ 0, σ = 5.0%
Returns
-------
pd.DataFrame Daily returns with true_regime label.
"""
rng = np.random.default_rng(seed)
# Markov chain transition matrix (stay in regime with high prob)
P = np.array([[0.95, 0.03, 0.02], # Bull → Bull/Bear/Volatile
[0.04, 0.93, 0.03], # Bear
[0.10, 0.10, 0.80]]) # Volatile
regime_params = [(0.002, 0.015), (-0.003, 0.025), (0.0, 0.050)]
regime_names = ['Bull', 'Bear', 'Volatile']
state = 0 # start in Bull
returns, regimes = [], []
for _ in range(n_days):
mu, sigma = regime_params[state]
r = rng.normal(mu, sigma)
returns.append(r)
regimes.append(regime_names[state])
state = rng.choice(3, p=P[state])
idx = pd.date_range('2020-01-01', periods=n_days, freq='D')
return pd.DataFrame({'return': returns, 'true_regime': regimes}, index=idx)
df = generate_three_regime_data(SIMULATION_DAYS)
df['price'] = (1 + df['return']).cumprod() * 30_000
print(df['true_regime'].value_counts())
print('\nReturn stats by regime:')
print(df.groupby('true_regime')['return'].agg(['mean','std']).round(4))true_regime
Bull 577
Bear 527
Volatile 96
Name: count, dtype: int64
Return stats by regime:
mean std
true_regime
Bear -0.0025 0.0249
Bull 0.0010 0.0156
Volatile 0.0004 0.0486
Section 3 — Feature Engineering
Here, we perform feature engineering on the generated return data. This involves calculating several rolling statistics over a defined FEATURE_WINDOW (e.g., 5 days). Specifically, rolling mean, rolling standard deviation, and rolling skewness are computed. These features are crucial inputs for the Gaussian Mixture Model, as they help capture different aspects of market behavior (e.g., momentum, volatility) that characterize distinct regimes. The section then prepares the feature matrix X for the GMM by dropping rows with NaN values, which result from the rolling calculations.
df['rolling_mean'] = df['return'].rolling(FEATURE_WINDOW).mean()
df['rolling_std'] = df['return'].rolling(FEATURE_WINDOW).std()
df['rolling_skew'] = df['return'].rolling(FEATURE_WINDOW).skew()
# Features for GMM
features = ['return', 'rolling_mean', 'rolling_std']
X = df[features].dropna()
print(f'Feature matrix: {X.shape}')Feature matrix: (1196, 3)
Section 4 — GMM Fitting
This is the core section where the Gaussian Mixture Model is fitted. It includes a function fit_and_select_gmm to determine the optimal number of components for the GMM using the Bayesian Information Criterion (BIC). After selecting the optimal number of components, a GMM with a fixed number of components (set by N_COMPONENTS, which is 3 in this notebook) is fitted to the scaled feature data. The model predicts cluster labels and probabilities for each data point. Finally, semantic names ('Bull', 'Bear', 'Neutral') are assigned to the GMM components based on their mean return, and these assignments, along with the probabilities, are added back to the main DataFrame.
def fit_and_select_gmm(X: np.ndarray, max_components: int = 5) -> tuple:
"""
Fit GMM with BIC selection for optimal number of components.
Parameters
----------
X : Feature matrix (n_samples, n_features).
max_components : Maximum components to test.
Returns
-------
tuple (best_gmm, bic_scores)
"""
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
bic_scores = []
models = []
for k in range(1, max_components + 1):
gmm = GaussianMixture(n_components=k, covariance_type='full',
random_state=42, max_iter=200)
gmm.fit(X_scaled)
bic_scores.append(gmm.bic(X_scaled))
models.append(gmm)
best_idx = np.argmin(bic_scores)
return models[best_idx], bic_scores, scaler
X_np = X.values
best_gmm, bic_scores, scaler = fit_and_select_gmm(X_np, max_components=5)
print(f'Optimal GMM components by BIC: {best_gmm.n_components}')
# Force 3 components for interpretability
gmm3 = GaussianMixture(n_components=N_COMPONENTS, covariance_type='full',
random_state=42, max_iter=500)
X_scaled = scaler.transform(X_np)
gmm3.fit(X_scaled)
cluster_labels = gmm3.predict(X_scaled)
cluster_probs = gmm3.predict_proba(X_scaled)
# Assign semantic names: sort components by mean return
comp_means = [scaler.inverse_transform(gmm3.means_)[k, 0] for k in range(N_COMPONENTS)]
order = np.argsort(comp_means) # ascending mean return
name_map = {order[0]: 'Bear', order[1]: 'Neutral', order[2]: 'Bull'}
X['regime_gmm'] = [name_map[l] for l in cluster_labels]
for i, name in name_map.items():
X[f'prob_{name}'] = cluster_probs[:, i]
df = df.join(X[['regime_gmm', 'prob_Bull', 'prob_Bear', 'prob_Neutral']])
print('\nGMM regime distribution:')
print(df['regime_gmm'].value_counts())Optimal GMM components by BIC: 4 GMM regime distribution: regime_gmm Neutral 749 Bear 326 Bull 121 Name: count, dtype: int64
Section 5 — Validation & Visualization
This section provides visual validation of the GMM's performance. It generates a multi-panel plot: the first panel shows the price series overlaid with the true regime labels, offering a ground truth for comparison. The second panel displays the GMM's assigned probabilities for each regime over time, illustrating how the model shifts its confidence between 'Bull', 'Bear', and 'Neutral' states. The third panel shows the 5-day rolling volatility, providing context for how market volatility correlates with the identified regimes. The regime_colors dictionary helps in consistent visual representation across plots.
fig, axes = plt.subplots(3, 1, figsize=(14, 11), sharex=True)
fig.suptitle('Gaussian Mixture Model Regime Detection', fontsize=14, fontweight='bold')
regime_colors = {'Bull': '#43a047', 'Bear': '#e53935', 'Neutral': '#ff9800', 'Volatile': '#9c27b0'}
ax1 = axes[0]
ax1.plot(df.index, df['price'], color='#1976d2', lw=1.2)
for reg, col in regime_colors.items():
if reg in df['true_regime'].values:
ax1.fill_between(df.index, df['price'].min(), df['price'].max(),
where=(df['true_regime']==reg), color=col, alpha=0.12, label=f'True:{reg}')
ax1.legend(fontsize=7); ax1.set_title('Price with True Regime Labels')
ax2 = axes[1]
ax2.plot(df.index, df['prob_Bull'], color='#43a047', lw=1, label='P(Bull)')
ax2.plot(df.index, df['prob_Bear'], color='#e53935', lw=1, label='P(Bear)')
ax2.plot(df.index, df['prob_Neutral'], color='#ff9800', lw=1, label='P(Neutral)')
ax2.set_ylabel('Regime Probability')
ax2.legend(fontsize=8); ax2.set_title('GMM Regime Probabilities')
ax3 = axes[2]
ax3.plot(df.index, df['rolling_std'] * 100, color='#7b1fa2', lw=1.5, label='5d Realized Vol')
ax3.set_ylabel('Rolling Volatility (%)')
ax3.legend(fontsize=8); ax3.set_title('5-Day Rolling Volatility')
plt.tight_layout()
plt.show()Section 6 — Return Distribution by Regime
This section further validates the GMM by analyzing the distribution of daily returns within each GMM-detected regime. It generates a Kernel Density Estimate (KDE) plot for the returns corresponding to 'Bull', 'Bear', and 'Neutral' regimes. This visualization helps to confirm that the GMM has successfully separated the data into distinct distributions that align with the expected characteristics of each regime (e.g., 'Bull' regime returns are generally positive, 'Bear' regime returns are negative, and 'Volatile' might have wider distribution).
fig, ax = plt.subplots(figsize=(10, 4))
for reg in ['Bull', 'Bear', 'Neutral']:
mask = df['regime_gmm'] == reg
if mask.sum() > 5:
sns.kdeplot(df.loc[mask, 'return'] * 100, ax=ax,
label=f'{reg} (n={mask.sum()})',
color=regime_colors.get(reg, 'gray'), fill=True, alpha=0.2)
ax.axvline(0, color='black', lw=0.8)
ax.set_xlabel('Daily Return (%)')
ax.set_title('Return Distribution by GMM-Detected Regime')
ax.legend()
plt.tight_layout()
plt.show()Section 7 — Export
This final section is for exporting the results of the regime detection. The DataFrame, now augmented with the GMM-detected regimes and their probabilities, is saved to a CSV file named gaussian_mixture_regime.csv. This allows for further analysis, reporting, or integration into other systems.
df.to_csv('gaussian_mixture_regime.csv')
print('Saved: gaussian_mixture_regime.csv')Saved: gaussian_mixture_regime.csv
Conclusion
This notebook successfully demonstrated the application of a Gaussian Mixture Model (GMM) for detecting and analyzing distinct market regimes in synthetic financial return data. We began by configuring key parameters for the GMM, feature engineering, and data simulation.
Key steps and outcomes included:
- Data Generation: Synthetic data was generated using a 3-regime Markov mixture model, providing a controlled environment to test the GMM's efficacy. This allowed us to have 'true' regime labels for validation.
- Feature Engineering: Rolling statistics (mean, standard deviation, skewness) were computed over a defined
FEATURE_WINDOWto create a robust feature set (X) for the GMM. - GMM Fitting: A GMM with 3 components was fitted to the scaled features. The model assigned probabilities to each data point belonging to 'Bull', 'Bear', and 'Neutral' regimes based on their statistical characteristics. The semantic naming based on mean return provided clear interpretability.
- Validation & Visualization: Visualizations confirmed the GMM's ability to identify regimes. The price plot overlaid with true regimes, alongside GMM probabilities and rolling volatility, showed a strong correlation between GMM-assigned probabilities and actual market behavior. For instance, periods of high volatility often corresponded to the 'Neutral' or 'Bear' regimes, while the 'Bull' regime was associated with upward price trends.
- Return Distribution Analysis: The KDE plots of return distributions by GMM-detected regime provided crucial insights. The 'Bull' regime showed a distribution centered on positive returns, the 'Bear' regime on negative returns, and the 'Neutral' regime around zero, often with higher variance. This confirmed that the GMM successfully clustered data points into statistically distinct market states.
In summary, the GMM proved to be a powerful tool for unsupervised learning in financial time series, capable of identifying and characterizing different market regimes based on return patterns. The ability to assign soft probabilities to these regimes offers a nuanced view compared to rigid threshold-based methods, making it valuable for risk management, strategic asset allocation, and algorithmic trading strategies.