Back to Blog
Python & Code

Building RSI from Scratch in Python (No Libraries)

QFQuantForge Team·April 3, 2026·9 min read

The Relative Strength Index is the most commonly referenced indicator in crypto trading. It appears in 12 of our 40 registered strategies, either as the primary signal or as a confirmation filter. But most traders use RSI as a black box, calling a library function without understanding what it actually computes. Building it from scratch takes about 30 lines of Python and gives you insight into why certain parameter choices matter.

What RSI Actually Measures

RSI quantifies the ratio of recent upward price movements to the total magnitude of recent price movements. It outputs a value between 0 and 100. High values (above 70, traditionally) suggest a string of recent gains, and low values (below 30) suggest a string of recent losses.

The formula has three steps: calculate gains and losses, smooth them, then compute the ratio.

Step 1: Gain and Loss Separation

Starting from a series of closing prices, compute the change between consecutive closes, then separate positive changes (gains) from negative changes (losses).

def compute_rsi(closes, period=14):
    deltas = [closes[i] - closes[i-1] for i in range(1, len(closes))]
    gains = [d if d > 0 else 0.0 for d in deltas]
    losses = [-d if d < 0 else 0.0 for d in deltas]

Note that losses are stored as positive numbers. A price drop from 100 to 95 produces a delta of -5, a gain of 0, and a loss of 5. This is important because the smoothing step averages gains and losses separately, and you do not want negative values in the loss series.

Step 2: Wilder's Smoothing

This is where RSI differs from a simple moving average. Welles Wilder specified a particular smoothing method (now called Wilder's smoothing or exponential moving average with alpha = 1/period) that gives more weight to recent values.

The first average is a simple mean of the first N periods. After that, each subsequent value uses the recursive formula: new_avg = (prev_avg * (period - 1) + current_value) / period.

    avg_gain = sum(gains[:period]) / period
    avg_loss = sum(losses[:period]) / period
    rsi_values = []
    for i in range(period, len(gains)):
        avg_gain = (avg_gain * (period - 1) + gains[i]) / period
        avg_loss = (avg_loss * (period - 1) + losses[i]) / period

This is not a simple moving average (SMA), which weights all values equally. It is not a standard EMA either, which uses alpha = 2 / (period + 1). Wilder's smoothing uses alpha = 1 / period, which decays more slowly. A 14-period Wilder smooth is roughly equivalent to a 27-period EMA in terms of effective lookback.

This matters for trading because the smoothing determines how quickly RSI reacts to price changes. A faster smoothing means RSI whips between oversold and overbought more frequently, generating more signals but also more false signals.

Step 3: The RS Ratio and Final RSI

The Relative Strength (RS) is the ratio of average gain to average loss. RSI converts this to a 0-100 scale.

        if avg_loss == 0:
            rsi_values.append(100.0)
        else:
            rs = avg_gain / avg_loss
            rsi_values.append(100.0 - (100.0 / (1.0 + rs)))
    return rsi_values

When average loss is zero (a period of nothing but gains), RSI is 100. When average gain is zero, RSI is 0. In practice, extreme values (above 95 or below 5) are rare for any period longer than 7.

Why We Use Period 10 in Crypto

Wilder developed RSI in 1978 for daily stock charts. The 14-period default was designed for a market that trades 5 days per week with overnight closes. Crypto markets run 24/7 with no closing bell.

A 14-period RSI on 15-minute crypto candles is looking back 3.5 hours. On daily stock candles, it covers nearly 3 trading weeks. The information content is very different. We found through parameter sweeps across our 25-symbol universe that period 10 consistently outperforms period 14 for 15-minute crypto strategies.

Our momentum_rsi_macd strategy uses rsi_period=10 as its sweep-winner parameter. This was discovered through a grid search testing periods 7, 10, 12, 14, and 20 across all 13 high-beta altcoins. Period 10 produced the highest average Sharpe ratio (5.2 across symbols) while period 14 came in at 3.8.

The intuition: crypto moves faster than equities. A shorter RSI period reacts faster to momentum shifts, which matters in a market where a 10% move in SHIB can happen in two hours. Period 14 is too sluggish, generating signals after the move is already well underway.

For 4-hour candles, the story changes. Our momentum_rsi_macd_4h strategy also uses rsi_period=10, but it adjusts the thresholds. Instead of the classic 30/70 overbought/oversold levels, we use 35/65. At the 4h timeframe, waiting for RSI to reach 30 means you are already deep in a selloff. Relaxing to 35 catches entries earlier in the reversal, which our validation showed improves Sharpe from 1.3 to 2.4 on average across BTC, ETH, SOL, ADA, SHIB, and AVAX.

The pandas-ta Shortcut

In production, we do not use our from-scratch implementation. We use pandas-ta, which implements Wilder's smoothing correctly and handles edge cases (all-NaN inputs, insufficient data length).

import pandas_ta as ta
df["rsi"] = ta.rsi(df["close"], length=10)

One line replaces our 30-line implementation. The output is identical to the penny (we verified this during development). The advantage of the library is that it handles the NaN padding at the beginning of the series, integrates with pandas DataFrames, and has been tested by thousands of users.

But understanding the from-scratch version matters for three reasons.

First, debugging. When your RSI values look wrong, you need to know whether the issue is in the smoothing method, the gain/loss separation, or the period parameter. Without understanding the internals, you are debugging a black box.

Second, customization. Our RSI divergence strategy (rsi_divergence) computes RSI on both price and volume, then looks for disagreements. The standard library computes RSI on a single series. Knowing the formula lets you apply it to any data.

Third, testing. Our backtest engine compares library outputs against hand-calculated values for known inputs. If a library update changes the smoothing behavior (it has happened with other indicator libraries), our tests catch it immediately.

Common RSI Mistakes

There are several patterns we see traders get wrong with RSI.

Using SMA instead of Wilder's smoothing produces a visually similar but numerically different indicator. The SMA version reacts more slowly and produces different crossover timings. If you are comparing your RSI to TradingView's, make sure you are using Wilder's method. TradingView uses Wilder's by default.

Treating RSI as a standalone signal without context produces terrible results. RSI below 30 does not mean "buy." In a strong downtrend, RSI can stay below 30 for days. Our mean reversion strategy combines RSI with Bollinger Band position: price must be below the lower band AND RSI must be oversold. The double confirmation reduces false signals by roughly 60% compared to RSI alone in our backtests.

Optimizing RSI thresholds without validation leads to overfitting. We tested RSI overbought thresholds from 60 to 80 in steps of 5. The optimal value varies by symbol, timeframe, and market regime. Picking the single best value from a backtest and assuming it will work forward is the classic overfitting trap. Our validation pipeline tests the chosen parameters across 5 distinct market regime periods (2021-2026) to ensure the edge survives regime changes.

Ignoring the period's interaction with timeframe is another common error. RSI(14) on 1-minute candles looks back 14 minutes. On daily candles, it looks back 14 days. The same parameter value produces completely different behavior at different timeframes. Always think in terms of the effective lookback in real time, not just the period number.

RSI in Our Strategy Lineup

RSI appears across our codebase in several roles. In momentum_rsi_macd, it is the primary entry signal: RSI crossing above oversold level triggers a long, confirmed by MACD histogram direction. In mean_reversion_bb, it serves as a confirmation filter alongside Bollinger Bands. In stochastic_rsi, we compute a stochastic oscillator of the RSI itself, creating a faster-reacting signal suitable for 4-hour timeframes.

The rsi_divergence strategy takes a different approach: it does not care about absolute RSI levels. Instead, it looks for price making new lows while RSI makes higher lows (bullish divergence) or price making new highs while RSI makes lower highs (bearish divergence). This measures the internal momentum behind price moves rather than the position within a range.

After testing RSI in dozens of configurations, our conclusion is straightforward: RSI is most useful as a confirmation filter, not a standalone signal. Pair it with a price-structure indicator (Bollinger Bands, support/resistance, VWAP) and a trend filter, and it becomes a reliable component. Used alone, it generates too many false signals in the fast-moving crypto market.