From 22d8c2efffdd1e5a6d15679f4ce1a1c4babd65a4 Mon Sep 17 00:00:00 2001 From: zenix Date: Tue, 19 Apr 2022 19:22:22 +0900 Subject: [PATCH] feature: add some new ma indicators --- pkg/indicator/dema.go | 68 +++++++++++++++++++++++++ pkg/indicator/dema_callbacks.go | 15 ++++++ pkg/indicator/ewma.go | 26 ---------- pkg/indicator/hull.go | 73 +++++++++++++++++++++++++++ pkg/indicator/hull_callbacks.go | 15 ++++++ pkg/indicator/line.go | 4 +- pkg/indicator/tema.go | 73 +++++++++++++++++++++++++++ pkg/indicator/tema_callbacks.go | 15 ++++++ pkg/indicator/till.go | 1 + pkg/indicator/tsf.go | 1 + pkg/indicator/util.go | 29 +++++++++++ pkg/indicator/vidya.go | 1 + pkg/indicator/wwma.go | 87 ++++++++++++++++++++++++++++++++ pkg/indicator/wwma_callbacks.go | 15 ++++++ pkg/indicator/zlema.go | 64 +++++++++++++++++++++++ pkg/indicator/zlema_callbacks.go | 15 ++++++ 16 files changed, 474 insertions(+), 28 deletions(-) create mode 100644 pkg/indicator/dema.go create mode 100644 pkg/indicator/dema_callbacks.go create mode 100644 pkg/indicator/hull.go create mode 100644 pkg/indicator/hull_callbacks.go create mode 100644 pkg/indicator/tema.go create mode 100644 pkg/indicator/tema_callbacks.go create mode 100644 pkg/indicator/till.go create mode 100644 pkg/indicator/tsf.go create mode 100644 pkg/indicator/util.go create mode 100644 pkg/indicator/vidya.go create mode 100644 pkg/indicator/wwma.go create mode 100644 pkg/indicator/wwma_callbacks.go create mode 100644 pkg/indicator/zlema.go create mode 100644 pkg/indicator/zlema_callbacks.go diff --git a/pkg/indicator/dema.go b/pkg/indicator/dema.go new file mode 100644 index 000000000..17dd388e0 --- /dev/null +++ b/pkg/indicator/dema.go @@ -0,0 +1,68 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: Double Exponential Moving Average +// Refer URL: https://investopedia.com/terms/d/double-exponential-moving-average.asp + +//go:generate callbackgen -type DEMA +type DEMA struct { + types.IntervalWindow + Values types.Float64Slice + a1 *EWMA + a2 *EWMA + + UpdateCallbacks []func(value float64) +} + +func (inc *DEMA) Update(value float64) { + if len(inc.Values) == 0 { + inc.a1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} + inc.a2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} + } + + inc.a1.Update(value) + inc.a2.Update(inc.a1.Last()) + inc.Values.Push(2*inc.a1.Last() - inc.a2.Last()) + if len(inc.Values) > MaxNumOfEWMA { + inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:] + } +} + +func (inc *DEMA) Last() float64 { + return inc.Values.Last() +} + +func (inc *DEMA) Index(i int) float64 { + if len(inc.Values)-i-1 >= 0 { + return inc.Values[len(inc.Values)-1-i] + } + return 0 +} + +func (inc *DEMA) Length() int { + return len(inc.Values) +} + +var _ types.Series = &DEMA{} + +func (inc *DEMA) calculateAndUpdate(allKLines []types.KLine) { + for _, k := range allKLines { + inc.Update(k.Close.Float64()) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *DEMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.calculateAndUpdate(window) +} + +func (inc *DEMA) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/dema_callbacks.go b/pkg/indicator/dema_callbacks.go new file mode 100644 index 000000000..e7c4f6676 --- /dev/null +++ b/pkg/indicator/dema_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type DEMA"; DO NOT EDIT. + +package indicator + +import () + +func (inc *DEMA) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *DEMA) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/ewma.go b/pkg/indicator/ewma.go index cf5023451..d94fb7953 100644 --- a/pkg/indicator/ewma.go +++ b/pkg/indicator/ewma.go @@ -124,32 +124,6 @@ func ewma(prices []float64, multiplier float64) float64 { return prices[end]*multiplier + (1-multiplier)*ewma(prices[:end], multiplier) } -type KLinePriceMapper func(k types.KLine) float64 - -func KLineOpenPriceMapper(k types.KLine) float64 { - return k.Open.Float64() -} - -func KLineClosePriceMapper(k types.KLine) float64 { - return k.Close.Float64() -} - -func KLineTypicalPriceMapper(k types.KLine) float64 { - return (k.High.Float64() + k.Low.Float64() + k.Close.Float64()) / 3. -} - -func MapKLinePrice(kLines []types.KLine, f KLinePriceMapper) (prices []float64) { - for _, k := range kLines { - prices = append(prices, f(k)) - } - - return prices -} - -type KLineWindowUpdater interface { - OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow)) -} - func (inc *EWMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { if inc.Interval != interval { return diff --git a/pkg/indicator/hull.go b/pkg/indicator/hull.go new file mode 100644 index 000000000..bb7e3f2c0 --- /dev/null +++ b/pkg/indicator/hull.go @@ -0,0 +1,73 @@ +package indicator + +import ( + "math" + + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: Hull Moving Average +// Refer URL: https://fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/hull-moving-average +//go:generate callbackgen -type HULL +type HULL struct { + types.IntervalWindow + ma1 *EWMA + ma2 *EWMA + result *EWMA + + UpdateCallbacks []func(value float64) +} + +func (inc *HULL) Update(value float64) { + if inc.result.Length() == 0 { + inc.ma1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window/2}} + inc.ma2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} + inc.result = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, int(math.Sqrt(float64(inc.Window)))}} + } + inc.ma1.Update(value) + inc.ma2.Update(value) + inc.result.Update(2 * inc.ma1.Last() - inc.ma2.Last()) +} + +func (inc *HULL) Last() float64 { + return inc.result.Last() +} + +func (inc *HULL) Index(i int) float64 { + return inc.result.Index(i) +} + +func (inc *HULL) Length() int { + return inc.result.Length() +} + +var _ types.Series = &HULL{} + +// TODO: should we just ignore the possible overlapping? +func (inc *HULL) calculateAndUpdate(allKLines []types.KLine) { + doable := false + if inc.ma1.Length() == 0 { + doable = true + } + for _, k := range allKLines { + if !doable && k.StartTime.After(inc.ma1.LastOpenTime) { + doable = true + } + if doable { + inc.Update(k.Close.Float64()) + inc.EmitUpdate(inc.Last()) + } + } +} + +func (inc *HULL) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.calculateAndUpdate(window) +} + +func (inc *HULL) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/hull_callbacks.go b/pkg/indicator/hull_callbacks.go new file mode 100644 index 000000000..aa95c8dd9 --- /dev/null +++ b/pkg/indicator/hull_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type HULL"; DO NOT EDIT. + +package indicator + +import () + +func (inc *HULL) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *HULL) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/line.go b/pkg/indicator/line.go index 5c509ab79..763d58f89 100644 --- a/pkg/indicator/line.go +++ b/pkg/indicator/line.go @@ -37,11 +37,11 @@ func (l *Line) Bind(updater KLineWindowUpdater) { } func (l *Line) Last() float64 { - return (l.end-l.start) / float64(l.startIndex - l.endIndex) * float64(l.endIndex) + l.end + return (l.end-l.start)/float64(l.startIndex-l.endIndex)*float64(l.endIndex) + l.end } func (l *Line) Index(i int) float64 { - return (l.end-l.start) / float64(l.startIndex - l.endIndex) * float64(l.endIndex - i) + l.end + return (l.end-l.start)/float64(l.startIndex-l.endIndex)*float64(l.endIndex-i) + l.end } func (l *Line) Length() int { diff --git a/pkg/indicator/tema.go b/pkg/indicator/tema.go new file mode 100644 index 000000000..d93ca00b5 --- /dev/null +++ b/pkg/indicator/tema.go @@ -0,0 +1,73 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: Triple Exponential Moving Average (TEMA) +// URL: https://investopedia.com/terms/t/triple-exponential-moving-average.asp + +//go:generate callbackgen -type TEMA +type TEMA struct { + types.IntervalWindow + Values types.Float64Slice + A1 *EWMA + A2 *EWMA + A3 *EWMA + + UpdateCallbacks []func(value float64) +} + +func (inc *TEMA) Update(value float64) { + if len(inc.Values) == 0 { + inc.A1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} + inc.A2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} + inc.A3 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} + } + inc.A1.Update(value) + a1 := inc.A1.Last() + inc.A2.Update(a1) + a2 := inc.A2.Last() + inc.A3.Update(a2) + a3 := inc.A3.Last() + inc.Values.Push(3*a1 - 3*a2 + a3) +} + +func (inc *TEMA) Last() float64 { + if len(inc.Values) > 0 { + return inc.Values[len(inc.Values)-1] + } + return 0.0 +} + +func (inc *TEMA) Index(i int) float64 { + if i >= len(inc.Values) { + return 0 + } + return inc.Values[len(inc.Values)-i-1] +} + +func (inc *TEMA) Length() int { + return len(inc.Values) +} + +var _ types.Series = &TEMA{} + +func (inc *TEMA) calculateAndUpdate(allKLines []types.KLine) { + for _, k := range allKLines { + inc.Update(k.Close.Float64()) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *TEMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.calculateAndUpdate(window) +} + +func (inc *TEMA) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/tema_callbacks.go b/pkg/indicator/tema_callbacks.go new file mode 100644 index 000000000..ed63757ba --- /dev/null +++ b/pkg/indicator/tema_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type TEMA"; DO NOT EDIT. + +package indicator + +import () + +func (inc *TEMA) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *TEMA) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/till.go b/pkg/indicator/till.go new file mode 100644 index 000000000..722d45a36 --- /dev/null +++ b/pkg/indicator/till.go @@ -0,0 +1 @@ +package indicator diff --git a/pkg/indicator/tsf.go b/pkg/indicator/tsf.go new file mode 100644 index 000000000..722d45a36 --- /dev/null +++ b/pkg/indicator/tsf.go @@ -0,0 +1 @@ +package indicator diff --git a/pkg/indicator/util.go b/pkg/indicator/util.go new file mode 100644 index 000000000..05f4c6a69 --- /dev/null +++ b/pkg/indicator/util.go @@ -0,0 +1,29 @@ +package indicator + +import "github.com/c9s/bbgo/pkg/types" + +type KLinePriceMapper func(k types.KLine) float64 + +func KLineOpenPriceMapper(k types.KLine) float64 { + return k.Open.Float64() +} + +func KLineClosePriceMapper(k types.KLine) float64 { + return k.Close.Float64() +} + +func KLineTypicalPriceMapper(k types.KLine) float64 { + return (k.High.Float64() + k.Low.Float64() + k.Close.Float64()) / 3. +} + +func MapKLinePrice(kLines []types.KLine, f KLinePriceMapper) (prices []float64) { + for _, k := range kLines { + prices = append(prices, f(k)) + } + + return prices +} + +type KLineWindowUpdater interface { + OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow)) +} diff --git a/pkg/indicator/vidya.go b/pkg/indicator/vidya.go new file mode 100644 index 000000000..722d45a36 --- /dev/null +++ b/pkg/indicator/vidya.go @@ -0,0 +1 @@ +package indicator diff --git a/pkg/indicator/wwma.go b/pkg/indicator/wwma.go new file mode 100644 index 000000000..022da3ed6 --- /dev/null +++ b/pkg/indicator/wwma.go @@ -0,0 +1,87 @@ +package indicator + +import ( + "time" + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: Welles Wilder's Moving Average +// Refer URL: http://fxcorporate.com/help/MS/NOTFIFO/i_WMA.html + +const MaxNumOfWWMA = 5_000 +const MaxNumOfWWMATruncateSize = 100 + +//go:generate callbackgen -type WWMA +type WWMA struct { + types.IntervalWindow + Values types.Float64Slice + LastOpenTime time.Time + + UpdateCallbacks []func(value float64) +} + +func (inc *WWMA) Update(value float64) { + if len(inc.Values) == 0 { + inc.Values.Push(value) + return + } else if len(inc.Values) > MaxNumOfWWMA { + inc.Values = inc.Values[MaxNumOfWWMATruncateSize-1:] + } + + last := inc.Last() + wma := last + (value-last)/float64(inc.Window) + inc.Values.Push(wma) +} + +func (inc *WWMA) Last() float64 { + if len(inc.Values) == 0 { + return 0 + } + + return inc.Values[len(inc.Values)-1] +} + +func (inc *WWMA) Index(i int) float64 { + if i >= len(inc.Values) { + return 0 + } + + return inc.Values[len(inc.Values)-1-i] +} + +func (inc *WWMA) Length() int { + return len(inc.Values) +} + +func (inc *WWMA) calculateAndUpdate(allKLines []types.KLine) { + if len(allKLines) < inc.Window { + // we can't calculate + return + } + + doable := false + for _, k := range allKLines { + if !doable && k.StartTime.After(inc.LastOpenTime) { + doable = true + } + if doable { + inc.Update(k.Close.Float64()) + inc.LastOpenTime = k.StartTime.Time() + inc.EmitUpdate(inc.Last()) + } + } +} + +func (inc *WWMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.calculateAndUpdate(window) +} + +func (inc *WWMA) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +var _ types.Series = &WWMA{} diff --git a/pkg/indicator/wwma_callbacks.go b/pkg/indicator/wwma_callbacks.go new file mode 100644 index 000000000..2c5f57b18 --- /dev/null +++ b/pkg/indicator/wwma_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type WWMA"; DO NOT EDIT. + +package indicator + +import () + +func (inc *WWMA) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *WWMA) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/zlema.go b/pkg/indicator/zlema.go new file mode 100644 index 000000000..1a45a2d45 --- /dev/null +++ b/pkg/indicator/zlema.go @@ -0,0 +1,64 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: Zero Lag Exponential Moving Average +// Refer URL: https://en.wikipedia.org/wiki/Zero_lag_exponential_moving_average + +//go:generate callbackgen -type ZLEMA +type ZLEMA struct { + types.IntervalWindow + + data *EWMA + zlema *EWMA + lag int + + UpdateCallbacks []func(value float64) +} + +func (inc *ZLEMA) Index(i int) float64 { + return inc.zlema.Index(i) +} + +func (inc *ZLEMA) Last() float64 { + return inc.zlema.Last() +} + +func (inc *ZLEMA) Length() int { + return inc.zlema.Length() +} + +func (inc *ZLEMA) Update(value float64) { + if inc.lag == 0 || inc.zlema == nil { + inc.data = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} + inc.zlema = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} + inc.lag = (inc.Window - 1) / 2 + } + inc.data.Update(value) + data := inc.data.Last() + emaData := 2*data - inc.data.Index(inc.lag) + inc.zlema.Update(emaData) +} + +var _ types.Series = &ZLEMA{} + +func (inc *ZLEMA) calculateAndUpdate(allKLines []types.KLine) { + for _, k := range allKLines { + inc.Update(k.Close.Float64()) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *ZLEMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.calculateAndUpdate(window) +} + +func (inc *ZLEMA) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/zlema_callbacks.go b/pkg/indicator/zlema_callbacks.go new file mode 100644 index 000000000..d70147699 --- /dev/null +++ b/pkg/indicator/zlema_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type ZLEMA"; DO NOT EDIT. + +package indicator + +import () + +func (inc *ZLEMA) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *ZLEMA) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +}