Skip to content

Commit 0a08cc2

Browse files
authored
Merge pull request #825 from c9s/refactor/indicator-api
refactor: new indicator api
2 parents ed91fdc + ea08a61 commit 0a08cc2

12 files changed

+269
-199
lines changed

pkg/bbgo/marketdatastore.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import "github.com/c9s/bbgo/pkg/types"
55
const MaxNumOfKLines = 5_000
66
const MaxNumOfKLinesTruncate = 100
77

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

1616
kLineWindowUpdateCallbacks []func(interval types.Interval, klines types.KLineWindow)
17+
kLineClosedCallbacks []func(k types.KLine)
1718
}
1819

1920
func NewMarketDataStore(symbol string) *MarketDataStore {
@@ -47,18 +48,19 @@ func (store *MarketDataStore) handleKLineClosed(kline types.KLine) {
4748
store.AddKLine(kline)
4849
}
4950

50-
func (store *MarketDataStore) AddKLine(kline types.KLine) {
51-
window, ok := store.KLineWindows[kline.Interval]
51+
func (store *MarketDataStore) AddKLine(k types.KLine) {
52+
window, ok := store.KLineWindows[k.Interval]
5253
if !ok {
5354
var tmp = make(types.KLineWindow, 0, 1000)
54-
store.KLineWindows[kline.Interval] = &tmp
55+
store.KLineWindows[k.Interval] = &tmp
5556
window = &tmp
5657
}
57-
window.Add(kline)
58+
window.Add(k)
5859

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

63-
store.EmitKLineWindowUpdate(kline.Interval, *window)
64+
store.EmitKLineClosed(k)
65+
store.EmitKLineWindowUpdate(k.Interval, *window)
6466
}

pkg/bbgo/marketdatastore_callbacks.go

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/bbgo/session.go

+1-136
Original file line numberDiff line numberDiff line change
@@ -17,146 +17,11 @@ import (
1717

1818
exchange2 "github.com/c9s/bbgo/pkg/exchange"
1919
"github.com/c9s/bbgo/pkg/fixedpoint"
20-
"github.com/c9s/bbgo/pkg/indicator"
2120
"github.com/c9s/bbgo/pkg/service"
2221
"github.com/c9s/bbgo/pkg/types"
2322
"github.com/c9s/bbgo/pkg/util"
2423
)
2524

26-
var (
27-
debugEWMA = false
28-
debugSMA = false
29-
)
30-
31-
func init() {
32-
// when using --dotenv option, the dotenv is loaded from command.PersistentPreRunE, not init.
33-
// hence here the env var won't enable the debug flag
34-
util.SetEnvVarBool("DEBUG_EWMA", &debugEWMA)
35-
util.SetEnvVarBool("DEBUG_SMA", &debugSMA)
36-
}
37-
38-
type StandardIndicatorSet struct {
39-
Symbol string
40-
// Standard indicators
41-
// interval -> window
42-
sma map[types.IntervalWindow]*indicator.SMA
43-
ewma map[types.IntervalWindow]*indicator.EWMA
44-
boll map[types.IntervalWindowBandWidth]*indicator.BOLL
45-
stoch map[types.IntervalWindow]*indicator.STOCH
46-
volatility map[types.IntervalWindow]*indicator.Volatility
47-
48-
store *MarketDataStore
49-
}
50-
51-
func NewStandardIndicatorSet(symbol string, store *MarketDataStore) *StandardIndicatorSet {
52-
set := &StandardIndicatorSet{
53-
Symbol: symbol,
54-
sma: make(map[types.IntervalWindow]*indicator.SMA),
55-
ewma: make(map[types.IntervalWindow]*indicator.EWMA),
56-
boll: make(map[types.IntervalWindowBandWidth]*indicator.BOLL),
57-
stoch: make(map[types.IntervalWindow]*indicator.STOCH),
58-
volatility: make(map[types.IntervalWindow]*indicator.Volatility),
59-
store: store,
60-
}
61-
62-
// let us pre-defined commonly used intervals
63-
for interval := range types.SupportedIntervals {
64-
for _, window := range []int{7, 25, 99} {
65-
iw := types.IntervalWindow{Interval: interval, Window: window}
66-
set.sma[iw] = &indicator.SMA{IntervalWindow: iw}
67-
set.sma[iw].Bind(store)
68-
if debugSMA {
69-
set.sma[iw].OnUpdate(func(value float64) {
70-
log.Infof("%s SMA %s: %f", symbol, iw.String(), value)
71-
})
72-
}
73-
74-
set.ewma[iw] = &indicator.EWMA{IntervalWindow: iw}
75-
set.ewma[iw].Bind(store)
76-
77-
// if debug EWMA is enabled, we add the debug handler
78-
if debugEWMA {
79-
set.ewma[iw].OnUpdate(func(value float64) {
80-
log.Infof("%s EWMA %s: %f", symbol, iw.String(), value)
81-
})
82-
}
83-
84-
}
85-
86-
// setup boll indicator, we may refactor boll indicator by subscribing SMA indicator,
87-
// however, since general used BOLLINGER band use window 21, which is not in the existing SMA indicator sets.
88-
// Pull out the bandwidth configuration as the boll Key
89-
iw := types.IntervalWindow{Interval: interval, Window: 21}
90-
91-
// set efault band width to 2.0
92-
iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: 2.0}
93-
set.boll[iwb] = &indicator.BOLL{IntervalWindow: iw, K: iwb.BandWidth}
94-
set.boll[iwb].Bind(store)
95-
}
96-
97-
return set
98-
}
99-
100-
// BOLL returns the bollinger band indicator of the given interval, the window and bandwidth
101-
func (set *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth float64) *indicator.BOLL {
102-
iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: bandWidth}
103-
inc, ok := set.boll[iwb]
104-
if !ok {
105-
inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth}
106-
inc.Bind(set.store)
107-
set.boll[iwb] = inc
108-
}
109-
110-
return inc
111-
}
112-
113-
// SMA returns the simple moving average indicator of the given interval and the window size.
114-
func (set *StandardIndicatorSet) SMA(iw types.IntervalWindow) *indicator.SMA {
115-
inc, ok := set.sma[iw]
116-
if !ok {
117-
inc = &indicator.SMA{IntervalWindow: iw}
118-
inc.Bind(set.store)
119-
set.sma[iw] = inc
120-
}
121-
122-
return inc
123-
}
124-
125-
// EWMA returns the exponential weighed moving average indicator of the given interval and the window size.
126-
func (set *StandardIndicatorSet) EWMA(iw types.IntervalWindow) *indicator.EWMA {
127-
inc, ok := set.ewma[iw]
128-
if !ok {
129-
inc = &indicator.EWMA{IntervalWindow: iw}
130-
inc.Bind(set.store)
131-
set.ewma[iw] = inc
132-
}
133-
134-
return inc
135-
}
136-
137-
func (set *StandardIndicatorSet) STOCH(iw types.IntervalWindow) *indicator.STOCH {
138-
inc, ok := set.stoch[iw]
139-
if !ok {
140-
inc = &indicator.STOCH{IntervalWindow: iw}
141-
inc.Bind(set.store)
142-
set.stoch[iw] = inc
143-
}
144-
145-
return inc
146-
}
147-
148-
// VOLATILITY returns the volatility(stddev) indicator of the given interval and the window size.
149-
func (set *StandardIndicatorSet) VOLATILITY(iw types.IntervalWindow) *indicator.Volatility {
150-
inc, ok := set.volatility[iw]
151-
if !ok {
152-
inc = &indicator.Volatility{IntervalWindow: iw}
153-
inc.Bind(set.store)
154-
set.volatility[iw] = inc
155-
}
156-
157-
return inc
158-
}
159-
16025
// ExchangeSession presents the exchange connection Session
16126
// It also maintains and collects the data returned from the stream.
16227
type ExchangeSession struct {
@@ -504,7 +369,7 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ
504369
marketDataStore.BindStream(session.MarketDataStream)
505370
session.marketDataStores[symbol] = marketDataStore
506371

507-
standardIndicatorSet := NewStandardIndicatorSet(symbol, marketDataStore)
372+
standardIndicatorSet := NewStandardIndicatorSet(symbol, session.MarketDataStream, marketDataStore)
508373
session.standardIndicatorSets[symbol] = standardIndicatorSet
509374

510375
// used kline intervals by the given symbol

pkg/bbgo/standard_indicator_set.go

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package bbgo
2+
3+
import (
4+
"github.com/sirupsen/logrus"
5+
6+
"github.com/c9s/bbgo/pkg/indicator"
7+
"github.com/c9s/bbgo/pkg/types"
8+
"github.com/c9s/bbgo/pkg/util"
9+
)
10+
11+
var (
12+
debugEWMA = false
13+
debugSMA = false
14+
debugBOLL = false
15+
)
16+
17+
func init() {
18+
// when using --dotenv option, the dotenv is loaded from command.PersistentPreRunE, not init.
19+
// hence here the env var won't enable the debug flag
20+
util.SetEnvVarBool("DEBUG_EWMA", &debugEWMA)
21+
util.SetEnvVarBool("DEBUG_SMA", &debugSMA)
22+
util.SetEnvVarBool("DEBUG_BOLL", &debugBOLL)
23+
}
24+
25+
type StandardIndicatorSet struct {
26+
Symbol string
27+
// Standard indicators
28+
// interval -> window
29+
sma map[types.IntervalWindow]*indicator.SMA
30+
ewma map[types.IntervalWindow]*indicator.EWMA
31+
boll map[types.IntervalWindowBandWidth]*indicator.BOLL
32+
stoch map[types.IntervalWindow]*indicator.STOCH
33+
34+
stream types.Stream
35+
store *MarketDataStore
36+
}
37+
38+
func NewStandardIndicatorSet(symbol string, stream types.Stream, store *MarketDataStore) *StandardIndicatorSet {
39+
return &StandardIndicatorSet{
40+
Symbol: symbol,
41+
sma: make(map[types.IntervalWindow]*indicator.SMA),
42+
ewma: make(map[types.IntervalWindow]*indicator.EWMA),
43+
boll: make(map[types.IntervalWindowBandWidth]*indicator.BOLL),
44+
stoch: make(map[types.IntervalWindow]*indicator.STOCH),
45+
store: store,
46+
stream: stream,
47+
}
48+
}
49+
50+
// BOLL returns the bollinger band indicator of the given interval, the window and bandwidth
51+
func (set *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth float64) *indicator.BOLL {
52+
iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: bandWidth}
53+
inc, ok := set.boll[iwb]
54+
if !ok {
55+
inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth}
56+
57+
if klines, ok := set.store.KLinesOfInterval(iw.Interval); ok {
58+
inc.LoadK(*klines)
59+
}
60+
61+
if debugBOLL {
62+
inc.OnUpdate(func(sma float64, upBand float64, downBand float64) {
63+
logrus.Infof("%s BOLL %s: sma=%f up=%f down=%f", set.Symbol, iw.String(), sma, upBand, downBand)
64+
})
65+
}
66+
67+
inc.BindK(set.stream, set.Symbol, iw.Interval)
68+
set.boll[iwb] = inc
69+
}
70+
71+
return inc
72+
}
73+
74+
// SMA returns the simple moving average indicator of the given interval and the window size.
75+
func (set *StandardIndicatorSet) SMA(iw types.IntervalWindow) *indicator.SMA {
76+
inc, ok := set.sma[iw]
77+
if !ok {
78+
inc = &indicator.SMA{IntervalWindow: iw}
79+
80+
if klines, ok := set.store.KLinesOfInterval(iw.Interval); ok {
81+
inc.LoadK(*klines)
82+
}
83+
84+
if debugSMA {
85+
inc.OnUpdate(func(value float64) {
86+
logrus.Infof("%s SMA %s: %f", set.Symbol, iw.String(), value)
87+
})
88+
}
89+
90+
inc.BindK(set.stream, set.Symbol, iw.Interval)
91+
set.sma[iw] = inc
92+
}
93+
94+
return inc
95+
}
96+
97+
// EWMA returns the exponential weighed moving average indicator of the given interval and the window size.
98+
func (set *StandardIndicatorSet) EWMA(iw types.IntervalWindow) *indicator.EWMA {
99+
inc, ok := set.ewma[iw]
100+
if !ok {
101+
inc = &indicator.EWMA{IntervalWindow: iw}
102+
103+
if klines, ok := set.store.KLinesOfInterval(iw.Interval); ok {
104+
inc.LoadK(*klines)
105+
}
106+
107+
if debugEWMA {
108+
inc.OnUpdate(func(value float64) {
109+
logrus.Infof("%s EWMA %s: value=%f", set.Symbol, iw.String(), value)
110+
})
111+
}
112+
113+
inc.BindK(set.stream, set.Symbol, iw.Interval)
114+
set.ewma[iw] = inc
115+
}
116+
117+
return inc
118+
}
119+
120+
func (set *StandardIndicatorSet) STOCH(iw types.IntervalWindow) *indicator.STOCH {
121+
inc, ok := set.stoch[iw]
122+
if !ok {
123+
inc = &indicator.STOCH{IntervalWindow: iw}
124+
125+
if klines, ok := set.store.KLinesOfInterval(iw.Interval); ok {
126+
inc.LoadK(*klines)
127+
}
128+
129+
inc.BindK(set.stream, set.Symbol, iw.Interval)
130+
set.stoch[iw] = inc
131+
}
132+
133+
return inc
134+
}

pkg/indicator/atr.go

+16-6
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,29 @@ func (inc *ATR) Length() int {
7979
}
8080

8181
func (inc *ATR) PushK(k types.KLine) {
82+
if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) {
83+
return
84+
}
85+
8286
inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64())
87+
inc.EndTime = k.EndTime.Time()
88+
inc.EmitUpdate(inc.Last())
89+
}
90+
91+
func (inc *ATR) LoadK(allKlines []types.KLine) {
92+
for _, k := range allKlines {
93+
inc.PushK(k)
94+
}
95+
}
96+
97+
func (inc *ATR) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) {
98+
target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK))
8399
}
84100

85101
func (inc *ATR) CalculateAndUpdate(kLines []types.KLine) {
86102
for _, k := range kLines {
87-
if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) {
88-
continue
89-
}
90103
inc.PushK(k)
91104
}
92-
93-
inc.EmitUpdate(inc.Last())
94-
inc.EndTime = kLines[len(kLines)-1].EndTime.Time()
95105
}
96106

97107
func (inc *ATR) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {

0 commit comments

Comments
 (0)