From a2a186cfbba2f921c8741a40701e66d78e291a32 Mon Sep 17 00:00:00 2001 From: zenix Date: Tue, 31 May 2022 16:28:38 +0900 Subject: [PATCH] feature: add emv indicator, fix: sma --- pkg/indicator/emv.go | 88 ++++++++++++++++++++++++++++++++++ pkg/indicator/emv_callbacks.go | 15 ++++++ pkg/indicator/emv_test.go | 34 +++++++++++++ pkg/indicator/sma.go | 11 +++-- 4 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 pkg/indicator/emv.go create mode 100644 pkg/indicator/emv_callbacks.go create mode 100644 pkg/indicator/emv_test.go diff --git a/pkg/indicator/emv.go b/pkg/indicator/emv.go new file mode 100644 index 000000000..08d439e45 --- /dev/null +++ b/pkg/indicator/emv.go @@ -0,0 +1,88 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: Ease of Movement +// Refer URL: https://www.investopedia.com/terms/e/easeofmovement.asp + +//go:generate callbackgen -type EMV +type EMV struct { + types.IntervalWindow + prevH float64 + prevL float64 + Values *SMA + EMVScale float64 + + UpdateCallbacks []func(value float64) +} + +const DefaultEMVScale float64 = 100000000. + +func (inc *EMV) Update(high, low, vol float64) { + if inc.EMVScale == 0 { + inc.EMVScale = DefaultEMVScale + } + if inc.prevH == 0 || inc.Values == nil { + inc.prevH = high + inc.prevL = low + inc.Values = &SMA{IntervalWindow: inc.IntervalWindow} + return + } + distanceMoved := (high+low)/2. - (inc.prevH+inc.prevL)/2. + boxRatio := vol / inc.EMVScale / (high - low) + result := distanceMoved / boxRatio + inc.prevH = high + inc.prevL = low + inc.Values.Update(result) +} + +func (inc *EMV) Index(i int) float64 { + if inc.Values == nil { + return 0 + } + return inc.Values.Index(i) +} + +func (inc *EMV) Last() float64 { + if inc.Values == nil { + return 0 + } + return inc.Values.Last() +} + +func (inc *EMV) Length() int { + if inc.Values == nil { + return 0 + } + return inc.Values.Length() +} + +var _ types.Series = &EMV{} + +func (inc *EMV) calculateAndUpdate(allKLines []types.KLine) { + if inc.Values == nil { + for _, k := range allKLines { + inc.Update(k.High.Float64(), k.Low.Float64(), k.Volume.Float64()) + if inc.Length() > 0 { + inc.EmitUpdate(inc.Last()) + } + } + } else { + k := allKLines[len(allKLines)-1] + inc.Update(k.High.Float64(), k.Low.Float64(), k.Volume.Float64()) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *EMV) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + inc.calculateAndUpdate(window) +} + +func (inc *EMV) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/emv_callbacks.go b/pkg/indicator/emv_callbacks.go new file mode 100644 index 000000000..89afd8a99 --- /dev/null +++ b/pkg/indicator/emv_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type EMV"; DO NOT EDIT. + +package indicator + +import () + +func (inc *EMV) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *EMV) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/emv_test.go b/pkg/indicator/emv_test.go new file mode 100644 index 000000000..fd9054f5f --- /dev/null +++ b/pkg/indicator/emv_test.go @@ -0,0 +1,34 @@ +package indicator + +import ( + "testing" + + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +// data from https://school.stockcharts.com/doku.php?id=technical_indicators:ease_of_movement_emv +func Test_EMV(t *testing.T) { + var Delta = 0.01 + emv := &EMV{ + EMVScale: 100000000, + IntervalWindow: types.IntervalWindow{Window: 14}, + } + emv.Update(63.74, 62.63, 32178836) + emv.Update(64.51, 63.85, 36461672) + assert.InDelta(t, 1.8, emv.Values.Cache.Last(), Delta) + emv.Update(64.57, 63.81, 51372680) + emv.Update(64.31, 62.62, 42476356) + emv.Update(63.43, 62.73, 29504176) + emv.Update(62.85, 61.95, 33098600) + emv.Update(62.70, 62.06, 30577960) + emv.Update(63.18, 62.69, 35693928) + emv.Update(62.47, 61.54, 49768136) + emv.Update(64.16, 63.21, 44759968) + emv.Update(64.38, 63.87, 33425504) + emv.Update(64.89, 64.29, 15895085) + emv.Update(65.25, 64.48, 37015388) + emv.Update(64.69, 63.65, 40672116) + emv.Update(64.26, 63.68, 35627200) + assert.InDelta(t, -0.03, emv.Last(), Delta) +} diff --git a/pkg/indicator/sma.go b/pkg/indicator/sma.go index d726cd209..d500c5d6f 100644 --- a/pkg/indicator/sma.go +++ b/pkg/indicator/sma.go @@ -18,6 +18,7 @@ var zeroTime time.Time type SMA struct { types.IntervalWindow Values types.Float64Slice + Cache types.Float64Slice EndTime time.Time UpdateCallbacks []func(value float64) @@ -46,11 +47,15 @@ func (inc *SMA) Length() int { var _ types.Series = &SMA{} func (inc *SMA) Update(value float64) { - length := len(inc.Values) - if length == 0 { - inc.Values = append(inc.Values, value) + if len(inc.Cache) < inc.Window { + inc.Cache = append(inc.Cache, value) + if len(inc.Cache) == inc.Window { + inc.Values = append(inc.Values, types.Mean(&inc.Cache)) + } return + } + length := len(inc.Values) newVal := (inc.Values[length-1]*float64(inc.Window-1) + value) / float64(inc.Window) inc.Values = append(inc.Values, newVal) }