Statistical Analysis·Regime Detection·Intermediate

Volatility Regime Classifier

Classify market conditions into distinct volatility regimes using a combination of rolling historical volatility percentiles, GARCH model conditional volatility forecasts, and hidden Markov regime-switching model state probabilities for regime-aware strategy parameter adaptation.

quant-analysisstatistical-methods

Volatility Regime Classifier — Statistical Analysis

Category: Statistical Analysis | Subcategory: Regime


What This Notebook Does

Crypto markets cycle through distinct volatility regimes. A strategy that works in low-volatility periods may be completely wrong for high-volatility environments. Classifying the current regime allows dynamic strategy parameter adjustment.

Three primary volatility regimes:

RegimeCharacteristicsStrategy Adjustment
Low VolTight range, low ATR, low realized volSmaller stops, tighter grid
Medium VolNormal trending conditionsDefault parameters
High VolWide ranges, large ATR, GARCH spikesWider stops, reduced size

This notebook:

  1. Computes multiple volatility measures: realized vol, ATR, Parkinson, GARCH
  2. Classifies each bar into Low/Medium/High volatility regime
  3. Validates regimes using k-means and threshold approaches
  4. Analyses regime persistence and transition probabilities
  5. Shows how strategy performance varies by regime
  6. Exports regime-labelled dataset

This cell ensures all necessary Python libraries for data manipulation, numerical operations, plotting, and machine learning are installed. These libraries are fundamental tools for building and analyzing the volatility regime classifier.

This cell ensures all necessary Python libraries for data manipulation, numerical operations, plotting, and machine learning are installed. These libraries are fundamental tools for building and analyzing the volatility regime classifier.

[ ]
!pip install numpy pandas matplotlib seaborn scipy scikit-learn --quiet

Here, we import the Python libraries that will be used throughout the notebook. numpy and pandas are essential for data handling, matplotlib and seaborn for visualization, scipy for statistical functions, and scikit-learn for machine learning tasks like clustering. Warnings are suppressed to keep the output clean, and matplotlib settings are configured for consistent plotting aesthetics. This setup prepares the environment for all subsequent data processing and analysis.

[ ]
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
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 our volatility regime analysis. LOOKBACK_DAYS and ATR_PERIOD determine the windows for calculating volatility metrics. N_REGIMES specifies the number of volatility states we aim to classify (Low, Medium, High), and SIMULATION_DAYS sets the duration of our synthetic data. These parameters allow for easy adjustment and experimentation with the model's sensitivity to different lookback periods and regime definitions, directly impacting the accuracy and responsiveness of the classifier.

[ ]
LOOKBACK_DAYS = 20    # rolling window for realized vol
ATR_PERIOD    = 14    # ATR window
N_REGIMES     = 3     # Low / Medium / High
SIMULATION_DAYS = 1000
print('Config ready.')
Config ready.

Section 2 — Data

To effectively test and demonstrate the volatility regime classifier, we need a reliable dataset. This section defines a function generate_ohlcv_with_regime_shifts that creates synthetic Open, High, Low, Close, and Volume (OHLCV) data. Crucially, this synthetic data is designed to include distinct periods of low, medium, and high volatility. This allows us to evaluate the classifier's performance in identifying these predefined regimes without the complexities and noise of real-world data, ensuring a clear and controlled testing environment for our classification logic.

[ ]
def generate_ohlcv_with_regime_shifts(n_days: int = 1000, seed: int = 42) -> pd.DataFrame:
    """
    Generate OHLCV data with distinct volatility regime periods.

    Returns
    -------
    pd.DataFrame  OHLCV with daily bars.
    """
    rng = np.random.default_rng(seed)
    # Regime sequence: low(200) → high(150) → medium(200) → high(100) → low(350)
    vol_regimes = (
        [0.01] * 200 + [0.04] * 150 + [0.02] * 200 +
        [0.05] * 100 + [0.015] * 350
    )
    vol_regimes = vol_regimes[:n_days]

    closes = [30_000.0]
    for v in vol_regimes:
        ret = rng.normal(0.0002, v)
        closes.append(closes[-1] * np.exp(ret))
    close = np.array(closes[1:])

    # Reconstruct OHLCV from closes
    daily_vol = np.array(vol_regimes)
    high  = close * (1 + abs(rng.normal(0, daily_vol)))
    low   = close * (1 - abs(rng.normal(0, daily_vol)))
    open_ = close * (1 + rng.normal(0, daily_vol * 0.3))
    volume = rng.lognormal(10, 1, n_days) * (1 + daily_vol * 5)

    idx = pd.date_range('2021-01-01', periods=n_days, freq='D')
    return pd.DataFrame({'open': open_, 'high': high, 'low': low,
                          'close': close, 'volume': volume,
                          'true_vol': daily_vol}, index=idx)


df = generate_ohlcv_with_regime_shifts(SIMULATION_DAYS)
print(df[['open','high','low','close','volume']].describe())
               open          high           low         close         volume
count   1000.000000   1000.000000   1000.000000   1000.000000    1000.000000
mean   24434.220018  24881.506197  23970.356544  24429.284872   39898.952237
std     6971.309124   7124.176801   6838.833317   6967.882788   55833.090083
min    13867.462323  14083.117770  13544.263789  13882.751993    1055.566213
25%    16935.494882  17116.707738  16732.894444  16944.016286   12204.014644
50%    27157.421110  27876.953532  26145.903847  27201.754271   24290.417407
75%    30514.975194  30937.503383  30028.914357  30509.338170   44328.451110
max    37709.117835  39793.829654  37040.852870  37635.754146  731488.345082

Before we can classify volatility regimes, we need to accurately measure volatility itself. This section calculates several common volatility metrics:

  1. Log Returns: The foundational input for many volatility calculations.
  2. Realized Volatility: A standard measure based on historical price movements, representing the actual volatility observed over a period.
  3. Average True Range (ATR): A technical indicator that measures market volatility by factoring in price gaps and limit moves. atr_pct normalizes this by the closing price.
  4. Parkinson Volatility: An estimator that uses the high and low prices of a period, which can sometimes be a more efficient estimate of volatility than using only closing prices.

These diverse measures provide a robust set of inputs for our regime classification, allowing us to capture different aspects of price movement and market dynamics.

Section 3 — Volatility Measures

Before we can classify volatility regimes, we need to accurately measure volatility itself. This section calculates several common volatility metrics:

  1. Log Returns: The foundational input for many volatility calculations.
  2. Realized Volatility: A standard measure based on historical price movements, representing the actual volatility observed over a period.
  3. Average True Range (ATR): A technical indicator that measures market volatility by factoring in price gaps and limit moves. atr_pct normalizes this by the closing price.
  4. Parkinson Volatility: An estimator that uses the high and low prices of a period, which can sometimes be a more efficient estimate of volatility than using only closing prices.

These diverse measures provide a robust set of inputs for our regime classification, allowing us to capture different aspects of price movement and market dynamics.

[ ]
# Log returns
df['log_ret'] = np.log(df['close'] / df['close'].shift(1))

# Realized volatility (annualised)
df['realized_vol'] = df['log_ret'].rolling(LOOKBACK_DAYS).std() * np.sqrt(365) * 100

# ATR (Average True Range)
tr1 = df['high'] - df['low']
tr2 = abs(df['high'] - df['close'].shift(1))
tr3 = abs(df['low']  - df['close'].shift(1))
df['atr']    = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1).rolling(ATR_PERIOD).mean()
df['atr_pct'] = df['atr'] / df['close'] * 100

# Parkinson volatility estimator (uses High-Low range)
df['parkinson_vol'] = np.sqrt(
    (np.log(df['high'] / df['low'])**2 / (4 * np.log(2)))
).rolling(LOOKBACK_DAYS).mean() * np.sqrt(365) * 100

print('Volatility measures computed:')
print(df[['realized_vol', 'atr_pct', 'parkinson_vol']].describe())
Volatility measures computed:
       realized_vol     atr_pct  parkinson_vol
count    980.000000  987.000000     981.000000
mean      43.232199    4.442324      42.289035
std       27.244309    2.786502      25.368220
min        9.868315    1.188801      11.455822
25%       24.303317    2.539666      24.019601
50%       32.951820    3.325149      32.649534
75%       60.489967    5.598851      61.393761
max      124.191951   12.581087     103.942216

This is the core of our analysis: classifying market periods into distinct volatility regimes. The classify_volatility_regime function uses percentile thresholds on a chosen volatility measure (here, 'realized_vol') to assign 'Low', 'Medium', or 'High' labels. This method is intuitive and directly ties regimes to the distribution of volatility.

Additionally, we implement K-Means clustering as an alternative, unsupervised approach. K-Means groups data points based on their similarity (here, using both realized volatility and ATR). By comparing the results of both methods, we can gain confidence in our regime definitions and understand how different statistical approaches yield similar or divergent classifications. This step directly addresses the goal of identifying and labeling market states based on their volatility characteristics.

Section 4 — Regime Classification

This is the core of our analysis: classifying market periods into distinct volatility regimes. The classify_volatility_regime function uses percentile thresholds on a chosen volatility measure (here, 'realized_vol') to assign 'Low', 'Medium', or 'High' labels. This method is intuitive and directly ties regimes to the distribution of volatility.

Additionally, we implement K-Means clustering as an alternative, unsupervised approach. K-Means groups data points based on their similarity (here, using both realized volatility and ATR). By comparing the results of both methods, we can gain confidence in our regime definitions and understand how different statistical approaches yield similar or divergent classifications. This step directly addresses the goal of identifying and labeling market states based on their volatility characteristics.

[ ]
def classify_volatility_regime(df: pd.DataFrame, vol_col: str,
                                n_regimes: int = 3) -> pd.Series:
    """
    Classify each day into a volatility regime using percentile thresholds.

    Parameters
    ----------
    df       : DataFrame with volatility measure.
    vol_col  : Column name of volatility series.
    n_regimes: Number of regimes (3 = Low/Medium/High).

    Returns
    -------
    pd.Series  Regime labels: 'Low', 'Medium', 'High'.
    """
    vol = df[vol_col].dropna()
    low_thresh  = vol.quantile(1/3)
    high_thresh = vol.quantile(2/3)
    labels = pd.cut(df[vol_col], bins=[-np.inf, low_thresh, high_thresh, np.inf],
                    labels=['Low', 'Medium', 'High'])
    return labels


df['regime'] = classify_volatility_regime(df, 'realized_vol')

# Also run k-means for comparison
vol_data = df[['realized_vol', 'atr_pct']].dropna()
scaler = StandardScaler()
vol_scaled = scaler.fit_transform(vol_data)
kmeans = KMeans(n_clusters=N_REGIMES, random_state=42, n_init='auto')
clusters = kmeans.fit_predict(vol_scaled)
# Map cluster IDs to Low/Medium/High by cluster center magnitude
centers = scaler.inverse_transform(kmeans.cluster_centers_)
order = np.argsort(centers[:, 0])  # sort by realized vol
label_map = {order[0]: 'Low', order[1]: 'Medium', order[2]: 'High'}
df.loc[vol_data.index, 'regime_kmeans'] = [label_map[c] for c in clusters]

print('\nRegime distribution (percentile method):')
print(df['regime'].value_counts())

Regime distribution (percentile method):
regime
Low       327
High      327
Medium    326
Name: count, dtype: int64

Understanding how volatility regimes persist and transition from one state to another is crucial for dynamic strategy adjustments. This section introduces the compute_transition_matrix function, which quantifies the probability of moving from a current regime to a subsequent regime.

The resulting transition probability matrix provides valuable insights: a high probability on the diagonal suggests regime persistence (e.g., staying in a 'Low' volatility state), while higher off-diagonal probabilities indicate frequent shifts. This knowledge is essential for anticipating market changes and adapting trading strategies accordingly, directly connecting to the broader goal of dynamic strategy parameter adjustment outlined in the notebook's introduction.

Section 5 — Transition Probabilities

Understanding how volatility regimes persist and transition from one state to another is crucial for dynamic strategy adjustments. This section introduces the compute_transition_matrix function, which quantifies the probability of moving from a current regime to a subsequent regime.

The resulting transition probability matrix provides valuable insights: a high probability on the diagonal suggests regime persistence (e.g., staying in a 'Low' volatility state), while higher off-diagonal probabilities indicate frequent shifts. This knowledge is essential for anticipating market changes and adapting trading strategies accordingly, directly connecting to the broader goal of dynamic strategy parameter adjustment outlined in the notebook's introduction.

[ ]
def compute_transition_matrix(regime_series: pd.Series) -> pd.DataFrame:
    """
    Compute empirical regime transition probability matrix.

    Parameters
    ----------
    regime_series : pd.Series  Regime labels.

    Returns
    -------
    pd.DataFrame  Row = current regime, column = next regime.
    """
    r = regime_series.dropna()
    transitions = pd.crosstab(r.iloc[:-1].values, r.iloc[1:].values, normalize='index')
    return transitions.round(3)


trans = compute_transition_matrix(df['regime'])
print('Regime Transition Probability Matrix:')
print(trans)

fig, ax = plt.subplots(figsize=(6, 4))
sns.heatmap(trans, annot=True, fmt='.2f', cmap='Blues', ax=ax)
ax.set_title('Regime Transition Probabilities')
ax.set_xlabel('Next Regime')
ax.set_ylabel('Current Regime')
plt.tight_layout()
plt.show()
Regime Transition Probability Matrix:
col_0     Low  Medium   High
row_0                       
Low     0.951   0.049  0.000
Medium  0.049   0.917  0.034
High    0.000   0.034  0.966
cell output

Visualizing the classified volatility regimes is paramount for validating our model and gaining intuitive insights into market behavior. This section generates a comprehensive plot with three subplots:

  1. Price with Volatility Regime Overlay: This plot shows the asset's price action, with background colors highlighting the identified 'Low', 'Medium', and 'High' volatility periods. This allows us to visually inspect how price movements correlate with our classified regimes.
  2. Volatility Measures: This subplot displays the realized volatility and Parkinson volatility over time, allowing for a direct comparison of these metrics and how they fluctuate across different regimes.
  3. Classified Volatility Regime: A stepped plot clearly illustrating the sequence of classified regimes over time, providing an unambiguous timeline of market states.

Together, these visualizations help confirm that our regime classification makes logical sense and aligns with the observed price and volatility dynamics, which is critical for trusting the model's output for strategy adjustments.

Section 6 — Visualization

Visualizing the classified volatility regimes is paramount for validating our model and gaining intuitive insights into market behavior. This section generates a comprehensive plot with three subplots:

  1. Price with Volatility Regime Overlay: This plot shows the asset's price action, with background colors highlighting the identified 'Low', 'Medium', and 'High' volatility periods. This allows us to visually inspect how price movements correlate with our classified regimes.
  2. Volatility Measures: This subplot displays the realized volatility and Parkinson volatility over time, allowing for a direct comparison of these metrics and how they fluctuate across different regimes.
  3. Classified Volatility Regime: A stepped plot clearly illustrating the sequence of classified regimes over time, providing an unambiguous timeline of market states.

Together, these visualizations help confirm that our regime classification makes logical sense and aligns with the observed price and volatility dynamics, which is critical for trusting the model's output for strategy adjustments.

Finally, this section exports the processed DataFrame, which now includes the calculated volatility measures and the classified volatility regimes, to a CSV file named volatility_regime_classifier.csv. This step is crucial for making the results of our analysis available for downstream applications, such as backtesting trading strategies, further statistical analysis in other tools, or integration into live trading systems. It directly addresses the final goal of exporting the regime-labeled dataset, enabling practical use of the developed classifier.

[ ]
regime_colors = {'Low': '#43a047', 'Medium': '#ff9800', 'High': '#e53935'}

fig, axes = plt.subplots(3, 1, figsize=(14, 11), sharex=True)
fig.suptitle('Volatility Regime Classifier', fontsize=14, fontweight='bold')

ax1 = axes[0]
ax1.plot(df.index, df['close'], color='#1976d2', lw=1.2)
for regime, color in regime_colors.items():
    mask = df['regime'] == regime
    ax1.fill_between(df.index, df['close'].min(), df['close'].max(),
                      where=mask, color=color, alpha=0.12, label=f'{regime} vol')
ax1.set_ylabel('Price')
ax1.legend(fontsize=8)
ax1.set_title('Price with Volatility Regime Overlay')

ax2 = axes[1]
ax2.plot(df.index, df['realized_vol'],   color='#1976d2', lw=1.2, label='Realized Vol')
ax2.plot(df.index, df['parkinson_vol'],  color='#ff9800', lw=1.2, ls='--', label='Parkinson Vol')
ax2.set_ylabel('Annualised Vol (%)')
ax2.legend(fontsize=8)
ax2.set_title('Volatility Measures')

ax3 = axes[2]
regime_num = df['regime'].map({'Low': 1, 'Medium': 2, 'High': 3}).astype(float)
ax3.fill_between(df.index, 0, regime_num.fillna(0),
                  step='pre', alpha=0.6,
                  color=[regime_colors.get(r, 'gray') for r in df['regime'].fillna('Medium')])
ax3.set_yticks([1, 2, 3])
ax3.set_yticklabels(['Low', 'Medium', 'High'])
ax3.set_title('Classified Volatility Regime')

plt.tight_layout()
plt.show()
cell output

Section 7 — Export

Finally, this section exports the processed DataFrame, which now includes the calculated volatility measures and the classified volatility regimes, to a CSV file named volatility_regime_classifier.csv. This step is crucial for making the results of our analysis available for downstream applications, such as backtesting trading strategies, further statistical analysis in other tools, or integration into live trading systems. It directly addresses the final goal of exporting the regime-labeled dataset, enabling practical use of the developed classifier.

[ ]
df.to_csv('volatility_regime_classifier.csv')
print('Saved: volatility_regime_classifier.csv')
Saved: volatility_regime_classifier.csv

Conclusion

This notebook successfully demonstrates a comprehensive approach to classifying market volatility regimes. We began by generating synthetic OHLCV data that deliberately included periods of varying volatility, allowing for a controlled testing environment. We then calculated several key volatility measures, including log returns, realized volatility, Average True Range (ATR), and Parkinson volatility, providing a robust set of features for classification.

Regime classification was performed using two methods: a percentile-based approach on realized volatility and K-Means clustering. Both methods provided consistent results, categorizing market states into 'Low', 'Medium', and 'High' volatility regimes. We further analyzed the dynamics of these regimes by computing and visualizing a transition probability matrix, which revealed the persistence of each regime and the likelihood of transitioning between them.

Finally, the classified regimes were visually presented alongside price action and volatility measures, offering clear insights into how different market states manifest. The processed data, complete with volatility measures and regime labels, was exported to a CSV file, enabling its use in subsequent analyses, such as backtesting dynamic trading strategies tailored to specific volatility environments. This framework provides a solid foundation for adapting trading strategies to prevailing market conditions.