Tutorial
RSI indicator: formula, Wilder smoothing and Python code
The relative strength index is fourteen days of arithmetic that most people use without ever checking. Do it once by hand and the usual bugs stop being mysterious.
On this page
Key takeaways
- RSI = 100 − 100 ÷ (1 + RS), where RS is the average gain divided by the average loss over the lookback, 14 periods by default.
- Wilder's original RSI smooths the averages by a factor of 1/14, which in pandas is `ewm(alpha=1/14)`, not `ewm(span=14)`.
- Wilder smoothing has a long memory: seeded on 17 September, US:KO's RSI on 25 September 2026 read 44.65; seeded in May it read 48.36.
- Above 70 is conventionally overbought and below 30 oversold, yet BTCUSD stayed above 70 for 11 straight days in August 2026 while its price kept rising.
- RSI describes recent momentum; it is not a buy or sell instruction, and this tutorial is education, not advice.
The RSI indicator (relative strength index) measures how strong recent gains have been compared with recent losses, on a scale from 0 to 100. The formula is RSI = 100 − 100 ÷ (1 + RS), where RS is the average gain divided by the average loss over the last 14 periods. Readings above 70 are conventionally called overbought and readings below 30 oversold.
This tutorial computes the RSI by hand on 21 real daily closes of US:KO, shows where Wilder's smoothing and the simple version part ways, charts a real overbought and oversold episode on BTCUSD, and ends with pandas code that pulls daily bars and reproduces every number. It pairs with the golden cross backtest: one measures trend, the other momentum.
RS = average gain ÷ average lossRSI = 100 − 100 ÷ (1 + RS)
- average gain
- The mean of the up-moves over the lookback, with down days counted as zero.
- average loss
- The mean of the down-moves as positive numbers, with up days counted as zero.
- lookback
- 14 periods by default: 14 daily bars on a daily chart, 14 minutes on a one-minute chart.
The RSI formula, one step at a time
- Take the changesSubtract each close from the one before it. Fifteen closes give fourteen changes.
- Split gains and lossesA positive change is a gain and a zero loss; a negative change is a loss, written as a positive number, and a zero gain.
- Seed the averagesThe first average gain is the plain mean of the first 14 gains. The same for losses.
- Smooth from thereEach new average is (previous average × 13 + today's value) ÷ 14. This is Wilder's smoothing.
- Take the ratioRS = average gain ÷ average loss. If the average loss is zero, RSI is 100.
- Scale itRSI = 100 − 100 ÷ (1 + RS), which maps any RS onto 0 to 100.
A worked RSI example on real closes
Here are 15 daily closes of US:KO from 27 August to 17 September 2026, taken from the stock bars endpoint. There is no bar for 7 September: it was Labor Day and the market was closed.
| Date | Close | Change | Gain | Loss |
|---|---|---|---|---|
| 27 Aug | 89.06 | |||
| 28 Aug | 89.66 | +0.60 | 0.60 | 0 |
| 31 Aug | 88.67 | -0.99 | 0 | 0.99 |
| 1 Sep | 88.00 | -0.67 | 0 | 0.67 |
| 2 Sep | 88.24 | +0.24 | 0.24 | 0 |
| 3 Sep | 88.81 | +0.57 | 0.57 | 0 |
| 4 Sep | 88.07 | -0.74 | 0 | 0.74 |
| 8 Sep | 88.36 | +0.29 | 0.29 | 0 |
| 9 Sep | 87.55 | -0.81 | 0 | 0.81 |
| 10 Sep | 87.83 | +0.28 | 0.28 | 0 |
| 11 Sep | 88.29 | +0.46 | 0.46 | 0 |
| 14 Sep | 89.35 | +1.06 | 1.06 | 0 |
| 15 Sep | 88.71 | -0.64 | 0 | 0.64 |
| 16 Sep | 87.87 | -0.84 | 0 | 0.84 |
| 17 Sep | 88.06 | +0.19 | 0.19 | 0 |
| Sum of 14 | 3.69 | 4.69 |
The first averages are 3.69 ÷ 14 = 0.2636 for gains and 4.69 ÷ 14 = 0.3350 for losses. RS is 0.7868 and the RSI on 17 September is 44.03: losses slightly outweighed gains over the two weeks, nowhere near either extreme. From there, each new day updates the averages with Wilder's smoothing instead of recomputing a 14-day mean.
| Date | Close | Gain | Loss | Avg gain | Avg loss | RS | RSI |
|---|---|---|---|---|---|---|---|
| 18 Sep | 88.25 | 0.19 | 0 | 0.2583 | 0.3111 | 0.830 | 45.37 |
| 21 Sep | 87.12 | 0 | 1.13 | 0.2399 | 0.3696 | 0.649 | 39.36 |
| 22 Sep | 88.61 | 1.49 | 0 | 0.3292 | 0.3432 | 0.959 | 48.96 |
| 23 Sep | 88.09 | 0 | 0.52 | 0.3056 | 0.3558 | 0.859 | 46.21 |
| 24 Sep | 88.10 | 0.01 | 0 | 0.2845 | 0.3304 | 0.861 | 46.27 |
| 25 Sep | 87.81 | 0 | 0.29 | 0.2642 | 0.3275 | 0.807 | 44.65 |
That ends at 44.65 on 25 September. Run the script at the end of this article and it prints 48.36 for the same day. Neither is wrong. The script seeds its averages in May, the table seeded them on 17 September, and Wilder's smoothing never quite forgets its seed: each day keeps 13/14 of the old average, so after six steps the seed still carries 64% of the weight. After 100 bars it is below 0.1%. Load at least 100 bars before you trust a Wilder RSI.
Wilder vs simple smoothing, and the ewm bug
Wilder's smoothing, from his 1978 book, is an exponential average with a factor of 1 ÷ 14. The simple version, sometimes called Cutler's RSI, swaps it for a plain 14-day rolling mean: it has no memory beyond two weeks, so it jumps whenever a big day drops out of the window. And there is a third version nobody means to write, pandas ewm(span=14), which uses a factor of 2 ÷ 15 and reacts almost twice as fast as Wilder.
| Method | pandas | US:KO, 25 Sep | BTCUSD, 27 Sep |
|---|---|---|---|
| Wilder, seeded with a simple mean | the loop in the script below | 48.36 | 65.15 |
| Wilder with exponential weights | ewm(alpha=1/14, adjust=False) | 48.34 | 65.15 |
| Simple (rolling mean) | rolling(14).mean() | 48.41 | 71.28 |
| The common bug | ewm(span=14, adjust=False) | 45.04 | 66.09 |
Overbought and oversold on a real chart
BTCUSD RSI(14), May to September 2026
On 6 June the RSI of BTCUSD fell to 15.0 with the price at 60,885. By 30 June the price had made a lower low at 58,625, but the RSI held above 30, at 30.2: a higher low on the indicator under a lower low in price, the pattern traders call a bullish divergence. The price then climbed through July and August.
August shows the other lesson. RSI went above 70 on 19 August and stayed there for 11 straight days, peaking at 85.9 on 21 August, while the price rose from about 69,000 to above 80,000. Overbought describes a strong run, not a reversal. Read 70 and 30 as a description of the last two weeks, and decide with other evidence, such as the trend filter from the golden cross article.
Python code: calculate RSI from daily bars
The script pulls daily bars for a stock and a crypto pair, computes Wilder's RSI with an explicit loop, so it matches the tables above step for step, and the simple version with a rolling mean. Stocks use market-qualified symbols such as US:KO; crypto pairs such as BTCUSD take no prefix. Bars are plain OHLC records, so RSI inherits every detail of how they are built; OHLC bars explained covers those.
import os
import sys
import pandas as pd
import requests
BASE_URL = "https://api.tickerlayer.com"
API_KEY = os.environ["TICKERLAYER_API_KEY"]
PERIOD = 14
def daily_closes(asset, symbol, start, end):
"""Daily closes, oldest first, following pagination until next_offset is null."""
url = f"{BASE_URL}/{asset}/agg/{symbol}/1/day/{start}/{end}"
params = {"sort": "asc", "limit": 5000, "offset": 0}
rows = []
while True:
resp = requests.get(url, params=params, headers={"x-api-key": API_KEY}, timeout=15)
if resp.status_code != 200:
sys.exit(f"{symbol}: HTTP {resp.status_code} {resp.text[:160]}")
body = resp.json()
rows.extend(body["results"])
if body.get("next_offset") is None:
break
params["offset"] = body["next_offset"]
if len(rows) <= PERIOD:
sys.exit(f"{symbol}: need more than {PERIOD} bars, got {len(rows)}")
df = pd.DataFrame(rows)
df["date"] = pd.to_datetime(df["t"], unit="ms", utc=True).dt.date
return df.set_index("date")["c"].astype(float)
def wilder_average(values, period=PERIOD):
"""Seed with the plain mean of the first `period` values, then smooth by 1/period."""
out = pd.Series(float("nan"), index=values.index)
out.iloc[period] = values.iloc[1 : period + 1].mean()
for i in range(period + 1, len(values)):
out.iloc[i] = (out.iloc[i - 1] * (period - 1) + values.iloc[i]) / period
return out
def rsi(close, period=PERIOD):
change = close.diff()
gain, loss = change.clip(lower=0), -change.clip(upper=0)
wilder = 100 - 100 / (1 + wilder_average(gain, period) / wilder_average(loss, period))
simple = 100 - 100 / (1 + gain.rolling(period).mean() / loss.rolling(period).mean())
return pd.DataFrame({"close": close, "rsi_wilder": wilder, "rsi_simple": simple})
for asset, symbol, start, end in [
("stocks", "US:KO", "2026-05-01", "2026-09-25"),
("crypto", "BTCUSD", "2026-05-01", "2026-09-27"),
]:
table = rsi(daily_closes(asset, symbol, start, end))
print()
print(f"{symbol}, {len(table)} daily bars")
print(table.tail(4).round(2).to_string())US:KO, 102 daily bars
close rsi_wilder rsi_simple
date
2026-09-22 88.61 52.62 53.42
2026-09-23 88.09 49.92 49.19
2026-09-24 88.10 49.97 45.90
2026-09-25 87.81 48.36 48.41
BTCUSD, 150 daily bars
close rsi_wilder rsi_simple
date
2026-09-24 84410.24 65.52 71.32
2026-09-25 84099.99 64.32 69.05
2026-09-26 84433.10 65.06 69.52
2026-09-27 84472.00 65.15 71.28A few implementation notes. The loop is fast enough for daily bars; for millions of intraday bars use ewm(alpha=1/14, adjust=False), which matches the loop to within a tenth of a point once the seed has faded. When the average loss is exactly zero, RS is infinite and pandas returns an RSI of 100, which is the right answer. When both averages are zero, a flat market, the result is undefined, so decide whether to show 50 or nothing.
Keeping RSI current on live data
Wilder's formula needs only yesterday's two averages and today's change, so a live RSI costs almost nothing to maintain: store avg_gain, avg_loss and the last close, and update them once per closed bar. Compute on closed bars only, because an RSI on a still-forming bar flickers with every trade. For US stocks, the stocks.agg WebSocket channel delivers settled bars from one minute to one day (bar message format). For crypto, build bars from the trade stream; the Python trading bot tutorial runs a signal engine on that stream in paper mode.
def update(state, close, period=14):
"""Advance Wilder's RSI by one closed bar. state holds avg_gain, avg_loss, last_close."""
change = close - state["last_close"]
gain, loss = max(change, 0.0), max(-change, 0.0)
state["avg_gain"] = (state["avg_gain"] * (period - 1) + gain) / period
state["avg_loss"] = (state["avg_loss"] * (period - 1) + loss) / period
state["last_close"] = close
if state["avg_loss"] == 0:
return 100.0
return 100 - 100 / (1 + state["avg_gain"] / state["avg_loss"])
# US:KO after the 24 September close, from the worked table above.
state = {"avg_gain": 0.2845, "avg_loss": 0.3304, "last_close": 88.10}
print(round(update(state, 87.81), 2)) # the 25 September close, prints 44.65Before you ship an RSI
- Smoothing is alpha 1/14 (Wilder) or a rolling mean, chosen on purpose and documented.
- At least 100 bars of warm-up before the first value you display or trade on.
- Only closed bars go into the calculation.
- A zero average loss returns 100, and a flat market returns a defined value.
- Thresholds are paired with context, such as trend or VWAP, rather than used alone.
Questions
How is RSI calculated?
Take 14 price changes, average the gains and the losses separately, divide the average gain by the average loss to get RS, and compute RSI = 100 − 100 ÷ (1 + RS). Wilder's version then smooths each average by a factor of 1/14 from one bar to the next.
What does an RSI of 70 mean?
At 70, the average gain is about 2.3 times the average loss over the lookback. It is conventionally called overbought, but in strong trends RSI can stay above 70 for weeks.
What is a good RSI to buy?
There is no single number. Many traders watch 30 as oversold, but RSI can stay low through a long decline, so it is not a buy signal on its own. This is education, not advice.
Why does my RSI differ from my charting platform?
Usually one of four things: a different smoothing (span 14 instead of alpha 1/14), a shorter warm-up, a still-forming bar being included, or a different daily close. Align those and the values converge.
What period should I use for RSI?
Fourteen is Wilder's default and the most common. Shorter periods such as 9 react faster and hit the extremes more often; longer ones such as 25 are smoother.
Does RSI work on crypto?
The calculation works on any price series. Crypto trades every day, so 14 daily bars cover two calendar weeks, while 14 stock bars cover nearly three.