$npx -y skills add agiprolabs/claude-trading-skills --skill mean-reversionMean-reversion strategy tools including Hurst exponent, half-life estimation, z-score signals, ADF testing, and Ornstein-Uhlenbeck modeling
| 1 | # Mean Reversion |
| 2 | |
| 3 | Mean reversion is the statistical tendency for prices, spreads, or other financial variables to return toward a long-run average after deviating from it. A mean-reverting series overshoots its mean, then corrects back -- creating predictable oscillations that can be traded. |
| 4 | |
| 5 | ## When Mean Reversion Works |
| 6 | |
| 7 | - **Ranging markets**: Sideways price action with clear support/resistance |
| 8 | - **Pairs spreads**: Spread between cointegrated assets reverts to equilibrium |
| 9 | - **Oversold/overbought extremes**: RSI, Bollinger Band, or z-score extremes in stationary series |
| 10 | - **Funding rate arbitrage**: Perpetual funding rates revert to baseline |
| 11 | - **Stablecoin depegs**: Classic mean-reversion opportunity (peg = known mean) |
| 12 | - **Post-dump recovery**: Brief mean-reversion windows after initial PumpFun dumps |
| 13 | |
| 14 | ## When Mean Reversion Fails |
| 15 | |
| 16 | - Strong trending markets (most crypto most of the time) |
| 17 | - Regime changes: what was stationary becomes non-stationary |
| 18 | - Structural breaks: token migration, protocol upgrade, delistings |
| 19 | - Low liquidity: wide spreads consume mean-reversion profits |
| 20 | |
| 21 | --- |
| 22 | |
| 23 | ## Testing for Mean Reversion |
| 24 | |
| 25 | Before trading mean reversion, you must statistically confirm the series is mean-reverting. Three complementary tests: |
| 26 | |
| 27 | ### 1. Augmented Dickey-Fuller (ADF) Test |
| 28 | |
| 29 | Tests the null hypothesis that a series has a unit root (non-stationary). |
| 30 | |
| 31 | ```python |
| 32 | from scipy import stats |
| 33 | import numpy as np |
| 34 | |
| 35 | def adf_test(series: np.ndarray, max_lag: int = 0) -> dict: |
| 36 | """Run ADF test. Reject null (p < 0.05) → stationary → mean-reverting.""" |
| 37 | # See references/statistical_tests.md for full implementation |
| 38 | # Use statsmodels.tsa.stattools.adfuller for production |
| 39 | pass |
| 40 | ``` |
| 41 | |
| 42 | - **p < 0.01**: Strong evidence of stationarity |
| 43 | - **p < 0.05**: Evidence of stationarity |
| 44 | - **p > 0.10**: Cannot reject unit root -- likely non-stationary |
| 45 | |
| 46 | ### 2. Hurst Exponent |
| 47 | |
| 48 | Measures the long-range dependence of a time series. |
| 49 | |
| 50 | | Hurst Value | Interpretation | Trading Implication | |
| 51 | |-------------|---------------|---------------------| |
| 52 | | H < 0.5 | Mean-reverting | Trade mean reversion | |
| 53 | | H = 0.5 | Random walk | No edge | |
| 54 | | H > 0.5 | Trending | Trade momentum | |
| 55 | |
| 56 | ```python |
| 57 | def hurst_exponent(series: np.ndarray) -> float: |
| 58 | """Compute Hurst exponent via R/S method. H < 0.5 → mean-reverting.""" |
| 59 | # See references/statistical_tests.md for full R/S algorithm |
| 60 | pass |
| 61 | ``` |
| 62 | |
| 63 | ### 3. Variance Ratio Test |
| 64 | |
| 65 | Compares variance of multi-period returns to single-period variance. |
| 66 | |
| 67 | - **VR < 1**: Negative autocorrelation (mean-reverting) |
| 68 | - **VR = 1**: Random walk |
| 69 | - **VR > 1**: Positive autocorrelation (trending) |
| 70 | |
| 71 | ```python |
| 72 | def variance_ratio(series: np.ndarray, q: int = 5) -> float: |
| 73 | """Compute variance ratio at horizon q. VR < 1 → mean-reverting.""" |
| 74 | returns = np.diff(np.log(series)) |
| 75 | var_1 = np.var(returns) |
| 76 | returns_q = np.diff(np.log(series[::q])) |
| 77 | var_q = np.var(returns_q) |
| 78 | return var_q / (q * var_1) |
| 79 | ``` |
| 80 | |
| 81 | See `references/statistical_tests.md` for complete implementations and interpretation guides. |
| 82 | |
| 83 | --- |
| 84 | |
| 85 | ## Half-Life Estimation |
| 86 | |
| 87 | The half-life tells you how many periods it takes for a deviation to decay to half its size. This is the single most important parameter for mean-reversion trading. |
| 88 | |
| 89 | ### AR(1) Regression Method |
| 90 | |
| 91 | Fit the autoregressive model: `delta_X_t = alpha + beta * X_{t-1} + epsilon` |
| 92 | |
| 93 | ```python |
| 94 | def half_life(series: np.ndarray) -> float: |
| 95 | """Estimate mean-reversion half-life from AR(1) regression. |
| 96 | |
| 97 | Returns: |
| 98 | Half-life in periods. Negative means non-mean-reverting. |
| 99 | """ |
| 100 | y = np.diff(series) |
| 101 | x = series[:-1] |
| 102 | x = np.column_stack([np.ones(len(x)), x]) |
| 103 | beta = np.linalg.lstsq(x, y, rcond=None)[0][1] |
| 104 | if beta >= 0: |
| 105 | return -1.0 # Not mean-reverting |
| 106 | return -np.log(2) / np.log(1 + beta) |
| 107 | ``` |
| 108 | |
| 109 | ### Using Half-Life |
| 110 | |
| 111 | | Parameter | Rule of Thumb | |
| 112 | |-----------|--------------| |
| 113 | | Lookback window | 2x half-life | |
| 114 | | Holding period | 1x half-life | |
| 115 | | Maximum hold | 3x half-life (stop) | |
| 116 | | Signal recalc | 0.5x half-life | |
| 117 | |
| 118 | --- |
| 119 | |
| 120 | ## Z-Score Signal Framework |
| 121 | |
| 122 | The z-score normalizes the deviation from the mean, providing standardized entry/exit signals. |
| 123 | |
| 124 | ``` |
| 125 | z = (price - rolling_mean) / rolling_std |
| 126 | ``` |
| 127 | |
| 128 | ### Signal Rules |
| 129 | |
| 130 | | Condition | Signal | Action | |
| 131 | |-----------|--------|--------| |
| 132 | | z < -2.0 | Buy | Enter long (price below mean) | |
| 133 | | z > +2.0 | Sell | Enter short (price above mean) | |
| 134 | | z crosses 0 | Exit | Close position (returned to mean) | |
| 135 | | abs(z) > 3.0 | Stop | Close position (reversion failed) | |
| 136 | |
| 137 | ### Lookback Window |
| 138 | |
| 139 | Set the rolling window to approximately **2x the half-life**: |
| 140 | |
| 141 | ```python |
| 142 | def z_score_signals( |
| 143 | prices: np.ndarray, |
| 144 | lookback: int, |
| 145 | entry_z: float = 2.0, |
| 146 | exit_z: float = 0.0, |
| 147 | stop_z: float = 3.0, |
| 148 | ) -> np.ndarray: |
| 149 | """Generate z-score-based m |