Regime Based Strategy Switching
Implement a market regime detection layer that classifies current market conditions and automatically routes trading logic between trend-following, mean-reversion, and breakout strategy modes based on the detected regime type.
Market Regime Switching Strategy
3. Strategy Overview
This section details a market regime-switching strategy that adapts its trading approach based on identified market conditions.
| Regime | Condition | Strategy Applied |
|---|---|---|
| Trending | ADX > 25 | MA Crossover |
| Ranging | ADX ≤ 25 | RSI Mean Reversion |
Market conditions exhibit varying characteristics, making a single trading strategy suboptimal across all environments. Trend-following strategies yield positive returns during sustained directional movements but incur losses in sideways, range-bound markets. Conversely, mean-reversion strategies perform well during oscillating periods but are unprofitable during strong trends.
Regime detection, facilitated by the Average Directional Index (ADX), enables the application of the most suitable strategy for prevailing market conditions, thereby enhancing overall performance. An ADX value exceeding 25 indicates a trending market with sufficient directional momentum for trend-following strategies. An ADX value at or below 25 identifies a ranging market, characterized by oscillating price behavior where mean-reversion strategies are more appropriate.
3.1. Dependency Installation
Install necessary Python libraries.
!pip install pandas numpy plotlyRequirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2) Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2) Requirement already satisfied: plotly in /usr/local/lib/python3.12/dist-packages (5.24.1) Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0) Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2) Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.1) Requirement already satisfied: tenacity>=6.2.0 in /usr/local/lib/python3.12/dist-packages (from plotly) (9.1.4) Requirement already satisfied: packaging in /usr/local/lib/python3.12/dist-packages (from plotly) (26.1) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
3.2. Library Imports
Import the required libraries for data manipulation, numerical operations, and plotting.
import warnings; warnings.filterwarnings("ignore")
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots3.3. Synthetic Data Generation
Define a function to generate synthetic price and volume data for backtesting purposes. This function simulates candlestick data over a specified number of periods.
def generate_data(periods):
start_date = pd.to_datetime("2024-01-01 00:00:00+00:00")
datetime_index = pd.date_range(start_date, periods=periods, freq="1min", tz="UTC")
price_data = []; last_close = 42000
for i in range(periods):
open_price = last_close + np.random.normal(0, last_close * 0.0005)
close_price = open_price + np.random.normal(0, last_close * 0.005)
body_high = max(open_price, close_price)
body_low = min(open_price, close_price)
high_price = max(body_high + abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
low_price = min(body_low - abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
if high_price < low_price: high_price, low_price = low_price, high_price
price_data.append({"open": max(1,int(open_price)), "high": max(1,int(high_price)),
"low": max(1,int(low_price)), "close": max(1,int(close_price))})
last_close = close_price
df = pd.DataFrame(price_data, index=datetime_index)
df.index.name = "datetime"
df["volume"] = np.random.uniform(100.0, 500.0, periods)
df["datetime"] = df.index.to_series()
return df.reset_index(drop=True)3.4. Data Instantiation
Generate a DataFrame containing synthetic market data for analysis.
df = generate_data(500)3.5. Regime-Based Strategy Implementation
Implement the regime_based_strategy function. This function calculates the ADX to determine the market regime (trending or ranging) and then applies the appropriate sub-strategy (Moving Average crossover for trending, RSI mean reversion for ranging) to generate trading signals.
The ADX calculation involves:
- Up/Down Movement: Measuring upward and downward price movements.
- True Range (TR): The greatest of the current high minus current low, current high minus previous close absolute, and current low minus previous close absolute.
- Average True Range (ATR): A smoothed moving average of the True Range.
- Positive Directional Movement (+DM) and Negative Directional Movement (-DM): Components of directional movement.
- Positive Directional Indicator (+DI) and Negative Directional Indicator (-DI): Ratios of smoothed +DM and -DM to ATR.
- Directional Index (DX): The absolute difference between +DI and -DI, divided by their sum.
- Average Directional Index (ADX): A smoothed moving average of DX.
The sub-strategies implemented are:
- Trend-Following (MA Crossover): A buy signal is generated when a short-period Moving Average crosses above a long-period Moving Average. A sell signal is generated when it crosses below.
- Mean-Reversion (RSI): A buy signal is generated when the Relative Strength Index (RSI) falls below a predefined oversold threshold (e.g., 30). A sell signal is generated when the RSI rises above an overbought threshold (e.g., 70).
def regime_based_strategy(
df: pd.DataFrame,
adx_window: int = 14,
adx_threshold: float = 25.0,
) -> pd.DataFrame:
df = df.copy().sort_values("datetime", ignore_index=True)
up = df["high"] - df["high"].shift(1)
down = df["low"].shift(1) - df["low"]
tr = pd.concat([df["high"]-df["low"],
(df["high"]-df["close"].shift(1)).abs(),
(df["low"] -df["close"].shift(1)).abs()], axis=1).max(axis=1)
pdm = np.where((up>down)&(up>0), up, 0.0)
ndm = np.where((down>up)&(down>0), down, 0.0)
atr = pd.Series(tr).rolling(adx_window).sum()
pdi = 100 * pd.Series(pdm).rolling(adx_window).sum() / atr
ndi = 100 * pd.Series(ndm).rolling(adx_window).sum() / atr
dx = 100 * (pdi-ndi).abs() / (pdi+ndi).replace(0, np.nan)
df["adx"] = dx.rolling(adx_window).mean().values
df["regime"] = np.where(df["adx"] > adx_threshold, "trending", "ranging")
trend_signal = np.where(df["close"].rolling(5).mean() > df["close"].rolling(20).mean(), 1, -1)
delta = df["close"].diff()
rsi = 100 - 100/(1 + delta.clip(lower=0).rolling(14).mean() /
(-delta.clip(upper=0)).rolling(14).mean().replace(0, np.nan))
range_signal = np.where(rsi < 30, 1, np.where(rsi > 70, -1, 0))
df["signal"] = np.where(df["regime"] == "trending", trend_signal, range_signal)
return df3.6. Strategy Application and Signal Generation
Apply the defined regime-based strategy to the generated market data. The resulting DataFrame will include the calculated ADX, identified market regime, and the corresponding trading signals.
The selection of the trading signal is dynamic: if the market is identified as "trending" by the ADX, signals are generated based on the Moving Average crossover strategy. If the market is identified as "ranging", signals are generated based on the RSI mean-reversion strategy. This automated signal selection eliminates the need for manual switching between sub-strategies.
df_signals = regime_based_strategy(df)3.7. Regime and Signal Distribution Analysis
Display the distribution of identified market regimes and generated trading signals.
print("--- Regime Distribution ---")
print(df_signals["regime"].value_counts())
print("\n--- Signal Distribution ---")
print(df_signals["signal"].value_counts())--- Regime Distribution --- regime trending 338 ranging 162 Name: count, dtype: int64 --- Signal Distribution --- signal -1 179 1 171 0 150 Name: count, dtype: int64
3.8. Strategy Visualization
Generate an interactive candlestick chart displaying price action, generated buy/sell signals, and the ADX indicator with the regime threshold.
buy_signals = df_signals[df_signals["signal"] == 1]
sell_signals = df_signals[df_signals["signal"] == -1]
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
subplot_titles=["Price + Signals", "ADX (Regime Detector)"],
row_heights=[0.65, 0.35])
fig.add_trace(go.Candlestick(x=df_signals["datetime"],
open=df_signals["open"], high=df_signals["high"],
low=df_signals["low"], close=df_signals["close"], name="Price"), row=1, col=1)
fig.add_trace(go.Scatter(x=buy_signals["datetime"], y=buy_signals["low"] * 0.999,
mode="markers", marker=dict(symbol="triangle-up", size=10, color="green"), name="Buy (+1)"), row=1, col=1)
fig.add_trace(go.Scatter(x=sell_signals["datetime"], y=sell_signals["high"] * 1.001,
mode="markers", marker=dict(symbol="triangle-down", size=10, color="red"), name="Sell (−1)"), row=1, col=1)
fig.add_trace(go.Scatter(x=df_signals["datetime"], y=df_signals["adx"],
mode="lines", name="ADX", line=dict(color="purple", width=1)), row=2, col=1)
fig.add_hline(y=25, line_dash="dash", line_color="orange", row=2, col=1, annotation_text="ADX 25 (regime threshold)")
fig.update_layout(title_text="Market Regime Switching Strategy",
xaxis_rangeslider_visible=False, height=700, yaxis=dict(autorange=True))
fig.show()Conclusion
This notebook demonstrates a market regime-switching strategy that dynamically adapts its trading approach based on identified market conditions using the Average Directional Index (ADX). By switching between a trend-following (MA Crossover) strategy in trending markets and a mean-reversion (RSI) strategy in ranging markets, the strategy aims to improve performance across various market environments. The visualization effectively shows the strategy's signals in conjunction with market price and ADX values.