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.
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).
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,
"bars": bars
}, headers=HEADERS)
candles = resp.json()["data"]["candles"]
df = pd.DataFrame(candles)
df["timestamp"] = pd.to_datetime(df["timestamp"])
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:
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.
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
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
# 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. Fetch both with indicators=RSI_14,MACD.
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
Try this with live data
Every account gets $2.50 in free PAYG credits. No card required — paste your API key and run the code above against live broker data.
Keep reading
All articles- Tutorial 11 min read
24/7 Crypto Monitoring: Building Always-On Analysis Systems
Build a monitoring system that watches crypto markets around the clock, detects significant moves, and sends alerts when conditions match your criteria.
March 28, 2026
- Tutorial 12 min read
How to Build an AI Market Analyst That Runs 24/7
Build a production-ready AI market analyst that monitors forex and crypto markets around the clock, generates daily briefings, and alerts you to opportunities via Telegram.
March 28, 2026
- Tutorial 10 min read
Using ATR for Dynamic Stop-Loss Placement
Learn how to use Average True Range (ATR) to set volatility-adjusted stop losses that adapt to market conditions, with full code examples.
March 28, 2026