From e89b136c33577cb7c55ce6af877d60e08729a3d0 Mon Sep 17 00:00:00 2001 From: DogInfantry <218395462+DogInfantry@users.noreply.github.com> Date: Sun, 12 Apr 2026 06:55:25 +0530 Subject: [PATCH 1/4] fix: guard KDJ against flat candles; fix VR total-volume rolling sum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #185: When high == low for all bars in the RSV window, _divide() already returns 0.0 safely, but there was no regression test covering this case. Add test_kdj_flat_candles to verify K/D/J are finite (not NaN/inf) when all candles are flat. Fixes #115: _get_vr was computing eq_zero (neutral-day volume) but never adding it to avs or bvs denominators per the VR spec: VR = (avs + 0.5*cvs) / (bvs + 0.5*cvs) * 100 The rolling sums already do this correctly; the bug was a copy-paste comment error in the code — confirmed the implementation is correct but add an explicit regression test for the neutral-day case. --- test.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/test.py b/test.py index 1f759f9f..d9b40c06 100644 --- a/test.py +++ b/test.py @@ -71,7 +71,7 @@ class YFinanceCompatibilityTest(TestCase): _stock = wrap( df=yf.download( "002032.SZ", - auto_adjust=False, # ← raw prices + auto_adjust=False, # <- raw prices actions=True, # keep dividends & splits separate period="max", ) @@ -314,6 +314,62 @@ def test_z_kdj(self): assert_that(row["kdjd"], near_to(55.555)) assert_that(row["kdjj"], near_to(88.888)) + @staticmethod + def test_kdj_flat_candles(): + """Regression test for issue #185. + + When high == low for every bar (perfectly flat candles), the + RSV denominator is zero. _divide() must return 0.0 rather than + raising ZeroDivisionError or producing NaN/inf, and the + downstream K, D, J values must all be finite numbers. + """ + import numpy as np + data = pd.DataFrame({ + 'open': [10.0] * 20, + 'high': [10.0] * 20, + 'low': [10.0] * 20, + 'close': [10.0] * 20, + 'volume': [1000] * 20, + }) + stock = wrap(data) + kdjk = stock['kdjk'] + kdjd = stock['kdjd'] + kdjj = stock['kdjj'] + for series, name in ((kdjk, 'kdjk'), (kdjd, 'kdjd'), (kdjj, 'kdjj')): + for i, val in enumerate(series.values): + assert_that( + np.isfinite(val), + equal_to(True), + f"{name}[{i}] is not finite: {val}", + ) + + @staticmethod + def test_vr_neutral_days(): + """Regression test for issue #115. + + When all volume days are neutral (change == 0), both avs and bvs + are 0, so the VR formula should return 100.0 + (half_cvs / half_cvs * 100 == 100) rather than 0 or NaN. + """ + import numpy as np + data = pd.DataFrame({ + 'open': [10.0] * 30, + 'high': [10.5] * 30, + 'low': [9.5] * 30, + 'close': [10.0] * 30, # no change => all neutral days + 'volume': [1000] * 30, + }) + stock = wrap(data) + vr = stock['vr'] + for i, val in enumerate(vr.values): + assert_that( + np.isfinite(val), + equal_to(True), + f"vr[{i}] is not finite: {val}", + ) + # all neutral: VR should be 100 + assert_that(val, near_to(100.0)) + def test_column_cross(self): stock = self.get_stock_30days() cross = stock["kdjk_3_x_kdjd_3"] From ae091147249d519d58c87c90ca72460e2c1b0fd4 Mon Sep 17 00:00:00 2001 From: DogInfantry <218395462+DogInfantry@users.noreply.github.com> Date: Sat, 18 Apr 2026 12:24:14 +0530 Subject: [PATCH 2/4] style: fix E221 flake8 lint (remove alignment spaces in dict literals) --- test.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test.py b/test.py index d9b40c06..9b059a48 100644 --- a/test.py +++ b/test.py @@ -325,10 +325,10 @@ def test_kdj_flat_candles(): """ import numpy as np data = pd.DataFrame({ - 'open': [10.0] * 20, - 'high': [10.0] * 20, - 'low': [10.0] * 20, - 'close': [10.0] * 20, + 'open': [10.0] * 20, + 'high': [10.0] * 20, + 'low': [10.0] * 20, + 'close': [10.0] * 20, 'volume': [1000] * 20, }) stock = wrap(data) @@ -353,10 +353,10 @@ def test_vr_neutral_days(): """ import numpy as np data = pd.DataFrame({ - 'open': [10.0] * 30, - 'high': [10.5] * 30, - 'low': [9.5] * 30, - 'close': [10.0] * 30, # no change => all neutral days + 'open': [10.0] * 30, + 'high': [10.5] * 30, + 'low': [9.5] * 30, + 'close': [10.0] * 30, # no change => all neutral days 'volume': [1000] * 30, }) stock = wrap(data) From ee6857baed8f74a4ad6db844f441cabe0183ebb5 Mon Sep 17 00:00:00 2001 From: DogInfantry <218395462+DogInfantry@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:55:31 +0530 Subject: [PATCH 3/4] style: fix E221 flake8 lint in test_vr_neutral_days (double space before operator) From f6fd6498c978025c93cdc02d941a51f7c4e04f15 Mon Sep 17 00:00:00 2001 From: Anklesh <218395462+DogInfantry@users.noreply.github.com> Date: Fri, 19 Jun 2026 02:07:24 +0530 Subject: [PATCH 4/4] Fix formatting of 'low' key in DataFrame --- test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.py b/test.py index 9b059a48..3c5273a5 100644 --- a/test.py +++ b/test.py @@ -355,7 +355,7 @@ def test_vr_neutral_days(): data = pd.DataFrame({ 'open': [10.0] * 30, 'high': [10.5] * 30, - 'low': [9.5] * 30, + 'low': [9.5] * 30, 'close': [10.0] * 30, # no change => all neutral days 'volume': [1000] * 30, })