From a5715c6aeef60b48acddfaa8a695cf460eff9756 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 14 Jul 2022 12:51:27 +0800 Subject: [PATCH] indicator: rewrite boll indicator with stddev indicator --- pkg/indicator/boll.go | 92 ++++++++++++------------------ pkg/indicator/stddev.go | 85 +++++++++++++++++++++++++++ pkg/indicator/stddev_callbacks.go | 15 +++++ pkg/strategy/bollmaker/strategy.go | 4 +- pkg/strategy/rsmaker/strategy.go | 2 +- 5 files changed, 141 insertions(+), 57 deletions(-) create mode 100644 pkg/indicator/stddev.go create mode 100644 pkg/indicator/stddev_callbacks.go diff --git a/pkg/indicator/boll.go b/pkg/indicator/boll.go index ebc810fe5..e16b51e89 100644 --- a/pkg/indicator/boll.go +++ b/pkg/indicator/boll.go @@ -3,9 +3,6 @@ package indicator import ( "time" - log "github.com/sirupsen/logrus" - "gonum.org/v1/gonum/stat" - "github.com/c9s/bbgo/pkg/types" ) @@ -24,13 +21,15 @@ Bollinger Bands Technical indicator guide: //go:generate callbackgen -type BOLL type BOLL struct { + types.SeriesBase types.IntervalWindow - // times of Std, generally it's 2 + // K is the multiplier of Std, generally it's 2 K float64 - SMA types.Float64Slice - StdDev types.Float64Slice + SMA *SMA + StdDev *StdDev + UpBand types.Float64Slice DownBand types.Float64Slice @@ -50,11 +49,11 @@ func (inc *BOLL) GetDownBand() types.SeriesExtend { } func (inc *BOLL) GetSMA() types.SeriesExtend { - return types.NewSeries(&inc.SMA) + return types.NewSeries(inc.SMA) } func (inc *BOLL) GetStdDev() types.SeriesExtend { - return types.NewSeries(&inc.StdDev) + return types.NewSeries(inc.StdDev) } func (inc *BOLL) LastUpBand() float64 { @@ -73,64 +72,49 @@ func (inc *BOLL) LastDownBand() float64 { return inc.DownBand[len(inc.DownBand)-1] } -func (inc *BOLL) LastStdDev() float64 { - if len(inc.StdDev) == 0 { - return 0.0 +func (inc *BOLL) Update(value float64) { + if inc.SMA == nil { + inc.SeriesBase.Series = inc + inc.SMA = &SMA{IntervalWindow: inc.IntervalWindow} } - return inc.StdDev[len(inc.StdDev)-1] -} - -func (inc *BOLL) LastSMA() float64 { - if len(inc.SMA) > 0 { - return inc.SMA[len(inc.SMA)-1] - } - return 0.0 -} - -func (inc *BOLL) CalculateAndUpdate(kLines []types.KLine) { - if len(kLines) < inc.Window { - return + if inc.StdDev == nil { + inc.StdDev = &StdDev{IntervalWindow: inc.IntervalWindow} } - var index = len(kLines) - 1 - var kline = kLines[index] + inc.SMA.Update(value) + inc.StdDev.Update(value) - if inc.EndTime != zeroTime && kline.EndTime.Before(inc.EndTime) { - return - } - - var recentK = kLines[index-(inc.Window-1) : index+1] - sma, err := calculateSMA(recentK, inc.Window, KLineClosePriceMapper) - if err != nil { - log.WithError(err).Error("SMA error") - return - } - - inc.SMA.Push(sma) - - var prices []float64 - for _, k := range recentK { - prices = append(prices, k.Close.Float64()) - } - - var std = stat.StdDev(prices, nil) - inc.StdDev.Push(std) - - var band = inc.K * std + var sma = inc.SMA.Last() + var stdDev = inc.StdDev.Last() + var band = inc.K * stdDev var upBand = sma + band - inc.UpBand.Push(upBand) - var downBand = sma - band + + inc.UpBand.Push(upBand) inc.DownBand.Push(downBand) +} - // update end time - inc.EndTime = kLines[index].EndTime.Time() +func (inc *BOLL) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} - // log.Infof("update boll: sma=%f, up=%f, down=%f", sma, upBand, downBand) +func (inc *BOLL) CalculateAndUpdate(allKLines []types.KLine) { + var last = allKLines[len(allKLines)-1] - inc.EmitUpdate(sma, upBand, downBand) + if inc.SMA == nil { + for _, k := range allKLines { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + continue + } + inc.PushK(k) + } + } else { + inc.PushK(last) + } + + inc.EmitUpdate(inc.SMA.Last(), inc.UpBand.Last(), inc.DownBand.Last()) } func (inc *BOLL) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { diff --git a/pkg/indicator/stddev.go b/pkg/indicator/stddev.go new file mode 100644 index 000000000..3c6a5bdca --- /dev/null +++ b/pkg/indicator/stddev.go @@ -0,0 +1,85 @@ +package indicator + +import ( + "time" + + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type StdDev +type StdDev struct { + types.SeriesBase + types.IntervalWindow + Values types.Float64Slice + rawValues *types.Queue + + EndTime time.Time + updateCallbacks []func(value float64) +} + +func (inc *StdDev) Last() float64 { + if inc.Values.Length() == 0 { + return 0.0 + } + return inc.Values.Last() +} + +func (inc *StdDev) Index(i int) float64 { + if i >= inc.Values.Length() { + return 0.0 + } + + return inc.Values.Index(i) +} + +func (inc *StdDev) Length() int { + return inc.Values.Length() +} + +var _ types.SeriesExtend = &StdDev{} + +func (inc *StdDev) Update(value float64) { + if inc.rawValues == nil { + inc.rawValues = types.NewQueue(inc.Window) + inc.SeriesBase.Series = inc + } + + inc.rawValues.Update(value) + + var std = inc.rawValues.Stdev() + inc.Values.Push(std) +} + +func (inc *StdDev) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) + inc.EndTime = k.EndTime.Time() +} + +func (inc *StdDev) CalculateAndUpdate(allKLines []types.KLine) { + var last = allKLines[len(allKLines)-1] + + if inc.rawValues == nil { + for _, k := range allKLines { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + continue + } + inc.PushK(k) + } + } else { + inc.PushK(last) + } + + inc.EmitUpdate(inc.Values.Last()) +} + +func (inc *StdDev) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *StdDev) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/stddev_callbacks.go b/pkg/indicator/stddev_callbacks.go new file mode 100644 index 000000000..745f006ee --- /dev/null +++ b/pkg/indicator/stddev_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type StdDev"; DO NOT EDIT. + +package indicator + +import () + +func (inc *StdDev) OnUpdate(cb func(value float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *StdDev) EmitUpdate(value float64) { + for _, cb := range inc.updateCallbacks { + cb(value) + } +} diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index ff3c31eee..86e971313 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -280,7 +280,7 @@ func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, k downBand := s.defaultBoll.LastDownBand() upBand := s.defaultBoll.LastUpBand() - sma := s.defaultBoll.LastSMA() + sma := s.defaultBoll.SMA.Last() log.Infof("%s bollinger band: up %f sma %f down %f", s.Symbol, upBand, sma, downBand) bandPercentage := calculateBandPercentage(upBand, downBand, sma, midPrice.Float64()) @@ -402,7 +402,7 @@ func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, k canSell = false } - if s.BuyBelowNeutralSMA && midPrice.Float64() > s.neutralBoll.LastSMA() { + if s.BuyBelowNeutralSMA && midPrice.Float64() > s.neutralBoll.SMA.Last() { canBuy = false } diff --git a/pkg/strategy/rsmaker/strategy.go b/pkg/strategy/rsmaker/strategy.go index 1eada96c2..10ee81ff4 100644 --- a/pkg/strategy/rsmaker/strategy.go +++ b/pkg/strategy/rsmaker/strategy.go @@ -338,7 +338,7 @@ func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, k downBand := s.defaultBoll.LastDownBand() upBand := s.defaultBoll.LastUpBand() - sma := s.defaultBoll.LastSMA() + sma := s.defaultBoll.SMA.Last() log.Infof("bollinger band: up %f sma %f down %f", upBand, sma, downBand) bandPercentage := calculateBandPercentage(upBand, downBand, sma, midPrice.Float64())