Technique explained as code
Relative strength index (RSI)
RSI turns recent gains and losses into one number between 0 and 100. Here it is computed step by step with no library, then used in a rule so you can see how one is tested.
Risk warning: CFDs and spread bets are complex instruments and come with a high risk of losing money rapidly due to leverage. You can lose money on these products, and each provider publishes its own loss figure under FCA rules. You should consider whether you understand how they work and whether you can afford to take the high risk of losing your money. Automated and API trading can amplify losses through bugs, connectivity failures and runaway orders.
Teaching material only. This page explains how a technique works as code and how to test an idea honestly. It is not investment advice, not a recommendation and not an invitation to trade. Every price on it is invented, so no result here says anything about any real market, and nothing here suggests that a technique makes money.
What it is
The relative strength index looks at the daily price changes over a period, usually fourteen days. It averages the rises, averages the falls, and compares the two.
The answer is scaled to run from 0 to 100. If every recent day rose it reads 100. If every day fell it reads 0. Equal rises and falls give 50. Readings under 30 are commonly called oversold and readings over 70 overbought.
The idea behind it
It was published by J. Welles Wilder in 1978 as a way to compare the strength of recent buying with recent selling on one fixed scale, whatever the price of the instrument.
Charting packages give it to you as a finished line. Writing it yourself shows what it is: a ratio of two smoothed averages of yesterday's changes. It contains no information that was not already in the closing prices.
The code
The function returns one RSI value per day. The smoothing is Wilder's own: after the first plain average, each new day counts for one fourteenth and the old average for the rest. Many libraries differ slightly in how they start the series, which is why two tools can disagree for the first few weeks of a file.
The second half of the script uses the reading in a simple rule, only to show how a rule built on an indicator is tested. The thresholds are the textbook ones.
Data the script reads: invented-prices.csv. Invented values, never a real market. Save it in one folder and run the script from that folder.
The file that was run: rsi.py
# The relative strength index (RSI), computed from scratch with no library.
# Prices come from invented-prices.csv: a random walk made by a script, never a real market.
# The result therefore says nothing about any real market. Read it for the method.
import csv
closes = []
with open("invented-prices.csv", newline="") as f:
for row in csv.DictReader(f):
closes.append(float(row["close"]))
def rsi(prices, period):
"""Return a list the same length as `prices`. Positions with too little history hold None.
The value at position i uses prices up to and including i, and nothing after it.
"""
values = [None] * len(prices)
gains, losses = [], []
for i in range(1, len(prices)):
change = prices[i] - prices[i - 1]
gains.append(max(change, 0.0))
losses.append(max(-change, 0.0))
# Start with a plain average of the first `period` gains and losses.
avg_gain = sum(gains[:period]) / period
avg_loss = sum(losses[:period]) / period
for i in range(period, len(prices)):
if i > period:
# Wilder's smoothing: keep most of the old average, blend in one new day.
avg_gain = (avg_gain * (period - 1) + gains[i - 1]) / period
avg_loss = (avg_loss * (period - 1) + losses[i - 1]) / period
if avg_loss == 0:
values[i] = 100.0
else:
strength = avg_gain / avg_loss
values[i] = 100 - 100 / (1 + strength)
return values
# Example values, picked only because they are easy to follow. They are not a suggestion.
PERIOD = 14
LOW, MIDDLE, HIGH = 30, 50, 70
COST = 0.10 # points paid on every completed trade, a stand in for the spread
index = rsi(closes, PERIOD)
known = [v for v in index if v is not None]
print("Last five RSI values:", [round(v, 1) for v in known[-5:]])
print("Lowest and highest:", round(min(known), 1), "and", round(max(known), 1))
print(f"Days below {LOW}:", len([v for v in known if v < LOW]), "of", len(known))
print(f"Days above {HIGH}:", len([v for v in known if v > HIGH]), "of", len(known))
# A rule built on it, to show how one is tested: in when RSI was under LOW, out when back over MIDDLE.
holding = False
entry = 0.0
results = []
START = PERIOD + 1
for day in range(START, len(closes)):
reading = index[day - 1] # yesterday's RSI, which only knows prices up to yesterday
price = closes[day]
if not holding and reading < LOW:
holding, entry = True, price
elif holding and reading > MIDDLE:
results.append(price - entry - COST)
holding = False
if holding:
results.append(closes[-1] - entry - COST)
print("Trades:", len(results))
print("Trades that ended above zero:", len([r for r in results if r > 0]))
print("Total points after costs:", round(sum(results), 2))
print("Simply holding over the same days:", round(closes[-1] - closes[START] - COST, 2))What the run printed
Last five RSI values: [47.6, 46.9, 44.8, 44.5, 41.7] Lowest and highest: 17.4 and 73.7 Days below 30: 38 of 736 Days above 70: 12 of 736 Trades: 4 Trades that ended above zero: 2 Total points after costs: -1.93 Simply holding over the same days: -9.96
Output of the run on , copied as printed. Every input to this run was invented, so the result says nothing about any real market.
How to read it
The first four lines describe the indicator itself. On this file the reading spent most of its time between the two thresholds, dipped under 30 on 38 days and went over 70 on only 12. Days under the threshold come in clusters, which is why 38 days produced so few trades.
The rule made four trades in 736 days. Nothing can be concluded from four trades, whichever way they went. The page on how a backtest lies shows how often pure chance produces a good looking record from a handful of trades.
The total is below zero and the comparison with holding reflects being out of a falling file most of the time. The prices are a random walk, so a low reading here does not make a rise more likely. It says nothing about any real market.
How it fails or misleads
- Oversold is a label, not a forecast. In a steady fall the reading goes under 30 and stays there while the price keeps falling. The rule buys early in the fall.
- It measures the balance of rises and falls, not their cause. A reading of 25 after bad news that changed the outlook and a reading of 25 after a quiet drift look identical.
- Extreme readings are rare, so rules built on them trade rarely, and a test with few trades proves little.
- The period and both thresholds are free settings. Three settings give a great many combinations to try, and one of them will always have suited the past.
- Different tools start the smoothing differently, so a rule tested with one library can behave differently when run with another.
The page on how a backtest lies shows the mistakes that make any rule look better in a test than it is.
What to try changing
These are programming exercises. Each one changes the code and asks you to explain what you see. None of them is a search for settings to use.
- Write a second function that uses a plain average of the last fourteen gains and losses with no smoothing. Print both versions side by side for the last ten days and see how far apart they are.
- Check your function by hand. Take the first fifteen closing prices, work out the first RSI value on paper or in a spreadsheet, and compare.
- Print the length of each run of days spent under 30.
- Change PERIOD to 7 and to 28 and count the days under 30 each time. Explain the direction of the change.
- Add a test that fails loudly if any RSI value is outside 0 to 100.
How this page was made
Each script on this page was run with Python 3.12 on Windows, standard library only, from a folder holding the data files listed. No network and no account. Each exited with code 0, and what is shown is everything it printed.
The data files whose names begin with the word invented are written by make_invented_data.py from fixed seeds, so anyone can make the same files again and get the same output. This site is not published if the code or a data file has changed since the run.