Tutorial 11 min read

How to Detect RSI Divergence Programmatically

A step-by-step tutorial on detecting bullish and bearish RSI divergence using Python and the TickAtlas API. Includes peak/trough detection algorithms and working code.

8Sections
5Code samples
11Min read
TutorialCategory

What is RSI Divergence?

Divergence occurs when price and RSI move in opposite directions. It is one of the most reliable reversal signals in technical analysis — but detecting it programmatically requires comparing swing points, not just raw values.

Bullish Divergence

Price makes a lower low but RSI makes a higher low. Downward momentum is weakening — potential reversal upward.

Bearish Divergence

Price makes a higher high but RSI makes a lower high. Upward momentum is weakening — potential reversal downward.

Step 1: Fetch Historical Data

Divergence detection requires multiple candles. Fetch OHLC data with enough history to identify at least two swing points (50-100 bars is typical).

Python Step 1: Fetch Historical Data
import requests
import pandas as pd

API_KEY = "your_api_key_here"
BASE_URL = "https://tickatlas.com/v1"
HEADERS = {"X-API-Key": API_KEY}

def fetch_data(symbol: str, timeframe: str, bars: int = 100):
    """Fetch OHLC data from the API."""
    resp = requests.get(f"{BASE_URL}/ohlc", params={
        "symbol": symbol,
        "timeframe": timeframe,
        "limit": bars
    }, headers=HEADERS)
    candles = resp.json()["data"]["candles"]
    df = pd.DataFrame(candles)
    df["time"] = pd.to_datetime(df["time"])
    return df

Step 2: Compute RSI (or Fetch It)

You can fetch pre-calculated RSI from the API for the current bar, or compute it yourself from historical OHLC data for the full series:

Python Step 2: Compute RSI (or Fetch It)
def compute_rsi(closes: pd.Series, period: int = 14) -> pd.Series:
    """Calculate RSI from a series of close prices."""
    delta = closes.diff()
    gain = delta.where(delta > 0, 0.0)
    loss = -delta.where(delta < 0, 0.0)

    avg_gain = gain.rolling(window=period).mean()
    avg_loss = loss.rolling(window=period).mean()

    rs = avg_gain / avg_loss
    rsi = 100 - (100 / (1 + rs))
    return rsi

Step 3: Find Swing Points

A swing low is a candle with a lower low than the candles on either side. A swing high is the opposite. This is the core algorithm for divergence detection.

Python Step 3: Find Swing Points
def find_swing_lows(series: pd.Series, lookback: int = 5) -> list[tuple]:
    """Find local minima in a series.
    Returns list of (index, value) tuples."""
    swings = []
    for i in range(lookback, len(series) - lookback):
        window = series.iloc[i - lookback:i + lookback + 1]
        if series.iloc[i] == window.min():
            swings.append((i, series.iloc[i]))
    return swings

def find_swing_highs(series: pd.Series, lookback: int = 5) -> list[tuple]:
    """Find local maxima in a series."""
    swings = []
    for i in range(lookback, len(series) - lookback):
        window = series.iloc[i - lookback:i + lookback + 1]
        if series.iloc[i] == window.max():
            swings.append((i, series.iloc[i]))
    return swings

Step 4: Detect Divergence

Python Step 4: Detect Divergence
def detect_divergence(df: pd.DataFrame) -> list[dict]:
    """Detect RSI divergence in OHLC data."""
    df["rsi"] = compute_rsi(df["close"])
    divergences = []

    # Find swing lows for bullish divergence
    price_lows = find_swing_lows(df["close"])
    rsi_lows = find_swing_lows(df["rsi"])

    # Compare last two swing lows
    if len(price_lows) >= 2 and len(rsi_lows) >= 2:
        p1_idx, p1_val = price_lows[-2]
        p2_idx, p2_val = price_lows[-1]
        r1_idx, r1_val = rsi_lows[-2]
        r2_idx, r2_val = rsi_lows[-1]

        # Bullish: price lower low, RSI higher low
        if p2_val < p1_val and r2_val > r1_val:
            divergences.append({
                "type": "BULLISH",
                "price_low_1": p1_val,
                "price_low_2": p2_val,
                "rsi_low_1": r1_val,
                "rsi_low_2": r2_val,
                "bar_index": p2_idx
            })

    # Find swing highs for bearish divergence
    price_highs = find_swing_highs(df["close"])
    rsi_highs = find_swing_highs(df["rsi"])

    if len(price_highs) >= 2 and len(rsi_highs) >= 2:
        p1_idx, p1_val = price_highs[-2]
        p2_idx, p2_val = price_highs[-1]
        r1_idx, r1_val = rsi_highs[-2]
        r2_idx, r2_val = rsi_highs[-1]

        # Bearish: price higher high, RSI lower high
        if p2_val > p1_val and r2_val < r1_val:
            divergences.append({
                "type": "BEARISH",
                "price_high_1": p1_val,
                "price_high_2": p2_val,
                "rsi_high_1": r1_val,
                "rsi_high_2": r2_val,
                "bar_index": p2_idx
            })

    return divergences

Step 5: Scan Multiple Pairs

Python Step 5: Scan Multiple Pairs
# Scan for divergence across multiple pairs and timeframes
pairs = ["EURUSD", "GBPUSD", "USDJPY", "XAUUSD", "BTCUSD"]
timeframes = ["H1", "H4"]

for pair in pairs:
    for tf in timeframes:
        df = fetch_data(pair, tf, 100)
        divs = detect_divergence(df)
        for d in divs:
            print(f"{pair} ({tf}): {d['type']} divergence detected!")
            if d["type"] == "BULLISH":
                print(f"  Price: {d['price_low_1']:.5f} -> {d['price_low_2']:.5f} (lower)")
                print(f"  RSI:   {d['rsi_low_1']:.1f} -> {d['rsi_low_2']:.1f} (higher)")
            else:
                print(f"  Price: {d['price_high_1']:.5f} -> {d['price_high_2']:.5f} (higher)")
                print(f"  RSI:   {d['rsi_high_1']:.1f} -> {d['rsi_high_2']:.1f} (lower)")

Improving Accuracy

Filter by RSI Zone

Bullish divergence is more reliable when RSI is below 40. Bearish divergence is more reliable when RSI is above 60. Divergence in the middle zone is often noise.

Require Proximity

The two swing points should be within 5-30 bars of each other. Divergence between points 100 bars apart is rarely actionable.

Confirm with MACD

Check if MACD histogram also shows divergence. When both RSI and MACD diverge, the signal is significantly stronger. A single /v1/indicators call returns both RSI_14 and MACD_hist.

Use Higher Timeframes

Divergence on H4 and D1 is far more reliable than M5 or M15. The signal-to-noise ratio improves dramatically on higher timeframes.

Further Reading

Run this against live data.

Every account starts pay-as-you-go with $2.50 of credit and no card. Paste the key into the samples above and the requests work unchanged.

Prefer the API to the article

Build it instead of reading about it.

Create a key, run the first request, then extend one layer at a time. Every account starts pay-as-you-go with $2.50 of credit — no card required.