How I use regime detection to switch trading strategies automatically
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 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.
"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.
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.