diff --git a/pkg/indicator/cci.go b/pkg/indicator/cci.go index f11f33713..571d206ac 100644 --- a/pkg/indicator/cci.go +++ b/pkg/indicator/cci.go @@ -11,6 +11,12 @@ import ( // 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 // +// CCI = (Typical Price - n-period SMA of TP) / (Constant x Mean Deviation) +// +// Typical Price (TP) = (High + Low + Close)/3 +// +// Constant = .015 +// // The Commodity Channel Index (CCI) is a technical analysis indicator that is used to identify potential overbought or oversold conditions // in a security's price. It was originally developed for use in commodity markets, but can be applied to any security that has a sufficient // amount of price data. The CCI is calculated by taking the difference between the security's typical price (the average of its high, low, and @@ -43,16 +49,18 @@ func (inc *CCI) Update(value float64) { } inc.Input.Push(value) - tp := inc.TypicalPrice.Last(0) - inc.Input.Index(inc.Window) + value + tp := inc.TypicalPrice.Last(0) - inc.Input.Last(inc.Window) + value inc.TypicalPrice.Push(tp) if len(inc.Input) < inc.Window { return } + ma := tp / 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++ { diff := inc.Input.Last(i) - ma diff --git a/pkg/indicator/cci_test.go b/pkg/indicator/cci_test.go index 8d93947ef..ae056e313 100644 --- a/pkg/indicator/cci_test.go +++ b/pkg/indicator/cci_test.go @@ -4,8 +4,9 @@ import ( "encoding/json" "testing" - "github.com/c9s/bbgo/pkg/types" "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/types" ) /* @@ -19,7 +20,7 @@ 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 + var delta = 4.3e-2 if err := json.Unmarshal(randomPrices, &input); err != nil { panic(err) } @@ -30,8 +31,8 @@ func Test_CCI(t *testing.T) { } last := cci.Last(0) - assert.InDelta(t, 93.250481, last, Delta) - assert.InDelta(t, 81.813449, cci.Index(1), Delta) + 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/indicator/v2/cci.go b/pkg/indicator/v2/cci.go new file mode 100644 index 000000000..77ca9e0bf --- /dev/null +++ b/pkg/indicator/v2/cci.go @@ -0,0 +1,65 @@ +package indicatorv2 + +import ( + "math" + + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: Commodity Channel Index +// 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 +// +// CCI = (Typical Price - n-period SMA of TP) / (Constant x Mean Deviation) +// +// Typical Price (TP) = (High + Low + Close)/3 +// +// Constant = .015 +// +// The Commodity Channel Index (CCI) is a technical analysis indicator that is used to identify potential overbought or oversold conditions +// in a security's price. It was originally developed for use in commodity markets, but can be applied to any security that has a sufficient +// amount of price data. The CCI is calculated by taking the difference between the security's typical price (the average of its high, low, and +// closing prices) and its moving average, and then dividing the result by the mean absolute deviation of the typical price. This resulting value +// is then plotted as a line on the price chart, with values above +100 indicating overbought conditions and values below -100 indicating +// oversold conditions. The CCI can be used by traders to identify potential entry and exit points for trades, or to confirm other technical +// analysis signals. + +type CCIStream struct { + *types.Float64Series + + TypicalPrice *types.Float64Series + + source types.Float64Source + window int +} + +func CCI(source types.Float64Source, window int) *CCIStream { + s := &CCIStream{ + Float64Series: types.NewFloat64Series(), + TypicalPrice: types.NewFloat64Series(), + source: source, + window: window, + } + s.Bind(source, s) + return s +} + +func (s *CCIStream) Calculate(value float64) float64 { + var tp = value + if s.TypicalPrice.Length() > 0 { + tp = s.TypicalPrice.Last(0) - s.source.Last(s.window) + value + } + + s.TypicalPrice.Push(tp) + + ma := tp / float64(s.window) + md := 0. + for i := 0; i < s.window; i++ { + diff := s.source.Last(i) - ma + md += diff * diff + } + + md = math.Sqrt(md / float64(s.window)) + cci := (value - ma) / (0.015 * md) + return cci +} diff --git a/pkg/indicator/v2/cci_test.go b/pkg/indicator/v2/cci_test.go new file mode 100644 index 000000000..a662553ba --- /dev/null +++ b/pkg/indicator/v2/cci_test.go @@ -0,0 +1,39 @@ +package indicatorv2 + +import ( + "encoding/json" + "testing" + + "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) { + price := Price(nil, nil) + cci := CCI(price, 16) + for _, value := range input { + price.PushAndEmit(value) + } + + t.Logf("cci: %+v", cci.Slice) + + last := cci.Last(0) + assert.InDelta(t, 93.250481, last, delta) + assert.InDelta(t, 81.813449, cci.Index(1), delta) + assert.Equal(t, 50, cci.Length(), "length") + }) +} diff --git a/pkg/indicator/v2/const.go b/pkg/indicator/v2/const.go new file mode 100644 index 000000000..7ae2f2914 --- /dev/null +++ b/pkg/indicator/v2/const.go @@ -0,0 +1,5 @@ +package indicatorv2 + +import "github.com/c9s/bbgo/pkg/fixedpoint" + +var three = fixedpoint.NewFromInt(3) diff --git a/pkg/indicator/v2/price.go b/pkg/indicator/v2/price.go index 7f261a14e..6f48439e0 100644 --- a/pkg/indicator/v2/price.go +++ b/pkg/indicator/v2/price.go @@ -65,3 +65,7 @@ func OpenPrices(source KLineSubscription) *PriceStream { func Volumes(source KLineSubscription) *PriceStream { return Price(source, types.KLineVolumeMapper) } + +func HLC3(source KLineSubscription) *PriceStream { + return Price(source, types.KLineHLC3Mapper) +} diff --git a/pkg/indicator/v2/stoch.go b/pkg/indicator/v2/stoch.go index 808169e43..fe0d30086 100644 --- a/pkg/indicator/v2/stoch.go +++ b/pkg/indicator/v2/stoch.go @@ -47,8 +47,8 @@ func Stoch(source KLineSubscription, window, dPeriod int) *StochStream { lowest := s.lowPrices.Slice.Tail(s.window).Min() highest := s.highPrices.Slice.Tail(s.window).Max() - var k float64 = 50.0 - var d float64 = 0.0 + var k = 50.0 + var d = 0.0 if highest != lowest { k = 100.0 * (kLine.Close.Float64() - lowest) / (highest - lowest) diff --git a/pkg/types/kline.go b/pkg/types/kline.go index e405ba94b..6bf863c30 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -10,14 +10,15 @@ import ( "github.com/c9s/bbgo/pkg/style" ) +var Two = fixedpoint.NewFromInt(2) +var Three = fixedpoint.NewFromInt(3) + type Direction int const DirectionUp = 1 const DirectionNone = 0 const DirectionDown = -1 -var Two = fixedpoint.NewFromInt(2) - type KLineOrWindow interface { GetInterval() string Direction() Direction @@ -668,6 +669,10 @@ func KLineVolumeMapper(k KLine) float64 { return k.Volume.Float64() } +func KLineHLC3Mapper(k KLine) float64 { + return k.High.Add(k.Low).Add(k.Close).Div(Three).Float64() +} + func MapKLinePrice(kLines []KLine, f KLineValueMapper) (prices []float64) { for _, k := range kLines { prices = append(prices, f(k)) diff --git a/pkg/types/series_float64.go b/pkg/types/series_float64.go index 6dd6704aa..f12e05d58 100644 --- a/pkg/types/series_float64.go +++ b/pkg/types/series_float64.go @@ -29,6 +29,10 @@ func (f *Float64Series) Length() int { return len(f.Slice) } +func (f *Float64Series) Push(x float64) { + f.Slice.Push(x) +} + func (f *Float64Series) PushAndEmit(x float64) { f.Slice.Push(x) f.EmitUpdate(x)