KhueApps
Home/Python/RSI in Python for Algo Trading: Wilder’s Method and Signals

RSI in Python for Algo Trading: Wilder’s Method and Signals

Last updated: October 03, 2025

Overview

The Relative Strength Index (RSI) is a momentum oscillator that measures the speed and magnitude of recent price changes on a 0–100 scale. In trading systems, it’s commonly used to identify overbought (>70) and oversold (<30) conditions. The standard lookback is 14 periods with Wilder’s smoothing (RMA), not a simple moving average.

Formula (Wilder):

  • delta = close_t − close_{t−1}
  • gain = max(delta, 0), loss = max(−delta, 0)
  • avg_gain_t and avg_loss_t are Wilder-smoothed with alpha = 1/period
  • RS_t = avg_gain_t / avg_loss_t
  • RSI_t = 100 − 100 / (1 + RS_t)

Quickstart

  • Requirements: Python 3.9+, pandas, numpy
  • Data: a pandas Series of closing prices
  • Method: Use pandas ewm with alpha = 1/period to match Wilder’s smoothing

Install:

  • pip install pandas numpy

Minimal working example (vectorized Wilder RSI + simple signals)

import numpy as np
import pandas as pd

# 1) Create example price data (random walk)
rng = np.random.default_rng(42)
N = 1000
rets = rng.normal(0, 0.001, N)
prices = 100 * np.exp(np.cumsum(rets))
df = pd.DataFrame({"Close": prices})

# 2) RSI (Wilder) implementation
def rsi_wilder(close: pd.Series, period: int = 14) -> pd.Series:
    """Vectorized RSI using Wilder's smoothing (RMA).
    Returns a Series aligned to 'close' with NaNs during warm-up.
    """
    delta = close.diff()
    gain = delta.clip(lower=0.0)
    loss = (-delta).clip(lower=0.0)

    # Wilder's RMA via EWM with alpha=1/period
    avg_gain = gain.ewm(alpha=1/period, adjust=False, min_periods=period).mean()
    avg_loss = loss.ewm(alpha=1/period, adjust=False, min_periods=period).mean()

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

    # Edge cases: if avg_loss is 0 => RSI = 100; if both 0 => RSI = 50
    rsi = rsi.mask(avg_loss == 0, 100.0)
    rsi = rsi.mask((avg_loss == 0) & (avg_gain == 0), 50.0)

    return rsi

# 3) Compute RSI and simple signals (cross above 30 -> long; cross below 70 -> flat)
df["RSI14"] = rsi_wilder(df["Close"], period=14)

rsi = df["RSI14"]
entry = (rsi.shift(1) <= 30) & (rsi > 30)
exit_ = (rsi.shift(1) >= 70) & (rsi < 70)

signal = pd.Series(np.nan, index=df.index)
signal = signal.mask(entry, 1).mask(exit_, 0)
position = signal.ffill().fillna(0)  # 0 or 1

# 4) Naive backtest on next-bar returns (no fees/slippage)
ret = df["Close"].pct_change()
strategy_ret = position.shift(1) * ret  # act next bar
cum = (1 + strategy_ret.fillna(0)).cumprod()

print(df[["Close", "RSI14"]].tail())
print("Final equity:", float(cum.iloc[-1]))

Step-by-step

  1. Prepare closing prices as a pandas Series (no missing timestamps within your bar interval).
  2. Compute deltas with Series.diff().
  3. Compute gains and losses via clip: gain = max(delta, 0), loss = max(-delta, 0).
  4. Apply ewm(alpha=1/period, adjust=False, min_periods=period) to both gain and loss.
  5. Compute RS and then RSI = 100 − 100/(1 + RS).
  6. Handle edge cases where avg_loss is zero; cap to [0, 100] if needed.
  7. Generate signals using threshold crossovers (e.g., 30/70) with shift(1) to avoid lookahead.
  8. Evaluate performance using next-bar returns and proper position lagging.

Interpretation and parameters

  • Typical period: 14. Lower => faster, more signals; higher => smoother, fewer signals.
  • Thresholds: 30/70 are common. Alternatives: 20/80 for stricter extremes.
  • Source: close-to-close RSI is standard.

Parameter cheat sheet:

ParameterTypicalEffect
period14Shorter reacts faster; longer reduces noise
oversold30Higher gives earlier entries; more false signals
overbought70Lower exits sooner; tighter risk control

Pitfalls to avoid

  • Lookahead bias: Always use shift(1) when converting RSI into trades executed on the next bar.
  • Warm-up NaNs: First period−1 RSI values are NaN; don’t use them for signals/backtests.
  • Dividing by zero: Flat series can yield avg_loss = 0. Handle with masks (100 or 50 when both avg_gain and avg_loss are 0).
  • Inconsistent smoothing: Wilder’s RMA is not the same as SMA; use ewm(alpha=1/period, adjust=False).
  • Data gaps: Missing bars distort deltas; forward-filling prices can create artificial zero deltas.
  • Multiple timeframes: Compute RSI on the intended timeframe; resample/aggregate before RSI to avoid mixed bars.

Performance notes

  • Vectorization: The pandas EWM approach is O(n) and fast for typical equities/crypto time series.
  • Memory: RSI stores only a few Series; fits millions of rows on modern machines.
  • Faster paths:
    • Use NumPy arrays and numba for JIT speedups if processing many symbols.
    • Precompute deltas once per symbol; reuse across indicators.
  • Streaming/real-time: After warm-up, Wilder averages update in O(1) per tick using the recurrence: avg_t = (avg_{t−1}*(period−1) + value_t)/period.

Variations

  • SMA-based RSI: Replace EWM with rolling(period).mean() for avg_gain/avg_loss; it’s not Wilder’s method and behaves differently.
  • Different sources: Some use typical price (H+L+C)/3; if you do, stay consistent across backtests and live trading.

Tiny FAQ

  • Q: Why is my RSI flat at 100 or 0? A: Likely avg_loss or avg_gain is zero over the window (one-sided price moves). Handle with masks and check for flat data.

  • Q: My results differ from another platform. Why? A: Differences often stem from smoothing (Wilder vs SMA), handling of initial values, or data alignment/NaNs.

  • Q: Can I use RSI on intraday data? A: Yes. Ensure consistent session handling and no missing bars; re-run backtests with realistic fees/slippage.

  • Q: Should I smooth RSI again (e.g., RSI of RSI)? A: You can, but test thoroughly. Extra smoothing can reduce responsiveness and may overfit.

  • Q: What period should I choose? A: Start with 14, then tune with walk-forward or cross-validation. Favor stability over marginal backtest gains.

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

Series: Algorithmic Trading with Python

Python