From 89aa63dd643e0b2abbbfe4d22d7cba8f8169cf4d Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 29 May 2023 17:12:34 +0800 Subject: [PATCH 01/18] floats: add floats LSM --- pkg/datatype/floats/funcs_test.go | 6 ++++++ pkg/datatype/floats/pivot.go | 4 ++-- pkg/datatype/floats/slice.go | 27 +++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pkg/datatype/floats/funcs_test.go b/pkg/datatype/floats/funcs_test.go index 25c37f908..52401fbd4 100644 --- a/pkg/datatype/floats/funcs_test.go +++ b/pkg/datatype/floats/funcs_test.go @@ -15,3 +15,9 @@ func TestHigher(t *testing.T) { out := Higher([]float64{10.0, 11.0, 12.0, 13.0, 15.0}, 12.0) assert.Equal(t, []float64{13.0, 15.0}, out) } + +func TestLSM(t *testing.T) { + slice := Slice{1., 2., 3., 4.} + slope := LSM(slice) + assert.Equal(t, 1.0, slope) +} diff --git a/pkg/datatype/floats/pivot.go b/pkg/datatype/floats/pivot.go index f79403047..b7536cbe3 100644 --- a/pkg/datatype/floats/pivot.go +++ b/pkg/datatype/floats/pivot.go @@ -1,10 +1,10 @@ package floats func (s Slice) Pivot(left, right int, f func(a, pivot float64) bool) (float64, bool) { - return CalculatePivot(s, left, right, f) + return FindPivot(s, left, right, f) } -func CalculatePivot(values Slice, left, right int, f func(a, pivot float64) bool) (float64, bool) { +func FindPivot(values Slice, left, right int, f func(a, pivot float64) bool) (float64, bool) { length := len(values) if right == 0 { diff --git a/pkg/datatype/floats/slice.go b/pkg/datatype/floats/slice.go index 40b3a427b..f9efa2d6c 100644 --- a/pkg/datatype/floats/slice.go +++ b/pkg/datatype/floats/slice.go @@ -187,3 +187,30 @@ func (s Slice) Index(i int) float64 { func (s Slice) Length() int { return len(s) } + +func (s Slice) LSM() float64 { + return LSM(s) +} + +func LSM(values Slice) float64 { + var sumX, sumY, sumXSqr, sumXY = .0, .0, .0, .0 + + end := len(values) - 1 + for i := end; i >= 0; i-- { + val := values[i] + per := float64(end - i + 1) + sumX += per + sumY += val + sumXSqr += per * per + sumXY += val * per + } + + length := float64(len(values)) + slope := (length*sumXY - sumX*sumY) / (length*sumXSqr - sumX*sumX) + + average := sumY / length + tail := average - slope*sumX/length + slope + head := tail + slope*(length-1) + slope2 := (tail - head) / (length - 1) + return slope2 +} From e91142f4e96db0d0c622166c882fdcc7a3cc27bd Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 13:14:36 +0800 Subject: [PATCH 02/18] indicator: rename func to floats.FindPivot --- pkg/indicator/pivotlow.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/indicator/pivotlow.go b/pkg/indicator/pivotlow.go index 135b61868..c195c488c 100644 --- a/pkg/indicator/pivotlow.go +++ b/pkg/indicator/pivotlow.go @@ -64,13 +64,13 @@ func (inc *PivotLow) PushK(k types.KLine) { } func calculatePivotHigh(highs floats.Slice, left, right int) (float64, bool) { - return floats.CalculatePivot(highs, left, right, func(a, pivot float64) bool { + return floats.FindPivot(highs, left, right, func(a, pivot float64) bool { return a < pivot }) } func calculatePivotLow(lows floats.Slice, left, right int) (float64, bool) { - return floats.CalculatePivot(lows, left, right, func(a, pivot float64) bool { + return floats.FindPivot(lows, left, right, func(a, pivot float64) bool { return a > pivot }) } From f067c9273395fe685cd068885e4040f7d7f82e8c Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 29 May 2023 17:55:57 +0800 Subject: [PATCH 03/18] floats: document LSM --- pkg/datatype/floats/slice.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/datatype/floats/slice.go b/pkg/datatype/floats/slice.go index f9efa2d6c..727cfe9bb 100644 --- a/pkg/datatype/floats/slice.go +++ b/pkg/datatype/floats/slice.go @@ -192,6 +192,7 @@ func (s Slice) LSM() float64 { return LSM(s) } +// LSM is the least squares method for linear regression func LSM(values Slice) float64 { var sumX, sumY, sumXSqr, sumXY = .0, .0, .0, .0 From 8c7962f07fa613f83db90542ed6342e6374fca38 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 29 May 2023 21:37:31 +0800 Subject: [PATCH 04/18] indicator: move out subtract stream --- pkg/indicator/macd2.go | 40 -------------------------------- pkg/indicator/subtract.go | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 40 deletions(-) create mode 100644 pkg/indicator/subtract.go diff --git a/pkg/indicator/macd2.go b/pkg/indicator/macd2.go index c9d763aa2..8b4c51533 100644 --- a/pkg/indicator/macd2.go +++ b/pkg/indicator/macd2.go @@ -72,43 +72,3 @@ func (s *EWMAStream) calculate(v float64) float64 { m := s.multiplier return (1.0-m)*last + m*v } - -type SubtractStream struct { - Float64Updater - types.SeriesBase - - a, b, c floats.Slice - i int -} - -func Subtract(a, b Float64Source) *SubtractStream { - s := &SubtractStream{} - s.SeriesBase.Series = s.c - - a.OnUpdate(func(v float64) { - s.a.Push(v) - s.calculate() - }) - b.OnUpdate(func(v float64) { - s.b.Push(v) - s.calculate() - }) - return s -} - -func (s *SubtractStream) calculate() { - if s.a.Length() != s.b.Length() { - return - } - - if s.a.Length() > s.c.Length() { - var numNewElems = s.a.Length() - s.c.Length() - var tailA = s.a.Tail(numNewElems) - var tailB = s.b.Tail(numNewElems) - var tailC = tailA.Sub(tailB) - for _, f := range tailC { - s.c.Push(f) - s.EmitUpdate(f) - } - } -} diff --git a/pkg/indicator/subtract.go b/pkg/indicator/subtract.go new file mode 100644 index 000000000..fa03d85eb --- /dev/null +++ b/pkg/indicator/subtract.go @@ -0,0 +1,49 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +// SubtractStream subscribes 2 upstream data, and then subtract these 2 values +type SubtractStream struct { + Float64Updater + types.SeriesBase + + a, b, c floats.Slice + i int +} + +// Subtract creates the SubtractStream object +// subtract := Subtract(longEWMA, shortEWMA) +func Subtract(a, b Float64Source) *SubtractStream { + s := &SubtractStream{} + s.SeriesBase.Series = s.c + + a.OnUpdate(func(v float64) { + s.a.Push(v) + s.calculate() + }) + b.OnUpdate(func(v float64) { + s.b.Push(v) + s.calculate() + }) + return s +} + +func (s *SubtractStream) calculate() { + if s.a.Length() != s.b.Length() { + return + } + + if s.a.Length() > s.c.Length() { + var numNewElems = s.a.Length() - s.c.Length() + var tailA = s.a.Tail(numNewElems) + var tailB = s.b.Tail(numNewElems) + var tailC = tailA.Sub(tailB) + for _, f := range tailC { + s.c.Push(f) + s.EmitUpdate(f) + } + } +} From 68570e1eeb80694402ef53f5044f869ad55de42a Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 29 May 2023 21:42:22 +0800 Subject: [PATCH 05/18] indicator: move EWMA2 to ewma2.go --- pkg/indicator/ewma2.go | 46 ++++++++++++++++++++++++++++++++++++++++++ pkg/indicator/macd2.go | 41 ------------------------------------- 2 files changed, 46 insertions(+), 41 deletions(-) create mode 100644 pkg/indicator/ewma2.go diff --git a/pkg/indicator/ewma2.go b/pkg/indicator/ewma2.go new file mode 100644 index 000000000..ba5bea29b --- /dev/null +++ b/pkg/indicator/ewma2.go @@ -0,0 +1,46 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type EWMAStream +type EWMAStream struct { + Float64Updater + types.SeriesBase + + slice floats.Slice + + window int + multiplier float64 +} + +func EWMA2(source Float64Source, window int) *EWMAStream { + s := &EWMAStream{ + window: window, + multiplier: 2.0 / float64(1+window), + } + + s.SeriesBase.Series = s.slice + + if sub, ok := source.(Float64Subscription); ok { + sub.AddSubscriber(s.calculateAndPush) + } else { + source.OnUpdate(s.calculateAndPush) + } + + return s +} + +func (s *EWMAStream) calculateAndPush(v float64) { + v2 := s.calculate(v) + s.slice.Push(v2) + s.EmitUpdate(v2) +} + +func (s *EWMAStream) calculate(v float64) float64 { + last := s.slice.Last() + m := s.multiplier + return (1.0-m)*last + m*v +} diff --git a/pkg/indicator/macd2.go b/pkg/indicator/macd2.go index 8b4c51533..b1ef53bd9 100644 --- a/pkg/indicator/macd2.go +++ b/pkg/indicator/macd2.go @@ -1,7 +1,6 @@ package indicator import ( - "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -32,43 +31,3 @@ type Float64Subscription interface { types.Series AddSubscriber(f func(v float64)) } - -//go:generate callbackgen -type EWMAStream -type EWMAStream struct { - Float64Updater - types.SeriesBase - - slice floats.Slice - - window int - multiplier float64 -} - -func EWMA2(source Float64Source, window int) *EWMAStream { - s := &EWMAStream{ - window: window, - multiplier: 2.0 / float64(1+window), - } - - s.SeriesBase.Series = s.slice - - if sub, ok := source.(Float64Subscription); ok { - sub.AddSubscriber(s.calculateAndPush) - } else { - source.OnUpdate(s.calculateAndPush) - } - - return s -} - -func (s *EWMAStream) calculateAndPush(v float64) { - v2 := s.calculate(v) - s.slice.Push(v2) - s.EmitUpdate(v2) -} - -func (s *EWMAStream) calculate(v float64) float64 { - last := s.slice.Last() - m := s.multiplier - return (1.0-m)*last + m*v -} From e094f422fcdff13fd331b67d7ee8d5e05a7ec6eb Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 29 May 2023 21:44:23 +0800 Subject: [PATCH 06/18] indicator: rename v2 indicator file --- pkg/indicator/{macd2.go => v2.go} | 0 pkg/indicator/{macd2_test.go => v2_test.go} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename pkg/indicator/{macd2.go => v2.go} (100%) rename pkg/indicator/{macd2_test.go => v2_test.go} (95%) diff --git a/pkg/indicator/macd2.go b/pkg/indicator/v2.go similarity index 100% rename from pkg/indicator/macd2.go rename to pkg/indicator/v2.go diff --git a/pkg/indicator/macd2_test.go b/pkg/indicator/v2_test.go similarity index 95% rename from pkg/indicator/macd2_test.go rename to pkg/indicator/v2_test.go index 5fe59773d..bbaeb24d3 100644 --- a/pkg/indicator/macd2_test.go +++ b/pkg/indicator/v2_test.go @@ -9,7 +9,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -func TestSubtract(t *testing.T) { +func Test_v2_Subtract(t *testing.T) { stream := &types.StandardStream{} kLines := KLines(stream) closePrices := ClosePrices(kLines) From 1450d193a47a6264a47afc5e653c8e5cf8e1db10 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 11:35:24 +0800 Subject: [PATCH 07/18] indicator: refactor/add float64 series --- pkg/indicator/atr2.go | 37 ++++++++++++ pkg/indicator/ewma2.go | 11 +--- pkg/indicator/ewmastream_callbacks.go | 5 -- pkg/indicator/float64updater.go | 28 ++++++++++ pkg/indicator/float64updater_callbacks.go | 8 +-- pkg/indicator/price.go | 5 +- pkg/indicator/rma2.go | 68 +++++++++++++++++++++++ pkg/indicator/subtract.go | 12 ++-- pkg/indicator/v2_test.go | 4 +- 9 files changed, 147 insertions(+), 31 deletions(-) create mode 100644 pkg/indicator/atr2.go delete mode 100644 pkg/indicator/ewmastream_callbacks.go create mode 100644 pkg/indicator/rma2.go diff --git a/pkg/indicator/atr2.go b/pkg/indicator/atr2.go new file mode 100644 index 000000000..ac0f4ba3d --- /dev/null +++ b/pkg/indicator/atr2.go @@ -0,0 +1,37 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +type ATRStream struct { + Float64Updater + + types.SeriesBase + + window int + multiplier float64 +} + +func ATR2(source KLineSubscription, window int) *ATRStream { + s := &ATRStream{ + window: window, + multiplier: 2.0 / float64(1+window), + } + + s.SeriesBase.Series = s.slice + + source.AddSubscriber(func(k types.KLine) { + // v := s.mapper(k) + // s.slice.Push(v) + // s.EmitUpdate(v) + }) + + return s +} + +func (s *ATRStream) calculateAndPush(k types.KLine) { + // v2 := s.calculate(v) + // s.slice.Push(v2) + // s.EmitUpdate(v2) +} diff --git a/pkg/indicator/ewma2.go b/pkg/indicator/ewma2.go index ba5bea29b..9041238c9 100644 --- a/pkg/indicator/ewma2.go +++ b/pkg/indicator/ewma2.go @@ -1,16 +1,7 @@ package indicator -import ( - "github.com/c9s/bbgo/pkg/datatype/floats" - "github.com/c9s/bbgo/pkg/types" -) - -//go:generate callbackgen -type EWMAStream type EWMAStream struct { - Float64Updater - types.SeriesBase - - slice floats.Slice + Float64Series window int multiplier float64 diff --git a/pkg/indicator/ewmastream_callbacks.go b/pkg/indicator/ewmastream_callbacks.go deleted file mode 100644 index 79da29114..000000000 --- a/pkg/indicator/ewmastream_callbacks.go +++ /dev/null @@ -1,5 +0,0 @@ -// Code generated by "callbackgen -type EWMAStream"; DO NOT EDIT. - -package indicator - -import () diff --git a/pkg/indicator/float64updater.go b/pkg/indicator/float64updater.go index a9743538e..f55fa9c2b 100644 --- a/pkg/indicator/float64updater.go +++ b/pkg/indicator/float64updater.go @@ -1,6 +1,34 @@ package indicator +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + //go:generate callbackgen -type Float64Updater type Float64Updater struct { updateCallbacks []func(v float64) + + slice floats.Slice +} + +type Float64Series struct { + types.SeriesBase + Float64Updater +} + +func (f *Float64Series) Last() float64 { + return f.slice.Last() +} + +func (f *Float64Series) Index(i int) float64 { + length := len(f.slice) + if length == 0 || length-i-1 < 0 { + return 0 + } + return f.slice[length-i-1] +} + +func (f *Float64Series) Length() int { + return len(f.slice) } diff --git a/pkg/indicator/float64updater_callbacks.go b/pkg/indicator/float64updater_callbacks.go index ff766f2d5..322660863 100644 --- a/pkg/indicator/float64updater_callbacks.go +++ b/pkg/indicator/float64updater_callbacks.go @@ -4,12 +4,12 @@ package indicator import () -func (F *Float64Updater) OnUpdate(cb func(v float64)) { - F.updateCallbacks = append(F.updateCallbacks, cb) +func (f *Float64Updater) OnUpdate(cb func(v float64)) { + f.updateCallbacks = append(f.updateCallbacks, cb) } -func (F *Float64Updater) EmitUpdate(v float64) { - for _, cb := range F.updateCallbacks { +func (f *Float64Updater) EmitUpdate(v float64) { + for _, cb := range f.updateCallbacks { cb(v) } } diff --git a/pkg/indicator/price.go b/pkg/indicator/price.go index fd944121c..a3c7c45a1 100644 --- a/pkg/indicator/price.go +++ b/pkg/indicator/price.go @@ -1,7 +1,6 @@ package indicator import ( - "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -10,10 +9,8 @@ type KLineSubscription interface { } type PriceStream struct { - types.SeriesBase - Float64Updater + Float64Series - slice floats.Slice mapper KLineValueMapper } diff --git a/pkg/indicator/rma2.go b/pkg/indicator/rma2.go new file mode 100644 index 000000000..b751b87a7 --- /dev/null +++ b/pkg/indicator/rma2.go @@ -0,0 +1,68 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +type RMAStream struct { + // embedded structs + Float64Updater + types.SeriesBase + + // config fields + types.IntervalWindow + Adjust bool + + counter int + sum, tmp float64 +} + +func RMA2(source Float64Source, iw types.IntervalWindow) *RMAStream { + s := &RMAStream{ + IntervalWindow: iw, + } + + s.SeriesBase.Series = s.slice + + if sub, ok := source.(Float64Subscription); ok { + sub.AddSubscriber(s.calculateAndPush) + } else { + source.OnUpdate(s.calculateAndPush) + } + + return s +} + +func (s *RMAStream) calculateAndPush(v float64) { + v2 := s.calculate(v) + s.slice.Push(v2) + s.EmitUpdate(v2) +} + +func (s *RMAStream) calculate(x float64) float64 { + lambda := 1 / float64(s.Window) + if s.counter == 0 { + s.sum = 1 + s.tmp = x + } else { + if s.Adjust { + s.sum = s.sum*(1-lambda) + 1 + s.tmp = s.tmp + (x-s.tmp)/s.sum + } else { + s.tmp = s.tmp*(1-lambda) + x*lambda + } + } + s.counter++ + + if s.counter < s.Window { + s.slice.Push(0) + } + + s.slice.Push(s.tmp) + + if len(s.slice) > MaxNumOfRMA { + s.slice = s.slice[MaxNumOfRMATruncateSize-1:] + } + + return s.tmp +} diff --git a/pkg/indicator/subtract.go b/pkg/indicator/subtract.go index fa03d85eb..287e34ca8 100644 --- a/pkg/indicator/subtract.go +++ b/pkg/indicator/subtract.go @@ -10,15 +10,15 @@ type SubtractStream struct { Float64Updater types.SeriesBase - a, b, c floats.Slice - i int + a, b floats.Slice + i int } // Subtract creates the SubtractStream object // subtract := Subtract(longEWMA, shortEWMA) func Subtract(a, b Float64Source) *SubtractStream { s := &SubtractStream{} - s.SeriesBase.Series = s.c + s.SeriesBase.Series = s.slice a.OnUpdate(func(v float64) { s.a.Push(v) @@ -36,13 +36,13 @@ func (s *SubtractStream) calculate() { return } - if s.a.Length() > s.c.Length() { - var numNewElems = s.a.Length() - s.c.Length() + if s.a.Length() > s.slice.Length() { + var numNewElems = s.a.Length() - s.slice.Length() var tailA = s.a.Tail(numNewElems) var tailB = s.b.Tail(numNewElems) var tailC = tailA.Sub(tailB) for _, f := range tailC { - s.c.Push(f) + s.slice.Push(f) s.EmitUpdate(f) } } diff --git a/pkg/indicator/v2_test.go b/pkg/indicator/v2_test.go index bbaeb24d3..a96812c1f 100644 --- a/pkg/indicator/v2_test.go +++ b/pkg/indicator/v2_test.go @@ -25,6 +25,6 @@ func Test_v2_Subtract(t *testing.T) { t.Logf("slowEMA: %+v", slowEMA.slice) assert.Equal(t, len(subtract.a), len(subtract.b)) - assert.Equal(t, len(subtract.a), len(subtract.c)) - assert.InDelta(t, subtract.c[0], subtract.a[0]-subtract.b[0], 0.0001) + assert.Equal(t, len(subtract.a), len(subtract.slice)) + assert.InDelta(t, subtract.slice[0], subtract.a[0]-subtract.b[0], 0.0001) } From 1bf44720e2b5045209bf1b50d9d331158b662fb5 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 11:35:59 +0800 Subject: [PATCH 08/18] indicator: update and clean up rma2 --- pkg/indicator/rma2.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/indicator/rma2.go b/pkg/indicator/rma2.go index b751b87a7..66a92e99d 100644 --- a/pkg/indicator/rma2.go +++ b/pkg/indicator/rma2.go @@ -6,8 +6,7 @@ import ( type RMAStream struct { // embedded structs - Float64Updater - types.SeriesBase + Float64Series // config fields types.IntervalWindow From da15f47f17596430e9e8b9b6a355f300d8d249bc Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 12:13:55 +0800 Subject: [PATCH 09/18] indicator: refactor Float64Series and improve RMA2 --- pkg/indicator/atr2.go | 9 ++++----- pkg/indicator/ewma2.go | 7 +++---- pkg/indicator/float64updater.go | 10 ++++++++-- pkg/indicator/rma2.go | 25 +++++++++++++++---------- pkg/indicator/subtract.go | 9 ++++----- 5 files changed, 34 insertions(+), 26 deletions(-) diff --git a/pkg/indicator/atr2.go b/pkg/indicator/atr2.go index ac0f4ba3d..a881562d2 100644 --- a/pkg/indicator/atr2.go +++ b/pkg/indicator/atr2.go @@ -5,7 +5,7 @@ import ( ) type ATRStream struct { - Float64Updater + Float64Series types.SeriesBase @@ -15,12 +15,11 @@ type ATRStream struct { func ATR2(source KLineSubscription, window int) *ATRStream { s := &ATRStream{ - window: window, - multiplier: 2.0 / float64(1+window), + Float64Series: NewFloat64Series(), + window: window, + multiplier: 2.0 / float64(1+window), } - s.SeriesBase.Series = s.slice - source.AddSubscriber(func(k types.KLine) { // v := s.mapper(k) // s.slice.Push(v) diff --git a/pkg/indicator/ewma2.go b/pkg/indicator/ewma2.go index 9041238c9..e452aaeda 100644 --- a/pkg/indicator/ewma2.go +++ b/pkg/indicator/ewma2.go @@ -9,12 +9,11 @@ type EWMAStream struct { func EWMA2(source Float64Source, window int) *EWMAStream { s := &EWMAStream{ - window: window, - multiplier: 2.0 / float64(1+window), + Float64Series: NewFloat64Series(), + window: window, + multiplier: 2.0 / float64(1+window), } - s.SeriesBase.Series = s.slice - if sub, ok := source.(Float64Subscription); ok { sub.AddSubscriber(s.calculateAndPush) } else { diff --git a/pkg/indicator/float64updater.go b/pkg/indicator/float64updater.go index f55fa9c2b..3c3746ffe 100644 --- a/pkg/indicator/float64updater.go +++ b/pkg/indicator/float64updater.go @@ -8,13 +8,19 @@ import ( //go:generate callbackgen -type Float64Updater type Float64Updater struct { updateCallbacks []func(v float64) - - slice floats.Slice } type Float64Series struct { types.SeriesBase Float64Updater + slice floats.Slice +} + +func NewFloat64Series(v ...float64) Float64Series { + s := Float64Series{} + s.slice = v + s.SeriesBase.Series = s.slice + return s } func (f *Float64Series) Last() float64 { diff --git a/pkg/indicator/rma2.go b/pkg/indicator/rma2.go index 66a92e99d..4e48c4bda 100644 --- a/pkg/indicator/rma2.go +++ b/pkg/indicator/rma2.go @@ -12,17 +12,16 @@ type RMAStream struct { types.IntervalWindow Adjust bool - counter int - sum, tmp float64 + counter int + sum, previous float64 } func RMA2(source Float64Source, iw types.IntervalWindow) *RMAStream { s := &RMAStream{ + Float64Series: NewFloat64Series(), IntervalWindow: iw, } - s.SeriesBase.Series = s.slice - if sub, ok := source.(Float64Subscription); ok { sub.AddSubscriber(s.calculateAndPush) } else { @@ -36,32 +35,38 @@ func (s *RMAStream) calculateAndPush(v float64) { v2 := s.calculate(v) s.slice.Push(v2) s.EmitUpdate(v2) + s.truncate() } func (s *RMAStream) calculate(x float64) float64 { lambda := 1 / float64(s.Window) + tmp := 0.0 if s.counter == 0 { s.sum = 1 - s.tmp = x + tmp = x } else { if s.Adjust { s.sum = s.sum*(1-lambda) + 1 - s.tmp = s.tmp + (x-s.tmp)/s.sum + tmp = s.previous + (x-s.previous)/s.sum } else { - s.tmp = s.tmp*(1-lambda) + x*lambda + tmp = s.previous*(1-lambda) + x*lambda } } s.counter++ if s.counter < s.Window { + // we can use x, but we need to use 0. to make the same behavior as the result from python pandas_ta s.slice.Push(0) } - s.slice.Push(s.tmp) + s.slice.Push(tmp) + s.previous = tmp + return tmp +} + +func (s *RMAStream) truncate() { if len(s.slice) > MaxNumOfRMA { s.slice = s.slice[MaxNumOfRMATruncateSize-1:] } - - return s.tmp } diff --git a/pkg/indicator/subtract.go b/pkg/indicator/subtract.go index 287e34ca8..7ccde2bf6 100644 --- a/pkg/indicator/subtract.go +++ b/pkg/indicator/subtract.go @@ -2,13 +2,11 @@ package indicator import ( "github.com/c9s/bbgo/pkg/datatype/floats" - "github.com/c9s/bbgo/pkg/types" ) // SubtractStream subscribes 2 upstream data, and then subtract these 2 values type SubtractStream struct { - Float64Updater - types.SeriesBase + Float64Series a, b floats.Slice i int @@ -17,8 +15,9 @@ type SubtractStream struct { // Subtract creates the SubtractStream object // subtract := Subtract(longEWMA, shortEWMA) func Subtract(a, b Float64Source) *SubtractStream { - s := &SubtractStream{} - s.SeriesBase.Series = s.slice + s := &SubtractStream{ + Float64Series: NewFloat64Series(), + } a.OnUpdate(func(v float64) { s.a.Push(v) From f65d6267fca0b4b19ef52bb070e158f377e65af0 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 12:29:50 +0800 Subject: [PATCH 10/18] indicator: refactor ATRStream --- pkg/indicator/atr2.go | 43 +++++++++++++++++++++++++++++++------------ pkg/indicator/rma2.go | 17 +++++++---------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/pkg/indicator/atr2.go b/pkg/indicator/atr2.go index a881562d2..af8fd9959 100644 --- a/pkg/indicator/atr2.go +++ b/pkg/indicator/atr2.go @@ -1,36 +1,55 @@ package indicator import ( + "math" + "github.com/c9s/bbgo/pkg/types" ) type ATRStream struct { + // embedded struct Float64Series - types.SeriesBase + // parameters + types.IntervalWindow - window int - multiplier float64 + // private states + rma *RMAStream + + window int + previousClose float64 } func ATR2(source KLineSubscription, window int) *ATRStream { s := &ATRStream{ Float64Series: NewFloat64Series(), window: window, - multiplier: 2.0 / float64(1+window), } + s.rma = RMA2(s, window, true) source.AddSubscriber(func(k types.KLine) { - // v := s.mapper(k) - // s.slice.Push(v) - // s.EmitUpdate(v) + s.calculateAndPush(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) }) - return s } -func (s *ATRStream) calculateAndPush(k types.KLine) { - // v2 := s.calculate(v) - // s.slice.Push(v2) - // s.EmitUpdate(v2) +func (s *ATRStream) calculateAndPush(high, low, cls float64) { + if s.previousClose == .0 { + s.previousClose = cls + return + } + + trueRange := high - low + hc := math.Abs(high - s.previousClose) + lc := math.Abs(low - s.previousClose) + if trueRange < hc { + trueRange = hc + } + if trueRange < lc { + trueRange = lc + } + + s.previousClose = cls + s.slice.Push(trueRange) + s.rma.EmitUpdate(trueRange) } diff --git a/pkg/indicator/rma2.go b/pkg/indicator/rma2.go index 4e48c4bda..f268aec52 100644 --- a/pkg/indicator/rma2.go +++ b/pkg/indicator/rma2.go @@ -1,25 +1,22 @@ package indicator -import ( - "github.com/c9s/bbgo/pkg/types" -) - type RMAStream struct { // embedded structs Float64Series // config fields - types.IntervalWindow Adjust bool + window int counter int sum, previous float64 } -func RMA2(source Float64Source, iw types.IntervalWindow) *RMAStream { +func RMA2(source Float64Source, window int, adjust bool) *RMAStream { s := &RMAStream{ - Float64Series: NewFloat64Series(), - IntervalWindow: iw, + Float64Series: NewFloat64Series(), + window: window, + Adjust: adjust, } if sub, ok := source.(Float64Subscription); ok { @@ -39,7 +36,7 @@ func (s *RMAStream) calculateAndPush(v float64) { } func (s *RMAStream) calculate(x float64) float64 { - lambda := 1 / float64(s.Window) + lambda := 1 / float64(s.window) tmp := 0.0 if s.counter == 0 { s.sum = 1 @@ -54,7 +51,7 @@ func (s *RMAStream) calculate(x float64) float64 { } s.counter++ - if s.counter < s.Window { + if s.counter < s.window { // we can use x, but we need to use 0. to make the same behavior as the result from python pandas_ta s.slice.Push(0) } From 811e624302e46b33fc279f32fbfcdbcce7b38420 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 12:51:44 +0800 Subject: [PATCH 11/18] indicator: simplify and refactor atr2 --- pkg/indicator/atr2.go | 15 ++----- pkg/indicator/atr2_test.go | 82 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 pkg/indicator/atr2_test.go diff --git a/pkg/indicator/atr2.go b/pkg/indicator/atr2.go index af8fd9959..65a214bc2 100644 --- a/pkg/indicator/atr2.go +++ b/pkg/indicator/atr2.go @@ -6,26 +6,20 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +// ATRStream is a RMAStream +// This ATRStream calcualtes the ATR first, and then push it to the RMAStream type ATRStream struct { // embedded struct Float64Series - // parameters - types.IntervalWindow - // private states - rma *RMAStream - - window int previousClose float64 } -func ATR2(source KLineSubscription, window int) *ATRStream { +func ATR2(source KLineSubscription) *ATRStream { s := &ATRStream{ Float64Series: NewFloat64Series(), - window: window, } - s.rma = RMA2(s, window, true) source.AddSubscriber(func(k types.KLine) { s.calculateAndPush(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) @@ -50,6 +44,5 @@ func (s *ATRStream) calculateAndPush(high, low, cls float64) { } s.previousClose = cls - s.slice.Push(trueRange) - s.rma.EmitUpdate(trueRange) + s.EmitUpdate(trueRange) } diff --git a/pkg/indicator/atr2_test.go b/pkg/indicator/atr2_test.go new file mode 100644 index 000000000..6355b476d --- /dev/null +++ b/pkg/indicator/atr2_test.go @@ -0,0 +1,82 @@ +package indicator + +import ( + "encoding/json" + "math" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +/* +python + +import pandas as pd +import pandas_ta as ta + + data = { + "high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0], + "low": [39870.71, 39834.98, 39866.31, 40108.31, 40016.09, 40094.66, 40105.0, 40196.48, 40154.99, 39800.0, 39959.21, 39922.98, 39940.02, 39632.0, 39261.39, 39254.63, 39473.91, 39555.51, 39819.0, 40006.84], + "close": [40105.78, 39935.23, 40183.97, 40182.03, 40212.26, 40149.99, 40378.0, 40618.37, 40401.03, 39990.39, 40179.13, 40097.23, 40014.72, 39667.85, 39303.1, 39519.99, + +39693.79, 39827.96, 40074.94, 40059.84] +} + +high = pd.Series(data['high']) +low = pd.Series(data['low']) +close = pd.Series(data['close']) +result = ta.atr(high, low, close, length=14) +print(result) +*/ +func Test_ATR2(t *testing.T) { + var bytes = []byte(`{ + "high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0], + "low": [39870.71, 39834.98, 39866.31, 40108.31, 40016.09, 40094.66, 40105.0, 40196.48, 40154.99, 39800.0, 39959.21, 39922.98, 39940.02, 39632.0, 39261.39, 39254.63, 39473.91, 39555.51, 39819.0, 40006.84], + "close": [40105.78, 39935.23, 40183.97, 40182.03, 40212.26, 40149.99, 40378.0, 40618.37, 40401.03, 39990.39, 40179.13, 40097.23, 40014.72, 39667.85, 39303.1, 39519.99, 39693.79, 39827.96, 40074.94, 40059.84] + }`) + + var buildKLines = func(bytes []byte) (kLines []types.KLine) { + var prices map[string][]fixedpoint.Value + _ = json.Unmarshal(bytes, &prices) + for i, h := range prices["high"] { + kLine := types.KLine{High: h, Low: prices["low"][i], Close: prices["close"][i]} + kLines = append(kLines, kLine) + } + return kLines + } + + tests := []struct { + name string + kLines []types.KLine + window int + want float64 + }{ + { + name: "test_binance_btcusdt_1h", + kLines: buildKLines(bytes), + window: 14, + want: 367.913903, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stream := &types.StandardStream{} + + kLines := KLines(stream) + atr := ATR2(kLines) + rma := RMA2(atr, tt.window, true) + + for _, k := range tt.kLines { + stream.EmitKLineClosed(k) + } + + got := rma.Last() + diff := math.Trunc((got-tt.want)*100) / 100 + if diff != 0 { + t.Errorf("calculateATR2() = %v, want %v", got, tt.want) + } + }) + } +} From a887eaf54229d442c4bcbc4b6897f1d11e744187 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 13:29:14 +0800 Subject: [PATCH 12/18] indicator: fix the comment --- pkg/indicator/atr2.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/indicator/atr2.go b/pkg/indicator/atr2.go index 65a214bc2..4df00502b 100644 --- a/pkg/indicator/atr2.go +++ b/pkg/indicator/atr2.go @@ -6,8 +6,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -// ATRStream is a RMAStream -// This ATRStream calcualtes the ATR first, and then push it to the RMAStream +// This ATRStream calculates the ATR first type ATRStream struct { // embedded struct Float64Series From ebf9c43cd589b923db1b7fa4f14411649229846e Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 13:46:51 +0800 Subject: [PATCH 13/18] indicator: separate TR + RMA and ATR = TR + RMA --- pkg/indicator/atr2.go | 44 +++----------------- pkg/indicator/atr2_test.go | 7 ++-- pkg/indicator/tr2.go | 47 ++++++++++++++++++++++ pkg/indicator/tr2_test.go | 82 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 43 deletions(-) create mode 100644 pkg/indicator/tr2.go create mode 100644 pkg/indicator/tr2_test.go diff --git a/pkg/indicator/atr2.go b/pkg/indicator/atr2.go index 4df00502b..f0cdc2c12 100644 --- a/pkg/indicator/atr2.go +++ b/pkg/indicator/atr2.go @@ -1,47 +1,13 @@ package indicator -import ( - "math" - - "github.com/c9s/bbgo/pkg/types" -) - -// This ATRStream calculates the ATR first type ATRStream struct { // embedded struct - Float64Series - - // private states - previousClose float64 + *RMAStream } -func ATR2(source KLineSubscription) *ATRStream { - s := &ATRStream{ - Float64Series: NewFloat64Series(), - } - - source.AddSubscriber(func(k types.KLine) { - s.calculateAndPush(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) - }) +func ATR2(source KLineSubscription, window int) *ATRStream { + s := &ATRStream{} + tr := TR2(source) + s.RMAStream = RMA2(tr, window, true) return s } - -func (s *ATRStream) calculateAndPush(high, low, cls float64) { - if s.previousClose == .0 { - s.previousClose = cls - return - } - - trueRange := high - low - hc := math.Abs(high - s.previousClose) - lc := math.Abs(low - s.previousClose) - if trueRange < hc { - trueRange = hc - } - if trueRange < lc { - trueRange = lc - } - - s.previousClose = cls - s.EmitUpdate(trueRange) -} diff --git a/pkg/indicator/atr2_test.go b/pkg/indicator/atr2_test.go index 6355b476d..1dd49501a 100644 --- a/pkg/indicator/atr2_test.go +++ b/pkg/indicator/atr2_test.go @@ -65,17 +65,16 @@ func Test_ATR2(t *testing.T) { stream := &types.StandardStream{} kLines := KLines(stream) - atr := ATR2(kLines) - rma := RMA2(atr, tt.window, true) + atr := ATR2(kLines, tt.window) for _, k := range tt.kLines { stream.EmitKLineClosed(k) } - got := rma.Last() + got := atr.Last() diff := math.Trunc((got-tt.want)*100) / 100 if diff != 0 { - t.Errorf("calculateATR2() = %v, want %v", got, tt.want) + t.Errorf("ATR2() = %v, want %v", got, tt.want) } }) } diff --git a/pkg/indicator/tr2.go b/pkg/indicator/tr2.go new file mode 100644 index 000000000..05c6350e2 --- /dev/null +++ b/pkg/indicator/tr2.go @@ -0,0 +1,47 @@ +package indicator + +import ( + "math" + + "github.com/c9s/bbgo/pkg/types" +) + +// This TRStream calculates the ATR first +type TRStream struct { + // embedded struct + Float64Series + + // private states + previousClose float64 +} + +func TR2(source KLineSubscription) *TRStream { + s := &TRStream{ + Float64Series: NewFloat64Series(), + } + + source.AddSubscriber(func(k types.KLine) { + s.calculateAndPush(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) + }) + return s +} + +func (s *TRStream) calculateAndPush(high, low, cls float64) { + if s.previousClose == .0 { + s.previousClose = cls + return + } + + trueRange := high - low + hc := math.Abs(high - s.previousClose) + lc := math.Abs(low - s.previousClose) + if trueRange < hc { + trueRange = hc + } + if trueRange < lc { + trueRange = lc + } + + s.previousClose = cls + s.EmitUpdate(trueRange) +} diff --git a/pkg/indicator/tr2_test.go b/pkg/indicator/tr2_test.go new file mode 100644 index 000000000..38195f55d --- /dev/null +++ b/pkg/indicator/tr2_test.go @@ -0,0 +1,82 @@ +package indicator + +import ( + "encoding/json" + "math" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +/* +python + +import pandas as pd +import pandas_ta as ta + + data = { + "high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0], + "low": [39870.71, 39834.98, 39866.31, 40108.31, 40016.09, 40094.66, 40105.0, 40196.48, 40154.99, 39800.0, 39959.21, 39922.98, 39940.02, 39632.0, 39261.39, 39254.63, 39473.91, 39555.51, 39819.0, 40006.84], + "close": [40105.78, 39935.23, 40183.97, 40182.03, 40212.26, 40149.99, 40378.0, 40618.37, 40401.03, 39990.39, 40179.13, 40097.23, 40014.72, 39667.85, 39303.1, 39519.99, + +39693.79, 39827.96, 40074.94, 40059.84] +} + +high = pd.Series(data['high']) +low = pd.Series(data['low']) +close = pd.Series(data['close']) +result = ta.atr(high, low, close, length=14) +print(result) +*/ +func Test_TR_and_RMA(t *testing.T) { + var bytes = []byte(`{ + "high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0], + "low": [39870.71, 39834.98, 39866.31, 40108.31, 40016.09, 40094.66, 40105.0, 40196.48, 40154.99, 39800.0, 39959.21, 39922.98, 39940.02, 39632.0, 39261.39, 39254.63, 39473.91, 39555.51, 39819.0, 40006.84], + "close": [40105.78, 39935.23, 40183.97, 40182.03, 40212.26, 40149.99, 40378.0, 40618.37, 40401.03, 39990.39, 40179.13, 40097.23, 40014.72, 39667.85, 39303.1, 39519.99, 39693.79, 39827.96, 40074.94, 40059.84] + }`) + + var buildKLines = func(bytes []byte) (kLines []types.KLine) { + var prices map[string][]fixedpoint.Value + _ = json.Unmarshal(bytes, &prices) + for i, h := range prices["high"] { + kLine := types.KLine{High: h, Low: prices["low"][i], Close: prices["close"][i]} + kLines = append(kLines, kLine) + } + return kLines + } + + tests := []struct { + name string + kLines []types.KLine + window int + want float64 + }{ + { + name: "test_binance_btcusdt_1h", + kLines: buildKLines(bytes), + window: 14, + want: 367.913903, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stream := &types.StandardStream{} + + kLines := KLines(stream) + atr := TR2(kLines) + rma := RMA2(atr, tt.window, true) + + for _, k := range tt.kLines { + stream.EmitKLineClosed(k) + } + + got := rma.Last() + diff := math.Trunc((got-tt.want)*100) / 100 + if diff != 0 { + t.Errorf("RMA(TR()) = %v, want %v", got, tt.want) + } + }) + } +} From 266016a278704a97483b879745fd4d8d84965fca Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 13:53:59 +0800 Subject: [PATCH 14/18] indicator: simplify ATR2 --- pkg/indicator/atr2.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/indicator/atr2.go b/pkg/indicator/atr2.go index f0cdc2c12..fac3e8e06 100644 --- a/pkg/indicator/atr2.go +++ b/pkg/indicator/atr2.go @@ -6,8 +6,7 @@ type ATRStream struct { } func ATR2(source KLineSubscription, window int) *ATRStream { - s := &ATRStream{} tr := TR2(source) - s.RMAStream = RMA2(tr, window, true) - return s + rma := RMA2(tr, window, true) + return &ATRStream{RMAStream: rma} } From ba0102e992dcef68b352c21eb64491f377ca0f8b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 31 May 2023 13:08:21 +0800 Subject: [PATCH 15/18] pivotshort: fix find pivot func call --- pkg/strategy/pivotshort/failedbreakhigh.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/pivotshort/failedbreakhigh.go b/pkg/strategy/pivotshort/failedbreakhigh.go index eb30dc91b..30ad0a32e 100644 --- a/pkg/strategy/pivotshort/failedbreakhigh.go +++ b/pkg/strategy/pivotshort/failedbreakhigh.go @@ -341,7 +341,7 @@ func (s *FailedBreakHigh) detectMacdDivergence() { var histogramPivots floats.Slice for i := pivotWindow; i > 0 && i < len(histogramValues); i++ { // find positive histogram and the top - pivot, ok := floats.CalculatePivot(histogramValues[0:i], pivotWindow, pivotWindow, func(a, pivot float64) bool { + pivot, ok := floats.FindPivot(histogramValues[0:i], pivotWindow, pivotWindow, func(a, pivot float64) bool { return pivot > 0 && pivot > a }) if ok { From e58db43067a342cddaa2c080f718caec0fa20c8b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 31 May 2023 13:08:40 +0800 Subject: [PATCH 16/18] indicator: rename v2 indicators --- pkg/indicator/{macdlegacy.go => macd.go} | 0 .../{macdlegacy_test.go => macd_test.go} | 0 pkg/indicator/{atr2.go => v2_atr.go} | 0 .../{atr2_test.go => v2_atr_test.go} | 0 pkg/indicator/{ewma2.go => v2_ewma.go} | 0 pkg/indicator/{rma2.go => v2_rma.go} | 0 pkg/indicator/v2_rsi.go | 30 +++++++++++++++++++ pkg/indicator/{tr2.go => v2_tr.go} | 0 pkg/indicator/{tr2_test.go => v2_tr_test.go} | 0 9 files changed, 30 insertions(+) rename pkg/indicator/{macdlegacy.go => macd.go} (100%) rename pkg/indicator/{macdlegacy_test.go => macd_test.go} (100%) rename pkg/indicator/{atr2.go => v2_atr.go} (100%) rename pkg/indicator/{atr2_test.go => v2_atr_test.go} (100%) rename pkg/indicator/{ewma2.go => v2_ewma.go} (100%) rename pkg/indicator/{rma2.go => v2_rma.go} (100%) create mode 100644 pkg/indicator/v2_rsi.go rename pkg/indicator/{tr2.go => v2_tr.go} (100%) rename pkg/indicator/{tr2_test.go => v2_tr_test.go} (100%) diff --git a/pkg/indicator/macdlegacy.go b/pkg/indicator/macd.go similarity index 100% rename from pkg/indicator/macdlegacy.go rename to pkg/indicator/macd.go diff --git a/pkg/indicator/macdlegacy_test.go b/pkg/indicator/macd_test.go similarity index 100% rename from pkg/indicator/macdlegacy_test.go rename to pkg/indicator/macd_test.go diff --git a/pkg/indicator/atr2.go b/pkg/indicator/v2_atr.go similarity index 100% rename from pkg/indicator/atr2.go rename to pkg/indicator/v2_atr.go diff --git a/pkg/indicator/atr2_test.go b/pkg/indicator/v2_atr_test.go similarity index 100% rename from pkg/indicator/atr2_test.go rename to pkg/indicator/v2_atr_test.go diff --git a/pkg/indicator/ewma2.go b/pkg/indicator/v2_ewma.go similarity index 100% rename from pkg/indicator/ewma2.go rename to pkg/indicator/v2_ewma.go diff --git a/pkg/indicator/rma2.go b/pkg/indicator/v2_rma.go similarity index 100% rename from pkg/indicator/rma2.go rename to pkg/indicator/v2_rma.go diff --git a/pkg/indicator/v2_rsi.go b/pkg/indicator/v2_rsi.go new file mode 100644 index 000000000..0e3f0a2f6 --- /dev/null +++ b/pkg/indicator/v2_rsi.go @@ -0,0 +1,30 @@ +package indicator + +type RSIStream struct { + // embedded structs + Float64Series + + // config fields + window int + + // private states +} + +func RSI2(source Float64Source, window int) *RSIStream { + s := &RSIStream{ + Float64Series: NewFloat64Series(), + window: window, + } + + if sub, ok := source.(Float64Subscription); ok { + sub.AddSubscriber(s.calculateAndPush) + } else { + source.OnUpdate(s.calculateAndPush) + } + + return s +} + +func (s *RSIStream) calculateAndPush(x float64) { + +} diff --git a/pkg/indicator/tr2.go b/pkg/indicator/v2_tr.go similarity index 100% rename from pkg/indicator/tr2.go rename to pkg/indicator/v2_tr.go diff --git a/pkg/indicator/tr2_test.go b/pkg/indicator/v2_tr_test.go similarity index 100% rename from pkg/indicator/tr2_test.go rename to pkg/indicator/v2_tr_test.go From 114e292d8f3f72b3c7558f9c26c4c4eecd315751 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 31 May 2023 16:30:04 +0800 Subject: [PATCH 17/18] indicator: rewrite RSI indicator --- pkg/indicator/price.go | 8 +++- pkg/indicator/types.go | 17 +++++++ pkg/indicator/util.go | 14 ++++++ pkg/indicator/v2.go | 14 ------ pkg/indicator/v2_rsi.go | 28 +++++++++++- pkg/indicator/v2_rsi_test.go | 87 ++++++++++++++++++++++++++++++++++++ 6 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 pkg/indicator/types.go create mode 100644 pkg/indicator/v2_rsi_test.go diff --git a/pkg/indicator/price.go b/pkg/indicator/price.go index a3c7c45a1..e80736715 100644 --- a/pkg/indicator/price.go +++ b/pkg/indicator/price.go @@ -23,12 +23,16 @@ func Price(source KLineSubscription, mapper KLineValueMapper) *PriceStream { source.AddSubscriber(func(k types.KLine) { v := s.mapper(k) - s.slice.Push(v) - s.EmitUpdate(v) + s.PushAndEmit(v) }) return s } +func (s *PriceStream) PushAndEmit(v float64) { + s.slice.Push(v) + s.EmitUpdate(v) +} + func ClosePrices(source KLineSubscription) *PriceStream { return Price(source, KLineClosePriceMapper) } diff --git a/pkg/indicator/types.go b/pkg/indicator/types.go new file mode 100644 index 000000000..78dcaabd3 --- /dev/null +++ b/pkg/indicator/types.go @@ -0,0 +1,17 @@ +package indicator + +import "github.com/c9s/bbgo/pkg/types" + +type Float64Calculator interface { + Calculate(x float64) float64 +} + +type Float64Source interface { + types.Series + OnUpdate(f func(v float64)) +} + +type Float64Subscription interface { + types.Series + AddSubscriber(f func(v float64)) +} diff --git a/pkg/indicator/util.go b/pkg/indicator/util.go index 722d45a36..f0a76d2b0 100644 --- a/pkg/indicator/util.go +++ b/pkg/indicator/util.go @@ -1 +1,15 @@ package indicator + +func max(x, y int) int { + if x > y { + return x + } + return y +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} diff --git a/pkg/indicator/v2.go b/pkg/indicator/v2.go index b1ef53bd9..80fd68095 100644 --- a/pkg/indicator/v2.go +++ b/pkg/indicator/v2.go @@ -1,9 +1,5 @@ package indicator -import ( - "github.com/c9s/bbgo/pkg/types" -) - /* NEW INDICATOR DESIGN: @@ -21,13 +17,3 @@ macd := Subtract(fastEMA, slowEMA) signal := EMA(macd, 16) histogram := Subtract(macd, signal) */ - -type Float64Source interface { - types.Series - OnUpdate(f func(v float64)) -} - -type Float64Subscription interface { - types.Series - AddSubscriber(f func(v float64)) -} diff --git a/pkg/indicator/v2_rsi.go b/pkg/indicator/v2_rsi.go index 0e3f0a2f6..16732f2bd 100644 --- a/pkg/indicator/v2_rsi.go +++ b/pkg/indicator/v2_rsi.go @@ -8,10 +8,12 @@ type RSIStream struct { window int // private states + source Float64Source } func RSI2(source Float64Source, window int) *RSIStream { s := &RSIStream{ + source: source, Float64Series: NewFloat64Series(), window: window, } @@ -25,6 +27,30 @@ func RSI2(source Float64Source, window int) *RSIStream { return s } -func (s *RSIStream) calculateAndPush(x float64) { +func (s *RSIStream) calculate(_ float64) float64 { + var gainSum, lossSum float64 + var sourceLen = s.source.Length() + var limit = min(s.window, sourceLen) + for i := 0; i < limit; i++ { + value := s.source.Index(i) + prev := s.source.Index(i + 1) + change := value - prev + if change >= 0 { + gainSum += change + } else { + lossSum += -change + } + } + avgGain := gainSum / float64(limit) + avgLoss := lossSum / float64(limit) + rs := avgGain / avgLoss + rsi := 100.0 - (100.0 / (1.0 + rs)) + return rsi +} + +func (s *RSIStream) calculateAndPush(x float64) { + rsi := s.calculate(x) + s.slice.Push(rsi) + s.EmitUpdate(rsi) } diff --git a/pkg/indicator/v2_rsi_test.go b/pkg/indicator/v2_rsi_test.go new file mode 100644 index 000000000..533a89e1a --- /dev/null +++ b/pkg/indicator/v2_rsi_test.go @@ -0,0 +1,87 @@ +package indicator + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/datatype/floats" +) + +func Test_RSI2(t *testing.T) { + // test case from https://school.stockcharts.com/doku.php?id=technical_indicators:relative_strength_index_rsi + 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 []float64 + err := json.Unmarshal(data, &values) + assert.NoError(t, err) + + tests := []struct { + name string + values []float64 + window int + want floats.Slice + }{ + { + name: "RSI", + values: values, + window: 14, + want: floats.Slice{ + 100.000000, + 99.439336, + 99.440090, + 98.251826, + 98.279242, + 98.297781, + 98.307626, + 98.319149, + 98.334036, + 98.342426, + 97.951933, + 97.957908, + 97.108036, + 97.147514, + 70.464135, + 70.020964, + 69.831224, + 80.567686, + 73.333333, + 59.806295, + 62.528217, + 60.000000, + 48.477752, + 53.878407, + 48.952381, + 43.862816, + 37.732919, + 32.263514, + 32.718121, + 38.142620, + 31.748252, + 25.099602, + 30.217670, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // RSI2() + prices := &PriceStream{} + rsi := RSI2(prices, tt.window) + + t.Logf("data length: %d", len(tt.values)) + for _, price := range tt.values { + prices.PushAndEmit(price) + } + + assert.Equal(t, floats.Slice(tt.values), prices.slice) + + if assert.Equal(t, len(tt.want), len(rsi.slice)) { + for i, v := range tt.want { + assert.InDelta(t, v, rsi.slice[i], 0.000001, "Expected rsi.slice[%d] to be %v, but got %v", i, v, rsi.slice[i]) + } + } + }) + } +} From 2a074ba11bae6b619f85d6439e696f8e13b8d55d Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 31 May 2023 16:30:19 +0800 Subject: [PATCH 18/18] floats: add Average method on floats.Slice --- pkg/datatype/floats/slice.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/pkg/datatype/floats/slice.go b/pkg/datatype/floats/slice.go index 727cfe9bb..84e1ff222 100644 --- a/pkg/datatype/floats/slice.go +++ b/pkg/datatype/floats/slice.go @@ -73,7 +73,10 @@ func (s Slice) Add(b Slice) (c Slice) { } func (s Slice) Sum() (sum float64) { - return floats.Sum(s) + for _, v := range s { + sum += v + } + return sum } func (s Slice) Mean() (mean float64) { @@ -97,6 +100,18 @@ func (s Slice) Tail(size int) Slice { return win } +func (s Slice) Average() float64 { + if len(s) == 0 { + return 0.0 + } + + total := 0.0 + for _, value := range s { + total += value + } + return total / float64(len(s)) +} + func (s Slice) Diff() (values Slice) { for i, v := range s { if i == 0 { @@ -171,17 +186,19 @@ func (s Slice) Addr() *Slice { func (s Slice) Last() float64 { length := len(s) if length > 0 { - return (s)[length-1] + return s[length-1] } return 0.0 } +// Index fetches the element from the end of the slice +// WARNING: it does not start from 0!!! func (s Slice) Index(i int) float64 { length := len(s) - if length-i <= 0 || i < 0 { + if i < 0 || length-1-i < 0 { return 0.0 } - return (s)[length-i-1] + return s[length-1-i] } func (s Slice) Length() int {