Skip to content

Commit

Permalink
Merge pull request c9s#500 from narumiruna/rsi
Browse files Browse the repository at this point in the history
feature: add Relative Strength Index (RSI) indicator
  • Loading branch information
c9s authored Mar 29, 2022
2 parents 0511a0f + e92a872 commit 98d4815
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 0 deletions.
90 changes: 90 additions & 0 deletions pkg/indicator/rsi.go
Original file line number Diff line number Diff line change
@@ -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 {
priceDifferences := inc.Prices.Diff()

avgGain = priceDifferences.PositiveValuesOrZero().AbsoluteValues().Sum() / float64(inc.Window)
avgLoss = priceDifferences.NegativeValuesOrZero().AbsoluteValues().Sum() / float64(inc.Window)
} else {
difference := price - inc.Prices[len(inc.Prices)-2]
currentGain := math.Max(difference, 0)
currentLoss := -math.Min(difference, 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)
}
15 changes: 15 additions & 0 deletions pkg/indicator/rsi_callbacks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 69 additions & 0 deletions pkg/indicator/rsi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package indicator

import (
"encoding/json"
"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.46413502109704,
66.24961855355505,
66.48094183471265,
69.34685316290864,
66.29471265892624,
57.91502067008556,
62.88071830996241,
63.208788718287764,
56.01158478954758,
62.33992931089789,
54.67097137765515,
50.386815195114224,
40.01942379131357,
41.49263540422282,
41.902429678458105,
45.499497238680405,
37.32277831337995,
33.090482572723396,
37.78877198205783,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rsi := RSI{IntervalWindow: types.IntervalWindow{Window: tt.window}}
rsi.calculateAndUpdate(tt.kLines)
assert.Equal(t, len(rsi.Values), len(tt.want))
for i, v := range rsi.Values {
assert.InDelta(t, v, tt.want[i], Delta)
}
})
}
}
52 changes: 52 additions & 0 deletions pkg/types/float_slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) PositiveValuesOrZero() Float64Slice {
var values Float64Slice
for _, v := range s {
values.Push(math.Max(v, 0))
}
return values
}

func (s Float64Slice) NegativeValuesOrZero() 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
}

0 comments on commit 98d4815

Please sign in to comment.