From 2311fbd95c143edecaa746c3066ab59e8d70164a Mon Sep 17 00:00:00 2001 From: zenix Date: Mon, 9 May 2022 19:55:14 +0900 Subject: [PATCH 1/2] feature: add cci indicator --- pkg/indicator/cci.go | 102 +++++++++++++++++++++++++++++++++ pkg/indicator/cci_callbacks.go | 15 +++++ 2 files changed, 117 insertions(+) create mode 100644 pkg/indicator/cci.go create mode 100644 pkg/indicator/cci_callbacks.go diff --git a/pkg/indicator/cci.go b/pkg/indicator/cci.go new file mode 100644 index 000000000..5e8acb9da --- /dev/null +++ b/pkg/indicator/cci.go @@ -0,0 +1,102 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: Commodity Channel Index +// Refer URL: https://www.investopedia.com/terms/c/commoditychannelindex.asp +//go:generate callbackgen -type CCI +type CCI struct { + types.IntervalWindow + HLC3 types.Float64Slice + TypicalPrice types.Float64Slice + MA types.Float64Slice + Values types.Float64Slice + + UpdateCallbacks []func(value float64) +} + +func (inc *CCI) Update(high, low, cloze float64) { + if len(inc.TypicalPrice) == 0 { + inc.HLC3.Push((high + low + cloze) / 3.) + inc.TypicalPrice.Push(inc.HLC3.Last()) + return + } else if len(inc.TypicalPrice) > MaxNumOfEWMA { + inc.TypicalPrice = inc.TypicalPrice[MaxNumOfEWMATruncateSize-1:] + inc.HLC3 = inc.HLC3[MaxNumOfEWMATruncateSize-1:] + } + + hlc3 := (high + low + cloze) / 3. + tp := inc.TypicalPrice.Last() - inc.HLC3.Index(inc.Window) + hlc3 + inc.TypicalPrice.Push(tp) + if len(inc.TypicalPrice) < inc.Window { + return + } + ma := 0. + for i := 0; i < inc.Window; i++ { + ma += inc.TypicalPrice.Index(i) + } + ma /= float64(inc.Window) + inc.MA.Push(ma) + if len(inc.MA) < MaxNumOfEWMA { + inc.MA = inc.MA[MaxNumOfEWMATruncateSize-1:] + } + md := 0. + for i := 0; i < inc.Window; i++ { + md += inc.TypicalPrice.Index(i) - inc.MA.Index(i) + } + md /= float64(inc.Window) + + cci := (tp - ma) / (0.15 * md) + + inc.Values.Push(cci) + if len(inc.Values) > MaxNumOfEWMA { + inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:] + } +} + +func (inc *CCI) Last() float64 { + if len(inc.Values) == 0 { + return 0 + } + return inc.Values[len(inc.Values) - 1] +} + +func (inc *CCI) Index(i int) float64 { + if i >= len(inc.Values) { + return 0 + } + return inc.Values[len(inc.Values)-1-i] +} + +func (inc *CCI) Length() int { + return len(inc.Values) +} + +var _ types.Series = &CCI{} + +func (inc *CCI) calculateAndUpdate(allKLines []types.KLine) { + if inc.HLC3.Length() == 0 { + for _, k := range allKLines { + inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) + inc.EmitUpdate(inc.Last()) + } + } else { + k := allKLines[len(allKLines)-1] + inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *CCI) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.calculateAndUpdate(window) +} + +func (inc *CCI) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/cci_callbacks.go b/pkg/indicator/cci_callbacks.go new file mode 100644 index 000000000..52251a1f9 --- /dev/null +++ b/pkg/indicator/cci_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type CCI"; DO NOT EDIT. + +package indicator + +import () + +func (inc *CCI) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *CCI) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} From 2bbb36031c2c72396eb887c4a1f86c0a6bbdc6cd Mon Sep 17 00:00:00 2001 From: zenix Date: Tue, 10 May 2022 17:15:26 +0900 Subject: [PATCH 2/2] fix: index range in float64slice and wrong formula given by investopedia, test: add cci test --- pkg/indicator/cci.go | 51 +++++++++++++++++++++------------------ pkg/indicator/cci_test.go | 37 ++++++++++++++++++++++++++++ pkg/types/float_slice.go | 2 +- 3 files changed, 65 insertions(+), 25 deletions(-) create mode 100644 pkg/indicator/cci_test.go diff --git a/pkg/indicator/cci.go b/pkg/indicator/cci.go index 5e8acb9da..9380ad816 100644 --- a/pkg/indicator/cci.go +++ b/pkg/indicator/cci.go @@ -1,54 +1,55 @@ package indicator import ( + "math" + + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) // Refer: Commodity Channel Index -// Refer URL: https://www.investopedia.com/terms/c/commoditychannelindex.asp +// Refer URL: http://www.andrewshamlet.net/2017/07/08/python-tutorial-cci +// with modification of ddof=0 to let standard deviation to be divided by N instead of N-1 //go:generate callbackgen -type CCI type CCI struct { types.IntervalWindow - HLC3 types.Float64Slice + Input types.Float64Slice TypicalPrice types.Float64Slice - MA types.Float64Slice - Values types.Float64Slice + MA types.Float64Slice + Values types.Float64Slice UpdateCallbacks []func(value float64) } -func (inc *CCI) Update(high, low, cloze float64) { +func (inc *CCI) Update(value float64) { if len(inc.TypicalPrice) == 0 { - inc.HLC3.Push((high + low + cloze) / 3.) - inc.TypicalPrice.Push(inc.HLC3.Last()) + inc.TypicalPrice.Push(value) + inc.Input.Push(value) return } else if len(inc.TypicalPrice) > MaxNumOfEWMA { inc.TypicalPrice = inc.TypicalPrice[MaxNumOfEWMATruncateSize-1:] - inc.HLC3 = inc.HLC3[MaxNumOfEWMATruncateSize-1:] + inc.Input = inc.Input[MaxNumOfEWMATruncateSize-1:] } - hlc3 := (high + low + cloze) / 3. - tp := inc.TypicalPrice.Last() - inc.HLC3.Index(inc.Window) + hlc3 + inc.Input.Push(value) + tp := inc.TypicalPrice.Last() - inc.Input.Index(inc.Window) + value inc.TypicalPrice.Push(tp) - if len(inc.TypicalPrice) < inc.Window { + if len(inc.Input) < inc.Window { return } - ma := 0. - for i := 0; i < inc.Window; i++ { - ma += inc.TypicalPrice.Index(i) - } - ma /= float64(inc.Window) + ma := tp / float64(inc.Window) inc.MA.Push(ma) - if len(inc.MA) < MaxNumOfEWMA { + if len(inc.MA) > MaxNumOfEWMA { inc.MA = inc.MA[MaxNumOfEWMATruncateSize-1:] } md := 0. for i := 0; i < inc.Window; i++ { - md += inc.TypicalPrice.Index(i) - inc.MA.Index(i) + diff := inc.Input.Index(i) - ma + md += diff * diff } - md /= float64(inc.Window) + md = math.Sqrt(md / float64(inc.Window)) - cci := (tp - ma) / (0.15 * md) + cci := (value - ma) / (0.015 * md) inc.Values.Push(cci) if len(inc.Values) > MaxNumOfEWMA { @@ -60,7 +61,7 @@ func (inc *CCI) Last() float64 { if len(inc.Values) == 0 { return 0 } - return inc.Values[len(inc.Values) - 1] + return inc.Values[len(inc.Values)-1] } func (inc *CCI) Index(i int) float64 { @@ -76,15 +77,17 @@ func (inc *CCI) Length() int { var _ types.Series = &CCI{} +var three = fixedpoint.NewFromInt(3) + func (inc *CCI) calculateAndUpdate(allKLines []types.KLine) { - if inc.HLC3.Length() == 0 { + if inc.TypicalPrice.Length() == 0 { for _, k := range allKLines { - inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) + inc.Update(k.High.Add(k.Low).Add(k.Close).Div(three).Float64()) inc.EmitUpdate(inc.Last()) } } else { k := allKLines[len(allKLines)-1] - inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) + inc.Update(k.High.Add(k.Low).Add(k.Close).Div(three).Float64()) inc.EmitUpdate(inc.Last()) } } diff --git a/pkg/indicator/cci_test.go b/pkg/indicator/cci_test.go new file mode 100644 index 000000000..4aeca6fca --- /dev/null +++ b/pkg/indicator/cci_test.go @@ -0,0 +1,37 @@ +package indicator + +import ( + "encoding/json" + "testing" + + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +/* +python: + +import pandas as pd +s = 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,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9]) +cci = pd.Series((s - s.rolling(16).mean()) / (0.015 * s.rolling(16).std(ddof=0)), name="CCI") +print(cci) +*/ +func Test_CCI(t *testing.T) { + 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, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]`) + var input []float64 + var Delta = 4.3e-2 + if err := json.Unmarshal(randomPrices, &input); err != nil { + panic(err) + } + t.Run("random_case", func(t *testing.T) { + cci := CCI{IntervalWindow: types.IntervalWindow{Window: 16}} + for _, value := range input { + cci.Update(value) + } + + last := cci.Last() + assert.InDelta(t, 93.250481, last, Delta) + assert.InDelta(t, 81.813449, cci.Index(1), Delta) + assert.Equal(t, 50-16+1, cci.Length()) + }) +} diff --git a/pkg/types/float_slice.go b/pkg/types/float_slice.go index f1f3b954f..3d53e3bc7 100644 --- a/pkg/types/float_slice.go +++ b/pkg/types/float_slice.go @@ -127,7 +127,7 @@ func (a *Float64Slice) Last() float64 { func (a *Float64Slice) Index(i int) float64 { length := len(*a) - if length-i < 0 || i < 0 { + if length-i <= 0 || i < 0 { return 0.0 } return (*a)[length-i-1]