← Back to list

Regime Detection Using Hidden Markov Models: Process and Implementation

Financial markets do not behave uniformly over time. Anyone who has studied a long-term price chart of crude oil, equities, or any other…

Steve Obasi · 2026-05-04 16:17 · 57 claps · 7.4 min read
#machine-learning #crude-oil-price #finance #quantitative-finance #hidden-markov-models
Open on Medium ↗
Wiki topics: ML · Machine Learning ECO · Economy · General EDU · Education & Learning

Regime Detection Using Hidden Markov Models: Process and Implementation

Financial markets do not behave uniformly over time. Anyone who has studied a long-term price chart of crude oil, equities, or any other commodity will immediately notice that markets seem to cycle through distinct phases — prolonged periods of rising prices, sustained downturns, and stretches of relative calm where prices move sideways without clear direction. These phases are what quantitative analysts call market regimes.

A regime, in the financial market sense, is a persistent state of the market characterised by a particular statistical behaviour. A bull regime is one in which prices trend upward with positive returns on average. A bear regime is one in which prices fall, generating negative returns. A stagnant regime is one in which prices oscillate without strong directional momentum, producing near-zero average returns. The challenge is that these regimes are never directly announced, you cannot look up in a table which regime the market was in on a given day. You can only observe the prices themselves and must infer the underlying regime from that observable evidence.

This is precisely the structure that Hidden Markov Models were designed to handle. The hidden states correspond to the unobservable market regimes, and the observable emissions correspond to the price changes we can actually measure. By fitting an HMM to historical price data, we can estimate not only the statistical character of each regime, but also the sequence of regimes most likely to have generated the observed price history. This recovered sequence then serves as a discretisation of the continuous price series, converting it from a stream of real-valued numbers into a categorical sequence of regime labels that can be fed into downstream models such as Bayesian Belief Networks.

Why Regime Detection is Necessary

The core modelling challenge of Bayesian Belief Networks requires their input variables to be discrete. They cannot directly consume a raw time-series of prices like $45.23, $47.80, $43.10. They need categorical inputs such as “bull”, “bear”, or “stagnant.”

Naive discretisation approaches, such as simply binning prices into low/medium/high based on fixed thresholds, are problematic because the thresholds are arbitrary and fail to adapt to the statistical structure of the data. A price of $50 might represent a bull regime in one decade and a stagnant or even bear regime in another, depending on the broader economic context.

HMM-based regime detection is superior because it is data-driven. It learns the statistical character of each regime directly from the data, without requiring any human to specify where the boundaries lie. The model discovers, for instance, that one hidden state tends to be associated with positive price changes and another with negative changes, and it assigns meaningful probabilistic labels accordingly. This makes the discretisation principled, adaptive, and consistent with the actual structure of the market.

Transforming the Price Series into an Emission Sequence

The first concrete step in the regime detection pipeline is to transform the raw price time-series into a sequence of binary emissions that the HMM can process. This is done as follows. For each consecutive pair of monthly observations in the price series, the model records whether the price increased or decreased. An increase is coded as 1, and a decrease is coded as 0. The result is a binary sequence of the same length as the original series.

We can implement this in Python below using the WTI crude price data from FRED:

price = train_data['WTISPLC']
price_diff = price.diff()[1:]
e_seq = np.array(price_diff.apply(lambda x: 1 if x > 0 else 0).values)

The price.diff() call computes the month-over-month change in the West Texas Intermediate spot price. The [1:] slice discards the first entry, which is always NaN because there is no previous month to compare against. The apply call then maps each numerical change to a binary label: 1 if the price rose, 0 if it fell or remained flat. The output e_seq is the emission sequence that will be fed into the HMM.

This transformation is conceptually important. The HMM does not see the actual price level at all, it only sees the direction of movement. This is intentional. Price levels are non-stationary (they trend over decades), while directional changes are more stationary and more informative about the underlying market dynamics. A bull regime is characterised by a high probability of emitting 1 (prices rising), a bear regime by a high probability of emitting 0 (prices falling), and a stagnant regime by probabilities closer to 0.5 (prices moving in either direction roughly equally).

Constructing and Training the HMM with Baum-Welch

With the emission sequence prepared, the next step is to define and train the Hidden Markov Model. The implementation uses a three-state HMM with two possible emissions, reflecting the assumption that markets occupy one of three regimes (bull, bear, stagnant) and emit one of two signals (increase or decrease). The model is initialised with random parameters and then trained using the Baum-Welch algorithm.

dhmm_r = hmms.DtHMM.random(3, 2)

The call hmms.DtHMM.random(3, 2) creates a Discrete-time HMM with 3 hidden states and 2 possible emission symbols, initialised with randomly generated transition matrix A, emission matrix B, and initial probability vector π. Because the parameters are random, the model at this point has no meaningful interpretation. Training will shape it into something statistically meaningful.

Because the hmms library imposes a maximum sequence length of 32 on any single training array, the emission sequence must be split before being passed to the training routine:

e_seq = np.array_split(e_seq, 32)

This divides the full emission sequence into 32 roughly equal sub-arrays. The Baum-Welch algorithm will treat each sub-array as an independent observation sequence and pool the evidence across all of them when updating the model parameters.

Training is then executed as:

dhmm_r.baum_welch(e_seq, 100)

The baum_welch method runs the Expectation-Maximisation algorithm for 100 iterations. In each iteration, the E-step computes the expected number of times each transition was taken and each emission was produced, using the forward and backward probabilities. The M-step then re-estimates A, B, and π to maximise the likelihood of the observed data under these expected counts. After convergence, the parameters reflect the statistical regularities present in the historical price data.

After training, the learned parameters can be inspected:

hmms.print_parameters(dhmm_r)

It is critical that the Baum-Welch training is performed only on the training dataset. The validation and testing datasets must never be used at this stage, because doing so would constitute data leakage and invalidate the model evaluation. This is explicitly enforced in the code by restricting the input to train_data.

Decoding Regimes with the Viterbi Algorithm

Once the HMM parameters have been learned, the Viterbi algorithm is applied to recover the most probable sequence of hidden states, that is, the sequence of market regimes most likely to have generated the observed sequence of price changes.

(log_prob, s_seq) = dhmm_r.viterbi(np.concatenate(e_seq).ravel())

The np.concatenate(e_seq).ravel() call reassembles the split sub-arrays back into a single flat array, restoring the original emission sequence. The viterbi method then processes this sequence left to right, filling out the dynamic programming trellis described in the theoretical treatment of the algorithm. At each time step t and for each state j, it computes the probability of the most probable path ending in state j having generated observations up to time t, and stores a backpointer to the previous state on that optimal path. Once all time steps are processed, it traces back through the backpointers to recover the full sequence s_seq, which is a sequence of integers (0, 1, or 2) representing the regime at each time step.

The log_prob return value gives the log-probability of the best path. This can be used to compare models or to assess fit.

Identifying the Meaning of Each State

A subtle but important step follows the Viterbi decoding. The HMM assigns integer labels (0, 1, 2) to the three states, but these labels are arbitrary — state 0 might be the bull regime in one run and the bear regime in another, depending on the random initialisation. The semantic meaning of each state must be recovered after the fact by examining what each state is associated with.

The approach taken in the code is elegant and practical. For each state, it computes the average price change (not the binary emission, but the actual numerical difference) in time periods assigned to that state:

means = price_plot.groupby(['Regime'])['diff'].mean()
lst_1 = means.index.tolist()
lst_2 = means.sort_values().index.tolist()
map_regimes = dict(zip(lst_2, lst_1))
price_plot['Regime'] = price_plot['Regime'].map(map_regimes)

The groupby and mean calls compute the average price change associated with each HMM state label. The sort_values() then ranks the states from lowest average change (bear) to highest (bull). A mapping dictionary is constructed that relabels the states so that the state with the most negative average change is consistently called the bear state, the one with the most positive average change is called the bull state, and the middle one is called stagnant. This relabelling is applied to the regime sequence, giving each time period a semantically meaningful label.

Extending to All Variables and Visualisation

The regime detection process described above is applied not just to the crude oil price series, but to every macroeconomic variable in the dataset. For each variable — OPEC production, non-OPEC production, OECD consumption, industrial production indices, and so on — an independent HMM is trained, regimes are decoded, and the resulting discrete labels are stored.

for series_id in datasets:
    if series_id == 'forecast':
        break
    else:
        dhmm = hmms.DtHMM.random(3, 2)
        data_diff = train_data[series_id].diff()[1:]
        emit_seq = np.array_split(data_diff.apply(
                       lambda x: 1 if x > 0 else 0).values, 32)
        dhmm.baum_welch(emit_seq, 100)
        path = "./hmms/" + series_id.replace(".", "_")
        dhmm.save_params(path)

The dhmm.save_params(path) call saves the trained model parameters to disk. This is important for two reasons. First, it ensures reproducibility, where the same parameters can be reloaded later without retraining. Second, and more critically, it allows the trained models to be applied to the validation and test datasets without any re-fitting, thereby preserving the integrity of the evaluation. When discretising the validation or test data, the saved models are loaded, and the Viterbi algorithm is run on the new data using the parameters learned from the training set only. This is the correct protocol and the code explicitly enforces it.

The discretised dataset is assembled into a single pandas DataFrame one column per variable, one row per time period, with integer entries (0, 1, or 2) representing regimes and saved to CSV for use in the Belief Network training stage

Recap

The regime detection process can be summarised as a clean four-stage pipeline. In the first stage, raw continuous time-series data is transformed into binary emission sequences by first-differencing and sign-coding. In the second stage, a three-state, two-emission HMM is trained on each variable’s emission sequence using the Baum-Welch algorithm, which iteratively estimates transition and emission probabilities from unlabelled data. In the third stage, the Viterbi algorithm decodes the most probable regime sequence for each variable, recovering the hidden market states that most plausibly generated the observed price movements. In the fourth stage, the integer regime labels are semantically interpreted by comparing each state’s average associated price change, and the relabelled discrete data is assembled into a structured dataset ready for Belief Network training.

Reference

Koller, Daphne, and Nir Friedman. Probabilistic Graphical Models: Principles and Techniques. MIT Press, 2009.

Oelschläger, Lennart, and Timo Adam. “Detecting Bearish and Bullish Markets in Financial Time Series Using Hierarchical Hidden Markov Models.” Statistical Modelling, vol. 23, no. 2, 2023, pp. 107–126.

Rabiner, Lawrence R. “A Tutorial on Hidden Markov Models and Selected Applications in Speech Recognition.” Proceedings of the IEEE, vol. 77, no. 2, 1989, pp. 257–286.


메타데이터
post_id
c88844b8a79e
slug
regime-detection-using-hidden-markov-models-process-and-implementation-c88844b8a79e
url
https://medium.com/@mapongo/regime-detection-using-hidden-markov-models-process-and-implementation-c88844b8a79e
canonical_url
https://medium.com/@mapongo/regime-detection-using-hidden-markov-models-process-and-implementation-c88844b8a79e
author_url
https://medium.com/@mapongo
status
ok
fetched_at
2026-06-27 23:56:40