From f4c4d631f8702996c043d36d8d80d3ad2e0865d8 Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 15 Jun 2022 20:09:33 +0900 Subject: [PATCH 1/2] feature: add Ehler's Super smoother filter --- pkg/indicator/ssf.go | 114 +++++++++++++++++++++++++++++++++ pkg/indicator/ssf_callbacks.go | 15 +++++ pkg/indicator/ssf_test.go | 71 ++++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 pkg/indicator/ssf.go create mode 100644 pkg/indicator/ssf_callbacks.go create mode 100644 pkg/indicator/ssf_test.go diff --git a/pkg/indicator/ssf.go b/pkg/indicator/ssf.go new file mode 100644 index 000000000..76450e1d7 --- /dev/null +++ b/pkg/indicator/ssf.go @@ -0,0 +1,114 @@ +package indicator + +import ( + "math" + + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: https://easylanguagemastery.com/indicators/predictive-indicators/ +// Refer: https://github.com/twopirllc/pandas-ta/blob/main/pandas_ta/overlap/ssf.py +// Ehler's Super Smoother Filter +// +// John F. Ehlers's solution to reduce lag and remove aliasing noise with his +// research in aerospace analog filter design. This indicator comes with two +// versions determined by the keyword poles. By default, it uses two poles but +// there is an option for three poles. Since SSF is a (Resursive) Digital Filter, +// the number of poles determine how many prior recursive SSF bars to include in +// the design of the filter. So two poles uses two prior SSF bars and three poles +// uses three prior SSF bars for their filter calculations. +// +//go:generate callbackgen -type SSF +type SSF struct { + types.IntervalWindow + Poles int + c1 float64 + c2 float64 + c3 float64 + c4 float64 + Values types.Float64Slice + + UpdateCallbacks []func(value float64) +} + +func (inc *SSF) Update(value float64) { + if inc.Poles == 3 { + if inc.Values == nil { + x := math.Pi / float64(inc.Window) + a0 := math.Exp(-x) + b0 := 2. * a0 * math.Cos(math.Sqrt(3.)*x) + c0 := a0 * a0 + + inc.c4 = c0 * c0 + inc.c3 = -c0 * (1. + b0) + inc.c2 = c0 + b0 + inc.c1 = 1. - inc.c2 - inc.c3 - inc.c4 + inc.Values = types.Float64Slice{} + } + + result := inc.c1*value + + inc.c2*inc.Values.Index(0) + + inc.c3*inc.Values.Index(1) + + inc.c4*inc.Values.Index(2) + inc.Values.Push(result) + } else { // poles == 2 + if inc.Values == nil { + x := math.Pi * math.Sqrt(2.) / float64(inc.Window) + a0 := math.Exp(-x) + inc.c3 = -a0 * a0 + inc.c2 = 2. * a0 * math.Cos(x) + inc.c1 = 1. - inc.c2 - inc.c3 + inc.Values = types.Float64Slice{} + } + result := inc.c1*value + + inc.c2*inc.Values.Index(0) + + inc.c3*inc.Values.Index(1) + inc.Values.Push(result) + } +} + +func (inc *SSF) Index(i int) float64 { + if inc.Values == nil { + return 0.0 + } + return inc.Values.Index(i) +} + +func (inc *SSF) Length() int { + if inc.Values == nil { + return 0 + } + return inc.Values.Length() +} + +func (inc *SSF) Last() float64 { + if inc.Values == nil { + return 0.0 + } + return inc.Values.Last() +} + +var _ types.Series = &SSF{} + +func (inc *SSF) calculateAndUpdate(allKLines []types.KLine) { + if inc.Values == nil { + for _, k := range allKLines { + inc.Update(k.Close.Float64()) + inc.EmitUpdate(inc.Last()) + } + } else { + inc.Update(allKLines[len(allKLines)-1].Close.Float64()) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *SSF) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + inc.calculateAndUpdate(window) +} + +func (inc *SSF) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/ssf_callbacks.go b/pkg/indicator/ssf_callbacks.go new file mode 100644 index 000000000..cdd2e8aca --- /dev/null +++ b/pkg/indicator/ssf_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type SSF"; DO NOT EDIT. + +package indicator + +import () + +func (inc *SSF) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *SSF) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/ssf_test.go b/pkg/indicator/ssf_test.go new file mode 100644 index 000000000..0eced9ca9 --- /dev/null +++ b/pkg/indicator/ssf_test.go @@ -0,0 +1,71 @@ +package indicator + +import ( + "encoding/json" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +/* +python: + +import pandas as pd +import pandas_ta as ta + +data = pd.Series([0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9]) +size = 5 + +result = ta.ssf(data, size, 2) +print(result) + +result = ta.ssf(data, size, 3) +print(result) +*/ +func Test_SSF(t *testing.T) { + var Delta = 0.00001 + var randomPrices = []byte(`[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]`) + var input []fixedpoint.Value + if err := json.Unmarshal(randomPrices, &input); err != nil { + panic(err) + } + tests := []struct { + name string + kLines []types.KLine + poles int + want float64 + next float64 + all int + }{ + { + name: "pole2", + kLines: buildKLines(input), + poles: 2, + want: 8.721776, + next: 7.723223, + all: 30, + }, + { + name: "pole3", + kLines: buildKLines(input), + poles: 3, + want: 8.687588, + next: 7.668013, + all: 30, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ssf := SSF{ + IntervalWindow: types.IntervalWindow{Window: 5}, + Poles: tt.poles, + } + ssf.calculateAndUpdate(tt.kLines) + assert.InDelta(t, tt.want, ssf.Last(), Delta) + assert.InDelta(t, tt.next, ssf.Index(1), Delta) + assert.Equal(t, tt.all, ssf.Length()) + }) + } +} From 0377ecd42d5ab915039288f56a1fc650dd2732f7 Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 16 Jun 2022 13:02:00 +0900 Subject: [PATCH 2/2] fix: ssf less indent --- pkg/indicator/ssf.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/indicator/ssf.go b/pkg/indicator/ssf.go index 76450e1d7..d8c1340d4 100644 --- a/pkg/indicator/ssf.go +++ b/pkg/indicator/ssf.go @@ -91,14 +91,14 @@ func (inc *SSF) Last() float64 { var _ types.Series = &SSF{} func (inc *SSF) calculateAndUpdate(allKLines []types.KLine) { - if inc.Values == nil { - for _, k := range allKLines { - inc.Update(k.Close.Float64()) - inc.EmitUpdate(inc.Last()) - } - } else { + if inc.Values != nil { inc.Update(allKLines[len(allKLines)-1].Close.Float64()) inc.EmitUpdate(inc.Last()) + return + } + for _, k := range allKLines { + inc.Update(k.Close.Float64()) + inc.EmitUpdate(inc.Last()) } }