Skip to content

RSI: Relative Strength Index

How RSI measures momentum, overbought/oversold signals, and divergence patterns.

💡
Before you start

Python 3 and a terminal. No trading platform, no broker, no account and no money. macOS and Linux already include Python; on Windows install it from python.org with “Add python.exe to PATH” ticked, then check with python3 --version. You do not need MetaTrader for this page.

You need no maths beyond averages and percentages, and every formula is written out in code you can read. Run the five files in order; the first writes a file the others read.

Nothing here is trading advice. The measurements are taken on one generated series with no spread, commission or slippage — they show how the indicator behaves, never what would have happened to an account.

What is RSI?

The Relative Strength Index (RSI) is a momentum oscillator developed by J. Welles Wilder in 1978. It measures the speed and magnitude of recent price changes on a scale from 0 to 100, helping traders identify overbought and oversold conditions.

RSI answers the question: "How strong is the current price movement compared to recent history?"

How RSI is Calculated

The calculation uses average gains and average losses over a lookback period (default: 14):

RS = Average Gain / Average Loss
RSI = 100 - (100 / (1 + RS))

The first calculation uses a simple average. Subsequent calculations use a smoothed average (Wilder's smoothing), which means older data gradually fades but never completely disappears. This is why RSI behaves slightly differently than a raw gain/loss ratio.

Reading RSI Signals

Overbought (RSI above 70): The asset has been rising strongly and may be due for a pullback or reversal. This does not mean "sell immediately" — in strong uptrends, RSI can stay above 70 for extended periods.

Oversold (RSI below 30): The asset has been falling sharply and may be due for a bounce. Similarly, in strong downtrends, RSI can remain below 30 for a long time.

Centerline (50): RSI above 50 indicates bullish momentum; below 50 indicates bearish momentum. Some traders use the 50 line as a trend filter.

⚠️
RSI is not a standalone signal

Overbought does not mean "sell" and oversold does not mean "buy." RSI works best as confirmation alongside trend analysis and price action. Trading RSI signals alone in trending markets leads to consistent losses.

RSI Divergence — The Most Powerful Signal

Divergence occurs when RSI and price move in opposite directions. It is one of the most reliable reversal signals in technical analysis:

Bullish Divergence: Price makes a lower low, but RSI makes a higher low. This means selling momentum is weakening even though price is still falling — a potential reversal upward.

Bearish Divergence: Price makes a higher high, but RSI makes a lower high. Buying momentum is fading despite higher prices — a potential reversal downward.

Divergences are most reliable when they occur at extreme RSI levels (above 70 or below 30) and on higher timeframes (H4, D1).

RSI Period Settings

  • RSI 14 (default) — Balanced between sensitivity and reliability. Suitable for most timeframes and instruments.
  • RSI 7-9 (short period) — More sensitive, generates more signals but more false ones. Useful for short-term scalping on lower timeframes.
  • RSI 21-25 (long period) — Smoother, fewer signals but more reliable. Better for swing trading and position trading.

Advanced RSI Techniques

RSI Trendlines: You can draw trendlines directly on the RSI indicator. A trendline break on RSI often precedes a price breakout.

RSI Range Shift: In bull markets, RSI tends to oscillate between 40-80 (not 30-70). In bear markets, it shifts to 20-60. Adjusting your overbought/oversold levels to match the trend improves signal quality.

Multi-timeframe RSI: Check RSI on a higher timeframe for the trend, then use a lower timeframe RSI for entries. For example, only take buy signals on M15 RSI when H4 RSI is above 50.

Compute RSI Yourself and Test What You Were Told About It, in Five Steps

RSI is taught with two rules: above 70 means overbought, below 30 means oversold. Both are testable, and in the next twenty-five minutes you will test them on four hundred bars rather than accept them. Along the way you will find out why an RSI you calculate with a plain fourteen-bar average disagrees with every trading platform, watch one of your own test cases fail and discover your expectation was wrong rather than the code, and measure how often the textbook divergence pattern actually appears. Every line of output below came from running these files.

1
Build the price series

Go: open a terminal in a folder you can write to — cd ~/Desktop on macOS or Linux, cd %USERPROFILE%\Desktop on Windows.

Do: save this as prices.py and run python3 prices.py. The seed is fixed, so your figures will match this page exactly.

"""Build a price series once, so every later step works on the same numbers."""
import random

random.seed(11)                     # fixed, so your figures match this page exactly

# Eight regimes of 50 bars: up, down, flat, repeating. The turns are at
# bar 50, 100, 150, 200, 250, 300, 350 -- known, because we put them there.
REGIMES = [0.35, -0.30, 0.0, 0.35, -0.30, 0.0, 0.35, -0.30]

price, series = 100.0, []
for bar in range(400):
    drift = REGIMES[bar // 50]
    price += drift + random.uniform(-1.2, 1.2)
    series.append(round(price, 2))

with open("closes.txt", "w") as f:
    for p in series:
        f.write("%.2f\n" % p)

print("bars written :", len(series))
print("first 5      :", series[:5])
print("last 5       :", series[-5:])
print("high / low   : %.2f / %.2f" % (max(series), min(series)))
print("regime turns : bars 50, 100, 150, 200, 250, 300, 350")
print()
print("Three kinds of market on purpose -- rising, falling and flat, each")
print("appearing more than once. Most indicator advice is written as if only")
print("the first two exist.")

You should see: four hundred bars, with the turning points stated:

bars written : 400
first 5      : [100.24, 100.73, 102.1, 102.36, 102.73]
last 5       : [116.86, 116.63, 115.51, 114.5, 114.51]
high / low   : 134.48 / 92.25
regime turns : bars 50, 100, 150, 200, 250, 300, 350

Three kinds of market on purpose -- rising, falling and flat, each
appearing more than once. Most indicator advice is written as if only
the first two exist.

The series alternates rising, falling and flat every fifty bars. That matters here because the two rules being tested behave completely differently in a trend and in a range, and a series containing only one of those would let either rule look good.

If not: PermissionError means the folder is not writable — cd somewhere you own. If your first five values differ, random.seed(11) is missing.

2
Compute RSI the way platforms actually do

Go: the same folder.

Do: save this as rsi.py and run python3 rsi.py.

"""RSI, using Wilder's smoothing -- the version every platform actually ships."""

closes = [float(line) for line in open("closes.txt")]

def rsi_series(values, period=14):
    out = [None] * len(values)
    gains = [max(values[i] - values[i-1], 0.0) for i in range(1, len(values))]
    losses = [max(values[i-1] - values[i], 0.0) for i in range(1, len(values))]

    avg_gain = sum(gains[:period]) / period       # the seed is a simple average
    avg_loss = sum(losses[:period]) / period
    out[period] = _rsi(avg_gain, avg_loss)

    for i in range(period, len(gains)):           # then Wilder's smoothing
        avg_gain = (avg_gain * (period - 1) + gains[i]) / period
        avg_loss = (avg_loss * (period - 1) + losses[i]) / period
        out[i + 1] = _rsi(avg_gain, avg_loss)
    return out

def _rsi(avg_gain, avg_loss):
    if avg_loss == 0:
        return 100.0
    rs = avg_gain / avg_loss
    return 100.0 - 100.0 / (1.0 + rs)

r = rsi_series(closes)
print("bar  close    RSI(14)")
for i in (13, 14, 15, 50, 100, 200):
    print("%3d  %6.2f  %8s" % (i, closes[i], "%.4f" % r[i] if r[i] else "   --   "))

print()
print("Wilder's smoothing is NOT a simple average of the last 14 changes.")
print("Each new value carries weight 1/14 and the running average keeps 13/14,")
print("so it is an exponential average with period 14 -- which is why an RSI")
print("computed with a plain 14-bar mean disagrees with every platform.")

You should see: values that stay inside 0 to 100, undefined until bar 14:

bar  close    RSI(14)
 13  104.69     --   
 14  105.51   79.8414
 15  104.76   73.1503
 50  117.31   73.1320
100   95.57   17.2581
200  121.45   80.9850

Wilder's smoothing is NOT a simple average of the last 14 changes.
Each new value carries weight 1/14 and the running average keeps 13/14,
so it is an exponential average with period 14 -- which is why an RSI
computed with a plain 14-bar mean disagrees with every platform.

The smoothing is the part people get wrong. RSI does not average the last fourteen changes; it keeps a running average where each new change enters with weight 1/14 and everything already there is kept at 13/14. That is Wilder's smoothing, and it is an exponential average, not a simple one.

The practical consequence: if you write RSI with a plain fourteen-bar mean, your numbers will be close enough to look right and different enough to disagree with your platform — and you will spend an afternoon hunting a bug that is in your understanding rather than your code.

If not: if every value prints --, the series is shorter than the period; check that closes.txt has 400 lines. If bar 14 is not the first defined value, the seeding loop is off by one — there are only n−1 changes for n prices.

3
Test the formula on inputs whose answer you already know

Go: the same folder. This step is the most useful one on the page, and not because of RSI.

Do: save this as invariants.py and run python3 invariants.py.

"""Prove the formula is right by checking what it MUST do, on inputs you control."""

def _rsi(avg_gain, avg_loss):
    if avg_loss == 0:
        return 100.0
    return 100.0 - 100.0 / (1.0 + avg_gain / avg_loss)

def rsi_of(values, period=14):
    gains = [max(values[i] - values[i-1], 0.0) for i in range(1, len(values))]
    losses = [max(values[i-1] - values[i], 0.0) for i in range(1, len(values))]
    ag, al = sum(gains[:period]) / period, sum(losses[:period]) / period
    for i in range(period, len(gains)):
        ag = (ag * (period - 1) + gains[i]) / period
        al = (al * (period - 1) + losses[i]) / period
    return _rsi(ag, al)

CASES = [
    ("price rises every single bar",       [100 + i for i in range(40)],       100.0),
    ("price falls every single bar",       [100 - i for i in range(40)],         0.0),
    ("price never moves at all",           [100.0] * 40,                       100.0),
    ("15 bars: 7 gains, 7 losses, no smoothing yet",
                                           [100 + (i % 2) for i in range(15)],  50.0),
]

print("%-46s %9s %9s %s" % ("INPUT", "EXPECTED", "GOT", "OK?"))
print("-" * 74)
for label, series, expected in CASES:
    got = rsi_of(series)
    print("%-46s %9.2f %9.4f %s" % (label, expected, got,
                                    "yes" if abs(got - expected) < 1e-9 else "NO"))

print()
print("Now the case that looks like it should also be 50, and is not:")
alt40 = [100 + (i % 2) for i in range(40)]
print("   40 bars alternating up/down  ->  %.4f" % rsi_of(alt40))
print()
print("Equal numbers of gains and losses, all the same size -- and RSI is not")
print("50. That is correct. Wilder's smoothing weights RECENT changes more, so")
print("the answer depends on whether the series happened to end on an up bar.")
print("This one does: bar 39 is 101 and bar 38 is 100.")
print()
print("The lesson is about testing, not about RSI. Three of these four cases")
print("passed and told me nothing; the one that FAILED was the only one that")
print("taught me something -- and what it taught me was that my expectation")
print("was wrong, not that the code was.")

You should see: four passes, and then a case that looks like it should be 50 and is not:

INPUT                                           EXPECTED       GOT OK?
--------------------------------------------------------------------------
price rises every single bar                      100.00  100.0000 yes
price falls every single bar                        0.00    0.0000 yes
price never moves at all                          100.00  100.0000 yes
15 bars: 7 gains, 7 losses, no smoothing yet       50.00   50.0000 yes

Now the case that looks like it should also be 50, and is not:
   40 bars alternating up/down  ->  52.1422

Equal numbers of gains and losses, all the same size -- and RSI is not
50. That is correct. Wilder's smoothing weights RECENT changes more, so
the answer depends on whether the series happened to end on an up bar.
This one does: bar 39 is 101 and bar 38 is 100.

The lesson is about testing, not about RSI. Three of these four cases
passed and told me nothing; the one that FAILED was the only one that
taught me something -- and what it taught me was that my expectation
was wrong, not that the code was.

Two genuine findings here. The first is that a flat line returns 100 — maximum strength for a market that has not moved at all — because there are no losses, so the division has no answer and the code picks one. Platforms differ on this. If you ever build a rule that fires at RSI 100, find out what yours does first.

The second is the alternating series. Equal numbers of gains and losses, all the same size, and the answer is 52.14 rather than 50 — because Wilder's smoothing weights recent changes more, so it matters that the series happened to end on an up bar. The code was right and my expectation was wrong, which is the more common of the two by a wide margin. When a test fails, suspect the test first.

If not: if the fourth row reports NO, the 15-bar case has an even number of bars or a different pattern — it needs exactly period changes so that no smoothing step runs at all, which is what makes the answer exactly 50.

4
Test 'overbought means sell' against the data

Go: the same folder.

Do: save this as overbought.py and run python3 overbought.py.

"""'Overbought means sell' -- test it against the series."""

closes = [float(line) for line in open("closes.txt")]

def rsi_series(values, period=14):
    out = [None] * len(values)
    g = [max(values[i]-values[i-1], 0.0) for i in range(1, len(values))]
    l = [max(values[i-1]-values[i], 0.0) for i in range(1, len(values))]
    ag, al = sum(g[:period])/period, sum(l[:period])/period
    out[period] = 100.0 if al == 0 else 100 - 100/(1 + ag/al)
    for i in range(period, len(g)):
        ag = (ag*(period-1) + g[i]) / period
        al = (al*(period-1) + l[i]) / period
        out[i+1] = 100.0 if al == 0 else 100 - 100/(1 + ag/al)
    return out

r = rsi_series(closes)

# How long does RSI stay above 70 at a stretch?
runs, current = [], 0
for v in r:
    if v is not None and v > 70:
        current += 1
    elif current:
        runs.append(current); current = 0
if current: runs.append(current)

print("stretches with RSI above 70 :", len(runs))
print("longest stretch             :", max(runs), "bars")
print("all stretch lengths         :", sorted(runs, reverse=True))
print()

# If you sold every time RSI first crossed above 70, what happened next?
sells, better_later = 0, 0
for i in range(1, len(r) - 10):
    if r[i-1] is not None and r[i] is not None and r[i-1] <= 70 < r[i]:
        sells += 1
        if closes[i + 10] > closes[i]:
            better_later += 1

print("times RSI first crossed above 70 :", sells)
print("times price was HIGHER 10 bars on :", better_later)
print("i.e. selling was early            : %.0f%% of the time" % (100.0 * better_later / sells))
print()
print("'Overbought' does not mean 'about to fall'. It means 'rising quickly',")
print("which in a real trend is exactly when you should not be selling. The")
print("longest stretch above 70 lasted %d bars -- an eternity to hold a losing" % max(runs))
print("short position taken on bar one of it.")

You should see: a 38-bar stretch above 70, and most sells being early:

stretches with RSI above 70 : 7
longest stretch             : 38 bars
all stretch lengths         : [38, 28, 22, 19, 9, 2, 1]

times RSI first crossed above 70 : 6
times price was HIGHER 10 bars on : 5
i.e. selling was early            : 83% of the time

'Overbought' does not mean 'about to fall'. It means 'rising quickly',
which in a real trend is exactly when you should not be selling. The
longest stretch above 70 lasted 38 bars -- an eternity to hold a losing
short position taken on bar one of it.

“Overbought” is a name, not a prediction. RSI above 70 says recent gains have outweighed recent losses by a wide margin — which is the definition of a strong uptrend, and the worst possible moment to sell into one. The 38-bar stretch is the whole argument: somebody who shorted on the first bar of it spent 38 bars being wrong.

Treat the 83% figure with the caution the previous page's last step described — it rests on six observations, which is far too few to conclude anything. What the six observations do establish is the more important point: the rule fails in a way that is not rare, and the failure is systematic rather than unlucky.

The usable version of the same indicator is as a filter: only take long signals while RSI is above 50, only short ones while it is below. That uses RSI to describe the current state rather than to predict a reversal, which is the only thing it measures.

If not: if stretches with RSI above 70 is zero, closes.txt is from a different run — re-run step 1. The exact stretch lengths depend on the seeded series and will match this page.

5
Count how often the divergence pattern actually happens

Go: the same folder.

Do: save this as divergence.py and run python3 divergence.py.

"""Bullish divergence: price makes a lower low, RSI makes a higher low."""

closes = [float(line) for line in open("closes.txt")]

def rsi_series(values, period=14):
    out = [None] * len(values)
    g = [max(values[i]-values[i-1], 0.0) for i in range(1, len(values))]
    l = [max(values[i-1]-values[i], 0.0) for i in range(1, len(values))]
    ag, al = sum(g[:period])/period, sum(l[:period])/period
    out[period] = 100.0 if al == 0 else 100 - 100/(1 + ag/al)
    for i in range(period, len(g)):
        ag = (ag*(period-1) + g[i]) / period
        al = (al*(period-1) + l[i]) / period
        out[i+1] = 100.0 if al == 0 else 100 - 100/(1 + ag/al)
    return out

def swing_lows(values, width=3):
    """A bar lower than `width` bars either side of it."""
    lows = []
    for i in range(width, len(values) - width):
        if values[i] is None:
            continue
        window = values[i-width:i+width+1]
        if None in window:
            continue
        if values[i] == min(window) and window.count(values[i]) == 1:
            lows.append(i)
    return lows

r = rsi_series(closes)
lows = swing_lows(closes)
print("swing lows found in price :", len(lows))

found, worked = 0, 0
for a, b in zip(lows, lows[1:]):
    if b - a > 40:                     # too far apart to be one pattern
        continue
    if closes[b] < closes[a] and r[b] is not None and r[a] is not None and r[b] > r[a]:
        found += 1
        after = min(b + 10, len(closes) - 1)
        rose = closes[after] > closes[b]
        worked += rose
        print("   bar %3d -> %3d : price %.2f -> %.2f (lower), RSI %.1f -> %.1f (higher)"
              " ; 10 bars later %s"
              % (a, b, closes[a], closes[b], r[a], r[b], "UP" if rose else "down"))

pairs = sum(1 for a, b in zip(lows, lows[1:]) if b - a <= 40)
print()
print("bars examined              :", len(closes))
print("swing lows found           :", len(lows))
print("adjacent pairs close enough:", pairs)
print("that qualified as divergence: %d  (%.0f%% of the pairs)"
      % (found, 100.0 * found / pairs))
if found:
    print("followed by a rise          : %d of %d" % (worked, found))
print()
print("THAT is the finding. The textbook pattern occurred once in %d bars." % len(closes))
print("It is not that divergence never works -- it is that it is far rarer")
print("than the amount written about it suggests, and a rule that fires once")
print("a year cannot be evaluated by anyone in a hurry.")
print()
print("Be suspicious of any chart showing several per screen. Producing them")
print("requires loosening 'lower low' and 'higher low' until they match")
print("whatever is already there, which is drawing, not detecting.")

You should see: one qualifying divergence in four hundred bars:

swing lows found in price : 24
   bar 257 -> 273 : price 102.75 -> 101.93 (lower), RSI 20.1 -> 29.8 (higher) ; 10 bars later UP

bars examined              : 400
swing lows found           : 24
adjacent pairs close enough: 21
that qualified as divergence: 1  (5% of the pairs)
followed by a rise          : 1 of 1

THAT is the finding. The textbook pattern occurred once in 400 bars.
It is not that divergence never works -- it is that it is far rarer
than the amount written about it suggests, and a rule that fires once
a year cannot be evaluated by anyone in a hurry.

Be suspicious of any chart showing several per screen. Producing them
requires loosening 'lower low' and 'higher low' until they match
whatever is already there, which is drawing, not detecting.

Twenty-four swing lows, twenty-one adjacent pairs close enough together to form a pattern, and one that satisfies the textbook definition. That is the finding, and it is not that divergence does not work — it is that a strict reading of the pattern is rare enough that almost nobody can have tested it properly.

Which explains the charts you have seen with several marked per screen. Producing those requires relaxing “lower low” and “higher low” until they fit whatever is already on the chart. Once the definition bends to the data, it is drawing rather than detecting, and it will fit anything — including a series that was generated by a random-number generator, as this one was.

If not: if it finds zero, the width in swing_lows was raised — a wider window demands a more pronounced low and finds fewer. Changing it is a worthwhile experiment: watch how the count of “patterns” moves with a parameter you chose.

🎉
Check yourself before moving on

Without scrolling up: someone shows you a backtest where buying whenever RSI(14) drops below 30 produced a 68% win rate over the last year on one instrument, and asks whether they should trade it. What would you want to know before answering, and what does the material above suggest will happen? Answer: the first question is how many trades that 68% covers — a year on one instrument might be a dozen, and a coin flip clears 68% on a dozen trades often enough to be unremarkable. The second is what the win rate actually pays: a rule that wins 68% of the time and loses more per loss than it makes per win is a losing rule, so I would want the average win against the average loss, with spread and commission deducted. The third is which market it was tested in: step 4 showed the mirror-image rule failing systematically during trends, so a year that happened to be range-bound would flatter it, and the same rule would bleed through a trending year. What the material above suggests is that the threshold rules describe the current state rather than predict a turn — so the honest expectation is that it works while the market ranges and fails while it trends, and the backtest is mostly telling you which of those the last year was.

Now do it without the page: change the period in rsi.py from 14 to 2 and re-run overbought.py. A very short RSI spends far more time beyond 70 and 30, so the number of signals explodes — and the interesting question is whether the proportion that were early changes at all. Then try period 50 and watch the opposite. You are discovering by measurement that the period does not make the indicator better or worse, it moves it along a trade-off, which is the same conclusion the moving-average page reached from a different direction.

RSI in MQL5

In MQL5, RSI is accessed through the iRSI() function:

int rsiHandle = iRSI(_Symbol, PERIOD_CURRENT, 14, PRICE_CLOSE);
double rsiBuffer[];
CopyBuffer(rsiHandle, 0, 0, 3, rsiBuffer);
// rsiBuffer[0] = most recent RSI value

This is useful when building Expert Advisors that need to make decisions based on RSI levels. We cover this in the MQL5 Programming section.