You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Donald Dorsey, "Refining the Relative Volatility Index", TASC V.13:9 (Sep 1995) 388-391 — the
revision of his V.11:6 (Jun 1993) close-only RVI. It runs the 1993 RVI over daily highs and over
daily lows and averages the two indices; the closes are the fallback when a feed carries no
high/low (Chart manual).
The leg this function is made of ships: TA_RVI landed 2026-09-05 in commit 8c0fedbcd
(#366), ta_codegen/input/rvi/{rvi.c,rvi.yaml,rvi.md} at HEAD. RVIR is that body run
twice and averaged; it invents nothing, and it inherits every variant choice that card made.
The refined form is a different series, not a re-parameterisation: MEASURED at (14,10) on the
252-bar corpus, max |RVIR − RVI(close)| = 18.05 index points, mean 5.52, Pearson r 0.709.
Name collision, and it must be said on both docs. The Chart manual ("which is the RVI used in
Chart") and DXcharts reserve the bare name RVI for this function and call #366's function RVIorig; pandas-ta-classic and trading-signals default the other
way. TA_RVI has shipped under the close form, so the pair cannot be swapped. rvi.md:21 already
carries the collision note; rvir.md owes the reciprocal.
Formula
n1 = standard-deviation window (default 10), n2 = Wilder smoothing period (default 14). RVI(...) is the shipped TA_RVIunchanged — its ties-feed-neither rule
(ta_codegen/input/rvi/rvi.c:137-141) and its 50.0 on a zero denominator (rvi.c:225) included.
RVIR_i = 0.5 * ( RVI(high)_i + RVI(low)_i )
Primary-source trace. The 1995 article's PDF is paywalled ($3.95) and was not purchased; the
store listing was fetched (HTTP 200) for title, author, pagination and the opening two paragraphs,
which are about trend and inertia and do not state the arithmetic. The arithmetic is traced instead
through a MetaStock formula-language reproduction of the article, published under the heading "New Relative Volatility Index" and labelled "Appeared in the September 95 issue of Stocks &
Commodities magazine" — code, not prose, in the formula language the article was written into
(provenance qualified under References: the host credits traders.com, not Equis):
That settles four things structurally, in one source:
Two full RVI legs, each with its own direction gate and its own Std — not RVI of an averaged
price.
Denominator is EMA(up) + EMA(down). A bar with HIGH == Ref(HIGH,-1) makes bothIf()
legs zero, so its sigma enters neither numerator nor denominator — ties feed neither, which is
exactly rvi.c:137-141.
Std 10, smoothing 14 — the shipped defaults.
No zero-denominator guard, so TA-Lib's 50.0 is a house choice. App::Chart and
trading-signals 8.3.0 independently pick 50 as well.
The averaging spelling is not a variant axis.0.5*(a+b), (a+b)*0.5, (a+b)/2.0 and 0.5*a + 0.5*b are bit-identical for operands in [0,100]: scaling by a power of two is exact, so
each is one rounding of a+b. MEASURED: 0 mismatches in 200 000 random pairs from [0,100]. No
spelling gate is owed.
Both legs are the same function at the same parameters, so they share a lookback and seed on the
same bar — no anchor question, no max(). Fuse them into one loop rather than KC's
malloc-two-scratch shape (ta_codegen/input/kc/kc.c:81,88): the result is bit-equal to two TA_RVI
calls by construction, which makes the differential gate below free.
lookbackTotal= (optInStdDevPeriod-1) + (optInTimePeriod-1)
+TA_GetUnstablePeriod(TA_FUNC_UNST_RVI); /* == TA_RVI_Lookback, rvi.c:15-19 */if( startIdx<lookbackTotal ) startIdx=lookbackTotal;
if( startIdx>endIdx ) { *outBegIdx=0; *outNBElement=0; returnTA_SUCCESS; }
/* Two independent copies of TA_RVI's state, one over inHigh, one over inLow: * var.c's shifted-sum cursors plus prevUp/prevDn. Coefficients spelled ONCE, * wBeta first (ta_codegen/input/rma/rma.c:51-52); both legs share them. * Every read of bar i in BOTH legs must precede the store to outReal[i]: with * outReal aliasing inHigh or inLow the slack is exactly zero at n2 == 1. */wBeta= (double)(optInTimePeriod-1) / (double)optInTimePeriod;
wAlpha=1.0-wBeta;
... seedbothlegsexactlyasTA_RVIdoes, thenperbar ...
outReal[outIdx++] =0.5* ( rviHigh+rviLow );
TA_RVIR_Lookback == TA_RVI_Lookback. MEASURED at (14,10), unstable period 0, on the 252-bar
reference: outBegIdx = 22, outNBElement = 230. The composed trading-signals arm reports the same beg at all four tuples — (n2,n1) = (14,10) → 22, (30,30) → 58, (1,10) → 9, (2,2) → 2.
Rejected variants — MEASURED at (14,10) on TA_SREF_{high,low}_daily_ref_0_PRIV (252 bars, src/tools/ta_regtest/test_data.c:38,65), max abs deviation from the proposed form
Variant
Source
Max dev
Why rejected
plain EMA2/(n2+1), ties→neither
community Pine; pandas' mamode default "ema"
12.94 (bar 73); tail bar 251 3.80
Dorsey defines RVI as "the relative strength index (RSI) with the standard deviation over the past 10 days used in place of daily price change" (V.11:6 p.253, quoted verbatim on the Equis 1993 page) — and RSI's smoother is Wilder's. The 1993 transcription is that recursion exactly, ((PREV*13)+x)/14, alpha = 1/14. Counter-evidence, stated because it is real: the 1995 page writes Mov(...,14,E), a plain EMA, against the 1993 page's exact Wilder form — the familiar MetaStock shorthand. It loses to the 1993 transcription and to Dorsey's own RSI sentence, and the smoother is #366's axis in any case.
plain EMA + ties→down
as above
13.78 (bar 73)
Both flips at once; the isolated EMA cost is the row above.
RVI( (high+low)/2 ) — average the prices, not the indices
none published; the reimplementer's shortcut
13.58
The 1995 code has two independent Std() calls and two independent gates. Looks like a simplification; is not one.
thirds: (RVI(h)+RVI(l)+RVI(c))/3
pandas thirds=True (rvi.py:45-55)
6.02 at (14,10); 33.33 at L=3 and 15.86 at L=5 in pandas' own arithmetic
Not Dorsey's; no other source carries it. A separate indicator, not a variant.
tie (x[i] == x[i-1]) → down
App::Chart 275 RVI.pm (den = EMA(stddev))
4.64 (bar 88); tail bar 251 still 2.24e-1
Both If() legs of the 1995 code are 0 on a tie. App::Chart is the documented negative control.
tie → up
ProRealCode
3.76 (bar 81)
Only source that does this.
9-period stddev
fmlabs; DXcharts prose
3.23 (bar 83)
One source family against five; also moves outBegIdx 22 → 21. DXcharts contradicts itself — prose says "9-period standard deviation", its own legend says "SD - 10-days' standard deviation" — so cite DXcharts for structure, never for the period.
Proposed API
TA_RetCodeTA_RVIR( intstartIdx,
intendIdx,
constdoubleinHigh[],
constdoubleinLow[],
intoptInTimePeriod, /* From 1 to 100000, default 14 */intoptInStdDevPeriod, /* From 2 to 100000, default 10 */int*outBegIdx,
int*outNBElement,
doubleoutReal[] );
intTA_RVIR_Lookback( intoptInTimePeriod, intoptInStdDevPeriod );
/* (n1-1) + (n2-1) + TA_GetUnstablePeriod(TA_FUNC_UNST_RVI) */
Group Volatility Indicators, matching the shipped rvi.yaml:2 and pandas' category. One
output outReal, flags: [line] — well under the cap of 4 (src/tools/ta_regtest/test_codegen.h:16).
Input bundle inPriceHL, price_components: [high, low] — the existing DONCHIAN bundle
(ta_codegen/input/donchian/donchian.yaml:6-8). Arity 2. This is why the refined form cannot be a
boolean flag on TA_RVI, which takes a bare inReal (rvi.yaml:5-7).
Parameters, ranges and defaults are the shipped rvi.yaml:8-22, unchanged and in that order.
App::Chart is a third witness that the two periods are separate and defaulted 10/14, after
thinkorswim and trading-signals 8.3.0.
No new TA_FUNC_UNST_* id, and no second one either. RVIR reads TA_FUNC_UNST_RVI
(id 26, src/tools/ta_regtest/test_internals.c:1158), the way efi.yaml:4 declares flags: [stream] only while efi.c:22 reads TA_FUNC_UNST_EMA. KC's two-id case does not arise:
KC inherits two because its legs are different functions with different lookbacks; RVIR's legs
are the same function at the same parameters, so a per-leg unstable period has no user story and
would break outBegIdx == lookback (BBANDS with MAType_MAMA and period >= 34: middle band misaligned #99) by over-warming one leg. One id means doRangeTestEx, not doRangeTestMulti — ta_test_priv.h:254-256 still names KC as the only
two-id function, and the shipped test_rvi.c:856 already uses doRangeTestEx.
Decisions
Ties feed neither leg — inherited from the shipped TA_RVI (rvi.c:137-141) and supported by
the 1995 and 1993 MetaStock reproductions, whose denominators are EMA(up) + EMA(down). Tally:
ties→neither = 1993 formula + 1995 formula + pandas-ta-classic 0.6.52 + trading-signals 8.3.0 +
shipped TA_RVI; ties→down = App::Chart alone, plus prose (Chart manual, DXcharts) that writes
the denominator as "EMA of SD".
Wilder smoothing (alpha = 1/n2), not 2/(n2+1) — inherited from (RVI) Relative Volatility Index #366; the 1993 ((PREV*13)+x)/14 transcription plus Dorsey's own RSI sentence are the primary-source support.
Cost of the flip is MEASURED at 12.94.
50.0 on a zero denominator — inherited; house choice, no source states a guard, and
App::Chart and trading-signals both land on 50 independently.
thirds declined. pandas ships it, nobody else does, and it is a different indicator, not a
variant: MEASURED 33.33 at L=3 and 15.86 at L=5 against refined=True in pandas' own arithmetic.
Not a parameter on this function; a separate card at most.
Group Volatility Indicators; flags: [stream]; one unstable id, borrowed.
References
[F] = fetched this session, HTTP 200. [C] = cited, fetched for #366 rather than here.
[F] MetaStock-language reproduction of the 1995 article — "New Relative Volatility Index" and "Inertia", labelled "Appeared in the September 95 issue of Stocks & Commodities magazine",
quoted in full under Formula; the source that settles the tie rule. Read at web.archive.org/web/20110803233955id_/http://trader.online.pl/MSZ/e-w-Inertia.html
(trader.online.pl is dead). Provenance, stated exactly: the page is a third-party MetaStock
formula collection and credits traders.com; it does not carry an Equis byline, unlike the
same site's 1993 page. App::Chart's header cites both, calling the 1993 one "Reproducing
equis.com original 1993 version" and this one "1995 revision".
[F] Equis/MetaStock, 1993 RVI (same site, credited to equis.com) — @RVI Down = ((PREV*13)+If(ROC(C,1,%)<0,Stdev(C,10),0))/14, @RVI Up likewise with >0, @RVI = (100*Fml("@RVI Up"))/(Fml("@RVI Up")+Fml("@RVI Down")). ((PREV*13)+x)/14is the
Wilder recursion. The same page quotes Dorsey verbatim from TASC V.11:6 (253-256): "The RVI is
simply the relative strength index (RSI) with the standard deviation over the past 10 days used in
place of daily price change." Read at web.archive.org/web/20110514040046id_/http://trader.online.pl/MSZ/e-w-Relative_Volatility_Index_RVI.html.
[F] Dorsey, "Refining the Relative Volatility Index", TASC V.13:9 (Sep 1995) 388-391 — https://store.traders.com/-v13-c09-refinin-pdf.html. Paywalled at $3.95, not purchased;
title, author, pagination and the opening two paragraphs are from the listing, and those
paragraphs are about trend and inertia, not the arithmetic.
[F] Chart manual (Kevin Ryde), https://user42.tuxfamily.org/chart/manual/Relative-Volatility-Index.html — "In 1995 Dorsey
revised his idea, to apply RVI to the daily highs and daily lows and average the result, thus the
following, which is the RVI used in Chart", then RVI = (RVIorig of highs + RVIorig of lows) / 2; "The stddev period of 10 days and the default EMA of 14 days are parameters"; "The EMA period
follows J. Welles Wilder's reckoning … the same as an RSI."
[F] App::Chart 275 (CPAN KRYDE/App-Chart-275, GPLv3), read from fastapi.metacpan.org. RVI.pmproc() = (high_proc(high//close) + low_proc(low//close)) / 2 over two independent proc_original closures — the refined form in code. proc_original is num = EMA(v > prev ? stddev : 0), den = EMA(stddev) — an EMA(S) denominator is ties→down —
and rvi = (den == 0 ? 50 : 100*num/den). Its own header cites the 1995 store listing: "TASC 1995, high/low combination, and introducing Inertia."EMA.pm carries Wilder as N = 2W−1 (alpha = 1/W exactly) but is weight-normalised from zero (sum/weight), a different
transient from an SMA-seeded Wilder — converged tail only. Stddev.pm divides by count: population.
Read locally this session: pandas-ta-classic 0.6.52 volatility/rvi.py (if refined: … return 0.5 * (high_rvi + low_rvi), "r"; _rvi_compute passes ONE length to both stdev and ma, so it
cannot witness n1 != n2), utils/_core.py (ties→neither), statistics/stdev.py (ddof 0);
trading-signals 8.3.0 dist/volatility/RVI/RelativeVolatilityIndex.js (WSMA on both legs,
ties→neither, 50 on zero total, separate interval/stddevInterval, stddevInterval < 2
rejected); tulip_serve/vendor/tulipindicators/beta/rvi.c.
Sample input/output (board policy)
Input: TA_SREF_{high,low}_daily_ref_0_PRIV, 252 bars — MEASURED equal element for element to trading_signals_serve/corpus.json. optInTimePeriod = 14, optInStdDevPeriod = 10, unstable 0; outBegIdx = 22, outNBElement = 230. Left column: the proposed body's spelling (var.c shifted-sum
reseed, rma.c wBeta-first, 100.0*(U/T) per leg, then 0.5*(a+b)). Right: the composed
trading-signals arm, run. The hex column is the oracle's bits — those are what a golden test
freezes, not the left column's.
bar
TA_RVIR (proposed spelling)
trading-signals 8.3.0 (2 legs, averaged)
oracle hex
22
43.834741790037505
43.834741790037512
4045ead8d1a86866
23
40.773744308768570
40.773744308768570
4044630a0db2d046
24
37.279696373902450
37.279696373902446
4042a3cd173d5c43
…
250
53.521391948420400
53.521391948420100
404ac2bcf8ab6e58
251
52.847645335992624
52.847645335991670
404a6c7fa4725878
MEASURED over all 230 values: max abs 9.52e-13, max relative 1.80e-14; 42/230 bit-equal. The
residual is #366's three house FP-spelling choices; it is ~4x that card's close-form
figure (2.42e-13) because averaging combines two legs' residuals.
Oracles
Oracle
Covers the refined form?
Arm status
Work to add the arm
inHigh == inLow reduction
Yes, exactly — both legs collapse to one, and 0.5*(a+a) == a bit-exact (MEASURED: 0/230 bit mismatches)
free, in-tree, no server
none. RVIR on a zero-range series must be bit-identical to the shipped TA_RVI on it. Zero tolerance.
Composite differential vs two TA_RVI calls
Yes, by construction
free, in-tree, no server
none — TA_RVI shipped in 8c0fedbcd. This is the primary gate.
pandas-ta-classic 0.6.52
Yes, natively — refined=True is 0.5*(RVI(high)+RVI(low)); mamode='rma', ties→neither; constrained to n1 == n2, so it cannot witness the shipped (14,10) default
must be added
one compute_RVIR + one SPECS row in pandas_serve/server.py, the compute_KC shape (2 price inputs), ~10 lines. length ∈ {3,5}, bars ≥ beg + 168.
trading-signals 8.3.0
Legs only — ships the close-form RVI; the average is the harness's
must be added
~9-line ARMS entry whose ctor builds two RelativeVolatilityIndex instances and returns {update(b){ … 0.5*(h+l) }}, feed: 'hl'. No bar() widening — hl already exists in capture.mjs. The only arm that reaches the shipped (14,10) default.
1995 MetaStock formula reproduction
Yes — a formula witness, not an executable arm
not an arm
The strongest evidence on this card; it is what settles the tie rule.
App::Chart 275 (Perl, GPLv3)
Yes, natively — but ties→down
not worth an arm
A fifth server, and a negative control: the flip it represents is MEASURED at 4.64. Value banked by reading the source.
Tulip 0.9.2
No, structurally
DO NOT ADD
beta/rvi.c takes inputs[0] only — one series, so it cannot express a high/low average, on top of #366's MEASURED 36.04 finding that it is a different indicator.
ta4j 0.24.1
No
impossible
Verified on 0.24.1 (already in ~/.m2, so the pin bump is a pom edit, not a download): 406 indicator .java files in the sources jar; no Relative Volatility, no Relative Vigor. Same verdict as the 0.22.6 pin, on the newer jar.
PyneCore / pine_serve
n/a
n/a
No rvi in pynecore/lib/ta.py; RVI is not a Pine ta.* builtin.
pandas convergence — MEASURED, ta.rvi(close, high=high, low=low, length=L, mamode='rma', refined=True) vs two TA_RVI calls averaged, max abs deviation from the given offset onward
(pandas-ta-classic 0.6.52 / pandas 3.0.3 / numpy 2.5.1):
length
skip 0
skip 56
skip 112
skip 168
3
13.2
1.66e-9
1.41e-11
4.72e-12
5
7.12
3.35e-5
9.96e-11
6.96e-12
10
11.4
2.81e-2
6.97e-5
1.55e-7
14
9.33
7.56e-2
1.36e-3
2.70e-5
pandas' overlap/rma.py seeds from a one-sample mean over an input whose first length-1 bars are
NaN, so the error decays as ((L−1)/L)^k and at small L the arm converges onto a correctly
SMA-seeded Wilder. It witnesses the averaging step, the high/low leg assignment, the
smoother, the up/down gate and the tie rule, from another codebase and language. It does not
witness n1 != n2: only the composed trading-signals arm reaches the shipped default (14,10), and
there the average is the harness's arithmetic. That split is the honest state of the coverage.
Verification plan
Composite differential, bit-exact memcmp, appended to src/tools/ta_regtest/ta_test_func/test_composite2.c. Reference: two TA_RVI calls, on inHigh
and on inLow, both at RVIR's own startIdx, then 0.5*(a+b) — bit-exact because the fused
legs execute the identical operation sequence. Grid (n1,n2) over {2,3,10,14,30} × {1,2,14,30}.
Every piece is a shipped function; the only extra arithmetic is one exact multiply by 0.5.
inHigh == inLow identity, zero tolerance.TA_RVIR(h,h,…) must memcmp-equal TA_RVI(h,…)
over the whole range. MEASURED exact today; free, needs no server.
External golden — trading-signals, composed. Freeze the arm's hex-of-IEEE-bits at (n2,n1) = (14,10), (30,30), (1,10), (2,2), compared with checkOracleValue(got, want, /*relTol*/ 1e-9, /*absTol*/ 1e-12) (ta_test_priv.h:294-319).
relTol is set by the MEASURED worst relative — 1.80e-14 / 6.09e-15 / 1.42e-16 / 3.65e-12,
worst at (2,2) — leaving 274x. absTol is the ta_test_priv.h abs-near-zero rule, not a number
the data forces: at (1,10) want is exactly 0.0 on 88 of 243 bars, where relative error is
undefined, and MEASURED both sides are bit-equal on every one of them. Size it from the (1,10)
series max abs, 1.42e-14, so absTol 1e-12 leaves 70x.
Second oracle — pandas converged tail, L ∈ {3,5}, bars ≥ beg + 168, checkOracleValue(relTol 1e-9, absTol 1e-9) (MEASURED worst 4.72e-12 and 6.96e-12, ~150x
headroom). Record that the same arm is 13.2 and 7.12 away at skip 0, so the offset cannot be
mistaken for arbitrary.
Tie rule — an exact, non-decaying, SIX-bar probe.MEASURED tie census: high 128 up /
120 down / 3 ties (bars 66, 88, 250); low 133 / 115 / 3 ties (bars 81, 176, 191); close
123 / 127 / 1 (bar 101). At optInTimePeriod = 1 each leg has no memory, so a tie reads a literal
and the average takes a quarter-point value:
bar
ties→neither
ties→down
ties→up
66, 81, 88, 176
75.0
50.0
100.0
191, 250
25.0
0.0
50.0
MEASURED, the whole 243-value series at (1,10) takes only {0, 25, 50, 75, 100} — counts {0:88, 25:2, 50:47, 75:4, 100:102}, nothing else — and only {0, 50, 100} under either flip:
the presence of a 25 or a 75 decides the rule in one assert. Assert the six literals and the
value set. At (14,10) the flip is far more visible than in the close form — MEASURED max 4.64
(ties→down, bar 88) and 2.24e-1 at the last bar, against (RVI) Relative Volatility Index #366's 1.97 and 5.33e-5,
because a tie sits at bar 250.
Batch in-place aliasing must be bit-identical — the opposite of item 6. Template exists and
takes the same inPriceHL bundle: src/tools/ta_regtest/ta_test_func/test_donchian.c:430-460
("each output over each input in turn must match"). Alias outReal over inHigh, then over inLow, separately — two cases, not one. Both legs must finish reading bar i before the
store; at n2 == 1 with unstable 0 the slack is exactly zero. ASan cannot see a violation.
No NaN/Inf can escape. Each leg is bounded to [0, 100] by its division-first combine; the
average of two values in [0, 100] is in [0, 100]. The averaging adds no division and no
overflow path.
Open questions
Name — one word, and it is the only thing outstanding. Recommend TA_RVIR: it matches
pandas' output naming (RVIr_<length>) and sorts adjacent to rvi. Alternative: TA_RVIHL.
Swapping the pair so TA_RVI becomes this function is not available — TA_RVI shipped under the
close form in 8c0fedbcd. There is no evidence that decides between the two candidates; it is a
naming call, and the C symbol is part of the contract, so the card stays a proposal until it is
made. Nothing else about the formula, the contract, the oracles or the gates waits on it.
Notes
Streaming is not a risk. The shipped rvi.yaml:4 carries flags: [unstable_period, stream]
and Core::rvi_open exists at ta_codegen/output/rust/library/src/ta_func/rvi.rs:1016 —
stream-census passes on RVI's IR. If RVI streams, RVIR streams. Each leg is a TA_RVI stream; the
ring is n1 highs plus n1 lows, and the up/down gate needs no extra slot.
ABI and sorting: one new function; there is no C TA_FuncID — the ordinal ABI is the Rust
crate's FuncId (ta_codegen/output/rust/library/src/abstract_api.rs:44), where a new variant
renumbers every later discriminant. No new unstable id. rvir sorts between rvi and rvol
(rvol landed as (RVOL) Relative Volume #370), so confirm ta_ref_serve still builds
((AC) Accelerator/Decelerator Oscillator #228 AC).
Follow-up, no card exists (0 hits for "inertia" on the board): the same 1995 article also
introduced Inertia, a 20-period smoothing of RVIR. The sources disagree on the smoother — the
1995 page's prose says "a Linear Regression indicator" and the Chart manual says a 20-day LSQMA,
while that same page's code says Mov(Fml("New Relative Volatility Index"),20,SIMPLE) — so an
INERTIA card must arbitrate it. TA_LINEARREG (the endpoint MA) is already shipped.
Gotchas: a fullcargo run -- generate (a --func=RVIR filter skips Core.java and the
servers); scripts/build.py servers, not build; register the new test .c in both CMakeLists.txt and src/tools/ta_regtest/Makefile.am; the CHANGELOG entry; and ta_regtest --function=RVIR matches the group tag, so confirm the filter ran tests instead of
exiting 0 vacuously.
What it is
Donald Dorsey, "Refining the Relative Volatility Index", TASC V.13:9 (Sep 1995) 388-391 — the
revision of his V.11:6 (Jun 1993) close-only RVI. It runs the 1993 RVI over daily highs and over
daily lows and averages the two indices; the closes are the fallback when a feed carries no
high/low (Chart manual).
The leg this function is made of ships:
TA_RVIlanded 2026-09-05 in commit8c0fedbcd(#366),
ta_codegen/input/rvi/{rvi.c,rvi.yaml,rvi.md}at HEAD. RVIR is that body runtwice and averaged; it invents nothing, and it inherits every variant choice that card made.
The refined form is a different series, not a re-parameterisation: MEASURED at (14,10) on the
252-bar corpus, max |RVIR − RVI(close)| = 18.05 index points, mean 5.52, Pearson r 0.709.
Name collision, and it must be said on both docs. The Chart manual ("which is the RVI used in
Chart") and DXcharts reserve the bare name RVI for this function and call
#366's function
RVIorig; pandas-ta-classic and trading-signals default the otherway.
TA_RVIhas shipped under the close form, so the pair cannot be swapped.rvi.md:21alreadycarries the collision note;
rvir.mdowes the reciprocal.Formula
n1= standard-deviation window (default 10),n2= Wilder smoothing period (default 14).RVI(...)is the shippedTA_RVIunchanged — its ties-feed-neither rule(
ta_codegen/input/rvi/rvi.c:137-141) and its50.0on a zero denominator (rvi.c:225) included.Primary-source trace. The 1995 article's PDF is paywalled ($3.95) and was not purchased; the
store listing was fetched (HTTP 200) for title, author, pagination and the opening two paragraphs,
which are about trend and inertia and do not state the arithmetic. The arithmetic is traced instead
through a MetaStock formula-language reproduction of the article, published under the heading
"New Relative Volatility Index" and labelled "Appeared in the September 95 issue of Stocks &
Commodities magazine" — code, not prose, in the formula language the article was written into
(provenance qualified under References: the host credits
traders.com, not Equis):That settles four things structurally, in one source:
Std— not RVI of an averagedprice.
EMA(up) + EMA(down). A bar withHIGH == Ref(HIGH,-1)makes bothIf()legs zero, so its sigma enters neither numerator nor denominator — ties feed neither, which is
exactly
rvi.c:137-141.Std10, smoothing 14 — the shipped defaults.50.0is a house choice. App::Chart andtrading-signals 8.3.0 independently pick 50 as well.
The averaging spelling is not a variant axis.
0.5*(a+b),(a+b)*0.5,(a+b)/2.0and0.5*a + 0.5*bare bit-identical for operands in[0,100]: scaling by a power of two is exact, soeach is one rounding of
a+b. MEASURED: 0 mismatches in 200 000 random pairs from [0,100]. Nospelling gate is owed.
Both legs are the same function at the same parameters, so they share a lookback and seed on the
same bar — no anchor question, no
max(). Fuse them into one loop rather than KC'smalloc-two-scratch shape (
ta_codegen/input/kc/kc.c:81,88): the result is bit-equal to twoTA_RVIcalls by construction, which makes the differential gate below free.
TA_RVIR_Lookback == TA_RVI_Lookback. MEASURED at (14,10), unstable period 0, on the 252-barreference:
outBegIdx = 22,outNBElement = 230. The composed trading-signals arm reports the samebegat all four tuples —(n2,n1) = (14,10) → 22,(30,30) → 58,(1,10) → 9,(2,2) → 2.Rejected variants — MEASURED at (14,10) on
TA_SREF_{high,low}_daily_ref_0_PRIV(252 bars,src/tools/ta_regtest/test_data.c:38,65), max abs deviation from the proposed form2/(n2+1), ties→neithermamodedefault"ema"((PREV*13)+x)/14, alpha = 1/14. Counter-evidence, stated because it is real: the 1995 page writesMov(...,14,E), a plain EMA, against the 1993 page's exact Wilder form — the familiar MetaStock shorthand. It loses to the 1993 transcription and to Dorsey's own RSI sentence, and the smoother is #366's axis in any case.RVI( (high+low)/2 )— average the prices, not the indicesStd()calls and two independent gates. Looks like a simplification; is not one.(RVI(h)+RVI(l)+RVI(c))/3thirds=True(rvi.py:45-55)x[i] == x[i-1]) → downRVI.pm(den = EMA(stddev))If()legs of the 1995 code are 0 on a tie. App::Chart is the documented negative control.outBegIdx22 → 21. DXcharts contradicts itself — prose says "9-period standard deviation", its own legend says "SD - 10-days' standard deviation" — so cite DXcharts for structure, never for the period.Proposed API
Volatility Indicators, matching the shippedrvi.yaml:2and pandas'category. Oneoutput
outReal,flags: [line]— well under the cap of 4 (src/tools/ta_regtest/test_codegen.h:16).inPriceHL,price_components: [high, low]— the existing DONCHIAN bundle(
ta_codegen/input/donchian/donchian.yaml:6-8). Arity 2. This is why the refined form cannot be aboolean flag on
TA_RVI, which takes a bareinReal(rvi.yaml:5-7).rvi.yaml:8-22, unchanged and in that order.App::Chart is a third witness that the two periods are separate and defaulted 10/14, after
thinkorswim and trading-signals 8.3.0.
[stream]— notunstable_period, notpath_dependent.TA_FUNC_UNST_*id, and no second one either. RVIR readsTA_FUNC_UNST_RVI(id 26,
src/tools/ta_regtest/test_internals.c:1158), the wayefi.yaml:4declaresflags: [stream]only whileefi.c:22readsTA_FUNC_UNST_EMA. KC's two-id case does not arise:KC inherits two because its legs are different functions with different lookbacks; RVIR's legs
are the same function at the same parameters, so a per-leg unstable period has no user story and
would break
outBegIdx == lookback(BBANDS with MAType_MAMA and period >= 34: middle band misaligned #99) by over-warming one leg. One id meansdoRangeTestEx, notdoRangeTestMulti—ta_test_priv.h:254-256still names KC as the onlytwo-id function, and the shipped
test_rvi.c:856already usesdoRangeTestEx.Decisions
TA_RVI(rvi.c:137-141) and supported bythe 1995 and 1993 MetaStock reproductions, whose denominators are
EMA(up) + EMA(down). Tally:ties→neither = 1993 formula + 1995 formula + pandas-ta-classic 0.6.52 + trading-signals 8.3.0 +
shipped
TA_RVI; ties→down = App::Chart alone, plus prose (Chart manual, DXcharts) that writesthe denominator as "EMA of SD".
alpha = 1/n2), not2/(n2+1)— inherited from (RVI) Relative Volatility Index #366; the 1993((PREV*13)+x)/14transcription plus Dorsey's own RSI sentence are the primary-source support.Cost of the flip is MEASURED at 12.94.
50.0on a zero denominator — inherited; house choice, no source states a guard, andApp::Chart and trading-signals both land on 50 independently.
thirdsdeclined. pandas ships it, nobody else does, and it is a different indicator, not avariant: MEASURED 33.33 at L=3 and 15.86 at L=5 against
refined=Truein pandas' own arithmetic.Not a parameter on this function; a separate card at most.
Volatility Indicators;flags: [stream]; one unstable id, borrowed.References
[F] = fetched this session, HTTP 200. [C] = cited, fetched for #366 rather than here.
"Inertia", labelled "Appeared in the September 95 issue of Stocks & Commodities magazine",
quoted in full under Formula; the source that settles the tie rule. Read at
web.archive.org/web/20110803233955id_/http://trader.online.pl/MSZ/e-w-Inertia.html(
trader.online.plis dead). Provenance, stated exactly: the page is a third-party MetaStockformula collection and credits
traders.com; it does not carry an Equis byline, unlike thesame site's 1993 page. App::Chart's header cites both, calling the 1993 one "Reproducing
equis.com original 1993 version" and this one "1995 revision".
equis.com) —@RVI Down = ((PREV*13)+If(ROC(C,1,%)<0,Stdev(C,10),0))/14,@RVI Uplikewise with>0,@RVI = (100*Fml("@RVI Up"))/(Fml("@RVI Up")+Fml("@RVI Down")).((PREV*13)+x)/14is theWilder recursion. The same page quotes Dorsey verbatim from TASC V.11:6 (253-256): "The RVI is
simply the relative strength index (RSI) with the standard deviation over the past 10 days used in
place of daily price change." Read at
web.archive.org/web/20110514040046id_/http://trader.online.pl/MSZ/e-w-Relative_Volatility_Index_RVI.html.https://store.traders.com/-v13-c09-refinin-pdf.html. Paywalled at $3.95, not purchased;
title, author, pagination and the opening two paragraphs are from the listing, and those
paragraphs are about trend and inertia, not the arithmetic.
https://user42.tuxfamily.org/chart/manual/Relative-Volatility-Index.html — "In 1995 Dorsey
revised his idea, to apply RVI to the daily highs and daily lows and average the result, thus the
following, which is the RVI used in Chart", then
RVI = (RVIorig of highs + RVIorig of lows) / 2;"The stddev period of 10 days and the default EMA of 14 days are parameters"; "The EMA period
follows J. Welles Wilder's reckoning … the same as an RSI."
"RVI = (RVIorig of highs + RVIorig of lows) / 2". Internally inconsistent on the stddev period
(prose "9-period", legend "SD - 10-days'"); cite for structure only.
KRYDE/App-Chart-275, GPLv3), read fromfastapi.metacpan.org.RVI.pmproc()=(high_proc(high//close) + low_proc(low//close)) / 2over two independentproc_originalclosures — the refined form in code.proc_originalisnum = EMA(v > prev ? stddev : 0),den = EMA(stddev)— anEMA(S)denominator is ties→down —and
rvi = (den == 0 ? 50 : 100*num/den). Its own header cites the 1995 store listing:"TASC 1995, high/low combination, and introducing Inertia."
EMA.pmcarries Wilder asN = 2W−1(alpha = 1/W exactly) but is weight-normalised from zero (sum/weight), a differenttransient from an SMA-seeded Wilder — converged tail only.
Stddev.pmdivides bycount:population.
card (RVI) Relative Volatility Index #366. TradingView's one structural sentence ("uses high and low price
standard deviation") describes this form, not (RVI) Relative Volatility Index #366's.
volatility/rvi.py(if refined: … return 0.5 * (high_rvi + low_rvi), "r";_rvi_computepasses ONElengthto both stdev and ma, so itcannot witness
n1 != n2),utils/_core.py(ties→neither),statistics/stdev.py(ddof 0);trading-signals 8.3.0
dist/volatility/RVI/RelativeVolatilityIndex.js(WSMA on both legs,ties→neither, 50 on zero total, separate
interval/stddevInterval,stddevInterval < 2rejected);
tulip_serve/vendor/tulipindicators/beta/rvi.c.Sample input/output (board policy)
Input:
TA_SREF_{high,low}_daily_ref_0_PRIV, 252 bars — MEASURED equal element for element totrading_signals_serve/corpus.json.optInTimePeriod = 14,optInStdDevPeriod = 10, unstable 0;outBegIdx = 22,outNBElement = 230. Left column: the proposed body's spelling (var.c shifted-sumwBeta-first,100.0*(U/T)per leg, then0.5*(a+b)). Right: the composedtrading-signals arm, run. The hex column is the oracle's bits — those are what a golden test
freezes, not the left column's.
4045ead8d1a868664044630a0db2d0464042a3cd173d5c43404ac2bcf8ab6e58404a6c7fa4725878MEASURED over all 230 values: max abs 9.52e-13, max relative 1.80e-14; 42/230 bit-equal. The
residual is #366's three house FP-spelling choices; it is ~4x that card's close-form
figure (2.42e-13) because averaging combines two legs' residuals.
Oracles
inHigh == inLowreduction0.5*(a+a) == abit-exact (MEASURED: 0/230 bit mismatches)TA_RVIon it. Zero tolerance.TA_RVIcallsTA_RVIshipped in8c0fedbcd. This is the primary gate.refined=Trueis0.5*(RVI(high)+RVI(low));mamode='rma', ties→neither; constrained ton1 == n2, so it cannot witness the shipped (14,10) defaultcompute_RVIR+ oneSPECSrow inpandas_serve/server.py, thecompute_KCshape (2 price inputs), ~10 lines.length ∈ {3,5}, bars≥ beg + 168.ARMSentry whosectorbuilds twoRelativeVolatilityIndexinstances and returns{update(b){ … 0.5*(h+l) }},feed: 'hl'. Nobar()widening —hlalready exists incapture.mjs. The only arm that reaches the shipped (14,10) default.beta/rvi.ctakesinputs[0]only — one series, so it cannot express a high/low average, on top of #366's MEASURED 36.04 finding that it is a different indicator.~/.m2, so the pin bump is a pom edit, not a download): 406 indicator.javafiles in the sources jar; no Relative Volatility, no Relative Vigor. Same verdict as the 0.22.6 pin, on the newer jar.pine_serverviinpynecore/lib/ta.py; RVI is not a Pineta.*builtin.pandas convergence — MEASURED,
ta.rvi(close, high=high, low=low, length=L, mamode='rma', refined=True)vs twoTA_RVIcalls averaged, max abs deviation from the given offset onward(pandas-ta-classic 0.6.52 / pandas 3.0.3 / numpy 2.5.1):
pandas'
overlap/rma.pyseeds from a one-sample mean over an input whose firstlength-1bars areNaN, so the error decays as
((L−1)/L)^kand at smallLthe arm converges onto a correctlySMA-seeded Wilder. It witnesses the averaging step, the high/low leg assignment, the
smoother, the up/down gate and the tie rule, from another codebase and language. It does not
witness
n1 != n2: only the composed trading-signals arm reaches the shipped default (14,10), andthere the average is the harness's arithmetic. That split is the honest state of the coverage.
Verification plan
Composite differential, bit-exact
memcmp, appended tosrc/tools/ta_regtest/ta_test_func/test_composite2.c. Reference: twoTA_RVIcalls, oninHighand on
inLow, both at RVIR's ownstartIdx, then0.5*(a+b)— bit-exact because the fusedlegs execute the identical operation sequence. Grid
(n1,n2)over{2,3,10,14,30} × {1,2,14,30}.Every piece is a shipped function; the only extra arithmetic is one exact multiply by 0.5.
inHigh == inLowidentity, zero tolerance.TA_RVIR(h,h,…)mustmemcmp-equalTA_RVI(h,…)over the whole range. MEASURED exact today; free, needs no server.
External golden — trading-signals, composed. Freeze the arm's hex-of-IEEE-bits at
(n2,n1) = (14,10),(30,30),(1,10),(2,2), compared withcheckOracleValue(got, want, /*relTol*/ 1e-9, /*absTol*/ 1e-12)(ta_test_priv.h:294-319).relTol is set by the MEASURED worst relative — 1.80e-14 / 6.09e-15 / 1.42e-16 / 3.65e-12,
worst at (2,2) — leaving 274x. absTol is the
ta_test_priv.habs-near-zero rule, not a numberthe data forces: at (1,10)
wantis exactly0.0on 88 of 243 bars, where relative error isundefined, and MEASURED both sides are bit-equal on every one of them. Size it from the (1,10)
series max abs, 1.42e-14, so absTol 1e-12 leaves 70x.
Second oracle — pandas converged tail,
L ∈ {3,5}, bars≥ beg + 168,checkOracleValue(relTol 1e-9, absTol 1e-9)(MEASURED worst 4.72e-12 and 6.96e-12, ~150xheadroom). Record that the same arm is 13.2 and 7.12 away at skip 0, so the offset cannot be
mistaken for arbitrary.
Tie rule — an exact, non-decaying, SIX-bar probe. MEASURED tie census: high 128 up /
120 down / 3 ties (bars 66, 88, 250); low 133 / 115 / 3 ties (bars 81, 176, 191); close
123 / 127 / 1 (bar 101). At
optInTimePeriod = 1each leg has no memory, so a tie reads a literaland the average takes a quarter-point value:
MEASURED, the whole 243-value series at (1,10) takes only
{0, 25, 50, 75, 100}— counts{0:88, 25:2, 50:47, 75:4, 100:102}, nothing else — and only{0, 50, 100}under either flip:the presence of a 25 or a 75 decides the rule in one assert. Assert the six literals and the
value set. At (14,10) the flip is far more visible than in the close form — MEASURED max 4.64
(ties→down, bar 88) and 2.24e-1 at the last bar, against (RVI) Relative Volatility Index #366's 1.97 and 5.33e-5,
because a tie sits at bar 250.
Streaming-tier aliasing rejection (Multi-output functions don't reject output-buffer aliasing (same buffer for two output args) #108, In-place aliasing (input==output) silently returns wrong values in 8 functions; 5 are an unreleased regression from #103 #130): the
_OpenAndFillguard must reject non-distinct output buffers. This is the rejection case, and it is not the
same thing as item 7.
Batch in-place aliasing must be bit-identical — the opposite of item 6. Template exists and
takes the same
inPriceHLbundle:src/tools/ta_regtest/ta_test_func/test_donchian.c:430-460("each output over each input in turn must match"). Alias
outRealoverinHigh, then overinLow, separately — two cases, not one. Both legs must finish reading baribefore thestore; at
n2 == 1with unstable 0 the slack is exactly zero. ASan cannot see a violation.Hand-written edge cases — flat input:
U + D == 0in both legs, so exactly50.0on everybar, no NaN (IMI: successful call emits NaN on an all-flat window (0/0) — guard the divide, return 50.0 #112). Plus (RVI) Relative Volatility Index #366's list, inherited.
Free once the YAML lands:
--codegen,--xlang-hash/server_verifyat zero tolerance,--fuzz-064, the Audit parameter-boundary OOB cases; extend ta_regtest to sweep min/default/default±1/max for every parameter #94 boundary sweep under ASan/UBSan, and the unstable sweep asdoRangeTestEx(…, TA_STABLE_CONVERGING, TA_FUNC_UNST_RVI, …).No NaN/Inf can escape. Each leg is bounded to
[0, 100]by its division-first combine; theaverage of two values in
[0, 100]is in[0, 100]. The averaging adds no division and nooverflow path.
Open questions
TA_RVIR: it matchespandas' output naming (
RVIr_<length>) and sorts adjacent torvi. Alternative:TA_RVIHL.Swapping the pair so
TA_RVIbecomes this function is not available —TA_RVIshipped under theclose form in
8c0fedbcd. There is no evidence that decides between the two candidates; it is anaming call, and the C symbol is part of the contract, so the card stays a proposal until it is
made. Nothing else about the formula, the contract, the oracles or the gates waits on it.
Notes
rvi.yaml:4carriesflags: [unstable_period, stream]and
Core::rvi_openexists atta_codegen/output/rust/library/src/ta_func/rvi.rs:1016—stream-census passes on RVI's IR. If RVI streams, RVIR streams. Each leg is a
TA_RVIstream; thering is
n1highs plusn1lows, and the up/down gate needs no extra slot.path_dependent(Declare start-dependent functions in YAML instead of the hardcoded rangeDependent[] test list #127): both legs seed atstartIdx − lookbackTotal, exactlylike RSI.
TA_FuncID— the ordinal ABI is the Rustcrate's
FuncId(ta_codegen/output/rust/library/src/abstract_api.rs:44), where a new variantrenumbers every later discriminant. No new unstable id.
rvirsorts betweenrviandrvol(
rvollanded as (RVOL) Relative Volume #370), so confirmta_ref_servestill builds((AC) Accelerator/Decelerator Oscillator #228 AC).
introduced Inertia, a 20-period smoothing of RVIR. The sources disagree on the smoother — the
1995 page's prose says "a Linear Regression indicator" and the Chart manual says a 20-day LSQMA,
while that same page's code says
Mov(Fml("New Relative Volatility Index"),20,SIMPLE)— so anINERTIA card must arbitrate it.
TA_LINEARREG(the endpoint MA) is already shipped.cargo run -- generate(a--func=RVIRfilter skipsCore.javaand theservers);
scripts/build.py servers, notbuild; register the new test.cin bothCMakeLists.txtandsrc/tools/ta_regtest/Makefile.am; the CHANGELOG entry; andta_regtest --function=RVIRmatches the group tag, so confirm the filter ran tests instead ofexiting 0 vacuously.