```html

RSI divergence signals momentum shifts before price confirms them. You can automate the detection across hundreds of stocks using Python's yfinance library for free historical data and pandas for the math. The scanner identifies when price makes a new high or low but RSI doesn't follow—a pattern that often precedes reversals. No paid APIs required.

Key Takeaways

  • This scanner uses yfinance for free historical price data and pandas for RSI calculation
  • Divergences appear when price swing highs/lows move opposite to RSI swing highs/lows
  • Output includes ticker symbol, divergence type, current price, and current RSI reading
Difficulty: Intermediate Time needed: 30–45 minutes For: Python users with basic pandas knowledge who want to build technical analysis tools

What RSI Divergence Actually Measures

RSI divergence occurs when price action and momentum decouple. Bullish divergence: price makes a lower low, RSI makes a higher low. Translation: selling pressure is weakening. Bearish divergence: price makes a higher high, RSI makes a lower high. Translation: buying pressure is fading.

The pattern appears frequently. Whether it's actionable depends on context. Divergences in strong trends often resolve as continuation, not reversal. Divergences near key support or resistance—combined with volume confirmation—carry more weight. This scanner automates detection. Interpretation still requires judgment.

What most coverage of divergence scanners misses: the hard part isn't calculating RSI. It's defining what constitutes a valid "swing" in noisy price data. Use too tight a window and every minor wiggle triggers a signal. Use too wide a window and you miss the divergence until it's already played out. The approach below balances sensitivity with reliability using a 5-bar swing detection window—tight enough to catch recent divergences, wide enough to filter noise.

What You Need

  • Python 3.8 or higher
  • pip package manager (included with standard Python installations)
  • Two libraries: yfinance and pandas (installation in Step 1)
  • A text editor or IDE (VS Code, PyCharm, or IDLE)
  • Internet connection for downloading historical stock data

Step 1: Install Required Libraries

Open terminal and run: pip install yfinance pandas

yfinance pulls historical price data from Yahoo Finance. pandas handles numerical calculations. Verify installation: python -c "import yfinance; import pandas"—no errors means you're ready.

Step 2: Set Up the Script Structure

Create rsi_divergence_scanner.py. Start with imports:

import yfinance as yf
import pandas as pd
import numpy as np

Define your ticker list. For testing: tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA']. Once the scanner works, expand to your full universe. Set lookback: lookback_days = 90 determines historical depth.

The script will loop through each ticker and analyze independently. That structure matters when you scale to 200+ symbols.

Step 3: Download Historical Price Data

For each ticker, use yfinance to pull daily OHLC data:

data = yf.download(ticker, period='3mo', interval='1d')

period='3mo' pulls three months—enough to calculate RSI and identify recent swing points. interval='1d' specifies daily bars. Store closing prices: close_prices = data['Close'].

Why three months? RSI divergence analysis requires sufficient history to establish meaningful swing highs and lows. Two weeks of data produces unreliable signals. Three months gives you 60+ trading days—adequate for 14-period RSI plus swing detection.

a close up of a computer screen with numbers on it
Photo by Compagnons / Unsplash

Step 4: Calculate RSI

RSI measures momentum on a 0-100 scale. Standard period: 14 days. The formula: average gain divided by average loss, normalized to 0-100.

def calculate_rsi(prices, period=14):
    delta = prices.diff()
    gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
    loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
    rs = gain / loss
    rsi = 100 - (100 / (1 + rs))
    return rsi

Apply to your close prices: rsi_values = calculate_rsi(close_prices). First 14 values will be NaN—expected behavior since RSI needs a full period.

Step 5: Identify Swing Highs and Swing Lows

A swing high: a price peak higher than surrounding bars. A swing low: a trough lower than surrounding bars. Use a 5-bar window (2 bars on each side):

def find_swings(series, window=5):
    highs = series[(series.shift(1) < series) & (series.shift(-1) < series)]
    lows = series[(series.shift(1) > series) & (series.shift(-1) > series)]
    return highs, lows

Run on both price and RSI:

price_highs, price_lows = find_swings(close_prices)
rsi_highs, rsi_lows = find_swings(rsi_values)

These swing points anchor your divergence detection. You need at least two recent swings to compare—no swings, no divergence signal.

Step 6: Detect Bullish Divergence

Bullish divergence requires two conditions: most recent price low is lower than the previous price low, AND most recent RSI low is higher than the previous RSI low.

if len(price_lows) >= 2 and len(rsi_lows) >= 2:
    recent_price_low = price_lows.iloc[-1]
    prior_price_low = price_lows.iloc[-2]
    recent_rsi_low = rsi_lows.iloc[-1]
    prior_rsi_low = rsi_lows.iloc[-2]
    if recent_price_low < prior_price_low and recent_rsi_low > prior_rsi_low:
        print(f"Bullish divergence in {ticker}")

Store results with ticker symbol, divergence type, current price, and current RSI. This logic identifies weakening selling pressure even as price continues lower.

Step 7: Detect Bearish Divergence

Bearish divergence is the inverse. Most recent price high is higher than the previous high, but most recent RSI high is lower than the previous high:

if len(price_highs) >= 2 and len(rsi_highs) >= 2:
    recent_price_high = price_highs.iloc[-1]
    prior_price_high = price_highs.iloc[-2]
    recent_rsi_high = rsi_highs.iloc[-1]
    prior_rsi_high = rsi_highs.iloc[-2]
    if recent_price_high > prior_price_high and recent_rsi_high < prior_rsi_high:
        print(f"Bearish divergence in {ticker}")

This warns that upward momentum is fading despite rising prices. Output specific price and RSI values for manual chart verification.

Step 8: Loop Through Tickers and Output Results

Wrap download, calculation, and divergence detection in a loop. Collect results in a list of dictionaries, convert to DataFrame:

results = []
for ticker in tickers:
    # download data, calculate RSI, detect divergences
    # append to results
df_results = pd.DataFrame(results)
print(df_results)

Final DataFrame columns: Ticker, Divergence_Type, Current_Price, Current_RSI. Export to CSV: df_results.to_csv('divergences.csv', index=False).

Common Problems and Fixes

Empty results for all tickers: Swing detection window may be too large for the lookback period. Three months with a 10-bar window often misses valid swings. Reduce to 3-5 bars.

yfinance returns no data: Yahoo Finance occasionally blocks requests or lacks data for certain symbols. Wrap yf.download() in try-except and skip failed tickers. One bad symbol shouldn't crash the entire scan.

RSI values all NaN: You're calculating RSI on a Series shorter than the period length. Download at least 30 days when using 14-day RSI. Check len(close_prices) before running the RSI function.

How to Use the Results

Run the scanner during market hours or shortly after close when yfinance data is current. Use a realistic ticker universe—scanning 500+ stocks hits rate limits. Batch large scans.

Add a timestamp column so you know when each scan ran. Validate divergences manually on a chart before acting—false signals occur frequently in choppy markets.

Filter by RSI level: bullish divergences near RSI 30 carry more weight than those near RSI 60. A divergence at oversold/overbought extremes suggests exhaustion. A divergence at midrange suggests noise.

When This Scanner Fails

Low-volatility consolidation produces few signals. Price and RSI move in tight ranges without forming clear swings. Strong trending markets often see divergences resolve as continuation rather than reversal—making them unreliable timing tools.

The scanner struggles with thinly traded stocks where price gaps create artificial swing points. It also requires confirmation: divergences work best combined with volume analysis, support/resistance levels, or sector rotation data.

Do not use this as a standalone trading system. It's a screening tool, not a buy/sell signal.

FAQ

How do I detect RSI divergence in Python without premium data?

Use yfinance to download free historical price data from Yahoo Finance, calculate RSI using pandas rolling window functions, then compare price swings versus RSI swings using conditional logic. No paid subscriptions or API keys required.

How do I calculate RSI in Python with pandas?

Calculate the 14-period average of up-days and down-days separately using .rolling(window=14).mean(), then apply the RSI formula: 100 - (100 / (1 + (avg_gain / avg_loss))). Use .diff() for daily changes, .where() to separate gains from losses, .rolling() for moving averages.

How do I scan for bullish divergence with yfinance?

Download historical data for your ticker list, calculate RSI for each, identify the two most recent price lows and RSI lows, then check if price made a lower low while RSI made a higher low. Loop through all tickers and collect those meeting the condition. Ensure you have enough historical data to establish valid swing points.

Can I backtest this divergence scanner?

Yes. Modify the script to scan historical dates by slicing the price and RSI Series at different endpoints. For each historical date, run the divergence logic and record results. Compare against forward price movement to measure signal accuracy. Store scan results with timestamps and calculate forward returns over 5, 10, or 20 trading days to assess predictive value.

```