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
- Prepare closing prices as a pandas Series (no missing timestamps within your bar interval).
- Compute deltas with Series.diff().
- Compute gains and losses via clip: gain = max(delta, 0), loss = max(-delta, 0).
- Apply ewm(alpha=1/period, adjust=False, min_periods=period) to both gain and loss.
- Compute RS and then RSI = 100 − 100/(1 + RS).
- Handle edge cases where avg_loss is zero; cap to [0, 100] if needed.
- Generate signals using threshold crossovers (e.g., 30/70) with shift(1) to avoid lookahead.
- 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:
Parameter | Typical | Effect |
---|---|---|
period | 14 | Shorter reacts faster; longer reduces noise |
oversold | 30 | Higher gives earlier entries; more false signals |
overbought | 70 | Lower 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.