$npx -y skills add agiprolabs/claude-trading-skills --skill regime-detectionMarket regime identification using volatility clustering, trend detection, and statistical methods for adaptive trading
| 1 | # Regime Detection |
| 2 | |
| 3 | Identify the current market regime so you can pick the right strategy, size positions correctly, and avoid deploying trend-following logic in a ranging market (or vice versa). |
| 4 | |
| 5 | ## Why Regime Detection Matters |
| 6 | |
| 7 | Every strategy has a "home regime." A momentum strategy prints money in a clean uptrend but bleeds in a choppy range. A mean-reversion grid thrives in low-volatility consolidation but gets steamrolled by a trending breakout. Regime detection tells you **which playbook to use right now**. |
| 8 | |
| 9 | Key benefits: |
| 10 | - **Strategy selection**: Route signals to the right strategy for the current environment |
| 11 | - **Position sizing**: Reduce exposure in hostile regimes, increase in favorable ones |
| 12 | - **Stop adaptation**: Wider stops in high-vol regimes, tighter in low-vol trends |
| 13 | - **Drawdown control**: Sit out "danger zone" regimes (high vol + no trend) |
| 14 | |
| 15 | ## Core Regime Dimensions |
| 16 | |
| 17 | Two orthogonal axes define the four-quadrant regime model: |
| 18 | |
| 19 | | | Low Volatility | High Volatility | |
| 20 | |---|---|---| |
| 21 | | **Trending** | Q1: Clean trend — best for trend following | Q2: Volatile trend — momentum with caution | |
| 22 | | **Ranging** | Q3: Quiet range — mean-reversion paradise | Q4: Choppy chaos — reduce or sit out | |
| 23 | |
| 24 | A third dimension — **mean-reversion tendency** (Hurst exponent) — refines Q3 by telling you how reliably price reverts. |
| 25 | |
| 26 | ## Simple Approaches (No ML Required) |
| 27 | |
| 28 | ### 1. ATR Volatility Percentile |
| 29 | |
| 30 | Rank the current ATR against its own recent history to get a 0–100 percentile score. |
| 31 | |
| 32 | ```python |
| 33 | import pandas as pd |
| 34 | import numpy as np |
| 35 | |
| 36 | def atr_percentile( |
| 37 | high: pd.Series, low: pd.Series, close: pd.Series, |
| 38 | atr_period: int = 14, lookback: int = 100 |
| 39 | ) -> pd.Series: |
| 40 | """ATR percentile rank over a rolling window.""" |
| 41 | tr = pd.concat([ |
| 42 | high - low, |
| 43 | (high - close.shift(1)).abs(), |
| 44 | (low - close.shift(1)).abs() |
| 45 | ], axis=1).max(axis=1) |
| 46 | atr = tr.rolling(atr_period).mean() |
| 47 | return atr.rolling(lookback).apply( |
| 48 | lambda x: pd.Series(x).rank(pct=True).iloc[-1], raw=False |
| 49 | ) |
| 50 | ``` |
| 51 | |
| 52 | - **< 25th percentile** → Low volatility regime |
| 53 | - **25th–75th** → Normal volatility |
| 54 | - **> 75th percentile** → High volatility regime |
| 55 | |
| 56 | ### 2. ADX Trend Strength |
| 57 | |
| 58 | ADX above 25 signals a trending market; below 20 signals a range. |
| 59 | |
| 60 | ```python |
| 61 | def compute_adx( |
| 62 | high: pd.Series, low: pd.Series, close: pd.Series, |
| 63 | period: int = 14 |
| 64 | ) -> pd.Series: |
| 65 | """Average Directional Index.""" |
| 66 | plus_dm = high.diff().clip(lower=0) |
| 67 | minus_dm = (-low.diff()).clip(lower=0) |
| 68 | # Zero out when the other is larger |
| 69 | plus_dm[plus_dm < minus_dm] = 0 |
| 70 | minus_dm[minus_dm < plus_dm] = 0 |
| 71 | |
| 72 | tr = pd.concat([ |
| 73 | high - low, |
| 74 | (high - close.shift(1)).abs(), |
| 75 | (low - close.shift(1)).abs() |
| 76 | ], axis=1).max(axis=1) |
| 77 | |
| 78 | atr = tr.ewm(span=period, adjust=False).mean() |
| 79 | plus_di = 100 * plus_dm.ewm(span=period, adjust=False).mean() / atr |
| 80 | minus_di = 100 * minus_dm.ewm(span=period, adjust=False).mean() / atr |
| 81 | dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di) |
| 82 | return dx.ewm(span=period, adjust=False).mean() |
| 83 | ``` |
| 84 | |
| 85 | ### 3. EMA Slope + Price Position |
| 86 | |
| 87 | ```python |
| 88 | def trend_direction(close: pd.Series, period: int = 20) -> pd.Series: |
| 89 | """Returns +1 (uptrend), -1 (downtrend), 0 (neutral).""" |
| 90 | ema = close.ewm(span=period, adjust=False).mean() |
| 91 | slope = ema.diff(5) # 5-bar slope |
| 92 | above = (close > ema).astype(int) |
| 93 | direction = pd.Series(0, index=close.index) |
| 94 | direction[(slope > 0) & (above == 1)] = 1 |
| 95 | direction[(slope < 0) & (above == 0)] = -1 |
| 96 | return direction |
| 97 | ``` |
| 98 | |
| 99 | ### 4. Bollinger Band Width Percentile |
| 100 | |
| 101 | BB width (upper - lower) / middle as a volatility proxy. A "squeeze" (low percentile) often precedes a breakout. |
| 102 | |
| 103 | ```python |
| 104 | def bb_width_percentile( |
| 105 | close: pd.Series, period: int = 20, |
| 106 | std_dev: float = 2.0, lookback: int = 100 |
| 107 | ) -> pd.Series: |
| 108 | """Bollinger Band width percentile.""" |
| 109 | sma = close.rolling(period).mean() |
| 110 | std = close.rolling(period).std() |
| 111 | width = (2 * std_dev * std) / sma |
| 112 | return width.rolling(lookback).apply( |
| 113 | lambda x: pd.Series(x).rank(pct=True).iloc[-1], raw=False |
| 114 | ) |
| 115 | ``` |
| 116 | |
| 117 | ## Statistical Approaches |
| 118 | |
| 119 | ### Rolling Hurst Exponent |
| 120 | |
| 121 | The Hurst exponent H classifies time series behavior: |
| 122 | - **H < 0.4** → Mean-reverting (anti-persistent) |
| 123 | - **0.4 ≤ H ≤ 0.6** → Random walk (no exploitable structure) |
| 124 | - **H > 0.6** → Trending (persistent) |
| 125 | |
| 126 | Computed via the Rescaled Range (R/S) method. See `references/methodology.md` for the full derivation. |
| 127 | |
| 128 | ```python |
| 129 | def hurst_exponent(series: pd.Series, max_lag: int = 50) -> float: |
| 130 | """Estimate Hurst exponent using R/S method.""" |
| 131 | lags = range(2, max_lag) |
| 132 | rs_values = [] |
| 133 | for lag in lags: |
| 134 | chunks = [series.iloc[i:i+lag] for i in range(0, len(series) - lag, lag)] |
| 135 | rs_list = [] |
| 136 | for chunk in chunks: |
| 137 | if len |