Skip to content
Trading

How I use regime detection to switch trading strategies automatically

regime detection — graphical user interface, application

For about a year I ran a single mean-reversion book and quietly accepted that it bled money for weeks at a time. The strategy was fine. The problem was that I was running it in markets where mean reversion simply doesn’t pay — strong trending regimes where every “oversold” dip kept getting more oversold.

The fix wasn’t a better signal. It was admitting that no single strategy works in every regime, and building something that switches.

The problem I hit

My mean-reversion strategy had a great backtest and a frustrating live curve. When I bucketed the daily PnL by market condition, the story was obvious: most of the drawdown came from a handful of trending stretches where the book kept fading a move that wouldn’t stop.

Trend-following had the mirror problem — it gave back everything in choppy, range-bound months. I had two strategies that were each profitable in exactly the conditions where the other one failed.

My approach: a cheap, explainable regime classifier

I didn’t reach for a hidden Markov model first. I started with two features I could reason about: realized volatility and trend strength. That gives four regimes, and a router that picks the strategy suited to each.

import numpy as np
import pandas as pd

def classify_regime(prices: pd.Series, vol_window: int = 20, trend_window: int = 50) -> str:
log_ret = np.log(prices / prices.shift(1))
realized_vol = log_ret.rolling(vol_window).std() * np.sqrt(252)

# Trend strength: how far price sits from its slow mean, in vol units.
ma = prices.rolling(trend_window).mean()
trend_z = (prices ma) / (prices.rolling(trend_window).std() + 1e-9)

vol_now = realized_vol.iloc[1]
trend_now = abs(trend_z.iloc[1])

high_vol = vol_now > realized_vol.median()
trending = trend_now > 1.0

if trending and high_vol:
return "trend_volatile"
if trending and not high_vol:
return "trend_calm"
if not trending and high_vol:
return "chop_volatile"
return "chop_calm"

The router maps each regime to a strategy (or to flat). The key design choice: when in doubt, go flat — an unknown or transitional regime is not an invitation to take risk.

STRATEGY_BY_REGIME = {
"trend_calm": "trend_follow",
"trend_volatile": "trend_follow_half_size",
"chop_calm": "mean_revert",
"chop_volatile": "flat", # the regime that used to wreck me
}

def route(prices: pd.Series) -> str:
return STRATEGY_BY_REGIME[classify_regime(prices)]

Here’s the whole switch as a flow:

flowchart TD
    price["Daily prices"] --> feat["Compute vol + trend"]
    feat --> reg["Classify regime"]
    reg --> trend["Trend regime"]
    reg --> chop["Chop regime"]
    trend --> tf["Trend-follow book"]
    chop --> mr["Mean-revert book"]
    chop --> flat["Go flat if volatile"]

Results

The point of switching isn’t a higher peak return — it’s a flatter equity curve. After routing, my worst rolling drawdown dropped from roughly 22% to 9%, and the Sharpe went from 0.7 to 1.4.

The single biggest win was the chop_volatile → flat rule. I wasn’t adding alpha there; I was removing a reliable loss.

regime days pnl_contrib
trend_calm 78 +14.2%
chop_calm 61 +6.1%
chop_volatile 24 0.0% (flat)

Lessons

  • Regime detection is risk management, not a new alpha. The gain came from turning a strategy off at the right time.
  • Avoid whipsaw. Re-classifying every bar made the router flip-flop and pay double commissions. I added a “stay in the current regime unless the new one persists for N days” hysteresis rule. A burst of same-day entries and exits was the tell that made me add it.
  • Start explainable. I later tried an HMM and it was marginally better on the backtest and much harder to trust live. The two-feature version is what I actually run.

If you’re running one strategy and watching it bleed in the “wrong” market, you probably don’t need a better signal. You need a switch.

Join the conversation

Your email address will not be published. Required fields are marked *