RSI in Python — Coding It from Scratch (pandas)

A correct, Wilder-smoothed RSI that matches every platform

RSI computed in Python
RSI computed in Python

If you read the explainer, you already know the RSI is just averaged gains over averaged losses, bounded to 0–100. So let’s build it — correctly, with Wilder’s smoothing, so it actually matches what your charting platform draws. The version below is verified to reproduce the MQL5, Pine, EasyLanguage and NinjaScript versions in this series to the last decimal.

What you’ll need

  • Python 3.9+
  • numpy (and pandas if you’re loading a CSV)
  • A close price series.

The from-scratch version

The only subtlety is the smoothing. Wilder’s average is seeded with a simple average of the first period moves, then each later bar updates the running average by 1/period. That seeding is the part naive implementations get wrong — and it’s exactly what TradingView’s ta.rma does under the hood.

import numpy as np


def rsi(close, period: int = 14):
    close = np.asarray(close, dtype=float)
    delta = np.diff(close)                 # bar-to-bar change
    gain = np.clip(delta, 0.0, None)
    loss = np.clip(-delta, 0.0, None)

    avg_gain = np.full(close.shape, np.nan)
    avg_loss = np.full(close.shape, np.nan)

    # Seed at bar `period` with the simple average of the first `period` moves.
    avg_gain[period] = gain[:period].mean()
    avg_loss[period] = loss[:period].mean()

    # Wilder's recursive smoothing (alpha = 1/period) thereafter.
    for i in range(period + 1, len(close)):
        avg_gain[i] = (avg_gain[i - 1] * (period - 1) + gain[i - 1]) / period
        avg_loss[i] = (avg_loss[i - 1] * (period - 1) + loss[i - 1]) / period

    rs = avg_gain / avg_loss
    out = 100.0 - (100.0 / (1.0 + rs))
    out[avg_loss == 0] = 100.0             # all-up window -> RSI pinned at 100
    return out

Use it on any close series:

import pandas as pd
df = pd.read_csv("prices.csv", parse_dates=["date"]).set_index("date")
df["rsi14"] = rsi(df["close"].to_numpy(), 14)
print(df[["close", "rsi14"]].tail())

Reading the output

The chart above is exactly what this produces: price on top, the RSI oscillating in its 0–100 box below, tagging the 70 and 30 rails as buying and selling pressure swing. Because it uses Wilder’s SMA-seeded smoothing, the numbers line up with TradingView’s RSI bar-for-bar, not just “roughly.”

Gotchas

  • Seeding is everything. The single most common RSI bug is smoothing with a plain average (or letting an EWM seed itself with the first value). Both drift several RSI points away from every platform during the warm-up. Seed with the simple average of the first period moves, as above.
  • Warm-up is NaN. Bars before index period genuinely have no RSI — leave them NaN, don’t backfill.
  • The tempting one-liner isn’t exact. gain.ewm(alpha=1/period, adjust=False).mean() looks clean but seeds on the first observation, so it disagrees with Wilder by up to several points early on before converging. Fine for a rough look; not fine if you need parity.

The lazy (also correct) one-liners

For production, don’t hand-roll it — both of these use proper Wilder smoothing and match the function above:

# pandas-ta
import pandas_ta as ta
df["rsi14"] = ta.rsi(df["close"], length=14)

# TA-Lib (C-backed, fast)
import talib
df["rsi14"] = talib.RSI(df["close"].to_numpy(), timeperiod=14)

Keep the from-scratch version in your head so you always know what the library is doing. Now the real test: don’t trust the line, backtest a rule built on it in AlgoGen. Same indicator elsewhere: MQL5, Pine Script, EasyLanguage, and NinjaScript.


This post is educational, not financial advice. Indicators describe the past; they don’t predict the future. Backtest anything before you risk real money on it.

Sources and further reading

  1. New Concepts in Technical Trading Systems (Windsor Books)
  2. Windowing operations (pandas documentation)

Historical research from the Algogen archive. Not investment advice.

Blog

Have a strategy idea?

Describe it in plain English, preview where your rules fire, then decide whether the evidence is worth a backtest.

Keep me postedHow it works