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 onlyloss = abs(min(change, 0)) # Absolute value of negative changesStep 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 / periodFirst Average Loss = Sum of Losses over period / period
Subsequent:Avg Gain = (Prev Avg Gain * (period - 1) + Current Gain) / periodAvg Loss = (Prev Avg Loss * (period - 1) + Current Loss) / periodStep 4: Calculate RS and RSI
RS = Average Gain / Average LossRSI = 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.valueInterpreting RSI Values
| RSI Range | Interpretation |
|---|---|
| 70-100 | Overbought — price has risen too fast, potential pullback |
| 50-70 | Bullish momentum — uptrend is healthy |
| 30-50 | Bearish momentum — downtrend pressure |
| 0-30 | Oversold — 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", "")
# ParametersRSI_PERIOD = 14OVERSOLD = 30OVERBOUGHT = 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 = Noneposition = Noneentry_price = 0.0trade_count = 0wins = 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", "")
# ParametersRSI_PERIOD = 14OVERSOLD = 35 # Slightly less extreme for trend-followingOVERBOUGHT = 65 # Slightly less extreme for trend-followingEMA_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 = Noneposition = Noneentry_price = 0.0trade_count = 0wins = 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:
| Period | Character |
|---|---|
| 7 | Very reactive, many signals, more noise |
| 14 | Standard, balanced (Wilder’s original) |
| 21 | Smoother, fewer signals, more lag |
| 28 | Very 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
- Breakout Trading — capture momentum moves when price breaks through support or resistance
- Risk Management in Code — add stop losses and daily limits to protect your capital
- Multi-Indicator Strategies — combine RSI with moving averages and volume for confluence