RSI Mean Reversion — Code Walkthrough
Walk through the RSI calculation step by step: first-pass averages, Wilder's smoothing, and the oversold/overbought signal logic.
RSI Mean Reversion — Code Walkthrough
RSI (Relative Strength Index) was developed by J. Welles Wilder in 1978. This strategy buys when RSI dips into "oversold" territory and sells when it becomes "overbought" — betting that extreme moves will revert to the mean.
Step 1 — Configuration
export interface RSIConfig {
rsiPeriod: number; // lookback window (default 14)
oversoldThreshold: number; // buy signal below this (default 30)
overboughtThreshold: number; // sell signal above this (default 70)
stopLossPct: number; // 0.05 = -5% stop loss
takeProfitPct: number; // 0.15 = +15% take profit
}
export const defaultConfig: RSIConfig = {
rsiPeriod: 14,
oversoldThreshold: 30,
overboughtThreshold: 70,
stopLossPct: 0.05,
takeProfitPct: 0.15,
};Wilder's original parameters (14, 30, 70) are used. The 30/70 thresholds are not magic numbers — they were chosen so signals fire ~5% of the time in trending markets, balancing frequency with quality.
Step 2 — calculateRSI: Two Phases of Calculation
export function calculateRSI(closes: number[], period = 14): number[] {
const rsi: number[] = new Array(period).fill(NaN); // no RSI for first N bars
let avgGain = 0;
let avgLoss = 0;
// PHASE 1: Simple average over first 'period' bars
for (let i = 1; i <= period; i++) {
const change = closes[i] - closes[i - 1];
if (change > 0) avgGain += change;
else avgLoss += Math.abs(change);
}
avgGain /= period;
avgLoss /= period;Why Fill NaN for the First 14 Bars?
RSI requires 14 previous closes to compute. Before that, the value is mathematically undefined — not zero. Returning NaN makes it explicit so the signal generator can skip those bars safely.
// PHASE 2: Wilder's Smoothed Moving Average from bar N onward
for (let i = period; i < closes.length; i++) {
const change = closes[i] - closes[i - 1];
const gain = change > 0 ? change : 0;
const loss = change < 0 ? Math.abs(change) : 0;
// Wilder's smoothing: (prev_avg × 13 + today) / 14
avgGain = (avgGain * (period - 1) + gain) / period;
avgLoss = (avgLoss * (period - 1) + loss) / period;
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
rsi.push(100 - 100 / (1 + rs));
}
return rsi;
}Wilder's smoothing is a variation of EMA with multiplier 1/period instead of 2/(period+1). It gives more weight to history, making RSI less "jumpy" than a plain EMA approach. The formula 100 − 100/(1+RS) maps the gain/loss ratio to 0–100, approaching 100 when there are no losses and 0 when there are no gains.
Step 3 — generateSignals: Threshold Crossings
export function generateSignals(closes, config) {
const rsiValues = calculateRSI(closes, config.rsiPeriod);
return rsiValues.map((rsi, i) => {
if (isNaN(rsi)) return { index: i, signal: 0, rsi }; // skip warmup period
if (rsi < config.oversoldThreshold) return { index: i, signal: 1, rsi }; // BUY
if (rsi > config.overboughtThreshold) return { index: i, signal: -1, rsi }; // SELL
return { index: i, signal: 0, rsi }; // HOLD
});
}Limitation: Level-Based, Not Crossing-Based
This signal fires every bar RSI is below 30, not just when it first crosses. In a deep oversold condition you'd get repeated BUY signals. Production strategies often check the "first cross" by comparing RSI[i-1] vs RSI[i] across the threshold, similar to MACD crossover logic.
- RSI measures the speed and magnitude of recent price changes on a 0–100 scale
- First 14 bars use a simple average gain/loss; subsequent bars use Wilder's smoothing
- RSI < 30 = oversold (potential reversal up); RSI > 70 = overbought (potential reversal down)
- Signal fires on the threshold crossing, not the extreme level