RSI

RSI (Relative Strength Index) measures the speed and magnitude of recent price changes, scaled to 0-100 — the standard "overbought / oversold" oscillator.

def rsi(series: pd.Series, period: int = 14) -> pd.Series:
    delta = series.diff()
    gain = delta.clip(lower=0)
    loss = -delta.clip(upper=0)

    avg_gain = gain.ewm(alpha=1/period, min_periods=period, adjust=False).mean()
    avg_loss = loss.ewm(alpha=1/period, min_periods=period, adjust=False).mean()

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

Why ewm(alpha=1/period), not a plain rolling mean — Wilder's smoothing

RSI's original (Wilder's) formulation uses a specific exponential smoothing (alpha = 1/period), not a simple rolling average of gains/losses. Using .rolling(period).mean() instead produces a noticeably different, non-standard RSI that won't match what your broker's chart or any standard library shows — this is a common source of "why doesn't my RSI match TradingView" confusion.

Standard signal levels — and why they're not universal constants

def rsi_signal(rsi_series: pd.Series, overbought: float = 70, oversold: float = 30) -> pd.Series:
    signal = pd.Series(0, index=rsi_series.index)
    signal[rsi_series > overbought] = -1   # potential reversal down
    signal[rsi_series < oversold] = 1       # potential reversal up
    return signal

70/30 are conventions, not laws of physics — a strongly trending instrument can stay above 70 (or below 30) for extended periods without reversing ("RSI staying overbought" is itself a trend-strength signal in a strong trend, not necessarily a sell signal). Consider adapting thresholds per instrument/regime rather than hardcoding 70/30 globally, and validate any threshold choice via backtesting (chapter 82), not convention alone.

RSI divergence — often more useful than the raw level

Price making a new high while RSI makes a *lower* high (bearish divergence), or price making a new low while RSI makes a *higher* low (bullish divergence), is a commonly cited early-reversal signal — chapter 116 builds a generic divergence-detection function that applies this same logic to RSI, MACD, or any oscillator.

Handling the zero-division edge case

def rsi_safe(series: pd.Series, period: int = 14) -> pd.Series:
    delta = series.diff()
    gain = delta.clip(lower=0)
    loss = -delta.clip(upper=0)
    avg_gain = gain.ewm(alpha=1/period, min_periods=period, adjust=False).mean()
    avg_loss = loss.ewm(alpha=1/period, min_periods=period, adjust=False).mean()
    rs = avg_gain / avg_loss.replace(0, 1e-10)   # avoid division by zero on a flat/all-gain stretch
    return 100 - (100 / (1 + rs))

A stretch of bars with zero losses (avg_loss == 0) produces a divide-by-zero without this guard — rare in practice on liquid instruments, but worth handling explicitly rather than letting it surface as a silent inf/NaN deep in a signal pipeline.

Next: 104 — Bollinger Bands