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) + } + } +}