Signals·TA Strategy Implementations·Intermediate

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.

ta-strategy-implementationstrading-signals

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 plotly
Requirement 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.