Skip to content

RSI Reversion Strategy

Mean reversion using the Relative Strength Index.

Strategy Overview

Use RSI to identify overbought and oversold conditions. Buy when RSI is low (oversold), short when RSI is high (overbought).

The RSI Indicator

Text Only
RSI = 100 - (100 / (1 + RS))
RS = Average Gain / Average Loss
  • RSI < 30: Oversold (buy signal)
  • RSI > 70: Overbought (sell signal)

The Signal

Text Only
signal rsi:
  // Calculate RSI
  change = diff(prices, 1)
  gains = where(change > 0, change, 0)
  losses = where(change < 0, -change, 0)

  avg_gain = ema(gains, 14)
  avg_loss = ema(losses, 14)

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

  emit rsi

signal rsi_reversion:
  // Buy oversold, short overbought
  reversion = 50 - rsi  // Positive when oversold

  emit zscore(reversion)

Complete Strategy

Text Only
data:
  source = "prices_with_sectors.parquet"
  format = parquet

// RSI calculation
signal rsi_14:
  change = diff(prices, 1)
  gains = where(change > 0, change, 0)
  losses = where(change < 0, -change, 0)

  avg_gain = ema(gains, 14)
  avg_loss = ema(losses, 14)

  rs = avg_gain / (avg_loss + 0.0001)
  emit 100 - (100 / (1 + rs))

// RSI reversion signal
signal rsi_reversion:
  rsi = rsi_14

  // Distance from neutral (50)
  deviation = 50 - rsi

  // Sector neutralize
  emit neutralize(zscore(deviation), by=sectors)

portfolio rsi_strategy:
  weights = rank(rsi_reversion).long_short(
    top = 0.2,
    bottom = 0.2,
    cap = 0.03
  )

  constraints:
    gross_exposure = 2.0
    net_exposure = 0.0
    max_sector = 0.20

  costs = tc.bps(10)

  backtest rebal=5 from 2015-01-01 to 2024-12-31

Variations

Different Periods

Text Only
// Shorter RSI (faster signals)
signal rsi_7:
  change = diff(prices, 1)
  gains = where(change > 0, change, 0)
  losses = where(change < 0, -change, 0)
  avg_gain = ema(gains, 7)
  avg_loss = ema(losses, 7)
  rs = avg_gain / (avg_loss + 0.0001)
  emit 100 - (100 / (1 + rs))

// Longer RSI (slower signals)
signal rsi_21:
  change = diff(prices, 1)
  gains = where(change > 0, change, 0)
  losses = where(change < 0, -change, 0)
  avg_gain = ema(gains, 21)
  avg_loss = ema(losses, 21)
  rs = avg_gain / (avg_loss + 0.0001)
  emit 100 - (100 / (1 + rs))

Multi-Timeframe RSI

Text Only
signal multi_rsi:
  rsi_7 = rsi_short
  rsi_14 = rsi_medium
  rsi_21 = rsi_long

  // Combine timeframes
  avg_rsi = (rsi_7 + rsi_14 + rsi_21) / 3

  emit zscore(50 - avg_rsi)

Extreme RSI Only

Text Only
signal extreme_rsi:
  rsi = rsi_14

  // Only trade at extremes
  extreme_low = rsi < 30
  extreme_high = rsi > 70

  signal = where(extreme_low, 50 - rsi,
           where(extreme_high, 50 - rsi, 0))

  emit zscore(signal)

RSI Divergence

Text Only
signal rsi_divergence:
  rsi = rsi_14

  // Price making new lows but RSI not
  price_low = prices == rolling_min(prices, 20)
  rsi_not_low = rsi > rolling_min(rsi, 20)
  bullish_div = price_low and rsi_not_low

  // Price making new highs but RSI not
  price_high = prices == rolling_max(prices, 20)
  rsi_not_high = rsi < rolling_max(rsi, 20)
  bearish_div = price_high and rsi_not_high

  signal = where(bullish_div, 1,
           where(bearish_div, -1, 0))

  emit zscore(signal)

With Trend Filter

Text Only
signal filtered_rsi:
  rsi = rsi_14
  reversion = 50 - rsi

  // Trend filter
  ma_50 = rolling_mean(prices, 50)
  ma_200 = rolling_mean(prices, 200)
  uptrend = ma_50 > ma_200

  // In uptrend, only take long signals
  // In downtrend, only take short signals
  signal = where(uptrend and reversion > 0, reversion,
           where(not(uptrend) and reversion < 0, reversion,
           reversion * 0.3))

  emit zscore(signal)

Stochastic RSI

Text Only
signal stoch_rsi:
  rsi = rsi_14

  // Stochastic of RSI
  rsi_min = rolling_min(rsi, 14)
  rsi_max = rolling_max(rsi, 14)

  stoch = (rsi - rsi_min) / (rsi_max - rsi_min + 0.0001)

  // Buy when stoch RSI low, sell when high
  emit zscore(0.5 - stoch)

Expected Results

Text Only
Backtest Results: rsi_reversion
===============================
Period: 2015-01-01 to 2024-12-31

Returns:
  Total Return: 48%
  Annual Return: 4.2%
  Annual Volatility: 7.8%
  Sharpe Ratio: 0.54

Turnover:
  Annual Turnover: 480%
  Avg Holding Period: 12 days

Signal Quality:
  Avg RSI (longs): 38.2
  Avg RSI (shorts): 62.5

Risk Considerations

RSI Can Stay Extreme

In strong trends, RSI can stay overbought/oversold:

Text Only
// Add time limit
signal time_limited:
  rsi = rsi_14
  days_extreme = ts_sum(rsi < 30 or rsi > 70, 20)

  // If extreme too long, reduce confidence
  extended = days_extreme > 10

  base_signal = 50 - rsi
  emit where(extended, base_signal * 0.5, base_signal)

See Also