Skip to content

Commit beff36b

Browse files
committed
fix(cci,correl,willr): divide by the value the guard tests, and make %R's range a guarantee (#395)
The three remaining functions carrying #390's shape: a guard that establishes one expression while the arithmetic below it divides by another. All three are reachable through the shipped batch API and all three return a wrong answer under TA_SUCCESS. CCI guards `tempReal2` and divided by `0.015*tempReal2`. The band is RELATIVE and the underflow is ABSOLUTE, so they cross: below |average| ~ 1.6e-308 the guard still answers "not flat" while the scaled copy is exactly 0.0. Dividing by the deviation and scaling after removes the mismatch structurally. It also fixes the silent half, which is larger than the loud one -- the pre-scaled form loses precision long before it reaches Inf, measured relative error 7.7e-08 at price scale 1e-314 rising to 4.8e-01 at 1e-321, against 2e-16 for the new form at every one of those magnitudes. CORREL guards ssX and ssY separately -- correctly, and correl.c explains why -- and divided by `sqrt(ssX*ssY)`. The product carries the fourth power of the window spread, so it leaves the double range at both ends while both factors are still ordinary normals with all 53 bits, and no test of the factors can see it. Where spXY is zero the result is NaN, which the [-1,1] clamp does not catch (`NaN > 1.0` is false); where it is not, the +-Inf is rewritten to exactly +-1.0, a perfect correlation reported from a degenerate window. A root of each factor cannot do either: sqrt maps any finite positive double into [2.3e-162, 1.4e154], so their product is representable at both ends. That also deletes the comment block arguing for the one-root form, whose overflow reasoning the tree's own oracle (ta_test_reference.c) already contradicted. WILLR is two defects, and the maintainer's ruling on the second is on #395. It still carried the exact `diff != 0.0` that #107 replaced in STOCH, so a machine-flat window divided sub-epsilon residue into full-scale noise, and a denormal range underflowed `diff` to 0.0 and answered 0 -- the value meaning a close at the period HIGH -- for a close sitting on the period LOW. Separately, the pre-scaled divisor cost the endpoint exactness that keeps %R in its documented range: the shipped library returns -100.00000000000001 on the reference series at periods 2, 3 and 4. Dividing by the range and scaling after makes the bound a theorem for well-formed input (fl(h-c) <= fl(h-l) by monotonicity, so the quotient is in [0,1] under every rounding mode); the clamp extends it to input TA-Lib does not validate, a close outside its own bar. `diff` leaves the body and the streaming handle of all four backends. Not scoped here, and checked rather than assumed: an exhaustive scan of every division in ta_codegen/input finds no fourth instance of the shape. The Hilbert family matches it lexically and is cleared -- `atan(Im/Re)*rad2Deg` can only reach +-Inf, which the `[0.67*prev, 1.5*prev]` then `[6,50]` clamp absorbs on the next statement -- so #396's seven rows are rewritten from "reachability unproven" to that clearance. The sweep from #396 now has no live instance to point at, so its self-test reconstructs CCI's pre-#395 divisor instead of reading a shipped defect, and pins the third half the other arms pin: silent when nothing bounds an operand. Two soundness fixes on the arm: its whole-divisor test accepted any operator, and its equality arm lacked the exclusivity its siblings carry. CORREL's fixed divisor is annotated rather than special-cased, which makes reverting correl.c:236 fail twice -- the key goes stale and the new divisor arrives unannotated. Measured, not assumed. LEGACY_TOL instrumented to record every function in one run: CCI 1.42e-14 and WILLR 7.11e-15 are new rows at 5e-14 and 3e-14; CORREL's 3.15e-13 is unchanged, so its #242 row stands. --fuzz-064 gives WILLR its own 5e-14 absolute bound rather than leaving it in the FMA bucket, whose 1e-9 is five orders looser than it needs -- the reason #338 gives for naming ATR. Absolute because %R is a bounded dimensionless oscillator: its floor is a ULP of 100 at any input magnitude. Six new legs, each sabotage-proven against a build of the pre-fix arithmetic: CCI subnormal (was +-Inf, now exactly +-1/0.015 -- period 2 makes the mean deviation equal the numerator, so the expectation is an equality); CORREL at 2^-283 (was NaN, -1, +1; now 0 and +-1/sqrt(15)); and WILLR's endpoint, range, denormal window, clamp domain (was +-10000) and machine-flat window. Green: full C suite, generator cargo test and clippy -D warnings, Rust clippy/doc/doctests/tests, --codegen against the frozen oracle (161 passed 0 failed in each of the four backends, CCI and WILLR both value-compared), --xlang-hash (338,957 golden cases, four backends bit-identical), --fuzz-064 (0 failures), regen-check, and the four source/lock/retcode/candle checks. Shipped-build A/B on a Zen 4 laptop, min of 12 alternating rounds x 200 reps, 20k bars, four untouched controls inside +-2%: WILLR batch 0.917 (the batch tier LOSES a division), WILLR streaming 1.054, CCI 1.115, CORREL 1.142. The streaming +5% is the ALT1 tier paying per bar for a hoist it used to amortise, the same order as KAMA's clamp in #390. CCI's and CORREL's costs are one extra divide and one extra sqrt per bar; both were checked for a cheaper correct form and neither has one that does not reintroduce an overflow band or retighten a guard #253 deliberately widened. CORREL's is well under the ~25% the deleted comment claimed. Closes #395. Claude-Session: https://claude.ai/code/session_01GesMuivQUwX4xHr4hEtnS8
1 parent 55190f6 commit beff36b

29 files changed

Lines changed: 1484 additions & 875 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ See [github commits](https://github.com/TA-Lib/ta-lib/commits) for complete list
116116
- (#253) Fix many TA_IS_ZERO vs TA_IS_ZERO_SCALED choices. Numerically better for edge cases, like very small inputs (<10e-8) or mostly flat input prices.
117117
- (#390) STOCH and STOCHF returned `inf` or `NaN` while reporting success, for prices near the bottom of the double range. A close sitting on the window high now comes out as exactly 100.
118118
- (#390) KAMA could return values outside the range of the prices it was smoothing, and ER values above 1. Both come from the same efficiency ratio exceeding its own maximum when floating-point drift left the running sum of price movement below the net move it bounds. The ratio is now clamped, making ER a hard 0..1.
119+
- (#395) CCI returned `+/-Inf` while reporting success, and CORREL returned `NaN` or a perfect +/-1 correlation from a window that had none, for prices near the bottom of the double range. Both now divide by the value their guard actually tests, so they stay accurate to the last bits at every price scale.
120+
- (#395) WILLR could return values outside its documented [-100, 0] range, and answered 0 - the value meaning a close at the period high - for a close sitting on the period low whenever the window's high-low range was very small. A close on the period low now comes out as exactly -100, the range is guaranteed even for a close outside its own bar, and a window flat to within rounding of its own prices answers 0, as STOCH and STOCHF already did.
119121

120122
## [0.7.1] 2026-07-03
121123
### Added

src/ta_func/ta_CCI.c

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@
6767
* 082326 MF,CC Fix #253. Scale that flatness test to the window's own price
6868
* level: the fixed band zeroed the whole output for any
6969
* instrument quoted small enough to fall under it.
70+
* 090626 MF,CC Fix #395. Divide by the mean deviation, scale after: the
71+
* pre-scaled `0.015*tempReal2` underflowed to 0.0 on a
72+
* denormal price that the guard still called "not flat".
7073
*/
7174

7275
TA_LIB_API int TA_CCI_Lookback( int optInTimePeriod )
@@ -203,7 +206,13 @@ TA_LIB_API TA_RetCode TA_CCI( int startIdx,
203206
tempReal2 /= optInTimePeriod;
204207
/* And finally, the CCI... */
205208
tempReal = lastValue - theAverage;
206-
/* Both tests are relative to the window's own price level (issue #253).
209+
/* Divide by the mean deviation itself and scale after: the guard has to
210+
* test the very expression the division uses, or a scaling step can
211+
* carry a guarded-non-zero into a zero divisor. The band is relative and
212+
* the underflow of the 0.015 product is absolute, so no band on the
213+
* deviation can cover the product.
214+
*
215+
* Both tests are relative to the window's own price level (issue #253).
207216
* They ask "is this window flat?", and flatness is a property of the
208217
* prices relative to each other -- but a deviation carries the quote
209218
* unit, so the fixed TA_IS_ZERO band these used to be answered "flat" for
@@ -215,7 +224,7 @@ TA_LIB_API TA_RetCode TA_CCI( int startIdx,
215224
tempReal3 = fabs(theAverage);
216225
if( !TA_IS_ZERO_SCALED(tempReal, tempReal3) && !TA_IS_ZERO_SCALED(tempReal2, tempReal3) )
217226
{
218-
outReal[outIdx++] = tempReal / (0.015 * tempReal2);
227+
outReal[outIdx++] = tempReal / tempReal2 / 0.015;
219228
} else
220229
{
221230
outReal[outIdx++] = 0.0;
@@ -335,7 +344,7 @@ TA_RetCode TA_S_CCI( int startIdx,
335344
tempReal3 = fabs(theAverage);
336345
if( !TA_IS_ZERO_SCALED(tempReal, tempReal3) && !TA_IS_ZERO_SCALED(tempReal2, tempReal3) )
337346
{
338-
outReal[outIdx++] = tempReal / (0.015 * tempReal2);
347+
outReal[outIdx++] = tempReal / tempReal2 / 0.015;
339348
} else
340349
{
341350
outReal[outIdx++] = 0.0;
@@ -403,7 +412,13 @@ static void TA_CCI_StepImpl( struct TA_CCI_Stream *sp, double inHigh, double inL
403412
tempReal2 /= sp->optInTimePeriod;
404413
/* And finally, the CCI... */
405414
tempReal = lastValue - theAverage;
406-
/* Both tests are relative to the window's own price level (issue #253).
415+
/* Divide by the mean deviation itself and scale after: the guard has to
416+
* test the very expression the division uses, or a scaling step can
417+
* carry a guarded-non-zero into a zero divisor. The band is relative and
418+
* the underflow of the 0.015 product is absolute, so no band on the
419+
* deviation can cover the product.
420+
*
421+
* Both tests are relative to the window's own price level (issue #253).
407422
* They ask "is this window flat?", and flatness is a property of the
408423
* prices relative to each other -- but a deviation carries the quote
409424
* unit, so the fixed TA_IS_ZERO band these used to be answered "flat" for
@@ -415,7 +430,7 @@ static void TA_CCI_StepImpl( struct TA_CCI_Stream *sp, double inHigh, double inL
415430
tempReal3 = fabs(theAverage);
416431
if( !TA_IS_ZERO_SCALED(tempReal, tempReal3) && !TA_IS_ZERO_SCALED(tempReal2, tempReal3) )
417432
{
418-
*outReal= tempReal / (0.015 * tempReal2);
433+
*outReal= tempReal / tempReal2 / 0.015;
419434
} else
420435
{
421436
*outReal= 0.0;
@@ -547,7 +562,13 @@ static TA_RetCode TA_CCI_OpenImpl( struct TA_CCI_Stream **stream, const double i
547562
tempReal2 /= optInTimePeriod;
548563
/* And finally, the CCI... */
549564
tempReal = lastValue - theAverage;
550-
/* Both tests are relative to the window's own price level (issue #253).
565+
/* Divide by the mean deviation itself and scale after: the guard has to
566+
* test the very expression the division uses, or a scaling step can
567+
* carry a guarded-non-zero into a zero divisor. The band is relative and
568+
* the underflow of the 0.015 product is absolute, so no band on the
569+
* deviation can cover the product.
570+
*
571+
* Both tests are relative to the window's own price level (issue #253).
551572
* They ask "is this window flat?", and flatness is a property of the
552573
* prices relative to each other -- but a deviation carries the quote
553574
* unit, so the fixed TA_IS_ZERO band these used to be answered "flat" for
@@ -559,7 +580,7 @@ static TA_RetCode TA_CCI_OpenImpl( struct TA_CCI_Stream **stream, const double i
559580
tempReal3 = fabs(theAverage);
560581
if( !TA_IS_ZERO_SCALED(tempReal, tempReal3) && !TA_IS_ZERO_SCALED(tempReal2, tempReal3) )
561582
{
562-
outReal[outIdx++ * outStride] = tempReal / (0.015 * tempReal2);
583+
outReal[outIdx++ * outStride] = tempReal / tempReal2 / 0.015;
563584
} else
564585
{
565586
outReal[outIdx++ * outStride] = 0.0;
@@ -683,7 +704,13 @@ TA_LIB_API TA_RetCode TA_CCI_Peek( const TA_CCI_Stream *stream, double inHigh, d
683704
tempReal2 /= sp->optInTimePeriod;
684705
/* And finally, the CCI... */
685706
tempReal = lastValue - theAverage;
686-
/* Both tests are relative to the window's own price level (issue #253).
707+
/* Divide by the mean deviation itself and scale after: the guard has to
708+
* test the very expression the division uses, or a scaling step can
709+
* carry a guarded-non-zero into a zero divisor. The band is relative and
710+
* the underflow of the 0.015 product is absolute, so no band on the
711+
* deviation can cover the product.
712+
*
713+
* Both tests are relative to the window's own price level (issue #253).
687714
* They ask "is this window flat?", and flatness is a property of the
688715
* prices relative to each other -- but a deviation carries the quote
689716
* unit, so the fixed TA_IS_ZERO band these used to be answered "flat" for
@@ -695,7 +722,7 @@ TA_LIB_API TA_RetCode TA_CCI_Peek( const TA_CCI_Stream *stream, double inHigh, d
695722
tempReal3 = fabs(theAverage);
696723
if( !TA_IS_ZERO_SCALED(tempReal, tempReal3) && !TA_IS_ZERO_SCALED(tempReal2, tempReal3) )
697724
{
698-
*outReal= tempReal / (0.015 * tempReal2);
725+
*outReal= tempReal / tempReal2 / 0.015;
699726
} else
700727
{
701728
*outReal= 0.0;

src/ta_func/ta_CORREL.c

Lines changed: 30 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,24 @@
4747
* Initial Name/description
4848
* -------------------------------------------------------------------
4949
* MF Mario Fortier
50+
* CC Claude Code (AI assistant)
5051
*
5152
*
5253
* Change history:
5354
*
54-
* MMDDYY BY Description
55+
* MMDDYY BY Description
5556
* -------------------------------------------------------------------
56-
* 120802 MF Template creation.
57-
* 101003 MF Initial Coding
58-
* 062804 MF Resolve div by zero bug on limit case.
59-
* 082326 MF Fix #242. Cancellation-free sums (shifted data + reseed, as
60-
* TA_VAR does since #118), per-factor degeneracy test and a
61-
* range clamp.
57+
* 120802 MF Template creation.
58+
* 101003 MF Initial Coding
59+
* 062804 MF Resolve div by zero bug on limit case.
60+
* 082326 MF Fix #242. Cancellation-free sums (shifted data + reseed, as
61+
* TA_VAR does since #118), per-factor degeneracy test and a
62+
* range clamp.
63+
* 090626 MF,CC Fix #395. A root of each guarded factor, not a root of their
64+
* product: sqrt(ssX*ssY) left the double range at both ends
65+
* while ssX and ssY were still ordinary normals, returning NaN
66+
* under TA_SUCCESS and a perfect correlation from a degenerate
67+
* window.
6268
*/
6369

6470
TA_LIB_API int TA_CORREL_Lookback( int optInTimePeriod )
@@ -295,25 +301,13 @@ TA_LIB_API TA_RetCode TA_CORREL( int startIdx,
295301
* is also the cheaper test: the two fabs() cost ~7% of this function's
296302
* runtime, and buy a wrong answer.
297303
*
298-
* sqrt(ssX*ssY) rather than sqrt(ssX)*sqrt(ssY): the guard has already
299-
* established both are positive, so the product needs no protection from
300-
* a negative operand, and the second square root is worth ~25% of the
301-
* runtime.
302-
*
303-
* The product CAN overflow to +Inf, and the one-root form is chosen with
304-
* that known. TA_REAL_MAX bounds optional PARAMETERS; a batch call's input
305-
* arrays are not range-checked, so ssX and ssY are bounded only by the
306-
* double range and their product exceeds it once |x| passes ~1e154. The
307-
* two-root form would not overflow there -- but the form this replaces
308-
* built exactly the same product (it tested ssX*ssY against TA_EPSILON), so
309-
* the exposure is unchanged, and an Inf here yields 0.0 rather than a wrong
310-
* correlation. Trading a quarter of the runtime for a case that already
311-
* behaved this way, on inputs 117 orders past any price, is not a trade
312-
* worth making. Revisit only if input range-checking is ever added.
304+
* A root of each guarded factor, never a root of their product: that
305+
* product leaves the double range at BOTH ends while ssX and ssY are
306+
* still ordinary normals, which no test of the factors can see (#395).
313307
*/
314308
if( ssX > 0.00000000000001 * sumX2 && ssY > 0.00000000000001 * sumY2 )
315309
{
316-
tempReal = spXY / sqrt(ssX * ssY);
310+
tempReal = spXY / (sqrt(ssX) * sqrt(ssY));
317311
/* A correlation coefficient cannot leave [-1,1]; rounding in the
318312
* three sums can still put it a few ulp outside.
319313
*/
@@ -490,7 +484,7 @@ TA_RetCode TA_S_CORREL( int startIdx,
490484
trailingIdx += 1;
491485
if( ssX > 0.00000000000001 * sumX2 && ssY > 0.00000000000001 * sumY2 )
492486
{
493-
tempReal = spXY / sqrt(ssX * ssY);
487+
tempReal = spXY / (sqrt(ssX) * sqrt(ssY));
494488
if( tempReal > 1.0 )
495489
{
496490
tempReal = 1.0;
@@ -701,25 +695,13 @@ static void TA_CORREL_StepImpl( struct TA_CORREL_Stream *sp, double inReal0, dou
701695
* is also the cheaper test: the two fabs() cost ~7% of this function's
702696
* runtime, and buy a wrong answer.
703697
*
704-
* sqrt(ssX*ssY) rather than sqrt(ssX)*sqrt(ssY): the guard has already
705-
* established both are positive, so the product needs no protection from
706-
* a negative operand, and the second square root is worth ~25% of the
707-
* runtime.
708-
*
709-
* The product CAN overflow to +Inf, and the one-root form is chosen with
710-
* that known. TA_REAL_MAX bounds optional PARAMETERS; a batch call's input
711-
* arrays are not range-checked, so ssX and ssY are bounded only by the
712-
* double range and their product exceeds it once |x| passes ~1e154. The
713-
* two-root form would not overflow there -- but the form this replaces
714-
* built exactly the same product (it tested ssX*ssY against TA_EPSILON), so
715-
* the exposure is unchanged, and an Inf here yields 0.0 rather than a wrong
716-
* correlation. Trading a quarter of the runtime for a case that already
717-
* behaved this way, on inputs 117 orders past any price, is not a trade
718-
* worth making. Revisit only if input range-checking is ever added.
698+
* A root of each guarded factor, never a root of their product: that
699+
* product leaves the double range at BOTH ends while ssX and ssY are
700+
* still ordinary normals, which no test of the factors can see (#395).
719701
*/
720702
if( ssX > 0.00000000000001 * sumX2 && ssY > 0.00000000000001 * sumY2 )
721703
{
722-
tempReal = spXY / sqrt(ssX * ssY);
704+
tempReal = spXY / (sqrt(ssX) * sqrt(ssY));
723705
/* A correlation coefficient cannot leave [-1,1]; rounding in the
724706
* three sums can still put it a few ulp outside.
725707
*/
@@ -973,25 +955,13 @@ static TA_RetCode TA_CORREL_OpenImpl( struct TA_CORREL_Stream **stream, const do
973955
* is also the cheaper test: the two fabs() cost ~7% of this function's
974956
* runtime, and buy a wrong answer.
975957
*
976-
* sqrt(ssX*ssY) rather than sqrt(ssX)*sqrt(ssY): the guard has already
977-
* established both are positive, so the product needs no protection from
978-
* a negative operand, and the second square root is worth ~25% of the
979-
* runtime.
980-
*
981-
* The product CAN overflow to +Inf, and the one-root form is chosen with
982-
* that known. TA_REAL_MAX bounds optional PARAMETERS; a batch call's input
983-
* arrays are not range-checked, so ssX and ssY are bounded only by the
984-
* double range and their product exceeds it once |x| passes ~1e154. The
985-
* two-root form would not overflow there -- but the form this replaces
986-
* built exactly the same product (it tested ssX*ssY against TA_EPSILON), so
987-
* the exposure is unchanged, and an Inf here yields 0.0 rather than a wrong
988-
* correlation. Trading a quarter of the runtime for a case that already
989-
* behaved this way, on inputs 117 orders past any price, is not a trade
990-
* worth making. Revisit only if input range-checking is ever added.
958+
* A root of each guarded factor, never a root of their product: that
959+
* product leaves the double range at BOTH ends while ssX and ssY are
960+
* still ordinary normals, which no test of the factors can see (#395).
991961
*/
992962
if( ssX > 0.00000000000001 * sumX2 && ssY > 0.00000000000001 * sumY2 )
993963
{
994-
tempReal = spXY / sqrt(ssX * ssY);
964+
tempReal = spXY / (sqrt(ssX) * sqrt(ssY));
995965
/* A correlation coefficient cannot leave [-1,1]; rounding in the
996966
* three sums can still put it a few ulp outside.
997967
*/
@@ -1276,25 +1246,13 @@ TA_LIB_API TA_RetCode TA_CORREL_Peek( const TA_CORREL_Stream *stream, double inR
12761246
* is also the cheaper test: the two fabs() cost ~7% of this function's
12771247
* runtime, and buy a wrong answer.
12781248
*
1279-
* sqrt(ssX*ssY) rather than sqrt(ssX)*sqrt(ssY): the guard has already
1280-
* established both are positive, so the product needs no protection from
1281-
* a negative operand, and the second square root is worth ~25% of the
1282-
* runtime.
1283-
*
1284-
* The product CAN overflow to +Inf, and the one-root form is chosen with
1285-
* that known. TA_REAL_MAX bounds optional PARAMETERS; a batch call's input
1286-
* arrays are not range-checked, so ssX and ssY are bounded only by the
1287-
* double range and their product exceeds it once |x| passes ~1e154. The
1288-
* two-root form would not overflow there -- but the form this replaces
1289-
* built exactly the same product (it tested ssX*ssY against TA_EPSILON), so
1290-
* the exposure is unchanged, and an Inf here yields 0.0 rather than a wrong
1291-
* correlation. Trading a quarter of the runtime for a case that already
1292-
* behaved this way, on inputs 117 orders past any price, is not a trade
1293-
* worth making. Revisit only if input range-checking is ever added.
1249+
* A root of each guarded factor, never a root of their product: that
1250+
* product leaves the double range at BOTH ends while ssX and ssY are
1251+
* still ordinary normals, which no test of the factors can see (#395).
12941252
*/
12951253
if( ssX > 0.00000000000001 * sumX2 && ssY > 0.00000000000001 * sumY2 )
12961254
{
1297-
tempReal = spXY / sqrt(ssX * ssY);
1255+
tempReal = spXY / (sqrt(ssX) * sqrt(ssY));
12981256
/* A correlation coefficient cannot leave [-1,1]; rounding in the
12991257
* three sums can still put it a few ulp outside.
13001258
*/

0 commit comments

Comments
 (0)