From c21685d4076f9d4f51ac910eb30ed78197b0796f Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 17 Jul 2026 20:47:04 +0800 Subject: [PATCH 1/2] fix(ta): align percent rank float comparisons --- src/ta_misc.cpp | 19 ++++++++++-- tests/test_ta.cpp | 76 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/ta_misc.cpp b/src/ta_misc.cpp index 79b4bc0..6700826 100644 --- a/src/ta_misc.cpp +++ b/src/ta_misc.cpp @@ -19,6 +19,21 @@ namespace pineforge { namespace ta { +namespace { + +constexpr double kPineFloatEqualityBand = 1e-10; + +bool percentrank_less_equal(double lhs, double rhs) { + if (is_na(lhs) || is_na(rhs)) return false; + const bool equal = + lhs == rhs || + (std::isfinite(lhs) && std::isfinite(rhs) && + std::fabs(lhs - rhs) <= kPineFloatEqualityBand); + return lhs < rhs || equal; +} + +} // namespace + // --- Linreg (Linear Regression) --- @@ -87,7 +102,7 @@ double PercentRank::compute(double src) { double v = buffer_[i]; if (is_na(v)) continue; valid++; - if (v <= current) { + if (percentrank_less_equal(v, current)) { count++; } } @@ -240,7 +255,7 @@ double PercentRank::recompute(double src) { double v = buffer_[i]; if (is_na(v)) continue; valid++; - if (v <= current) count++; + if (percentrank_less_equal(v, current)) count++; } if (valid == 0) return na(); return ((double)count / (double)valid) * 100.0; diff --git a/tests/test_ta.cpp b/tests/test_ta.cpp index ee639eb..a676e5d 100644 --- a/tests/test_ta.cpp +++ b/tests/test_ta.cpp @@ -171,6 +171,82 @@ static int test_percentrank_pine_semantics() { fails++; } } + { + // PercentRank uses Pine's fixed finite comparison band internally: + // values at most 1e-10 above the current sample compare equal. + ta::PercentRank causal(2); + causal.compute(-10.0); + causal.compute(-5.349999999999909); + double got = causal.compute(-5.350000000000136); + if (!near(got, 100.0)) { + std::printf("FAIL percentrank fixed-band causal: got %g want 100\n", got); + fails++; + } + + ta::PercentRank inside(2); + inside.compute(-1.0); + inside.compute(9e-11); + got = inside.compute(0.0); + if (!near(got, 100.0)) { + std::printf("FAIL percentrank fixed-band inside: got %g want 100\n", got); + fails++; + } + + ta::PercentRank endpoint(2); + endpoint.compute(-1.0); + endpoint.compute(1e-10); + got = endpoint.compute(0.0); + if (!near(got, 100.0)) { + std::printf("FAIL percentrank fixed-band endpoint: got %g want 100\n", got); + fails++; + } + + ta::PercentRank outside(2); + outside.compute(-1.0); + outside.compute(1.1e-10); + got = outside.compute(0.0); + if (!near(got, 50.0)) { + std::printf("FAIL percentrank fixed-band outside: got %g want 50\n", got); + fails++; + } + } + { + const double inf = std::numeric_limits::infinity(); + + ta::PercentRank positive_inf(2); + positive_inf.compute(-inf); + positive_inf.compute(inf); + double got = positive_inf.compute(inf); + if (!near(got, 100.0)) { + std::printf("FAIL percentrank positive infinity: got %g want 100\n", got); + fails++; + } + + ta::PercentRank negative_inf(2); + negative_inf.compute(-inf); + negative_inf.compute(inf); + got = negative_inf.compute(-inf); + if (!near(got, 50.0)) { + std::printf("FAIL percentrank negative infinity: got %g want 50\n", got); + fails++; + } + } + { + ta::PercentRank recomputed(2); + recomputed.compute(-1.0); + recomputed.compute(1.9e-10); + recomputed.compute(0.0); + double got = recomputed.recompute(1e-10); + + ta::PercentRank fresh(2); + fresh.compute(-1.0); + fresh.compute(1.9e-10); + double want = fresh.compute(1e-10); + if (!near(got, 100.0) || !near(got, want)) { + std::printf("FAIL percentrank fixed-band recompute: got %g want %g\n", got, want); + fails++; + } + } return fails; } From a1ca2277bfd2edad32a8e524a1cb576cec62a805 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 17 Jul 2026 20:51:48 +0800 Subject: [PATCH 2/2] validation: pin PercentRank equality artifacts --- corpus | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corpus b/corpus index b0d6d96..62af235 160000 --- a/corpus +++ b/corpus @@ -1 +1 @@ -Subproject commit b0d6d96f4c6f6e730b8b8cd5cfe24d4d1abc5530 +Subproject commit 62af2350917d62216c4fd85ca10ea926ad3e7ca2