From 38461167badeeae0bde8a75f30425751e84df6a4 Mon Sep 17 00:00:00 2001 From: zenix Date: Fri, 9 Dec 2022 17:52:42 +0900 Subject: [PATCH] feature: add tsi and klinger oscillator, fix wdrift div 0 issue --- pkg/indicator/klingeroscillator.go | 118 +++++++++++++++++++ pkg/indicator/klingeroscillator_callbacks.go | 15 +++ pkg/indicator/tsi.go | 107 +++++++++++++++++ pkg/indicator/tsi_callbacks.go | 15 +++ pkg/indicator/tsi_test.go | 1 + pkg/indicator/wdrift.go | 10 +- 6 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 pkg/indicator/klingeroscillator.go create mode 100644 pkg/indicator/klingeroscillator_callbacks.go create mode 100644 pkg/indicator/tsi.go create mode 100644 pkg/indicator/tsi_callbacks.go create mode 100644 pkg/indicator/tsi_test.go diff --git a/pkg/indicator/klingeroscillator.go b/pkg/indicator/klingeroscillator.go new file mode 100644 index 000000000..69376f591 --- /dev/null +++ b/pkg/indicator/klingeroscillator.go @@ -0,0 +1,118 @@ +package indicator + +import "github.com/c9s/bbgo/pkg/types" + +// Refer: Klinger Oscillator +// Refer URL: https://www.investopedia.com/terms/k/klingeroscillator.asp +// Explanation: +// The Klinger Oscillator is a technical indicator that was developed by Stephen Klinger. +// It is based on the assumption that there is a relationship between money flow and price movement in the stock market. +// The Klinger Oscillator is calculated by taking the difference between a 34-period and 55-period moving average. +// Usually the indicator is using together with a 9-period or 13-period of moving average as the signal line. +// This indicator is often used to identify potential turning points in the market, as well as to confirm the strength of a trend. +//go:generate callbackgen -type KlingerOscillator +type KlingerOscillator struct { + types.SeriesBase + types.IntervalWindow + Fast *EWMA + Slow *EWMA + VF VolumeForce + + updateCallbacks []func(value float64) +} + +func (inc *KlingerOscillator) Length() int { + if inc.Fast == nil || inc.Slow == nil { + return 0 + } + return inc.Fast.Length() +} + +func (inc *KlingerOscillator) Last() float64 { + if inc.Fast == nil || inc.Slow == nil { + return 0 + } + return inc.Fast.Last() - inc.Slow.Last() +} +func (inc *KlingerOscillator) Index(i int) float64 { + if inc.Fast == nil || inc.Slow == nil { + return 0 + } + return inc.Fast.Index(i) - inc.Slow.Index(i) +} + +func (inc *KlingerOscillator) Update(high, low, cloze, volume float64) { + if inc.Fast == nil { + inc.SeriesBase.Series = inc + inc.Fast = &EWMA{IntervalWindow: types.IntervalWindow{Window: 34, Interval: inc.Interval}} + inc.Slow = &EWMA{IntervalWindow: types.IntervalWindow{Window: 55, Interval: inc.Interval}} + } + inc.VF.Update(high, low, cloze, volume) + inc.Fast.Update(inc.VF.Value) + inc.Slow.Update(inc.VF.Value) +} + +var _ types.SeriesExtend = &KlingerOscillator{} + +func (inc *KlingerOscillator) PushK(k types.KLine) { + inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64(), k.Volume.Float64()) +} + +func (inc *KlingerOscillator) CalculateAndUpdate(allKLines []types.KLine) { + if inc.Fast == nil { + for _, k := range allKLines { + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } + } else { + k := allKLines[len(allKLines)-1] + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *KlingerOscillator) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *KlingerOscillator) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +// Utility to hold the state of calculation +type VolumeForce struct { + dm float64 + cm float64 + trend float64 + lastSum float64 + Value float64 +} + +func (inc *VolumeForce) Update(high, low, cloze, volume float64) { + if inc.Value == 0 { + inc.dm = high - low + inc.cm = inc.dm + inc.trend = 1. + inc.lastSum = high + low + cloze + inc.Value = volume * 100. + return + } + trend := 1. + if high+low+cloze <= inc.lastSum { + trend = -1. + } + dm := high - low + if inc.trend == trend { + inc.cm = inc.cm + dm + } else { + inc.cm = inc.dm + dm + } + inc.trend = trend + inc.lastSum = high + low + cloze + inc.dm = dm + inc.Value = volume * (2.*(inc.dm/inc.cm) - 1.) * trend * 100. +} diff --git a/pkg/indicator/klingeroscillator_callbacks.go b/pkg/indicator/klingeroscillator_callbacks.go new file mode 100644 index 000000000..3f03f97ea --- /dev/null +++ b/pkg/indicator/klingeroscillator_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type KlingerOscillator"; DO NOT EDIT. + +package indicator + +import () + +func (inc *KlingerOscillator) OnUpdate(cb func(value float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *KlingerOscillator) EmitUpdate(value float64) { + for _, cb := range inc.updateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/tsi.go b/pkg/indicator/tsi.go new file mode 100644 index 000000000..dd62945c1 --- /dev/null +++ b/pkg/indicator/tsi.go @@ -0,0 +1,107 @@ +package indicator + +import ( + "math" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: True Strength Index +// Refer URL: https://www.investopedia.com/terms/t/tsi.asp +//go:generate callbackgen -type TSI +type TSI struct { + types.SeriesBase + types.IntervalWindow + PrevValue float64 + Values floats.Slice + Pcs *EWMA + Pcds *EWMA + Apcs *EWMA + Apcds *EWMA + updateCallbacks []func(value float64) +} + +func (inc *TSI) Update(value float64) { + if inc.Pcs == nil { + inc.Pcs = &EWMA{ + IntervalWindow: types.IntervalWindow{ + Window: 25, + Interval: inc.Interval, + }, + } + inc.Pcds = &EWMA{ + IntervalWindow: types.IntervalWindow{ + Window: 13, + Interval: inc.Interval, + }, + } + inc.Apcs = &EWMA{ + IntervalWindow: types.IntervalWindow{ + Window: 25, + Interval: inc.Interval, + }, + } + inc.Apcds = &EWMA{ + IntervalWindow: types.IntervalWindow{ + Window: 13, + Interval: inc.Interval, + }, + } + inc.SeriesBase.Series = inc + } + if inc.PrevValue == 0 { + inc.PrevValue = value + return + } + pc := value - inc.PrevValue + inc.Pcs.Update(pc) + inc.Pcds.Update(inc.Pcs.Last()) + apc := math.Abs(pc) + inc.Apcs.Update(apc) + inc.Apcds.Update(inc.Apcs.Last()) + tsi := (inc.Pcds.Last() / inc.Apcds.Last()) * 100. + inc.Values.Push(tsi) + if inc.Values.Length() > MaxNumOfEWMA { + inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:] + } + inc.PrevValue = value +} + +func (inc *TSI) Last() float64 { + return inc.Values.Last() +} + +func (inc *TSI) Index(i int) float64 { + return inc.Values.Index(i) +} + +func (inc *TSI) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} + +var _ types.SeriesExtend = &TSI{} + +func (inc *TSI) CalculateAndUpdate(allKLines []types.KLine) { + if inc.PrevValue == 0 { + for _, k := range allKLines { + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } + } else { + k := allKLines[len(allKLines)-1] + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *TSI) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + inc.CalculateAndUpdate(window) +} + +func (inc *TSI) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/tsi_callbacks.go b/pkg/indicator/tsi_callbacks.go new file mode 100644 index 000000000..4c5155f9e --- /dev/null +++ b/pkg/indicator/tsi_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type TSI"; DO NOT EDIT. + +package indicator + +import () + +func (inc *TSI) OnUpdate(cb func(value float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *TSI) EmitUpdate(value float64) { + for _, cb := range inc.updateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/tsi_test.go b/pkg/indicator/tsi_test.go new file mode 100644 index 000000000..722d45a36 --- /dev/null +++ b/pkg/indicator/tsi_test.go @@ -0,0 +1 @@ +package indicator diff --git a/pkg/indicator/wdrift.go b/pkg/indicator/wdrift.go index e6150ee40..61060a747 100644 --- a/pkg/indicator/wdrift.go +++ b/pkg/indicator/wdrift.go @@ -23,23 +23,23 @@ type WeightedDrift struct { } func (inc *WeightedDrift) Update(value float64, weight float64) { - win := 10 - if inc.Window > win { - win = inc.Window + if weight == 0 { + inc.LastValue = value + return } if inc.chng == nil { inc.SeriesBase.Series = inc if inc.MA == nil { inc.MA = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window}} } - inc.Weight = types.NewQueue(win) + inc.Weight = types.NewQueue(inc.Window) inc.chng = types.NewQueue(inc.Window) inc.LastValue = value inc.Weight.Update(weight) return } inc.Weight.Update(weight) - base := inc.Weight.Lowest(win) + base := inc.Weight.Lowest(inc.Window) multiplier := int(weight / base) var chng float64 if value == 0 {