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.
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)