Implement Statistical Reversal Bands By Detecting Regimes, Quantifying Swing Outliers, and Price Zones
Most traders only react after the move. They never see the true reversal zones until it’s too late.
The methodology discussed here aims at addressing this issue. We aim at seeing probability zones in advanced, mapped from price action.
Each reversal zone is an arc and grounded in statistics, which updates with every new swing and shift in volatility.
We combine pivot detection, volatility regime labeling, and probability bands to see reversal areas.
The complete Python notebook for the analysis is provided below.
1. How the Probability Zones Work
We use 3 core steps: (i) pivot detection, (ii) regime labeling, and (iii) probability mapping.
1.1 Pivot Detection
We define pivots as local highs and lows within a moving window.
For each price bar, check if it is the maximum or minimum in a window of size 2k+1:

where L is the low price, H is the high price, and k is the window size.
Pivots mark significant swing points. These are the anchors for reversal arcs.
1.2 Regime Labeling
Volatility regimes are labeled using a hidden Markov model. We use the log returns of closing prices:
The HMM fits these returns and assigns a regime label to each bar. Each regime reflects a different volatility state.
For example:
- Regime 0: low volatility
- Regime 1: medium volatility
- Regime 2: high volatility
This step adapts the reversal zones to current market conditions.
1.3. Probability Mapping
For each swing (pivot-to-pivot move), calculate:
- Amplitude: relative price change between pivots
- Duration: number of bars between pivots
Collect amplitudes and durations for all swings in each regime.
Fit a kernel density estimate to the pairs (A,D). This gives a continuous probability surface for swing size and duration.
To find probability bands, sort the KDE values, integrate to get the cumulative probability, and find thresholds for desired bands (e.g. 50%, 75%).
These bands define the arcs.
1.4. Interpretation
Each reversal arc is a statistical envelope. If a move lands within the 50% arc, it is typical for this regime.
Moves outside the arc are outliers and signal either strong trends or market stress.
All calculations update with new price data. The zones shift as the market shifts.
2. Visualizing Reversal Zones in Python
2.1 Set Parameters
Set all model parameters up front.
These control how pivots sensitivity, how many volatility regimes, how wide the probability bands run, and how detailed the probability grid is.
Adjust these to match your timeframe, instrument, and noise tolerance.
import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import matplotlib.patches as mpatches
from hmmlearn.hmm import GaussianHMM
from scipy.stats import gaussian_kde
# ── PARAMETERS
TICKER = 'TSLA'
START = '2022-01-01'
END = '2024-12-31'
INTERVAL = '1d'
PIVOT_K = 5 # Pivot window size: higher = stronger, fewer pivots; lower = more, noisier pivots.
N_STATE = 3 # Number of HMM regimes: more states = finer volatility splits, but risk of overfitting.
BAND_P = [0.50] # Probability bands: lower = common moves, higher = rare outliers (add more for extra bands).
GRID_NX = 60 # KDE X grid (amplitude): higher = smoother arcs, slower plot; lower = faster, rougher arcs.
GRID_NY = 60 # KDE Y grid (duration): same as above, for swing duration.
2.2 Download and Prepare Price Data
Create a helper function to pull OHLCV data for the given symbol and timeframe. Clean the columns and drop rows with missing price values.
def download_ohlc(ticker, start, end, interval):
df = yf.download(ticker, start=start, end=end,
interval=interval, auto_adjust=True, progress=False)
# flatten MultiIndex: keep level-0 (“Open,High,…”)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
df.columns = df.columns.map(str.title)
df.dropna(subset=['Open','High','Low','Close'], inplace=True)
return df
3.3 Detect Fractal Pivots
Scan for local highs and lows using a rolling window. Each pivot marks a key swing point. This is where the arcs start.
Increasing the window size makes pivots rarer but more significant. Decreasing it catches more minor moves, but increases noise.
def detect_pivots(df, k):
pivots, last = [], 0
H, L = df['High'].values, df['Low'].values
for i in range(k, len(df)-k):
win_h = H[i-k:i+k+1]
win_l = L[i-k:i+k+1]
if L[i]==win_l.min() and last!= -1:
pivots.append((i, L[i], -1)); last=-1
elif H[i]==win_h.max() and last!= 1:
pivots.append((i, H[i], 1)); last=1
return pivots
3.4. Label Market Regimes
Use the Hidden Markov Model to assign a volatility regime to each bar. The model runs on log returns cluster periods by volatility.
Each bar now has a regime label to adapts the probability arcs to current conditions.
Newsletter