In partnership with

Invest right from your couch

Have you always been kind of interested in investing but found it too intimidating (or just plain boring)? Yeah, we get it. Luckily, today’s brokers are a little less Wall Street and much more accessible. Online stockbrokers provide a much more user-friendly experience to buy and sell stocks—right from your couch. Money.com put together a list of the Best Online Stock Brokers to help you open your first account. Check it out!

Elite Quant Plan – 14-Day Free Trial (Till December 31st ONLY)

No card needed. Cancel anytime. Zero risk.

You get immediate access to:

  • Full code from every article (including today’s HMM notebook)

  • Private GitHub repos & templates

  • All premium deep dives (3–5 per month)

  • 2 × 1-on-1 calls with me

  • One custom bot built/fixed for you

Try the entire Elite experience for 14 days — completely free.

→ Start your free trial now 👇

(Doors close in 7 days or when the post goes out of the spotlight — whichever comes first.)

See you on the inside.

👉 Upgrade Now

🔔 Limited-Time Holiday Deal: 20% Off Our Complete 2026 Playbook! 🔔

Level up before the year ends!

AlgoEdge Insights: 30+ Python-Powered Trading Strategies – The Complete 2026 Playbook

30+ battle-tested algorithmic trading strategies from the AlgoEdge Insights newsletter – fully coded in Python, backtested, and ready to deploy. Your full arsenal for dominating 2026 markets.

Special Promo: Use code WINTER2025 for 20% off

Valid only until December 25, 2025 — act fast!

👇 Buy Now & Save 👇

Instant access to every strategy we've shared, plus exclusive extras.

— AlgoEdge Insights Team

Premium Members – Your Full Notebook Is Ready

The complete Google Colab notebook from today’s article (with live data, full Hidden Markov Model, interactive charts, statistics, and one-click CSV export) is waiting for you.

Preview of what you’ll get:

Inside:

  • Automatic stock data download — Pulls daily TSLA OHLC data from 2022 onward via yfinance

  • Real 3-state Gaussian HMM — Classifies market into low, medium, and high volatility regimes based on log returns

  • Beautiful Matplotlib charts — Candlestick plot with overlaid reversal probability arcs (50% band shown), plus regime subplot for context

  • Swing & regime interpretation — Detailed summary of the last completed swing (amplitude/duration vs historical percentiles) and active pivot context

  • Ready-to-use Python code — Fully executable notebook with clear functions for pivots, regimes, and KDE mapping

  • Bonus flexibility — Change the ticker (e.g., AAPL, BTC-USD, ^GSPC) or timeframe with one line adjustment

Free readers – you already got the full breakdown and visuals in the article. Paid members – you get the actual tool.

Not upgraded yet? Fix that in 10 seconds here👇

Google Collab Notebook With Full Code Is Available In the End Of The Article Behind The Paywall 👇 (For Paid Subs Only)

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.

1. How the Probability Zones Work

We use 3 core steps: (i) pivot detection, (ii) regime labeling, and (iii) probability mapping.

But what can you actually DO about the proclaimed ‘AI bubble’? Billionaires know an alternative…

Sure, if you held your stocks since the dotcom bubble, you would’ve been up—eventually. But three years after the dot-com bust the S&P 500 was still far down from its peak. So, how else can you invest when almost every market is tied to stocks?

Lo and behold, billionaires have an alternative way to diversify: allocate to a physical asset class that outpaced the S&P by 15% from 1995 to 2025, with almost no correlation to equities. It’s part of a massive global market, long leveraged by the ultra-wealthy (Bezos, Gates, Rockefellers etc).

Contemporary and post-war art.

Masterworks lets you invest in multimillion-dollar artworks featuring legends like Banksy, Basquiat, and Picasso—without needing millions. Over 70,000 members have together invested more than $1.2 billion across over 500 artworks. So far, 25 sales have delivered net annualized returns like 14.6%, 17.6%, and 17.8%.*

Want access?

Investing involves risk. Past performance not indicative of future returns. Reg A disclosures at masterworks.com/cd

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.

def label_regimes(df, n_state):
    lr = np.log(df['Close']).diff().dropna().values.reshape(-1,1)
    model = GaussianHMM(n_components=n_state,
                        covariance_type='diag',
                        n_iter=200, random_state=0)
    model.fit(lr)
    states = model.predict(lr)
    states = np.insert(states, 0, states[0])
    return pd.Series(states, index=df.index)

3.5. Extract Swing Statistics

Measure the amplitude (percent move) and duration (number of bars) of each swing between pivots.

This is our raw sample for probability mapping. Larger amplitudes or longer durations signal bigger, slower moves.

def swing_stats(pivots):
    amp, dur = [], []
    for (i1,p1,t1),(i2,p2,t2) in zip(pivots, pivots[1:]):
        if t1==-1 and t2==1:
            amp.append((p2-p1)/p1); dur.append(i2-i1)
        elif t1==1 and t2==-1:
            amp.append((p1-p2)/p1); dur.append(i2-i1)
    return np.array(amp), np.array(dur)

3.6. Build Probability Maps by Regime

Fit a kernel density estimator to swing statistics for each regime.

For each state, you get a 2D probability surface to show how likely a swing of a given size and duration is under these volatility conditions.

def kde_grid(samples, levels, nx, ny):
    amp, dur = samples
    if len(amp)<15: return None
    x_max = np.percentile(amp,99)*1.2
    y_max = np.percentile(dur,99)*1.2
    xs = np.linspace(0, x_max, nx)
    ys = np.linspace(1, y_max, ny)
    X,Y = np.meshgrid(xs, ys)
    kde = gaussian_kde(np.vstack([amp,dur]))
    Z  = kde(np.vstack([X.ravel(),Y.ravel()])).reshape(X.shape)
    dx,dy = xs[1]-xs[0], ys[1]-ys[0]
    grid = np.sort(Z.ravel())[::-1]
    cdf  = np.cumsum(grid * dx * dy)
    thresh = []
    for p in levels:
        idx = np.searchsorted(cdf, p)
        idx = min(idx, len(grid)-1)
        thresh.append(grid[idx])
    return xs, ys, Z, thresh
def build_kde_by_regime(df, pivots, regimes):
    out = {}
    for st in regimes.unique():
        idxs = set(regimes[regimes==st].index)
        pv  = [p for p in pivots if df.index[p[0]] in idxs]
        out[st] = kde_grid(swing_stats(pv), BAND_P, GRID_NX, GRID_NY)
    return out  

3.7. Visualize Reversal Probability Zones

Plot the price candles, pivots, regime labels, and the reversal arcs.

Each arc shows where price is statistically likely to reverse from a given pivot, given the current volatility regime.


df       = download_ohlc(TICKER, START, END, INTERVAL)
pivots   = detect_pivots(df, PIVOT_K)
regimes  = label_regimes(df, N_STATE)
kde_dict = build_kde_by_regime(df, pivots, regimes)

plt.style.use('dark_background')
fig, (ax1, ax2) = plt.subplots(
    2,1, sharex=True,
    gridspec_kw={'height_ratios':[5,1]},
    figsize=(14,8)
)

green, red = '#089981','#F23645'
width = 0.6

# ── Top: candles + contours
for dt,row in df.iterrows():
    x = mdates.date2num(dt)
    o,h,l,c = row[['Open','High','Low','Close']]
    col = green if c>=o else red
    ax1.vlines(x, l, h, color=col)
    y0 = min(o,c)
    r = mpatches.Rectangle((x-width/2,y0),
                            width, abs(c-o),
                            facecolor=col, edgecolor=col)
    ax1.add_patch(r)

for i, price, typ in pivots:
    dt   = df.index[i]
    reg  = regimes.loc[dt]
    grid = kde_dict.get(reg)
    if grid is None: continue
    xs, ys, Z, th = grid
    A, T = np.meshgrid(xs, ys)
    sign = 1 if typ==-1 else -1
    base = mdates.date2num(dt)
    Xab = base + T
    Yab = price + sign * A * price
    col = green if sign==1 else red

    for lvl, p in zip(th, BAND_P):
        cs = ax1.contour(
            Xab, Yab, Z,
            levels=[lvl],
            colors=col,
            alpha=0.4
        )
        ax1.clabel(
            cs,
            fmt={lvl: f'{int(p*100)}%'},
            inline=True,
            fontsize=8
        )

ax1.set_title(f'{TICKER} reversal probability zones')
ax1.set_ylabel('Price')

# ── Bottom: regime scatter
scatter = ax2.scatter(df.index, regimes,
                      c=regimes, cmap='tab10', s=4)
states = sorted(regimes.unique())
ax2.set_yticks(states)
ax2.set_yticklabels([str(int(s)) for s in states])
ax2.set_ylabel('Regime')
ax2.xaxis_date()
plt.subplots_adjust(bottom=0.15)
fig.autofmt_xdate()
plt.tight_layout()
plt.show()   

3.8. Dynamic Interpretation

We also implement a step to automate the interpretation of the results given the complexity of the analysis.

We summarize the most recent swing and active pivot. Compare the move to historical percentiles for amplitude and duration.

If the swing falls inside the envelope, it’s a normal move for this regime. If it breaks outside, you have an outlier

# ── Last closed arc stats & active pivot interpretation
if len(pivots) < 2:
    print("Not enough pivots to compute stats.")

# last closed arc = second-last → last pivot
(i1,p1,t1), (i2,p2,t2) = pivots[-2], pivots[-1]
amp_arr, dur_arr = swing_stats(pivots)
amp     = abs(p2-p1)/p1
dur     = i2 - i1
pct75_a = np.percentile(amp_arr, 75)
pct75_d = np.percentile(dur_arr, 75)
inside  = (amp <= pct75_a) and (dur <= pct75_d)

# active pivot
act_i, act_p, act_t = pivots[-1]
dt_act = df.index[act_i]
pivot_type = "Low" if act_t==-1 else "High"
regime     = regimes.iloc[act_i]

print("\n=== Last Closed Arc Summary ===")
print(f" • Start: index {i1} at price {p1:.2f}")
print(f" • End  : index {i2} at price {p2:.2f}")
print(f" • Amplitude: {amp:.2%} (vs 75th percentile: {pct75_a:.2%})")
print(f" • Duration : {dur} bars (vs 75th percentile: {int(pct75_d)} bars)")

if inside:
    print(" → This swing landed within the 75% envelope.")
    print("    → That means the move was statistically normal — both in size and duration.")
    print("    → No outlier behavior; it followed historical patterns.")
else:
    print(" → This swing moved outside the 75% envelope.")
    if amp > pct75_a and dur > pct75_d:
        print("    → The move was both larger and slower than most historical swings.")
    elif amp > pct75_a:
        print("    → Price moved farther than usual — a large extension.")
    elif dur > pct75_d:
        print("    → The move took longer than normal to complete — a slow swing.")
    print("    → This might indicate a trend continuation or breakout behavior.")

print("\n=== Active Pivot Context ===")
print(f" • Latest pivot type: {pivot_type.upper()} (printed on {dt_act.date()}, price = {act_p:.2f})")
print(f" • Market regime assigned: {regime}")

print(" • Interpretation of current regime:")
if regime == 0:
    print("    → Regime 0: Low volatility environment.")
    print("       → Expect tighter reversal zones, shallow swings, lower momentum.")
    print("       → Price is likely to respect statistical envelopes closely.")
elif regime == 1:
    print("    → Regime 1: Medium volatility.")
    print("       → Swings behave near historical averages — typical amplitudes and durations.")
    print("       → Zones offer balanced risk/reward.")
elif regime == 2:
    print("    → Regime 2: High volatility environment.")
    print("       → Expect wide zones, strong and fast moves.")
    print("       → Price may overshoot typical reversal areas.")
else:
    print("    → Unknown regime label — check the HMM configuration or input data.")

print("\nThe active arc (reversal zone) now starts from this pivot.")
print("It represents where price is statistically likely to move next — based on historical swings from similar pivots, under this same regime.")
print("Watch how price behaves relative to this arc — if it enters and reverses, it's behaving normally.")
print("If it breaks through the arc boundaries (especially the 90% zone), you're likely in a strong trend or breakout phase.")
=== Last Closed Arc Summary ===
  Start: index 730 at price 326.59
  End  : index 744 at price 488.54
  Amplitude: 49.59% (vs 75th percentile: 24.77%)
  Duration : 14 bars (vs 75th percentile: 14 bars)
  This swing moved outside the 75% envelope.
     Price moved farther than usual  a large extension.
     This might indicate a trend continuation or breakout behavior.

=== Active Pivot Context ===
  Latest pivot type: HIGH (printed on 2024-12-18, price = 488.54)
  Market regime assigned: 2
  Interpretation of current regime:
     Regime 2: High volatility environment.
        Expect wide zones, strong and fast moves.
        Price may overshoot typical reversal areas.

The active arc (reversal zone) now starts from this pivot.
It represents where price is statistically likely to move next  based on historical swings from similar pivots, under this same regime.
Watch how price behaves relative to this arc  if it enters and reverses, it's behaving normally.
If it breaks through the arc boundaries (especially the 90% zone), you're likely in a strong trend or breakout phase.

4. Limitations and Extensions

This approach depends on accurate pivot detection, regime labeling, and a stable distribution of past swings.

If market structure changes or liquidity dries up, the statistical envelopes lose relevance. HMM regime shifts can lag during sudden volatility events.

The method assumes history repeats often enough for probabilities to hold. In trending or news-driven markets, moves can break well outside the computed bands.

KDE density requires enough swing samples in each regime. Rare states or sparse data can make the bands unreliable.

Extensions are straightforward. You can add more probability bands for finer risk calibration.

Apply the workflow to intraday or weekly bars.

Swap HMM for another regime detection method like GARCH, rolling volatility, or even clustering.

Add filters for volume, open interest, or macro triggers. For more robust edges, combine with order flow or sentiment signals.

Concluding Thoughts

This approach is about perspective. Statistical reversal zones offer a framework to spot structural changes as they happen.

If you want to quantify uncertainty and visualize risk, this is a practical way forward that can be customized further.

logo

Subscribe to our premium content to read the rest.

Become a paying subscriber to get access to this post and other subscriber-only content.

Upgrade

Keep Reading