mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 08:45:16 +00:00
Add RSI indicator
This commit is contained in:
parent
1a29bc7362
commit
b074f03507
90
pkg/indicator/rsi.go
Normal file
90
pkg/indicator/rsi.go
Normal file
|
@ -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)
|
||||||
|
}
|
15
pkg/indicator/rsi_callbacks.go
Normal file
15
pkg/indicator/rsi_callbacks.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
52
pkg/indicator/rsi_test.go
Normal file
52
pkg/indicator/rsi_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -53,3 +53,55 @@ func (s Float64Slice) Tail(size int) Float64Slice {
|
||||||
copy(win, s[length-size:])
|
copy(win, s[length-size:])
|
||||||
return win
|
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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user