Skip to content

Commit

Permalink
bbgo: refactor standard indicator
Browse files Browse the repository at this point in the history
  • Loading branch information
c9s committed Jul 20, 2022
1 parent 4300e00 commit 9f937f5
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 177 deletions.
14 changes: 8 additions & 6 deletions pkg/bbgo/marketdatastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "github.com/c9s/bbgo/pkg/types"
const MaxNumOfKLines = 5_000
const MaxNumOfKLinesTruncate = 100

// MarketDataStore receives and maintain the public market data
// MarketDataStore receives and maintain the public market data of a single symbol
//go:generate callbackgen -type MarketDataStore
type MarketDataStore struct {
Symbol string
Expand All @@ -14,6 +14,7 @@ type MarketDataStore struct {
KLineWindows map[types.Interval]*types.KLineWindow `json:"-"`

kLineWindowUpdateCallbacks []func(interval types.Interval, klines types.KLineWindow)
kLineClosedCallbacks []func(k types.KLine)
}

func NewMarketDataStore(symbol string) *MarketDataStore {
Expand Down Expand Up @@ -47,18 +48,19 @@ func (store *MarketDataStore) handleKLineClosed(kline types.KLine) {
store.AddKLine(kline)
}

func (store *MarketDataStore) AddKLine(kline types.KLine) {
window, ok := store.KLineWindows[kline.Interval]
func (store *MarketDataStore) AddKLine(k types.KLine) {
window, ok := store.KLineWindows[k.Interval]
if !ok {
var tmp = make(types.KLineWindow, 0, 1000)
store.KLineWindows[kline.Interval] = &tmp
store.KLineWindows[k.Interval] = &tmp
window = &tmp
}
window.Add(kline)
window.Add(k)

if len(*window) > MaxNumOfKLines {
*window = (*window)[MaxNumOfKLinesTruncate-1:]
}

store.EmitKLineWindowUpdate(kline.Interval, *window)
store.EmitKLineClosed(k)
store.EmitKLineWindowUpdate(k.Interval, *window)
}
10 changes: 10 additions & 0 deletions pkg/bbgo/marketdatastore_callbacks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

137 changes: 1 addition & 136 deletions pkg/bbgo/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,146 +17,11 @@ import (

exchange2 "github.com/c9s/bbgo/pkg/exchange"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/service"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
)

var (
debugEWMA = false
debugSMA = false
)

func init() {
// when using --dotenv option, the dotenv is loaded from command.PersistentPreRunE, not init.
// hence here the env var won't enable the debug flag
util.SetEnvVarBool("DEBUG_EWMA", &debugEWMA)
util.SetEnvVarBool("DEBUG_SMA", &debugSMA)
}

type StandardIndicatorSet struct {
Symbol string
// Standard indicators
// interval -> window
sma map[types.IntervalWindow]*indicator.SMA
ewma map[types.IntervalWindow]*indicator.EWMA
boll map[types.IntervalWindowBandWidth]*indicator.BOLL
stoch map[types.IntervalWindow]*indicator.STOCH
volatility map[types.IntervalWindow]*indicator.Volatility

store *MarketDataStore
}

func NewStandardIndicatorSet(symbol string, store *MarketDataStore) *StandardIndicatorSet {
set := &StandardIndicatorSet{
Symbol: symbol,
sma: make(map[types.IntervalWindow]*indicator.SMA),
ewma: make(map[types.IntervalWindow]*indicator.EWMA),
boll: make(map[types.IntervalWindowBandWidth]*indicator.BOLL),
stoch: make(map[types.IntervalWindow]*indicator.STOCH),
volatility: make(map[types.IntervalWindow]*indicator.Volatility),
store: store,
}

// let us pre-defined commonly used intervals
for interval := range types.SupportedIntervals {
for _, window := range []int{7, 25, 99} {
iw := types.IntervalWindow{Interval: interval, Window: window}
set.sma[iw] = &indicator.SMA{IntervalWindow: iw}
set.sma[iw].Bind(store)
if debugSMA {
set.sma[iw].OnUpdate(func(value float64) {
log.Infof("%s SMA %s: %f", symbol, iw.String(), value)
})
}

set.ewma[iw] = &indicator.EWMA{IntervalWindow: iw}
set.ewma[iw].Bind(store)

// if debug EWMA is enabled, we add the debug handler
if debugEWMA {
set.ewma[iw].OnUpdate(func(value float64) {
log.Infof("%s EWMA %s: %f", symbol, iw.String(), value)
})
}

}

// setup boll indicator, we may refactor boll indicator by subscribing SMA indicator,
// however, since general used BOLLINGER band use window 21, which is not in the existing SMA indicator sets.
// Pull out the bandwidth configuration as the boll Key
iw := types.IntervalWindow{Interval: interval, Window: 21}

// set efault band width to 2.0
iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: 2.0}
set.boll[iwb] = &indicator.BOLL{IntervalWindow: iw, K: iwb.BandWidth}
set.boll[iwb].Bind(store)
}

return set
}

// BOLL returns the bollinger band indicator of the given interval, the window and bandwidth
func (set *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth float64) *indicator.BOLL {
iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: bandWidth}
inc, ok := set.boll[iwb]
if !ok {
inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth}
inc.Bind(set.store)
set.boll[iwb] = inc
}

return inc
}

// SMA returns the simple moving average indicator of the given interval and the window size.
func (set *StandardIndicatorSet) SMA(iw types.IntervalWindow) *indicator.SMA {
inc, ok := set.sma[iw]
if !ok {
inc = &indicator.SMA{IntervalWindow: iw}
inc.Bind(set.store)
set.sma[iw] = inc
}

return inc
}

// EWMA returns the exponential weighed moving average indicator of the given interval and the window size.
func (set *StandardIndicatorSet) EWMA(iw types.IntervalWindow) *indicator.EWMA {
inc, ok := set.ewma[iw]
if !ok {
inc = &indicator.EWMA{IntervalWindow: iw}
inc.Bind(set.store)
set.ewma[iw] = inc
}

return inc
}

func (set *StandardIndicatorSet) STOCH(iw types.IntervalWindow) *indicator.STOCH {
inc, ok := set.stoch[iw]
if !ok {
inc = &indicator.STOCH{IntervalWindow: iw}
inc.Bind(set.store)
set.stoch[iw] = inc
}

return inc
}

// VOLATILITY returns the volatility(stddev) indicator of the given interval and the window size.
func (set *StandardIndicatorSet) VOLATILITY(iw types.IntervalWindow) *indicator.Volatility {
inc, ok := set.volatility[iw]
if !ok {
inc = &indicator.Volatility{IntervalWindow: iw}
inc.Bind(set.store)
set.volatility[iw] = inc
}

return inc
}

// ExchangeSession presents the exchange connection Session
// It also maintains and collects the data returned from the stream.
type ExchangeSession struct {
Expand Down Expand Up @@ -504,7 +369,7 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ
marketDataStore.BindStream(session.MarketDataStream)
session.marketDataStores[symbol] = marketDataStore

standardIndicatorSet := NewStandardIndicatorSet(symbol, marketDataStore)
standardIndicatorSet := NewStandardIndicatorSet(symbol, session.MarketDataStream, marketDataStore)
session.standardIndicatorSets[symbol] = standardIndicatorSet

// used kline intervals by the given symbol
Expand Down
135 changes: 135 additions & 0 deletions pkg/bbgo/standard_indicator_set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package bbgo

import (
"github.com/sirupsen/logrus"

"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
)

var (
debugEWMA = false
debugSMA = false
debugBOLL = false
)

func init() {
// when using --dotenv option, the dotenv is loaded from command.PersistentPreRunE, not init.
// hence here the env var won't enable the debug flag
util.SetEnvVarBool("DEBUG_EWMA", &debugEWMA)
util.SetEnvVarBool("DEBUG_SMA", &debugSMA)
util.SetEnvVarBool("DEBUG_BOLL", &debugBOLL)
}

type StandardIndicatorSet struct {
Symbol string
// Standard indicators
// interval -> window
sma map[types.IntervalWindow]*indicator.SMA
ewma map[types.IntervalWindow]*indicator.EWMA
boll map[types.IntervalWindowBandWidth]*indicator.BOLL
stoch map[types.IntervalWindow]*indicator.STOCH
volatility map[types.IntervalWindow]*indicator.Volatility

stream types.Stream
store *MarketDataStore
}

func NewStandardIndicatorSet(symbol string, stream types.Stream, store *MarketDataStore) *StandardIndicatorSet {
return &StandardIndicatorSet{
Symbol: symbol,
sma: make(map[types.IntervalWindow]*indicator.SMA),
ewma: make(map[types.IntervalWindow]*indicator.EWMA),
boll: make(map[types.IntervalWindowBandWidth]*indicator.BOLL),
stoch: make(map[types.IntervalWindow]*indicator.STOCH),
store: store,
stream: stream,
}
}

// BOLL returns the bollinger band indicator of the given interval, the window and bandwidth
func (set *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth float64) *indicator.BOLL {
iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: bandWidth}
inc, ok := set.boll[iwb]
if !ok {
inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth}

if klines, ok := set.store.KLinesOfInterval(iw.Interval); ok {
inc.LoadK(*klines)
}

if debugBOLL {
inc.OnUpdate(func(sma float64, upBand float64, downBand float64) {
logrus.Infof("%s BOLL %s: sma=%f up=%f down=%f", set.Symbol, iw.String(), sma, upBand, downBand)
})
}

inc.BindK(set.stream, set.Symbol, iw.Interval)
set.boll[iwb] = inc
}

return inc
}

// SMA returns the simple moving average indicator of the given interval and the window size.
func (set *StandardIndicatorSet) SMA(iw types.IntervalWindow) *indicator.SMA {
inc, ok := set.sma[iw]
if !ok {
inc = &indicator.SMA{IntervalWindow: iw}

if klines, ok := set.store.KLinesOfInterval(iw.Interval); ok {
inc.LoadK(*klines)
}

if debugSMA {
inc.OnUpdate(func(value float64) {
logrus.Infof("%s SMA %s: %f", set.Symbol, iw.String(), value)
})
}

inc.BindK(set.stream, set.Symbol, iw.Interval)
set.sma[iw] = inc
}

return inc
}

// EWMA returns the exponential weighed moving average indicator of the given interval and the window size.
func (set *StandardIndicatorSet) EWMA(iw types.IntervalWindow) *indicator.EWMA {
inc, ok := set.ewma[iw]
if !ok {
inc = &indicator.EWMA{IntervalWindow: iw}

if klines, ok := set.store.KLinesOfInterval(iw.Interval); ok {
inc.LoadK(*klines)
}

if debugEWMA {
inc.OnUpdate(func(value float64) {
logrus.Infof("%s EWMA %s: value=%f", set.Symbol, iw.String(), value)
})
}

inc.BindK(set.stream, set.Symbol, iw.Interval)
set.ewma[iw] = inc
}

return inc
}

func (set *StandardIndicatorSet) STOCH(iw types.IntervalWindow) *indicator.STOCH {
inc, ok := set.stoch[iw]
if !ok {
inc = &indicator.STOCH{IntervalWindow: iw}

if klines, ok := set.store.KLinesOfInterval(iw.Interval); ok {
inc.LoadK(*klines)
}

inc.BindK(set.stream, set.Symbol, iw.Interval)
set.stoch[iw] = inc
}

return inc
}
32 changes: 21 additions & 11 deletions pkg/indicator/boll.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,25 +96,35 @@ func (inc *BOLL) Update(value float64) {
inc.DownBand.Push(downBand)
}

func (inc *BOLL) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) {
target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK))
}

func (inc *BOLL) PushK(k types.KLine) {
if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) {
return
}
inc.Update(k.Close.Float64())
inc.EndTime = k.EndTime.Time()
inc.EmitUpdate(inc.SMA.Last(), inc.UpBand.Last(), inc.DownBand.Last())
}

func (inc *BOLL) CalculateAndUpdate(allKLines []types.KLine) {
var last = allKLines[len(allKLines)-1]
func (inc *BOLL) LoadK(allKLines []types.KLine) {
for _, k := range allKLines {
inc.PushK(k)
}

inc.EmitUpdate(inc.SMA.Last(), inc.UpBand.Last(), inc.DownBand.Last())
}

func (inc *BOLL) CalculateAndUpdate(allKLines []types.KLine) {
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.LoadK(allKLines)
return
}

inc.EmitUpdate(inc.SMA.Last(), inc.UpBand.Last(), inc.DownBand.Last())
var last = allKLines[len(allKLines)-1]
inc.PushK(last)
}

func (inc *BOLL) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
Expand Down
Loading

0 comments on commit 9f937f5

Please sign in to comment.