feat(rvir): add TA_RVIR, the 1995 refined Relative Volatility Index (drafts#68) - #413
feat(rvir): add TA_RVIR, the 1995 refined Relative Volatility Index (drafts#68)#413kevinlincg wants to merge 2 commits into
Conversation
Dorsey's own revision of RVI (S&C V.13:9): run the 1993 index over the
daily highs and again over the lows, and average the two. Proposed in
ta-lib-proposal-drafts#68.
Composed rather than fused. The proposal specifies one pass carrying two
copies of RVI's state; that body is expressible, but it carries two
window-start cursors and the streaming analyzer accepts exactly one
("extrema automaton: expected exactly one window-start variable"), so
fusing it would cost the function its streaming tier. Calling TA_RVI
twice keeps `flags: [stream]` for the reason KC's legs do -- each leg
streams as its own sub-handle -- at the cost of one scratch buffer.
The regression test's legs were chosen by what each can falsify, and the
discrimination was measured by mutating the generator input and
regenerating, not assumed:
averaging dropped, high leg alone -> differential, composite and tie RED
the two TA_RVI calls swapped -> all green, and correctly: TA_RVI's
write index trails its read index,
so neither order can corrupt an
aliased input
lookback raised by one -> degenerate RED, differential green
scratch one element short -> edges RED under AddressSanitizer
The differential leg rebuilds both legs from TA_STDDEV and TA_RMA rather
than from TA_RVI, so a defect shared by TA_RVI and TA_RVIR cannot hide in
it; the composite leg against two TA_RVI calls is structurally true
against this body and is there for a later fused rewrite.
Interfaces added, none removed or changed, so the generation goes c+1:0:a+1 by the rule beside it in configure.ac and the soname stays libta-lib.so.1. The twelve are TA_RVIR, TA_S_RVIR, TA_RVIR_Lookback and the nine streaming entry points -- the tier the composed shape was chosen to keep.
|
CI is green on all seven checks, including the two I said I could not compile here: Generated Java and Generated C# both pass. The one that failed was the one I had built locally. Twelve entry points added: Worth recording that |
Implements
TA_RVIR, Dorsey's 1995 refinement of his own RVI, from the proposal card in ta-lib-proposal-drafts#68. I claimed the card there before starting.RVIR[i] = 0.5 * ( RVI(high)[i] + RVI(low)[i] ), both legs at the same parameters.Two shape findings that changed the implementation
The fused body the card specifies cannot carry
flags: [stream]. The card asks for one pass driving two copies of RVI's state. That body is expressible and I wrote it first, but it carries two window-start cursors and the streaming analyzer accepts exactly one:Streaming is an API-surface contract and fusion is an implementation preference, so I kept the former: composed, like KC, where each leg streams as its own sub-handle. The cost is one scratch buffer sized
endIdx-startIdx+1.The IR cannot express a leg-indexed inner loop.
Expr::ArrayAccess(String, Box<Expr>)takes a name as its base, not an expression, andVarTypehas no pointer-array member, solegs[j][i]— the natural spelling of "do this twice" — has no representation. Worth knowing before anyone else reaches for it.What the tests can and cannot falsify
Discrimination was measured, not assumed: each mutation was applied to the generator input and regenerated.
TA_RVIcalls swappedThree things that follow, all of them in the file's header comment:
TA_RVI's write index trails its read index by the lookback and never overtakes it, so neither call order can corrupt an aliased input. I had written a comment claiming the order was what made aliasing safe; the mutation disproved it and the comment is now the corrected version. The aliasing leg therefore earns its keep against the scratch buffer's extent, not against the order.TA_RVItwice, so it cannot fail for an arithmetic reason. It is there for the fused rewrite, which would have to reproduce the same equality. The arithmetic is carried by the differential leg, which rebuilds both legs fromTA_STDDEVandTA_RMA— so a defect shared byTA_RVIandTA_RVIRcannot hide in it.outBegIdxthe function reports, so it follows a lookback drift instead of catching it; the degenerate leg (inHigh == inLow, which must reduce toTA_RVIexactly) comparesoutBegIdxagainst an established function and is the only leg that sees it.The scratch-extent case needs AddressSanitizer to be visible — a plain run of the one-element case is green either way. Control run: clean 0 errors → mutated
heap-buffer-overflow, abort → restored 0 errors, generated file byte-identical.Verified locally
python3 scripts/build.py generate— zero drift (generate twice,git diffunchanged)ta_regtest— full run green;--function=RVIRexercises all seven legs with literal coverage counts (39757 / 4609 / 13707 / 243 / 106592 / 9218)ta_codegen/generator— full suite green. Two lists needed RVIR and got it with its reason:stability_suite's inherited-instability set (RVIRinheritsTA_FUNC_UNST_RVIthrough both legs) andstreaming_suite's composed-sub-call set (safe for KDJ's reason — the low leg is handedoutBegIdx/outNBElementthemselves and they are returned unmodified, so the final count is that callee's count)ta_codegen/output/rust— 703 tests greenNot verified locally
The Java and C# outputs were never compiled. This machine has no JDK that accepts
release 17and its .NET SDK (7.0.200) cannot targetnet10.0. What I did instead is a syntax-level check against KC, the composed function this one is shaped after: brace and paren balance after stripping comments and string literals, identical to KC's. That is not a compiler. If CI is red on either, it will be in the generated fragment and I will fix it there.I do not think this blocks review — the same generator paths produce KC's Java and C# today and they are green in CI — but the claim "this compiles" is one I have not earned, so I am not making it.
Naming
The card left the name open between
RVIRand alternatives. I tookRVIR, the name the card recommends.rvir.mdandrvi.mdnow carry reciprocal notes about the collision, because some vendors reserve the bare name RVI for this revision and call the 1993 form RVIorig; this library ships the 1993 form asRVI.rvir.mdalso records the measured distance to five variants published under this name, so the next person to receive a "your RVIR is wrong" report can check which one they are holding: a plain exponential smoother instead of Wilder's, up to 12.9 index points; averaging the prices and taking one index of the result, 13.6; adding the close as a third leg, 6.0; a 9-period deviation, 3.2; routing ties to the down bucket, 4.6. AgainstRVIof the closes at shared defaults, 18.1 — this is not a re-parameterisation.