KhueApps
Home/Python/Auto-Detect RSI Bullish and Bearish Divergence in Python

Auto-Detect RSI Bullish and Bearish Divergence in Python

Last updated: October 05, 2025

Overview

This guide shows how to automatically recognize RSI positive (bullish) and negative (bearish) divergence in Python. We compute RSI, find swing highs/lows (pivots), and pair pivots to identify divergences in a reproducible, backtest-friendly way.

Definitions:

  • Bullish (positive) divergence: price makes a lower low while RSI makes a higher low.
  • Bearish (negative) divergence: price makes a higher high while RSI makes a lower high.

We use only pandas and numpy for portability.

Quickstart

  1. Install dependencies
    • pip install pandas numpy
  2. Prepare a Series of close prices (float index or DateTime index).
  3. Compute RSI (Wilder’s smoothing).
  4. Detect pivots (swing highs/lows) using left/right window sizes.
  5. Pair recent pivots to confirm divergence within a lookback window.
  6. Use the resulting signals for alerts, plots, or backtests.

Minimal working example

import numpy as np
import pandas as pd

# 1) Sample data (synthetic). Replace df['close'] with your price series.
np.random.seed(0)
n = 800
ret = np.random.normal(0, 0.5, n) / 100  # ~0.5% daily volatility
close = 100 * (1 + pd.Series(ret)).cumprod()
df = pd.DataFrame({'close': close})

# 2) RSI (Wilder) implementation

def rsi(series: pd.Series, period: int = 14) -> pd.Series:
    delta = series.diff()
    gain = delta.clip(lower=0.0)
    loss = -delta.clip(upper=0.0)
    avg_gain = gain.ewm(alpha=1/period, adjust=False).mean()
    avg_loss = loss.ewm(alpha=1/period, adjust=False).mean()
    rs = avg_gain / (avg_loss.replace(0, np.nan))
    rsi = 100 - (100 / (1 + rs))
    return rsi.fillna(50)  # neutral until warm-up completes

# 3) Pivot detection (left/right bars for confirmation)

def pivots_high(series: pd.Series, left: int = 3, right: int = 3) -> pd.Series:
    arr = series.values
    n = len(arr)
    out = np.zeros(n, dtype=bool)
    for i in range(left, n - right):
        if arr[i] == np.max(arr[i-left:i+right+1]) and arr[i] > np.max(arr[i-left:i]):
            out[i] = True
    return pd.Series(out, index=series.index)


def pivots_low(series: pd.Series, left: int = 3, right: int = 3) -> pd.Series:
    arr = series.values
    n = len(arr)
    out = np.zeros(n, dtype=bool)
    for i in range(left, n - right):
        if arr[i] == np.min(arr[i-left:i+right+1]) and arr[i] < np.min(arr[i-left:i]):
            out[i] = True
    return pd.Series(out, index=series.index)

# 4) Divergence detection

def detect_divergence(
    close: pd.Series,
    rsi_series: pd.Series,
    piv_low: pd.Series,
    piv_high: pd.Series,
    lookback: int = 150,
    min_rsi_diff: float = 2.0,    # min RSI separation (points)
    min_price_pct: float = 0.2,   # min price separation (% of price)
    require_rsi_extremes: bool = True,
) -> pd.DataFrame:
    idx = close.index
    bull = pd.Series(0, index=idx, dtype=int)
    bear = pd.Series(0, index=idx, dtype=int)

    low_idx = np.where(piv_low.values)[0]
    high_idx = np.where(piv_high.values)[0]

    # Bullish: lower price low, higher RSI low
    for k in range(1, len(low_idx)):
        i2 = low_idx[k]
        i1 = low_idx[k - 1]
        if i2 - i1 > lookback:
            continue
        p1, p2 = close.iat[i1], close.iat[i2]
        r1, r2 = rsi_series.iat[i1], rsi_series.iat[i2]
        price_lower_low = p2 < p1 * (1 - min_price_pct / 100)
        rsi_higher_low = r2 > r1 + min_rsi_diff
        if not price_lower_low:
            # allow equal-or-slightly-lower low with tolerance
            price_lower_low = (p2 < p1) and (abs(p2 - p1) / p1 * 100 >= min_price_pct)
        if require_rsi_extremes:
            cond_extreme = (r1 < 50) and (r2 < 60)
        else:
            cond_extreme = True
        if price_lower_low and rsi_higher_low and cond_extreme:
            bull.iat[i2] = 1

    # Bearish: higher price high, lower RSI high
    for k in range(1, len(high_idx)):
        i2 = high_idx[k]
        i1 = high_idx[k - 1]
        if i2 - i1 > lookback:
            continue
        p1, p2 = close.iat[i1], close.iat[i2]
        r1, r2 = rsi_series.iat[i1], rsi_series.iat[i2]
        price_higher_high = p2 > p1 * (1 + min_price_pct / 100)
        rsi_lower_high = r2 < r1 - min_rsi_diff
        if not price_higher_high:
            price_higher_high = (p2 > p1) and (abs(p2 - p1) / p1 * 100 >= min_price_pct)
        if require_rsi_extremes:
            cond_extreme = (r1 > 50) and (r2 > 40)
        else:
            cond_extreme = True
        if price_higher_high and rsi_lower_high and cond_extreme:
            bear.iat[i2] = -1

    return pd.DataFrame({'bull_div': bull, 'bear_div': bear})

# 5) Run pipeline

df['rsi'] = rsi(df['close'], period=14)
df['piv_high'] = pivots_high(df['close'], left=3, right=3)
df['piv_low']  = pivots_low(df['close'], left=3, right=3)
signals = detect_divergence(
    df['close'], df['rsi'], df['piv_low'], df['piv_high'],
    lookback=150, min_rsi_diff=2.0, min_price_pct=0.2, require_rsi_extremes=True
)

df = pd.concat([df, signals], axis=1)
df['signal'] = df['bull_div'] + df['bear_div']  # 1 bullish, -1 bearish, 0 none

print(df[['close','rsi','piv_low','piv_high','signal']].tail(10))
print('\nCounts:', df['signal'].value_counts().to_dict())

Notes

  • The pivot right-window waits for confirmation, preventing repainting during backtests.
  • For live use, consider i-1 detection (no right window) and expect more noise.

Algorithm steps

  1. Compute RSI on close.
  2. Identify swing lows/highs with left/right pivot windows.
  3. For each new pivot, search the previous pivot of the same type within a lookback window.
  4. Bullish: price lower low and RSI higher low; Bearish: price higher high and RSI lower high.
  5. Optional filters: require RSI to be below/above midline to reduce false positives.
  6. Emit signals on the bar where the pivot is confirmed (after right bars elapse).
ParameterTypical rangeDefault
RSI period10–2114
Pivot left/right2–53
Lookback (bars)50–250150
Min RSI diff (pts)1–52
Min price diff (%)0.1–1.00.2

Adjust per instrument and timeframe.

Using with your own data

  • CSV: df = pd.read_csv('prices.csv', parse_dates=['date'], index_col='date'); df['close'] = df['Close']
  • OHLC data: apply RSI to the close; pivots also use close by default. You can pivot highs/lows using high/low columns, but compare RSI from close.

Example tweak for OHLC pivots:

# Use H/L for pivots, close for RSI
piv_high = pivots_high(df['high'], left=3, right=3)
piv_low  = pivots_low(df['low'], left=3, right=3)
signals = detect_divergence(df['close'], df['rsi'], piv_low, piv_high)

Pitfalls and validation

  • Repainting: Using a right window delays signals. Without it, pivots can disappear; never backtest with future-aware pivots.
  • Parameter sensitivity: Small changes can dramatically alter counts. Cross-validate on multiple symbols/timeframes.
  • Equal highs/lows: Ties near pivots can create duplicates. Use strict vs non-strict comparisons carefully.
  • Regime bias: Divergences fail in strong trends. Consider trend filters (e.g., 200-SMA direction) or require RSI crosses.
  • Data quality: Missing bars and corporate actions can distort RSI and pivots. Clean and align data.

Performance notes

  • Complexity: O(n) for RSI and pivot scans; O(m) for pivot pairings (m << n). Suitable for millions of bars with vectorized ops and minimal Python loops.
  • Speedups:
    • Prefer numpy arrays inside tight loops (as shown).
    • Reduce pivot density by larger left/right windows.
    • Batch-process multiple symbols by column-wise operations.
    • Consider numba for the pivot loops if you profile a hotspot.

Extensions

  • Confirmations: Require RSI to cross above 50 (bullish) or below 50 (bearish) after divergence.
  • Multi-pivot: Compare more than two pivots to reduce noise.
  • Risk management: Place stops beyond the most recent swing; test fixed-R or ATR-based.

Tiny FAQ

  • Q: What RSI period should I use? A: 14 is common. Shorter (7–10) is more sensitive; longer (21–28) is smoother.

  • Q: Can I detect hidden divergence? A: Yes. Adapt conditions: bullish hidden uses higher low in price with lower low in RSI; bearish hidden uses lower high in price with higher high in RSI.

  • Q: Why are there few signals? A: Increase lookback, reduce min_price_pct/min_rsi_diff, or use smaller pivot windows. Expect trade-off with noise.

  • Q: How do I avoid repainting? A: Keep a nonzero right window for pivots. Emit signals only when the pivot is confirmed.

Next Article: Generating Real-Time Trading Signals with yfinance and Python

Series: Algorithmic Trading with Python

Python