RSI Mean Reversion
Build an RSI mean reversion strategy that identifies overbought and oversold market conditions for counter-trend mean-reverting entries, with configurable threshold levels and confluence confirmation filters to avoid trend-fading.
Strategy — RSI Mean Reversion
1–2. Installation and Imports
import warnings; warnings.filterwarnings("ignore")
import pandas as pd
import numpy as np
import plotly.graph_objects as go
!pip install pandas numpy plotlyRequirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2) Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2) Requirement already satisfied: plotly in /usr/local/lib/python3.12/dist-packages (5.24.1) Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0) Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2) Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.1) Requirement already satisfied: tenacity>=6.2.0 in /usr/local/lib/python3.12/dist-packages (from plotly) (9.1.4) Requirement already satisfied: packaging in /usr/local/lib/python3.12/dist-packages (from plotly) (26.1) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
3. Strategy 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.
RSI formula:
RSI = 100 − (100 / (1 + RS)) where RS = Average Gain / Average Loss
over the lookback period.
Mean reversion logic:
- RSI < 30 (oversold): Price has declined too far, too fast relative to recent history. Mean reversion predicts a bounce upward → Buy (+1).
- RSI > 70 (overbought): Price has risen too far, too fast. Mean reversion predicts a pullback downward → Sell (−1).
- 30 ≤ RSI ≤ 70 → No signal (0): Price is within its normal oscillation range.
Why it works: Markets frequently overshoot fair value in both directions due to emotional trading and short-term liquidity imbalances. RSI quantifies this overshoot by comparing the magnitude of recent gains to recent losses. Extreme readings indicate a statistically unusual condition that tends to self-correct — the mean reversion trade bets on this correction.
Limitations: RSI mean reversion fails in strongly trending markets. A strongly trending asset can remain overbought (RSI > 70) for extended periods, causing repeated false sell signals. RSI is most effective in ranging or oscillating market conditions.
4. Data Generation
def generate_data(periods: int) -> pd.DataFrame:
start_date = pd.to_datetime("2024-01-01 00:00:00+00:00")
datetime_index = pd.date_range(start_date, periods=periods, freq="1min", tz="UTC")
price_data = []; last_close = 42000
for i in range(periods):
open_price = last_close + np.random.normal(0, last_close * 0.0005)
close_price = open_price + np.random.normal(0, last_close * 0.005)
body_high = max(open_price, close_price)
body_low = min(open_price, close_price)
high_price = max(body_high + abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
low_price = min(body_low - abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
if high_price < low_price: high_price, low_price = low_price, high_price
price_data.append({"open": max(1,int(open_price)), "high": max(1,int(high_price)),
"low": max(1,int(low_price)), "close": max(1,int(close_price))})
last_close = close_price
df = pd.DataFrame(price_data, index=datetime_index)
df.index.name = "datetime"
df["volume"] = np.random.uniform(100.0, 500.0, periods)
df["datetime"] = df.index.to_series()
return df.reset_index(drop=True)
df = generate_data(500)
display(df.head())| open | high | low | close | volume | datetime | |
|---|---|---|---|---|---|---|
| 0 | 41963 | 42509 | 41830 | 42410 | 132.260108 | 2024-01-01 00:00:00+00:00 |
| 1 | 42434 | 42538 | 42113 | 42261 | 112.938085 | 2024-01-01 00:01:00+00:00 |
| 2 | 42309 | 42332 | 42222 | 42283 | 218.313748 | 2024-01-01 00:02:00+00:00 |
| 3 | 42307 | 42541 | 42238 | 42296 | 338.896291 | 2024-01-01 00:03:00+00:00 |
| 4 | 42325 | 42599 | 42301 | 42555 | 363.424570 | 2024-01-01 00:04:00+00:00 |
5. Strategy Function
def rsi_mean_reversion(
df: pd.DataFrame,
rsi_period: int = 14,
oversold: float = 30.0,
overbought: float = 70.0,
) -> pd.DataFrame:
df = df.copy().sort_values("datetime", ignore_index=True)
# Calculate price changes (delta)
delta = df["close"].diff()
# Calculate gains (positive changes) and losses (negative changes)
# .clip(lower=0) sets negative values to 0
# .clip(upper=0) sets positive values to 0, then we take the absolute value
gain = delta.clip(lower=0).rolling(rsi_period).mean()
loss = (-delta.clip(upper=0)).rolling(rsi_period).mean()
# Calculate Relative Strength (RS). Replace 0 in loss with NaN to avoid division by zero.
rs = gain / loss.replace(0, np.nan)
# Calculate RSI using the standard formula
df["rsi"] = 100 - (100 / (1 + rs))
# Generate trading signals based on oversold and overbought thresholds
# 1 for buy (RSI < oversold), -1 for sell (RSI > overbought), 0 for no signal
df["signal"] = np.where(df["rsi"] < oversold, 1,
np.where(df["rsi"] > overbought, -1, 0))
return df
df_signals = rsi_mean_reversion(df, rsi_period=14, oversold=30.0, overbought=70.0)
print("--- Signal Distribution ---")
print(df_signals["signal"].value_counts())
print("\n--- RSI Statistics ---")
print(df_signals["rsi"].describe().round(2))--- Signal Distribution --- signal 0 363 1 75 -1 62 Name: count, dtype: int64 --- RSI Statistics --- count 486.00 mean 49.58 std 17.45 min 8.88 25% 37.14 50% 50.24 75% 61.06 max 97.62 Name: rsi, dtype: float64
Explanation:
delta.clip(lower=0): Isolates only positive price changes (gains) — negative changes become zero.(-delta.clip(upper=0)): Isolates only the magnitude of negative price changes (losses) — positive changes become zero.- Rolling mean of gains and losses over
rsi_period: Provides a smoothed estimate of average up-move and down-move strength. RS = gain / loss: The ratio of average gains to average losses. An RS of 2 means the asset has been gaining twice as hard as it has been losing — reflected as RSI ≈ 67.- Extreme RSI values trigger the mean reversion signal — the assumption that extremes in the gain/loss ratio are unsustainable.
6. Visualization
from plotly.subplots import make_subplots
buy_signals = df_signals[df_signals["signal"] == 1]
sell_signals = df_signals[df_signals["signal"] == -1]
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
subplot_titles=["Price + Signals", "RSI (14)"],
row_heights=[0.65, 0.35])
fig.add_trace(go.Candlestick(
x=df_signals["datetime"],
open=df_signals["open"], high=df_signals["high"],
low=df_signals["low"], close=df_signals["close"],
name="Price"), row=1, col=1)
fig.add_trace(go.Scatter(
x=buy_signals["datetime"], y=buy_signals["low"] * 0.999,
mode="markers", marker=dict(symbol="triangle-up", size=10, color="green"), name="Buy (+1)"),
row=1, col=1)
fig.add_trace(go.Scatter(
x=sell_signals["datetime"], y=sell_signals["high"] * 1.001,
mode="markers", marker=dict(symbol="triangle-down", size=10, color="red"), name="Sell (−1)"),
row=1, col=1)
fig.add_trace(go.Scatter(
x=df_signals["datetime"], y=df_signals["rsi"],
mode="lines", name="RSI (14)",
line=dict(color="purple", width=1)), row=2, col=1)
fig.add_hline(y=70, line_dash="dash", line_color="red", row=2, col=1, annotation_text="Overbought 70")
fig.add_hline(y=30, line_dash="dash", line_color="green", row=2, col=1, annotation_text="Oversold 30")
fig.update_layout(
title_text="RSI Mean Reversion Strategy",
xaxis_rangeslider_visible=False,
height=700, yaxis=dict(autorange=True),
xaxis2_title="Datetime", yaxis_title="Price", yaxis2_title="RSI",
)
fig.show()Explanation: The lower panel displays RSI with overbought (70) and oversold (30) reference lines. Buy signals appear on the price chart when RSI crosses below 30; sell signals appear when RSI crosses above 70. The subplot layout allows direct visual correlation between RSI extremes and the corresponding signal markers on the price chart.
Conclusion
The RSI Mean Reversion strategy leverages the Relative Strength Index (RSI) to identify overbought and oversold market conditions. When RSI drops below 30, a buy signal is generated, anticipating an upward correction. Conversely, when RSI rises above 70, a sell signal is triggered, expecting a downward pullback. This strategy is based on the premise that markets often overshoot fair value, and extreme RSI readings indicate a statistically unusual condition prone to self-correction.
While effective in ranging or oscillating markets, it's crucial to acknowledge the strategy's primary limitation: its performance in strongly trending markets. A sustained trend can lead to prolonged periods of overbought or oversold RSI values, resulting in false signals. Therefore, this strategy is best applied in contexts where assets tend to revert to their mean rather than exhibit strong, continuous directional movements.