Skip to content
Back to App

RSI Strategies

The Relative Strength Index (RSI) is a momentum oscillator that measures the speed and magnitude of recent price changes. It oscillates between 0 and 100, giving you a clear signal when an asset is potentially overbought or oversold. In this tutorial, you will implement RSI from scratch and build two trading strategies around it.

How RSI Works

RSI compares the magnitude of recent gains to recent losses over a lookback period (typically 14 bars):

Step 1: Calculate Price Changes — For each bar, compute the change from the previous close:

change = close[i] - close[i - 1]

Step 2: Separate Gains and Losses

gain = max(change, 0) # Positive changes only
loss = abs(min(change, 0)) # Absolute value of negative changes

Step 3: Calculate Average Gain and Average Loss — For the first calculation, use a simple average over the period. For subsequent bars, use exponential smoothing:

First Average Gain = Sum of Gains over period / period
First Average Loss = Sum of Losses over period / period
Subsequent:
Avg Gain = (Prev Avg Gain * (period - 1) + Current Gain) / period
Avg Loss = (Prev Avg Loss * (period - 1) + Current Loss) / period

Step 4: Calculate RS and RSI

RS = Average Gain / Average Loss
RSI = 100 - (100 / (1 + RS))

RSI Implementation in Python

Here is a clean, efficient RSI class you can use in any strategy:

class RSI:
def __init__(self, period=14):
self.period = period
self.value = None
self._prev_close = None
self._avg_gain = None
self._avg_loss = None
self._gains = []
self._losses = []
self._count = 0
def update(self, close):
if self._prev_close is not None:
change = close - self._prev_close
gain = max(change, 0)
loss = abs(min(change, 0))
if self._avg_gain is None:
# Still collecting initial data
self._gains.append(gain)
self._losses.append(loss)
if len(self._gains) == self.period:
# First RSI calculation: simple average
self._avg_gain = sum(self._gains) / self.period
self._avg_loss = sum(self._losses) / self.period
self._gains = None # Free memory
self._losses = None
if self._avg_loss == 0:
self.value = 100.0
else:
rs = self._avg_gain / self._avg_loss
self.value = 100 - (100 / (1 + rs))
else:
# Smoothed average (Wilder's method)
self._avg_gain = (self._avg_gain * (self.period - 1) + gain) / self.period
self._avg_loss = (self._avg_loss * (self.period - 1) + loss) / self.period
if self._avg_loss == 0:
self.value = 100.0
else:
rs = self._avg_gain / self._avg_loss
self.value = 100 - (100 / (1 + rs))
self._prev_close = close
return self.value

Interpreting RSI Values

RSI RangeInterpretation
70-100Overbought — price has risen too fast, potential pullback
50-70Bullish momentum — uptrend is healthy
30-50Bearish momentum — downtrend pressure
0-30Oversold — price has fallen too fast, potential bounce

Strategy 1: RSI Overbought/Oversold

The simplest RSI strategy: buy when RSI drops below 30 (oversold) and then crosses back above it. Sell when RSI rises above 70 (overbought) and then crosses back below it.

#!/usr/bin/env python3
"""RSI Overbought/Oversold Strategy — TestMax Algo Playground"""
import os, time
ACCOUNT_ID = int(os.environ.get("ACCOUNT_ID", "0"))
CONTRACT_ID = os.environ.get("CONTRACT_ID", "")
TOTAL_BARS = int(os.environ.get("TOTAL_BARS", "5000"))
STEP_DELAY = float(os.environ.get("STEP_DELAY", "0.02"))
SPEED_FILE = os.environ.get("SPEED_FILE", "")
# Parameters
RSI_PERIOD = 14
OVERSOLD = 30
OVERBOUGHT = 70
print(f"[INFO] Strategy: RSI OB/OS ({RSI_PERIOD}) | Buy < {OVERSOLD}, Sell > {OVERBOUGHT}")
print("-" * 50)
# (Include the RSI class from above here)
rsi = RSI(RSI_PERIOD)
prev_rsi = None
position = None
entry_price = 0.0
trade_count = 0
wins = 0
try:
for i in range(TOTAL_BARS):
bar = get_next_bar()
if bar is None:
break
read_speed()
if STEP_DELAY > 0:
time.sleep(STEP_DELAY)
price = bar["c"]
rsi_val = rsi.update(price)
if rsi_val is None or prev_rsi is None:
prev_rsi = rsi_val
continue
# Buy: RSI crosses above oversold level (was below, now above)
if prev_rsi < OVERSOLD and rsi_val >= OVERSOLD and position is None:
result = place_order(ACCOUNT_ID, CONTRACT_ID, 0, 1)
if result.get("orderId"):
position = "LONG"
entry_price = price
print(f"[BUY] Bar {i} @ {price:.2f} | RSI: {rsi_val:.1f} (oversold bounce)")
# Sell: RSI crosses below overbought level (was above, now below)
elif prev_rsi > OVERBOUGHT and rsi_val <= OVERBOUGHT and position == "LONG":
result = place_order(ACCOUNT_ID, CONTRACT_ID, 1, 1)
if result.get("orderId"):
pnl = price - entry_price
trade_count += 1
if pnl > 0:
wins += 1
print(f"[SELL] Bar {i} @ {price:.2f} | RSI: {rsi_val:.1f} | P&L: {pnl:+.2f} pts")
position = None
# Short: RSI crosses below overbought level
elif prev_rsi > OVERBOUGHT and rsi_val <= OVERBOUGHT and position is None:
result = place_order(ACCOUNT_ID, CONTRACT_ID, 1, 1)
if result.get("orderId"):
position = "SHORT"
entry_price = price
print(f"[SHORT] Bar {i} @ {price:.2f} | RSI: {rsi_val:.1f} (overbought reversal)")
# Cover: RSI crosses above oversold level
elif prev_rsi < OVERSOLD and rsi_val >= OVERSOLD and position == "SHORT":
result = place_order(ACCOUNT_ID, CONTRACT_ID, 0, 1)
if result.get("orderId"):
pnl = entry_price - price
trade_count += 1
if pnl > 0:
wins += 1
print(f"[COVER] Bar {i} @ {price:.2f} | RSI: {rsi_val:.1f} | P&L: {pnl:+.2f} pts")
position = None
prev_rsi = rsi_val
finally:
try:
close_all_positions(ACCOUNT_ID, CONTRACT_ID)
except:
pass
account = get_account(ACCOUNT_ID)
balance = account["balance"] if account else 50000
print("-" * 50)
print(f"[DONE] Balance: ${balance:,.2f} | P&L: ${balance - 50000:+,.2f}")
print(f"[DONE] Trades: {trade_count} | Win Rate: {wins/trade_count*100:.1f}%" if trade_count else "[DONE] No trades")

Strategy 2: RSI + Trend Filter (EMA)

The pure overbought/oversold strategy gets crushed in trending markets because it fades the trend. The fix: add a trend filter. Only take long signals when the trend is up, and short signals when the trend is down.

#!/usr/bin/env python3
"""RSI + EMA Trend Filter Strategy — TestMax Algo Playground"""
import os, time
ACCOUNT_ID = int(os.environ.get("ACCOUNT_ID", "0"))
CONTRACT_ID = os.environ.get("CONTRACT_ID", "")
TOTAL_BARS = int(os.environ.get("TOTAL_BARS", "5000"))
STEP_DELAY = float(os.environ.get("STEP_DELAY", "0.02"))
SPEED_FILE = os.environ.get("SPEED_FILE", "")
# Parameters
RSI_PERIOD = 14
OVERSOLD = 35 # Slightly less extreme for trend-following
OVERBOUGHT = 65 # Slightly less extreme for trend-following
EMA_PERIOD = 50 # Trend filter
print(f"[INFO] Strategy: RSI ({RSI_PERIOD}) + EMA ({EMA_PERIOD}) Trend Filter")
print("-" * 50)
# (Include RSI and EMA classes here)
class EMA:
def __init__(self, period):
self.period = period
self.multiplier = 2 / (period + 1)
self.value = None
self._buffer = []
def update(self, price):
if self.value is None:
self._buffer.append(price)
if len(self._buffer) == self.period:
self.value = sum(self._buffer) / self.period
self._buffer = None
else:
self.value = (price - self.value) * self.multiplier + self.value
return self.value
rsi = RSI(RSI_PERIOD)
ema = EMA(EMA_PERIOD)
prev_rsi = None
position = None
entry_price = 0.0
trade_count = 0
wins = 0
try:
for i in range(TOTAL_BARS):
bar = get_next_bar()
if bar is None:
break
read_speed()
if STEP_DELAY > 0:
time.sleep(STEP_DELAY)
price = bar["c"]
rsi_val = rsi.update(price)
ema_val = ema.update(price)
if rsi_val is None or ema_val is None or prev_rsi is None:
prev_rsi = rsi_val
continue
trend_up = price > ema_val
trend_down = price < ema_val
# Long: trend is up AND RSI bounces from oversold
if trend_up and prev_rsi < OVERSOLD and rsi_val >= OVERSOLD and position is None:
result = place_order(ACCOUNT_ID, CONTRACT_ID, 0, 1)
if result.get("orderId"):
position = "LONG"
entry_price = price
print(f"[BUY] Bar {i} @ {price:.2f} | RSI: {rsi_val:.1f} | Uptrend dip buy")
# Short: trend is down AND RSI rejects from overbought
elif trend_down and prev_rsi > OVERBOUGHT and rsi_val <= OVERBOUGHT and position is None:
result = place_order(ACCOUNT_ID, CONTRACT_ID, 1, 1)
if result.get("orderId"):
position = "SHORT"
entry_price = price
print(f"[SHORT] Bar {i} @ {price:.2f} | RSI: {rsi_val:.1f} | Downtrend rally sell")
# Exit long: RSI reaches overbought OR trend flips
elif position == "LONG" and (rsi_val > OVERBOUGHT or trend_down):
result = place_order(ACCOUNT_ID, CONTRACT_ID, 1, 1)
if result.get("orderId"):
pnl = price - entry_price
trade_count += 1
if pnl > 0:
wins += 1
reason = "RSI overbought" if rsi_val > OVERBOUGHT else "Trend flip"
print(f"[EXIT] Bar {i} @ {price:.2f} | {reason} | P&L: {pnl:+.2f}")
position = None
# Exit short: RSI reaches oversold OR trend flips
elif position == "SHORT" and (rsi_val < OVERSOLD or trend_up):
result = place_order(ACCOUNT_ID, CONTRACT_ID, 0, 1)
if result.get("orderId"):
pnl = entry_price - price
trade_count += 1
if pnl > 0:
wins += 1
reason = "RSI oversold" if rsi_val < OVERSOLD else "Trend flip"
print(f"[EXIT] Bar {i} @ {price:.2f} | {reason} | P&L: {pnl:+.2f}")
position = None
prev_rsi = rsi_val
finally:
try:
close_all_positions(ACCOUNT_ID, CONTRACT_ID)
except:
pass
account = get_account(ACCOUNT_ID)
balance = account["balance"] if account else 50000
print("-" * 50)
print(f"[DONE] Balance: ${balance:,.2f} | P&L: ${balance - 50000:+,.2f}")
print(f"[DONE] Trades: {trade_count} | Win Rate: {wins/trade_count*100:.1f}%" if trade_count else "[DONE] No trades")

RSI Parameter Sensitivity

The RSI period dramatically affects signal behavior:

PeriodCharacter
7Very reactive, many signals, more noise
14Standard, balanced (Wilder’s original)
21Smoother, fewer signals, more lag
28Very smooth, only catches major moves

Similarly, the overbought/oversold thresholds matter:

  • 70/30 — standard, used in range-bound markets
  • 80/20 — stricter, fewer but stronger signals
  • 65/35 — looser, used with a trend filter (as in Strategy 2)
  • 60/40 — very loose, effectively a momentum filter

RSI Divergence (Advanced Concept)

Bullish divergence: Price makes a lower low, but RSI makes a higher low. This signals weakening selling pressure.

Bearish divergence: Price makes a higher high, but RSI makes a lower high. This signals weakening buying pressure.

Divergence detection requires tracking swing points in both price and RSI, making it significantly more complex to code. You will see a similar concept (market structure analysis) in the ICT Smart Money Strategy tutorial.

What’s Next