diff --git a/pkg/indicator/klingeroscillator.go b/pkg/indicator/klingeroscillator.go index 69376f591..198a5d29d 100644 --- a/pkg/indicator/klingeroscillator.go +++ b/pkg/indicator/klingeroscillator.go @@ -1,6 +1,8 @@ package indicator -import "github.com/c9s/bbgo/pkg/types" +import ( + "github.com/c9s/bbgo/pkg/types" +) // Refer: Klinger Oscillator // Refer URL: https://www.investopedia.com/terms/k/klingeroscillator.asp @@ -14,8 +16,8 @@ import "github.com/c9s/bbgo/pkg/types" type KlingerOscillator struct { types.SeriesBase types.IntervalWindow - Fast *EWMA - Slow *EWMA + Fast types.UpdatableSeries + Slow types.UpdatableSeries VF VolumeForce updateCallbacks []func(value float64) @@ -47,9 +49,14 @@ func (inc *KlingerOscillator) Update(high, low, cloze, volume float64) { 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) + + if inc.VF.lastSum > 0 { + inc.VF.Update(high, low, cloze, volume) + inc.Fast.Update(inc.VF.Value) + inc.Slow.Update(inc.VF.Value) + } else { + inc.VF.Update(high, low, cloze, volume) + } } var _ types.SeriesExtend = &KlingerOscillator{} @@ -58,29 +65,8 @@ 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) +func (inc *KlingerOscillator) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK)) } // Utility to hold the state of calculation @@ -93,12 +79,12 @@ type VolumeForce struct { } func (inc *VolumeForce) Update(high, low, cloze, volume float64) { - if inc.Value == 0 { + if inc.lastSum == 0 { inc.dm = high - low inc.cm = inc.dm inc.trend = 1. inc.lastSum = high + low + cloze - inc.Value = volume * 100. + inc.Value = volume // first volume is not calculated return } trend := 1. @@ -114,5 +100,5 @@ func (inc *VolumeForce) Update(high, low, cloze, volume float64) { inc.trend = trend inc.lastSum = high + low + cloze inc.dm = dm - inc.Value = volume * (2.*(inc.dm/inc.cm) - 1.) * trend * 100. + inc.Value = volume * (2.*(inc.dm/inc.cm) - 1.) * trend } diff --git a/pkg/indicator/klingeroscillator_test.go b/pkg/indicator/klingeroscillator_test.go new file mode 100644 index 000000000..9a0c3d3d2 --- /dev/null +++ b/pkg/indicator/klingeroscillator_test.go @@ -0,0 +1,53 @@ +package indicator + +import ( + "encoding/json" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +/* +import pandas as pd +import pandas_ta as ta +high = pd.Series([1.1, 1.3, 1.5, 1.7, 1.9, 2.2, 2.4, 2.1, 1.8, 1.7]) +low = pd.Series([0.9, 1.1, 1.2, 1.5, 1.7, 2.0, 2.2, 1.9, 1.6, 1.5]) +close = pd.Series([1.0, 1.2, 1.4, 1.6, 1.8, 2.1, 2.3, 2.0, 1.7, 1.6]) +vol = pd.Series([300., 200., 200., 150., 150., 200., 200., 150., 300., 350.]) +# kvo = ta.kvo(high, low, close, vol, fast=3, slow=5, signal=1) +# print(kvo) +# # The implementation of kvo in pandas_ta is different from the one defined in investopedia +# # VF is not simply multipying trend +# # Also the value is not multiplied by 100 in pandas_ta +*/ + +func Test_KlingerOscillator(t *testing.T) { + var high, low, cloze, vResult, vol []fixedpoint.Value + if err := json.Unmarshal([]byte(`[1.1, 1.3, 1.5, 1.7, 1.9, 2.2, 2.4, 2.1, 1.8, 1.7]`), &high); err != nil { + panic(err) + } + if err := json.Unmarshal([]byte(`[0.9, 1.1, 1.2, 1.5, 1.7, 2.0, 2.2, 1.9, 1.6, 1.5]`), &low); err != nil { + panic(err) + } + if err := json.Unmarshal([]byte(`[1.0, 1.2, 1.4, 1.6, 1.8, 2.1, 2.3, 2.0, 1.7, 1.6]`), &cloze); err != nil { + panic(err) + } + if err := json.Unmarshal([]byte(`[300.0, 200.0, 200.0, 150.0, 150.0, 200.0, 200.0, 150.0, 300.0, 350.0]`), &vol); err != nil { + panic(err) + } + if err := json.Unmarshal([]byte(`[300.0, 0.0, -28.5, -83, -95, -138, -146.7, 0, 100, 175]`), &vResult); err != nil { + panic(err) + } + + k := KlingerOscillator{ + Fast: &EWMA{IntervalWindow: types.IntervalWindow{Window: 3}}, + Slow: &EWMA{IntervalWindow: types.IntervalWindow{Window: 5}}, + } + var Delta = 0.5 + for i := 0; i < len(high); i++ { + k.Update(high[i].Float64(), low[i].Float64(), cloze[i].Float64(), vol[i].Float64()) + assert.InDelta(t, k.VF.Value, vResult[i].Float64(), Delta) + } +}