RSI calculation from MT5 prices: smoothing, seeds and flat markets

6 min readMetaKit

An RSI value is easy to display and surprisingly easy to calculate differently from the chart beside it. The period alone does not define the result: the price series, initial averages, smoothing rule and handling of unchanged prices all matter. A dashboard that hides those choices makes disagreements difficult to diagnose.

This guide builds a Wilder-style RSI calculation from a chronological sequence of MT5 closing prices. The function is an application-side calculation, with synthetic fixtures you can reproduce. MetaKit supplies candle data through its REST API; it does not expose a public RSI calculation endpoint. The goal is a transparent research series, with a comparison procedure for an existing indicator.

Separate upward and downward changes

For consecutive closes, define change = close_now - close_previous. A positive change contributes to gain; a negative change contributes its absolute magnitude to loss. The other contribution is zero. An unchanged close contributes zero to both.

MetaTrader's RSI reference expresses the oscillator using average positive and negative changes. For positive total movement, the equivalent formula used here is:

RSI = 100 × average_gain / (average_gain + average_loss)

This formulation avoids dividing by average loss before calculating the final ratio. A rising series with positive gains and no losses returns 100; a falling series with losses and no gains returns 0. Those are properties of the calculation, not instructions to enter or exit a trade.

When both averages are zero, the ratio is undefined. Our function explicitly returns 50 as a neutral display convention. Record that choice in your indicator specification. Never let a library's missing-value behavior decide it accidentally, and never assume all packages or terminal versions use the same convention.

Specify the seed and recursive update

For period n, use the first n price changes to initialize two arithmetic averages. That requires n + 1 closing prices. Thereafter, update each average as:

new_average = old_average × (n - 1) / n + new_observation / n

The TA-Lib RSI implementation documents a Wilder-style seed and recursive smoothing. The original function below implements the stated mathematics; it is not a port of that source or a promise of identical library output under every configuration.

A rolling simple average is a different calculation. So is an exponential moving average using weight 2 / (n + 1). Calling either one “14-period RSI” does not resolve the difference. The ATR calculation guide explains the same importance of an explicit smoothing definition for a different indicator.

A calculation you can check by hand

Use a short period of three and illustrative closes [100, 102, 101, 104, 102]. These are invented price units, not historical market observations.

The first three changes are +2, -1, +3. Average gain is 5/3; average loss is 1/3. The first RSI is therefore 100 × 5/6, approximately 83.3333.

The next change is -2. Updated average gain is 10/9; updated average loss is 8/9. RSI becomes 100 × 10/18, approximately 55.5556. Replacing the seed with the last three changes would produce a different answer and would not implement this recursion.

function wilderRsi(closes, period = 14) {
  if (!Number.isSafeInteger(period) || period < 2 ||
      !Array.isArray(closes) || closes.length <= period ||
      !closes.every((x) => Number.isFinite(x) && x > 0)) {
    throw new Error("Need positive finite closes and period + 1 prices");
  }
  const result = Array(closes.length).fill(null);
  let gain = 0, loss = 0;
  for (let i = 1; i <= period; i++) {
    const change = closes[i] - closes[i - 1];
    gain += Math.max(change, 0) / period;
    loss += Math.max(-change, 0) / period;
  }
  const value = () => {
    const total = gain + loss;
    if (!Number.isFinite(total)) throw new Error("Numeric range exceeded");
    return total === 0 ? 50 : 100 * (gain / total);
  };
  result[period] = value();
  for (let i = period + 1; i < closes.length; i++) {
    const change = closes[i] - closes[i - 1];
    gain = gain * ((period - 1) / period) + Math.max(change, 0) / period;
    loss = loss * ((period - 1) / period) + Math.max(-change, 0) / period;
    result[i] = value();
  }
  return result;
}

The null prefix means insufficient initialization history, not an RSI of zero. The function deliberately accepts only positive finite prices; datasets involving nonpositive prices need a separate input policy. Floating-point arithmetic is sufficient for these fixtures, but a very small movement can eventually underflow during a long unchanged sequence. This is a numerical calculation, not exact symbolic arithmetic.

Tests should cover the worked example, strictly rising and falling prices, a completely flat sequence, invalid inputs and a period longer than the available history. Also test prefix stability: appending future closes must not change previously calculated outputs. That catches accidental use of a centered window or a calculation over the full series.

Collect a consistent close series

MetaKit's GET /v1/accounts/{id}/candles accepts a symbol and timeframe and returns OHLC observations. Use the exact broker symbol and extract close only after checking chronology, duplicates and unexplained gaps. The candle quality procedure covers that input audit.

The latest returned candle can still be forming. For a closed-bar research series, define when a bar becomes eligible and exclude unfinished observations. Native copy_rates_from_pos documentation identifies bar zero as the current bar. A response arriving successfully does not prove that its last close is final.

Request initialization history before the reporting interval. Computing a fresh seed at the beginning of every visible chart window changes the state at that boundary. Save the input range and calculation version with the output. For incremental processing, retain the previous close and both smoothed averages, and recompute from an earlier checkpoint if historical prices are revised.

Diagnose a chart mismatch in a fixed order

First compare broker account, exact symbol and timeframe. Then compare the underlying closes for identical timestamps. A bid-based series and another price basis need not match. Next verify that both indicators use closing price rather than another applied price, share the same seed history, and treat the current bar consistently.

Only after the inputs agree should you compare smoothing and edge cases. Inspect the intermediate gain and loss averages around the first divergence. A single RSI screenshot cannot tell you which input or rule differed. Preserve a small failing fixture so the issue remains reproducible after the chart moves on.

For a hosted integration, use the MetaKit API documentation to retrieve the verified candle contract and perform the calculation in your own application. The official local Python package and the hosted HTTP interface are different integration paths; this pure function depends on neither and sends no orders.

Sources and verification

Verified 21 October 2026. Formula references were opened and product claims checked against the current routes and candle mapping. Examples were exercised with synthetic inputs.