From 5d00e824fff9941bc633ff9dbda65d3f3ef5c4ff Mon Sep 17 00:00:00 2001 From: Onur Cinar Date: Tue, 1 Sep 2026 06:45:50 +0000 Subject: [PATCH] Fix RSI producing NaN instead of neutral 50 on a flat market RSI's `ComputeWithContext` computed RS = averageGain / averageLoss and then RSI = 100 - (100 / (1 + RS)). When a window has zero gains and zero losses (a flat price run, e.g. an untraded bar or synthetic/test data), RS was 0/0 = NaN, so the output RSI was NaN for that bar instead of the conventional neutral value of 50. Replaced the Divide/IncrementBy/MultiplyBy/Pow pipeline with a single helper.OperateWithContext that zips averageGains and averageLosses and special-cases the 0/0 case to 50, otherwise computing the same RS and RSI formula as before. Added TestRsiFlatMarket covering a constant closing-price series and verified the existing CSV-fixture-based TestRsi is unaffected (its fixture has no flat windows). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Xv4stuAb6WuQ8rPZ4cupLp --- momentum/rsi.go | 28 +++++++++++----------------- momentum/rsi_test.go | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/momentum/rsi.go b/momentum/rsi.go index e7b722c..a006ec2 100644 --- a/momentum/rsi.go +++ b/momentum/rsi.go @@ -55,24 +55,18 @@ func (r *Rsi[T]) ComputeWithContext(ctx context.Context, closings <-chan T) <-ch -1, ) - rs := helper.DivideWithContext(ctx, averageGains, - averageLosses, - ) + // RSI = 100 - (100 / (1 + RS)), where RS = Average Gain / Average Loss. + // A flat window (no gains and no losses) makes RS an undefined 0/0, so it + // is treated as neutral (RSI = 50) instead of propagating NaN. + rsi := helper.OperateWithContext(ctx, averageGains, averageLosses, func(averageGain, averageLoss T) T { + if averageGain == 0 && averageLoss == 0 { + return 50 + } - // RSI = 100 - (100 / (1 + RS)) - rsi := helper.IncrementByWithContext(ctx, // - (100 / (1 + RS)) - helper.MultiplyByWithContext(ctx, // 100 / (1 + RS) - helper.MultiplyByWithContext(ctx, // 1 / (1 + RS) - helper.PowWithContext(ctx, // 1 + RS - helper.IncrementByWithContext(ctx, rs, 1), - -1, - ), - 100, - ), - -1, - ), - 100, - ) + rs := averageGain / averageLoss + + return 100 - (100 / (1 + rs)) + }) return rsi } diff --git a/momentum/rsi_test.go b/momentum/rsi_test.go index 87ef8e9..b63e589 100644 --- a/momentum/rsi_test.go +++ b/momentum/rsi_test.go @@ -37,3 +37,23 @@ func TestRsi(t *testing.T) { t.Fatal(err) } } + +func TestRsiFlatMarket(t *testing.T) { + closings := make([]float64, momentum.DefaultRsiPeriod+20) + for i := range closings { + closings[i] = 100 + } + + rsi := momentum.NewRsi[float64]() + actualRsi := helper.ChanToSlice(rsi.Compute(helper.SliceToChan(closings))) + + if len(actualRsi) == 0 { + t.Fatal("expected at least one RSI value") + } + + for _, v := range actualRsi { + if v != 50 { + t.Fatalf("expected RSI of 50 for flat market, got %v", v) + } + } +}