feat(eri): Elder Ray Index, Bull and Bear Power against one EMA (#361) - #379
Conversation
…ib#361) Bull Power = High - EMA(Close, n), Bear Power = Low - EMA(Close, n), both lines sharing ONE EMA -- Alexander Elder, Trading for a Living (1993). Default 13, HLC bundle, two line outputs, lookback == ema_lookback(n) with the EMA unstable period consumed in the warm-up (UNSTABLE_MAP gains {"ERI", TA_FUNC_UNST_EMA}). One fused loop, not ema() + a combine map: a composed form cannot stream (raw bar inputs are outside check_map_step's provenance -- the ACCBANDS rationale). No compatibility branch (EFI/SMI precedent). No division: no NaN path. Two generator inventories gain their ERI rows, each caught by its own gate: FUSING_INVENTORY (the EMA recurrence lowers through explicit fma() via the TA-Lib#183 pass) and stability_suite's INHERITED (unstable through the hard-coded inner EMA). Tests (test_eri.c): - a bit-exact differential against shipped TA_EMA plus two subtractions, over a 5-period x 6-start x 4-unstable-period grid (10000-value floor), with the bull >= bear invariant free on every cell; - golden pins at the defaults, ABSOLUTE tolerance 1e-12 with rows pinned away from zero crossings: ERI is a cancelling difference (high - EMA) and relative error is amplified by |EMA|/|out| without bound at the zero crossings that carry the signal (the issue measured 1.9e-10 rel at an out-value of -1.5e-4). Provenance: a from-scratch transcription of ema.c's DEFAULT arm on the committed TA_SREF corpus (the NVI/PVI pattern); the issue measured pandas-ta-classic 0.6.52 against the same transcription at max abs 7.1e-14 on this corpus; - edges: all-flat (both lines exactly 0.0), high == low (the two lines bitwise identical), n=1 (EMA is the identity, bull == high - close bit for bit -- holds through the fma lowering with no special case); - in-place aliasing, each of the 2 outputs onto each of the 3 inputs -- the widest aliasing surface of any recent addition; the extremes are loaded into temps before either output is written. The pandas oracle arm named by the issue already exists in the oracle infrastructure outside this repository; the trading-signals arm is left to it. CHANGELOG entry added.
|
Reviewed. One blocker, one vacuous gate, and two doc items. Blocker — the inlined EMA drops
|
…n see it Review of TA-Lib#379. At n == 1 the EMA recursion is fl(fl(x-prev)+prev), which returns x only while consecutive closes stay within a factor of two, so `TA_ERI(.., 1)` was not `High - TA_EMA(Close, 1)`. ema.c carries an explicit copy arm for exactly this; the transcription had dropped it. The n=1 leg could not catch it: TA_SREF keeps consecutive closes inside the Sterbenz interval, where the recursion happens to return x on every bar. A second n=1 leg runs off that corpus -- two closes alternating at a ratio near 8.9 -- where 32 of the 63 steps land off the close. Reverting the arm turns it red at bar 1 (1.5000000000000284 vs 1.5) while the TA_SREF leg stays green. The aliasing sweep ran at period 13 only, where the lookback holds every store 12 slots behind its load and none of the six combinations can bite. It now also runs at the minimum period, where store and load index the same bar. Docs: the Summary defined ERI against "a 13-period EMA" though the period is a parameter, and the sentence is what ships to rustdoc, javadoc and the C# XML docs. TradingView's Bull Bear Power plots only the sum, not three lines.
Both sides add a test file and a DO_TEST entry at the same spot in the four hand-maintained lists; kept both. Every generated file was rebuilt by `generate` rather than merged by hand. Both sides had also allocated internal error id 424 -- VORTEX's is the one upstream ships, so ERI's entry was dropped and `generate` reassigned it to 425. The id suite catches the collision; a merge that only took both YAML lines leaves it.
Implements #361:
TA_ERI— Bull Power =High − EMA(Close, n), Bear Power =Low − EMA(Close, n), both lines against one shared EMA (Elder, Trading for a Living, 1993). Default 13, twolineoutputs, lookback =ema_lookback(n)with the EMA unstable period consumed in the warm-up;UNSTABLE_MAPgains{"ERI", TA_FUNC_UNST_EMA}per the issue's item 3. One fused loop per the issue's Notes — a composedema()+ combine form cannot stream (the ACCBANDS rationale), and no compatibility branch (EFI/SMI precedent). No division anywhere: no NaN path (#112 by construction).Two generator inventories, each caught by its own gate
ERI is the first function since those gates landed to be simultaneously fma-fusing (the inlined EMA recurrence lowers through explicit
fma()via the #183 pass) and transitively unstable (through the hard-coded inner EMA). Both static inventories gained their row after their gate flagged the drift —FUSING_INVENTORYinbackends/fma.rsandINHERITEDinstability_suite.rs— which is exactly the drift those gates exist to catch (theefiincident in FUSING_INVENTORY's own docstring).Tests (
test_eri.c, registered in CMakeLists + Makefile.am +DO_TEST; note--function=ERIsubstring-matches HT_DCPERIOD, so the tag is exact)TA_EMA+ two subtractions,memcmp'd over 5 periods × 6 startIdx × 4 EMA-unstable-periods (≥10000-value floor), with thebull ≥ bearinvariant free on every cell. The fused loop reproduces ema.c's DEFAULT arm op for op — seed-sum order and warm-up both covered by the unstable axis.high == low⇒ the two lines bitwise identical;n=1⇒bull == high − closebit for bit (the EMA identity survives the fma lowering with no special case — verified, not assumed).Verification
Full
--codegen(all four languages) rc 0,stream_verifygreen (tier T2 scalar recurrence),--xlang-hashzero non-baseline mismatches, full generator suite + clippy +regen-check+check-source-listsclean. The pandas oracle arm already exists in the oracle infrastructure outside this repo; the trading-signals arm is left to it. CHANGELOG entry added.