TradingView indicator comparisons

RSI vs ROC: bounded relative moves vs raw change

RSI measures the balance of smoothed gains and losses on a 0 to 100 scale. ROC measures percentage change between the current price and the price N bars earlier. They can disagree even with equal lengths: the controlled paths below both gain 4% over four bars, but Wilder RSI is 84.179688 after four small rises and 78.571429 after one early jump. Choose RSI for the balance of directional changes, or ROC for a specified endpoint return.

See the decision guide

Keep the source, length and decision time fixed

Use the same exchange-qualified symbol, standard candle closes, timeframe, session, adjustment settings and loaded history. Calculate both indicators on every bar, then inspect both at the same completed chart bar. Comparing live RSI with yesterday's ROC introduces a timing difference before the formulas are even considered.

  • RSI = 100G/(G+L) equals 100 − 100/(1+G/L) when L > 0. The first form also handles G > 0, L = 0 without dividing by zero. Both-zero handling still needs an explicit choice.
  • A ratio indicator centered at 100 is a different ROC display convention. This page uses TradingView's percentage-change formula centered at zero. N counts chart bars, not calendar days.
MeasurementWilder RSIROC
InputChange d[t] = close[t] − close[t−1]; gain = max(d, 0), loss = max(−d, 0).Current close and close N chart bars earlier.
CalculationG[t] = ((N−1)G[t−1] + gain[t])/N; L uses the same recurrence. RSI = 100G/(G+L).100 × (close[t] − close[t−N]) / close[t−N].
First value in this experimentSeed G and L with the arithmetic means of the first N changes, requiring N+1 prices.Wait for the same N+1 consecutive valid prices and a positive reference price.
Reference level50 means the two smoothed averages are equal, provided their sum is positive.0% means the two endpoint prices are equal.
UnitsDimensionless index from 0 to 100 under the stated boundary policy.Percent price change; not price points, an annualized return or a probability.

Control the history before comparing four small rises with one gap

These are synthetic price units, not market observations. Both paths begin 100, 101, 100, 101, 100. At row 4, the four changes are +1, −1, +1, −1, so both RSI states start with G = L = 0.5 and RSI = 50. Path A then adds one unit on each of four bars. Path B jumps four units once and stays flat. For a literal gap interpretation, set each bar's open equal to its close; these close-based formulas do not use open, high or low.

Fixture rowA: small risesB: single gapPurpose
0100100Starting reference
1101101Shared +1
2100100Shared −1
3101101Shared +1
4100100Shared −1; initialize RSI(4)
5101104A +1; B +4
6102104A +1; B unchanged
7103104A +1; B unchanged
8104104Same endpoint and same four-bar return
9, 10, 11, 12104 on every row104 on every rowNo further price changes

Same four-bar return, different RSI

At row 5, A has G = (3 × 0.5 + 1)/4 = 0.625 and L = 0.375, giving RSI 62.5. B has G = (3 × 0.5 + 4)/4 = 1.375 and L = 0.375, giving RSI 100 × 11/14 = 78.571429. At row 8, A has G = 431/512 and L = 81/512; B has G = 297/512 and L = 81/512. Their ROC values are both 100 × (104−100)/100 = 4%. Values below are independently calculated and rounded to six decimals only for display.

  • The gap initially produces the larger RSI response. By row 8, the repeated recent gains produce the higher RSI. This is one controlled path comparison, not proof that one indicator is always faster.
  • RSI is not a count of up bars. It averages gain and loss magnitudes separately. One large gain can outweigh several smaller losses, and the position of a gain in the recursion matters.
RowRSI ARSI BROC A (%)ROC B (%)
0 through 3NANANANA
450.00000050.0000000.0000000.000000
562.50000078.5714290.0000002.970297
671.87500078.5714292.0000004.000000
778.90625078.5714291.9801982.970297
884.17968878.5714294.0000004.000000
984.17968878.5714292.9702970.000000
1084.17968878.5714291.9607840.000000
1184.17968878.5714290.9708740.000000
1284.17968878.5714290.0000000.000000

ROC can fall when the current price does not move

At row 8, B compares 104 with row 4's 100 and reports 4%. At row 9, the reference advances to row 5's 104, so ROC becomes zero even though the current close remains 104. No new selloff occurred. The old lower endpoint has left the four-bar comparison. A reaches zero only at row 12, when its reference also becomes 104.

  • During flat bars, both RSI averages multiply by (N−1)/N. Their ratio remains constant in exact arithmetic while both are positive. This is why neither RSI in the table drifts toward 50 just because prices stop moving.
  • For N > 1, a prior contribution to either average decays geometrically; it does not disappear after exactly N bars. An initial-state discrepancy also decays by ((N−1)/N)^k after k updates, but the resulting RSI discrepancy depends on both averages. There is no universal warm-up count that guarantees a given RSI tolerance.
  • At N = 1, the previous state has zero weight: a gain maps to 100, a loss to 0, and a flat change to the chosen flat policy. The longer-memory statement applies to N > 1.

Make startup, zero denominators and invalid prices explicit

The example implements a custom Wilder recurrence with documented boundary choices. Its flat value is a policy, not a claim about ta.rsi on an all-flat series. Built-in functions keep their own missing-value and initialization semantics; the script exposes them separately for inspection. Do not infer numerical behavior from a successful syntax check.

  • Positive prices can rise without a mathematical cap, so ROC has no upper bound. For example, 100 to 300 is +200%. A small positive denominator can produce an extremely large valid ROC; investigate the data rather than clipping it to an RSI-like band.
  • A flat segment after earlier movement is different from a constant series from inception. Prior RSI gain/loss state survives the flat segment. In a very long floating-point calculation, underflow can eventually make both averages zero; the explicit flat policy then applies.
  • A missing calendar interval with no chart bar is not an explicit missing input. The script cannot invent observations. Splits, roll adjustments, synthetic candles and different sessions can all change the input sequence and must be controlled separately.
ConditionControlled RSI policyControlled ROC policy
Fewer than N consecutive changesNA until N+1 valid consecutive prices exist.The paired comparison also waits for N+1 prices.
Constant positive prices from the startG = L = 0 after initialization; return 50 explicitly.0%.
Only gains, with at least one gainG > 0, L = 0: return 100. This need not hold if earlier losses remain in state.Positive when the current endpoint exceeds the reference.
Only losses, with at least one lossG = 0, L > 0: return 0. Earlier gains can keep it above zero.Negative when the current endpoint is below the reference.
Zero reference priceZero is allowed as a price input; changes still have meaning.NA because the percentage denominator is zero. Never substitute a small denominator.
Current price zero, positive referenceCompute from the valid changes.−100%; this is the nonnegative-price floor.
Missing or negative priceReset state and require a fresh run of N+1 nonnegative prices.NA until the same fresh window exists; negative-price percentages are outside this example's scope.

Reproduce both paths in Pine Script v6

Paste this full indicator into Pine Editor. Synthetic mode repeats rows 0 through 12 and resets the custom states at each row 0, so any complete cycle reproduces the table. Open the Data Window and match Fixture row, both sources, both RSI values and both ROC percentages. The pane selector displays one unit at a time; it does not rescale ROC to resemble RSI.

  • Disable synthetic mode to feed the identical chart close into both paths. A and B should then overlap. Change the shared length only after matching the source, loaded history and confirmation time.
  • The built-in RSI diagnostic does not reset every 13 bars. Compare it with the custom calculation on uninterrupted chart data with matching initialization; do not demand cycle-by-cycle fixture equality. Its all-flat and missing-data behavior is not established by this page.
  • Both indicator outputs use barstate.isconfirmed. This confirms the chart bar, not an unfinished higher-timeframe series. The script deliberately uses no request.security calls or external indicator inputs.
Original comparison indicator. Every calculation runs on each bar; the displayed values wait for chart-bar confirmation. It places no orders.pine
//@version=6
indicator("RSI vs ROC: path and endpoint audit", overlay = false, precision = 6, max_bars_back = 501)
bool fixture = input.bool(true, "Use synthetic paths (N = 4)")
int chartLength = input.int(14, "Chart close length", minval = 1, maxval = 500)
string pane = input.string("RSI", "Pane units", options = ["RSI", "ROC percent"])
int n = fixture ? 4 : chartLength
int row = bar_index % 13
var gradual = array.from(100.0, 101.0, 100.0, 101.0, 100.0, 101.0, 102.0, 103.0, 104.0, 104.0, 104.0, 104.0, 104.0)
var gap = array.from(100.0, 101.0, 100.0, 101.0, 100.0, 104.0, 104.0, 104.0, 104.0, 104.0, 104.0, 104.0, 104.0)
float a = fixture ? array.get(gradual, row) : close
float b = fixture ? array.get(gap, row) : close
bool resetCycle = fixture and row == 0

// Custom RSI: SMA seed of N consecutive changes, then Wilder recurrence.
// Reject negative/missing prices and restart; both zero averages map to 50.
compare(float src, int length, bool reset) =>
    var int count = 0
    var float gainSum = 0.0
    var float lossSum = 0.0
    var float avgGain = na
    var float avgLoss = na
    bool valid = not na(src) and src >= 0
    bool previousValid = not na(src[1]) and src[1] >= 0
    if reset or not valid or not previousValid
        count := 0
        gainSum := 0.0
        lossSum := 0.0
        avgGain := na
        avgLoss := na
    else
        float change = src - src[1]
        float gain = math.max(change, 0.0)
        float loss = math.max(-change, 0.0)
        if count < length
            gainSum += gain
            lossSum += loss
            count += 1
            if count == length
                avgGain := gainSum / length
                avgLoss := lossSum / length
        else
            avgGain := ((length - 1) * avgGain + gain) / length
            avgLoss := ((length - 1) * avgLoss + loss) / length
    bool ready = count >= length
    float total = avgGain + avgLoss
    float rsi = ready ? (total == 0 ? 50.0 : 100.0 * avgGain / total) : na
    float roc = ready and src[length] > 0 ? 100.0 * (src - src[length]) / src[length] : na
    [rsi, roc, avgGain, avgLoss]

[rsiA, rocA, gainA, lossA] = compare(a, n, resetCycle)
[rsiB, rocB, gainB, lossB] = compare(b, n, resetCycle)
// Built-ins are diagnostics only: they do not reset at fixture cycle boundaries.
float builtinRsiA = ta.rsi(a, n)
float builtinRocA = ta.roc(a, n)
bool show = barstate.isconfirmed
plot(show ? (pane == "RSI" ? rsiA : rocA) : na, "A: small rises / chart close", color.blue, 2, plot.style_linebr)
plot(show ? (pane == "RSI" ? rsiB : rocB) : na, "B: single gap / chart close", color.orange, 2, plot.style_linebr)
plot(show ? rsiA : na, "Custom RSI A", display = display.data_window)
plot(show ? rsiB : na, "Custom RSI B", display = display.data_window)
plot(show ? rocA : na, "ROC A percent", display = display.data_window)
plot(show ? rocB : na, "ROC B percent", display = display.data_window)
plot(show ? gainA : na, "Average gain A", display = display.data_window)
plot(show ? lossA : na, "Average loss A", display = display.data_window)
plot(show ? gainB : na, "Average gain B", display = display.data_window)
plot(show ? lossB : na, "Average loss B", display = display.data_window)
plot(show ? a : na, "Source A", display = display.data_window)
plot(show ? b : na, "Source B", display = display.data_window)
plot(fixture ? row : na, "Fixture row", display = display.data_window)
plot(show ? builtinRsiA : na, "Built-in RSI A (no cycle reset)", display = display.data_window)
plot(show ? builtinRocA : na, "Built-in ROC A", display = display.data_window)

Translate the measurement into a testable rule

An RSI threshold expresses a balance of smoothed gains and losses. A ROC threshold expresses a percentage change over a chosen number of bars. RSI 70 is not equivalent to ROC +7%, and either can remain positive during a trend. A high reading alone does not establish an impending reversal or profitable entry.

  • For a strategy comparison, specify entry and exit conditions, signal confirmation, order timing, sizing, fees and slippage. Keep evaluation dates fixed and examine results on data that did not choose the thresholds.
  • Give the Pine Script AI Coding Agent the formula, seed, flat policy, invalid-price policy and this fixture. Ask it to draft your selected rule and request a syntax check, then verify output values and decision timestamps in TradingView.
Where Pineify fits

Pine Script AI Coding Agent

Draft your RSI or ROC rule with explicit inputs and boundary behavior. Request a Pine syntax check and compare the generated output with the controlled example.

Draft my comparison rule

Frequently asked questions

Educational comparison, not investment advice. The fixture is synthetic and reports indicator arithmetic, not trades or investment returns. Syntax acceptance does not verify numerical output, execution assumptions or profitability.

Sources and verification

Published by Pineify. Pineify is an independent product and is not affiliated with TradingView. Sources support the technical explanations; examples do not establish trading performance.