From b074f03507b699007a4f2a17b1e030743ca675ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= Date: Tue, 29 Mar 2022 02:01:03 +0800 Subject: [PATCH] Add RSI indicator --- pkg/indicator/rsi.go | 90 ++++++++++++++++++++++++++++++++++ pkg/indicator/rsi_callbacks.go | 15 ++++++ pkg/indicator/rsi_test.go | 52 ++++++++++++++++++++ pkg/types/float_slice.go | 52 ++++++++++++++++++++ 4 files changed, 209 insertions(+) create mode 100644 pkg/indicator/rsi.go create mode 100644 pkg/indicator/rsi_callbacks.go create mode 100644 pkg/indicator/rsi_test.go diff --git a/pkg/indicator/rsi.go b/pkg/indicator/rsi.go new file mode 100644 index 000000000..f5d0f6a02 --- /dev/null +++ b/pkg/indicator/rsi.go @@ -0,0 +1,90 @@ +package indicator + +import ( + "math" + "time" + + "github.com/c9s/bbgo/pkg/types" +) + +/* +rsi implements Relative Strength Index (RSI) + +https://www.investopedia.com/terms/r/rsi.asp +*/ +//go:generate callbackgen -type RSI +type RSI struct { + types.IntervalWindow + Values types.Float64Slice + Prices types.Float64Slice + PreviousAvgLoss float64 + PreviousAvgGain float64 + + EndTime time.Time + UpdateCallbacks []func(value float64) +} + +func (inc *RSI) Update(kline types.KLine, priceF KLinePriceMapper) { + price := priceF(kline) + inc.Prices.Push(price) + + if len(inc.Prices) < inc.Window+1 { + return + } + + var avgGain float64 + var avgLoss float64 + if len(inc.Prices) == inc.Window+1 { + diffValues := inc.Prices.Diff() + + avgGain = diffValues.PositiveValues().AbsoluteValues().Sum() / float64(inc.Window) + avgLoss = diffValues.NegativeValues().AbsoluteValues().Sum() / float64(inc.Window) + } else { + diff := price - inc.Prices[len(inc.Prices)-2] + currentGain := math.Abs(math.Max(diff, 0)) + currentLoss := math.Abs(math.Min(diff, 0)) + + avgGain = (inc.PreviousAvgGain*13 + currentGain) / float64(inc.Window) + avgLoss = (inc.PreviousAvgLoss*13 + currentLoss) / float64(inc.Window) + } + + rs := avgGain / avgLoss + rsi := 100 - (100 / (1 + rs)) + inc.Values.Push(rsi) + + inc.PreviousAvgGain = avgGain + inc.PreviousAvgLoss = avgLoss +} + +func (inc *RSI) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-1] +} + +func (inc *RSI) calculateAndUpdate(kLines []types.KLine) { + var priceF = KLineClosePriceMapper + + for _, k := range kLines { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + continue + } + inc.Update(k, priceF) + } + + inc.EmitUpdate(inc.Last()) + inc.EndTime = kLines[len(kLines)-1].EndTime.Time() +} + +func (inc *RSI) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.calculateAndUpdate(window) +} + +func (inc *RSI) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/rsi_callbacks.go b/pkg/indicator/rsi_callbacks.go new file mode 100644 index 000000000..2c1a11f66 --- /dev/null +++ b/pkg/indicator/rsi_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type RSI"; DO NOT EDIT. + +package indicator + +import () + +func (inc *RSI) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *RSI) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/rsi_test.go b/pkg/indicator/rsi_test.go new file mode 100644 index 000000000..c7e717c6d --- /dev/null +++ b/pkg/indicator/rsi_test.go @@ -0,0 +1,52 @@ +package indicator + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func Test_calculateRSI(t *testing.T) { + // test case from https://school.stockcharts.com/doku.php?id=technical_indicators:relative_strength_index_rsi + buildKLines := func(prices []fixedpoint.Value) (kLines []types.KLine) { + for _, p := range prices { + kLines = append(kLines, types.KLine{High: p, Low: p, Close: p}) + } + return kLines + } + var data = []byte(`[44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00, 46.03, 46.41, 46.22, 45.64, 46.21, 46.25, 45.71, 46.45, 45.78, 45.35, 44.03, 44.18, 44.22, 44.57, 43.42, 42.66, 43.13]`) + var values []fixedpoint.Value + _ = json.Unmarshal(data, &values) + + tests := []struct { + name string + kLines []types.KLine + window int + want types.Float64Slice + }{ + { + name: "RSI", + kLines: buildKLines(values), + window: 14, + want: types.Float64Slice{70.53, 66.32, 66.55, 69.41, 66.36, 57.97, 62.93, 63.26, 56.06, 62.38, 54.71, 50.42, 39.99, 41.46, 41.87, 45.46, 37.30, 33.08, 37.77}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rsi := RSI{IntervalWindow: types.IntervalWindow{Window: tt.window}} + rsi.calculateAndUpdate(tt.kLines) + fmt.Println(len(tt.want), len(rsi.Values)) + assert.Equal(t, len(rsi.Values), len(tt.want)) + for i, v := range rsi.Values { + fmt.Println(v, tt.want[i]) + assert.InDelta(t, v, tt.want[i], 0.1) + } + }) + } +} diff --git a/pkg/types/float_slice.go b/pkg/types/float_slice.go index f3c6f65f9..d82877235 100644 --- a/pkg/types/float_slice.go +++ b/pkg/types/float_slice.go @@ -53,3 +53,55 @@ func (s Float64Slice) Tail(size int) Float64Slice { copy(win, s[length-size:]) return win } + +func (s Float64Slice) Diff() Float64Slice { + var values Float64Slice + for i, v := range s { + if i == 0 { + values.Push(0) + continue + } + values.Push(v - s[i-1]) + } + return values +} + +func (s Float64Slice) PositiveValues() Float64Slice { + var values Float64Slice + for _, v := range s { + values.Push(math.Max(v, 0)) + } + return values +} + +func (s Float64Slice) NegativeValues() Float64Slice { + var values Float64Slice + for _, v := range s { + values.Push(math.Min(v, 0)) + } + return values +} + +func (s Float64Slice) AbsoluteValues() Float64Slice { + var values Float64Slice + for _, v := range s { + values.Push(math.Abs(v)) + } + return values +} + +func (s Float64Slice) MulScalar(x float64) Float64Slice { + var values Float64Slice + for _, v := range s { + values.Push(v * x) + } + return values +} + +func (s Float64Slice) DivScalar(x float64) Float64Slice { + var values Float64Slice + for _, v := range s { + values.Push(v / x) + } + return values +}