Repository navigation
feat: KVO, Klinger Volume Oscillator (#484) - #549
Conversation
Each bar's volume is signed by the direction of its high-low-close sum and weighted by where the bar's own range sits inside the cumulative range of the current trend run; the resulting volume force is smoothed twice and differenced, and the trigger is a third smoothing of that. Stephen J. Klinger, Technical Analysis of Stocks & Commodities V15:12 (December 1997). The factor is the 1997 form |2*(dm/cm) - 1| (TA-Lib#484, form A), which the December 1997 Traders' Tips all print. It is not LEAN's, and the ruling gives up LEAN parity for it. Three things the shape forces: - The trend is HELD when the high-low-close sum repeats, which is the article's own tenet. It is one line, and a corpus with few ties barely exercises it: MEASURED, the committed 252-bar corpus holds exactly one tie, at bar 187, and reading it as +1, or as a reversal, moves KVO by up to 1.0e7 from the next bar on. Cheap to get wrong, not cheap to notice. - The cumulative range is reset only by a trend change, so two calls starting at different bars can carry different state into the same bar. That is what `path_dependent` declares. - All three smoothings are seeded raw from the volume force of the same anchor bar, not from a simple average of their own first inputs, so ONE ema_lookback of the longest period covers all three and the lookback is that plus the reference bar. KVO takes no TA_FUNC_UNST id of its own: every 1997 transcription writes the three smoothings as the platform's EMA call, so it inherits TA_FUNC_UNST_EMA. Its UNSTABLE_MAP row is there for the stream K-leg rather than the range envelope, as SUPERTREND's is: `path_dependent` makes stability_class() answer SKIP before the id is consulted.
…-Lib#484) KVO is not a chain of shipped calls, so there is no composition gate to borrow. Two identities stand in its place, and both are worth more than a golden table because neither depends on a reference value: - fast == slow makes the two averages the same series, so the oscillator is EXACTLY zero on every bar and the trigger, seeded on that zero, never leaves it. Measured: 232 of 232 bars. - Swapping fast and slow negates the output BIT FOR BIT, trigger included: a - b = -(b - a) is exact in IEEE and the trigger's recursion commutes with negation. Measured: 197 of 197 bars. Together they pin KVO = EMA(vf, fast) - EMA(vf, slow) and the trigger's dependence on it. An implementation that crossed the two averages, or let the trigger drift off the oscillator, fails one of them on every bar. The tie leg does not rest on the corpus, which holds one tie: it builds a series where every third bar repeats the sum while carrying a different range, so a mis-read tie changes the trend sign and resets the cumulative range at once. Its tie count is asserted, because a series that stopped containing ties would leave the leg green and empty. The prices are exact decimal values, so the frozen rows carry no dependence on the platform's libm. The golden leg holds the 1997 factor, from a 60-digit evaluation over the committed corpus; the library reproduces those 28 rows to 5.4e-15 by |a-b|/max(|b|,1), so the tolerance is 1e-12.
The four registration lists are resolved as a union: dev carries PSO and FISHER, this branch carries kvo, and taking either side alone would drop one of them without failing a build. Roll-called after the merge. Generated files regenerated, not merged by hand.
The merge took dev's function-count while both sides' entries came through, so the declared count and the actual function lines disagreed (3043 against 3056). scripts/sync.py on a clean tree, with a worktree holding dev; abi.py check is clean afterwards.
# Conflicts: # include/ta_common.h # ta_codegen/output/java/library/src/main/java/io/github/talib/BuildStamp.java # ta_codegen/output/java/tools/TaCodegenServe.java
# Conflicts: # include/ta_common.h
# Conflicts: # ABI.manifest # include/ta_common.h
…jects The Auto-Stabilization suite that landed on dev decides this card's open question by measurement. With flags: [stream, path_dependent] it reports, at the defaults and at tripled periods: auto-stabilization: KVO defaults: meets the converging criterion on every series and start: the path_dependent flag is wrong auto-stabilization: KVO periods tripled: meets the converging criterion on every series and start: the path_dependent flag is wrong Failed group [UNSTABLE,LOOKBACK,AUTO] with code=1720 Removing the flag and regenerating turns the whole suite green. Nothing else changed between the two runs, and the regtest binary was rebuilt (bc79b8d8a4 -> 40e92db9af), so the verdict belongs to the flag. KVO now reads "Initial Unstable Period, inherited from EMA", the same shape as ADOSC after its reclassification. Whether that inherited count needs an adjustment, and in what form, is the question left on TA-Lib#484.
# Conflicts: # include/ta_common.h # src/ta_abstract/ta_func_api.c # ta_codegen/output/csharp/library/src/metadata/CatalogFacts.g.cs # ta_codegen/output/java/library/src/main/java/io/github/talib/metadata/FunctionDescription.java
KVO inherits EMA's Auto-Stabilization count through ema_lookback and
carried no adjustment of its own, so the count it discards was short of
what it needs. Measured with docs/studies/auto-stabilization's probe,
40000 bars, three series, worst case over series and starts:
discarded need discarded need
PREC_4 PREC_4 PREC_8 PREC_8
defaults 34/55/13 275 473 523 764
102/165/39 825 1330 1568 2098
The shortfall is not a constant: 198 bars at the defaults and 505 at
tripled periods, and need/discarded climbs from 1.33 at 2/3/2 to 2.04 at
174/233/9 over a 36-point grid. Holding the slow period fixed shows why
it moves -- the need rises as the fast period approaches the slow one,
where the difference of the two averages shrinks while the seed error
does not. That is the opposite end from where ADOSC's rule puts its
weight, and ADOSC's rule applied unchanged is short at 26 of 36 points.
ta_auto_stabilization_kvo raises the level instead: K_eff = (4K + 23)/3,
which is 21 at PREC_4 and 33 at PREC_8. Over the same 36 points it has
no violation, the thinnest margin being +12 at 2/3/2. After the change
the defaults discard 578 and 908 against a need of 473 and 764.
Sized on the zigzag series, the worst of the three at every setting.
…MAs" This reverts commit 756f89b.
# Conflicts: # ABI.manifest # include/ta_common.h # src/ta_abstract/ta_func_api.c # src/tools/ta_regtest/Makefile.am # src/tools/ta_regtest/ta_meta_frame.h # src/tools/ta_regtest/ta_regtest.c # src/tools/ta_regtest/ta_stream_frame.h # src/tools/ta_regtest/ta_test_func.h # src/tools/ta_regtest/ta_variant_frame.h # ta_codegen/output/csharp/library/src/metadata/CatalogFacts.g.cs # ta_codegen/output/java/library/src/main/java/io/github/talib/BuildStamp.java # ta_codegen/output/java/library/src/main/java/io/github/talib/metadata/FunctionDescription.java # ta_codegen/output/java/tools/TaCodegenServe.java # ta_codegen/output/rust/library/src/abstract_api.rs # ta_codegen/output/rust/library/src/ta_func/no_phantom_io.rs
The emitter fuses KVO, so enums_suite::rust_fma_dispatch_fires_for_exactly_the_fusing_functions read it in the emitted set and not in the pinned one. The inventory feeds the suites, not the emitter: regenerating changes no product.
# Conflicts: # CMakeLists.txt # include/ta_common.h # src/ta_abstract/ta_func_api.c # ta_codegen/output/java/library/src/main/java/io/github/talib/BuildStamp.java # ta_codegen/output/java/library/src/main/java/io/github/talib/metadata/FunctionDescription.java # ta_codegen/output/java/tools/TaCodegenServe.java
# Conflicts: # include/ta_common.h # src/ta_abstract/ta_func_api.c # src/tools/ta_regtest/ta_regtest.c # src/tools/ta_regtest/ta_test_func.h # ta_codegen/output/java/library/src/main/java/io/github/talib/BuildStamp.java # ta_codegen/output/java/library/src/main/java/io/github/talib/metadata/FunctionDescription.java # ta_codegen/output/java/tools/TaCodegenServe.java
# Conflicts: # include/ta_common.h # ta_codegen/output/java/library/src/main/java/io/github/talib/BuildStamp.java # ta_codegen/output/java/tools/TaCodegenServe.java
# Conflicts: # include/ta_common.h # src/ta_abstract/ta_func_api.c # ta_codegen/output/csharp/library/src/metadata/CatalogFacts.g.cs # ta_codegen/output/java/library/src/main/java/io/github/talib/BuildStamp.java # ta_codegen/output/java/library/src/main/java/io/github/talib/metadata/FunctionDescription.java # ta_codegen/output/java/tools/TaCodegenServe.java
# Conflicts: # CMakeLists.txt # include/ta_common.h # src/ta_abstract/ta_func_api.c # src/tools/ta_regtest/Makefile.am # src/tools/ta_regtest/ta_regtest.c # src/tools/ta_regtest/ta_test_func.h # ta_codegen/output/java/library/src/main/java/io/github/talib/BuildStamp.java # ta_codegen/output/java/library/src/main/java/io/github/talib/metadata/FunctionDescription.java # ta_codegen/output/java/tools/TaCodegenServe.java
# Conflicts: # include/ta_common.h
# Conflicts: # include/ta_common.h # ta_codegen/output/java/library/src/main/java/io/github/talib/BuildStamp.java # ta_codegen/output/java/tools/TaCodegenServe.java
… on the name (TA-Lib#484) KVO is classed TA_STABLE_CONVERGING on the EMA id it inherits, as ADOSC is, not TA_STABLE_SKIP: the cumulative range two starts carry is reset for both by the first trend change after both, and what remains is the seed error of three EMA seeded on one anchor, fading at the slow average's rate (54/56 per bar at the defaults). The earlier claim that it never converges read a moving-bar table and was wrong; the body needs no change. It needs a longer grace than EMA's own because it is a small difference of two large averages: on the corpus mean |KVO| is 7% of either, so one seed error is ~14x the output and outlives the 100-bar grace. MEASURED on the 252-bar corpus, only periodToIgnore changed: 100 to 160 fail (rc=161), 175 compares bars 230 to 251 at worst 0.13 of the tolerance, 190 compares 590 values, 197 and up compare nothing (252 - 55 - 1 = 196). doRangeTestMulti takes the grace as a parameter (0 = the id's own) so it is keyed on the function name beside perFuncTol in test_codegen.c, not on a new unstable id; test_kvo.c reads the same table through codegen_range_grace() so the hand test and the codegen sweep cannot drift apart. STC and DOSC pass 0 and are unchanged. The UNSTABLE_MAP comment and kvo.md no longer say the function is path-dependent.
…A-Lib#484) The convergence envelope leaves a grace of bars uncompared, and nothing is reachable past MAX_RANGE_SIZE - lookback - 1: a grace at least that long, or a lookback that leaves fewer bars than the grace, let the sweep pass without holding a single value to its tolerance. KVO at a grace of 200 did exactly that on the 252-bar corpus (196 reachable bars) and read as rc=0. doRangeTestMulti now counts at the comparison -- a real value past the grace, bit-identical or not, and every integer value that is not skipped -- and fails a TA_STABLE_CONVERGING sweep whose count is 0 with TA_TESTUTIL_DRT_NO_COMPARISON (166). For CONVERGING the grace is decided before the bit-identical and near-zero early returns, so a bar inside the grace is not counted; the pass/fail answer of every branch is unchanged. MEASURED: KVO at a grace of 200 fails with 166 ("compared no value"), at 175 passes; the whole hand-written ta_regtest passes. The --codegen sweep trips ONE shipped function, STC, reported on TA-Lib#484 and not waived here: at its defaults TA_STC_Lookback is 67, so 184 bars are reachable against its own grace of 200, and its codegen range sweep had compared nothing and read as PASS. test_stc.c sweeps at 5/13/4 and is not affected. Every other function passes the sweep.
…A-Lib#484) The count ta_auto_stabilization_kvo carries on the EMA id it inherits -- an EMA at (4K+23)/3 e-folds at the longest of its three periods, less the EMA count it inherits there, so 21 and 33 e-folds at the two levels -- is now held the way ADOSC's is: - test_auto_stabilization.c: this file's copy of the rule in awCheckOffset, with a third integer period read so the longest of fast, slow and signal is the one sized, and a "signal longest" vector (10/20/40) so the trigger is the longest at least once. rZ2 in docs/spec-conformance.md and the specification name KVO beside CKSP and ADOSC. - auto_stabilization_census.c lists KVO: a function that owns no id carries no TA_FUNC_FLG_UNST_PER, so it is run by name as the owners are, and the nightly sample (TA-Lib#540) reads it. census_table.tsv gains its eight rows, calibrated on ONE host (macOS, Intel): two seeds of 20000 trials for the needs and worst trials, the sample for the shares. With one host the spread is 0 and the share ceilings carry MARGIN_FLOOR only; a calibration on the usual hosts would replace them. No start needs more than the count at either level, and the worst need is 335 of 578 at PREC_4 and 587 of 908 at PREC_8 at the defaults, which is the margin the count is reviewed on. - No PREC_8 floor row: TA_AUTO_STABILIZATION_FLOORS=1 prints none for KVO because two starts are bit-identical over the last quarter of every series at a setting of 0, so the floor of 0 is the measured one. MEASURED red: the file's copy of the rule at (4K+22)/3 fails awCheckOffset on every KVO vector, and so does the helper cut to a quarter or to zero. The two-starts leg does NOT fail at zero: on this file's six starts and three series the EMA count alone (275 at PREC_4) already converges KVO to the level's digits, so what holds the SIZE of the count is the census sample (worst need 335 of 20000 x 2 trials at PREC_4), not this leg. Said here so the count review reads the right gate.
|
Measurements for the STC finding above, so the ruling has numbers. Same corpus, same codegen range sweep, STC at its defaults 23/50/10 (lookback 67, so 184 bars are reachable past it). "worst" is the largest
Control: KVO at its 175 on the same probe reads worst 0.128, the figure in the So at the defaults the transient is gone by 156 and the margin is KVO-like from 165 on. Which of the three options to take is still yours; STC is unchanged on this branch. If it is a shorter grace, 165 keyed on the name in |
… full alias and Auto coverage (#484) The CONVERGING floor from #549 turned the full --codegen run red on STC: at its defaults the lookback is 67 and the id's grace of 200 leaves no bar reachable on the 252-bar corpus. STC takes a 175-bar grace beside KVO's: 150 fails on every seed, 175 holds on 48. The grace per unstable id lived in two switches in test_util.c; one function now answers for both. The KVO aliasing leg puts each output on each of the four inputs (8 of 8 pairs, not 4), and the Auto offset check gets a vector where the fast period is the longest. The KVO helper's comment no longer claims the count tracks the closest pair, and VIDYA's saturation note is back on VIDYA.
…cts (#484) The three averages were spelled (x - prev) * k + prev, which fuses to a subtraction and an FMA on the loop-carried chain. TA_EMA and ADOSC spell k * x + beta * prev with k + beta exactly 1.0, which leaves one FMA on the chain. The trend and the cumulative range were data-dependent branches; they are now selects, and the trend is carried as +-1.0 so no conversion is left in the loop. Values move in the last bits only. Against an exact-rational evaluation of the corpus the worst error is 1.6e-15 of scale at 34/55/13 (9.7e-16 before) and 7.1e-16 at 10/20/5 (4.7e-16 before); every golden row holds at 1e-12. Tulip's kvo agrees to 2.0e-15 of scale on every bar at 34/55, 8.9e-16 at 10/20 and 5.5e-16 at 5/11. kvo.md: the zero on the seed bar is never output, and the seed error fades at the longest period's rate. The change-history row credits the author of #549.
Closes #484. Adds
TA_KVO, Klinger Volume Oscillator (Stephen J. Klinger, TASC December 1997), with the 1997 factor|2*(dm/cm) - 1|, the article's tie rule, and three EMA seeded on one anchor bar. Ruled on the card: converging, nopath_dependent, no id of its own -- it inheritsTA_FUNC_UNST_EMAas ADOSC does.What is in it
ta_codegen/input/kvo/-- body, yaml, md;ta_auto_stabilization_kvoin the helpers, registered instability_suite.rsas inheriting EMA and in the fma inventory.test_kvo.c-- golden (60-digit reference, 1e-12), two analytic identities (fast == slow is exactly zero; fast <-> slow negates bit for bit), a built tie series, lookback at four unstable periods, output aliasing, and the range sweep asTA_STABLE_CONVERGING. Every comparison count is pinned;server_verifyon the golden leg.doRangeTestMulti, keyed on the function name inperFuncGrace[]besideperFuncTol;test_kvo.creads the same table.TA_STABLE_CONVERGINGsweep that compares no value fails withTA_TESTUTIL_DRT_NO_COMPARISON(166), counted at the comparison. Red control: KVO at a grace of 200.TA_STC_Lookbackis 67, so 184 bars are reachable against its own grace of 200; the sweep compared nothing and read as PASS.test_stc.csweeps at 5/13/4 and is not affected. STC is unchanged here.awCheckOffsetholds KVO's count with the file's copy of the rule (an EMA at(4K+23)/3e-folds at the longest of three periods, less the inherited count); the census runs KVO by name so the nightly sample (Nightly sampled check that Auto-Stabilization counts and the 1e-9 target still hold #540) reads it;census_table.tsvgains eight rows calibrated on one host (spread 0,MARGIN_FLOORceilings); rZ2 names KVO in the specification andspec-conformance.md. Measured margin for the count review: worst need 335 of 578 at PREC_4, 587 of 908 at PREC_8 at the defaults; no start of 2 x 20000 needs more. No PREC_8 floor row: two starts are bit-identical over the tail at a setting of 0.Checked
build.py generatethenregen-check: zero drift. Generatorcargo test --releasegreen.ta_regtest(all hand-written groups) green;--function=KVO,--function=AUTOgreen.--codegen --function=KVO: C, Rust, Java, C# pass.--xlang-hash --function=KVO: 0 mismatches / 6948 cases per language.--codegenover every function: green except STC, as above.scripts/auto_stabilization_sample.py: 288 rows, 576 shares and 576 replays held.(4K+22)/3->awCheckOffsetfails on every KVO vector; helper at a quarter or at zero ->awCheckOffsetfails. The two-starts leg does not fail at zero (EMA's own 275 bars already converge KVO on its six starts and three series), so the size of the count is held by the census sample, not by that leg.