2022-09-01 04:09:03 +00:00
|
|
|
package bbgo
|
|
|
|
|
|
|
|
import (
|
2022-10-19 04:17:44 +00:00
|
|
|
"context"
|
|
|
|
"sync"
|
2022-09-01 04:09:03 +00:00
|
|
|
"time"
|
|
|
|
|
2022-10-19 04:17:44 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
2022-09-01 04:09:03 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/types"
|
2022-10-19 05:48:46 +00:00
|
|
|
log "github.com/sirupsen/logrus"
|
2022-09-01 04:09:03 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type SerialMarketDataStore struct {
|
|
|
|
*MarketDataStore
|
2022-10-20 08:45:18 +00:00
|
|
|
UseMarketTrade bool
|
2022-10-19 04:17:44 +00:00
|
|
|
KLines map[types.Interval]*types.KLine
|
|
|
|
MinInterval types.Interval
|
|
|
|
Subscription []types.Interval
|
|
|
|
o, h, l, c, v, qv, price fixedpoint.Value
|
2022-10-19 05:42:34 +00:00
|
|
|
mu sync.Mutex
|
2022-09-01 04:09:03 +00:00
|
|
|
}
|
|
|
|
|
2022-10-19 04:17:44 +00:00
|
|
|
// @param symbol: symbol to trace on
|
|
|
|
// @param minInterval: unit interval, related to your signal timeframe
|
2022-10-20 08:45:18 +00:00
|
|
|
// @param useMarketTrade: if not assigned, default to false. if assigned to true, will use MarketTrade signal to generate klines
|
|
|
|
func NewSerialMarketDataStore(symbol string, minInterval types.Interval, useMarketTrade ...bool) *SerialMarketDataStore {
|
2022-09-01 04:09:03 +00:00
|
|
|
return &SerialMarketDataStore{
|
|
|
|
MarketDataStore: NewMarketDataStore(symbol),
|
|
|
|
KLines: make(map[types.Interval]*types.KLine),
|
2022-10-20 08:45:18 +00:00
|
|
|
UseMarketTrade: len(useMarketTrade) > 0 && useMarketTrade[0],
|
2022-09-01 04:09:03 +00:00
|
|
|
Subscription: []types.Interval{},
|
2022-10-19 04:17:44 +00:00
|
|
|
MinInterval: minInterval,
|
2022-09-01 04:09:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (store *SerialMarketDataStore) Subscribe(interval types.Interval) {
|
|
|
|
// dedup
|
|
|
|
for _, i := range store.Subscription {
|
|
|
|
if i == interval {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
store.Subscription = append(store.Subscription, interval)
|
|
|
|
}
|
|
|
|
|
2022-10-19 04:17:44 +00:00
|
|
|
func (store *SerialMarketDataStore) BindStream(ctx context.Context, stream types.Stream) {
|
2022-10-20 08:45:18 +00:00
|
|
|
if store.UseMarketTrade {
|
2022-10-19 05:48:46 +00:00
|
|
|
if IsBackTesting {
|
2022-10-19 08:59:00 +00:00
|
|
|
log.Errorf("right now in backtesting, aggTrade event is not yet supported. Use OnKLineClosed instead.")
|
2022-10-19 05:48:46 +00:00
|
|
|
stream.OnKLineClosed(store.handleKLineClosed)
|
|
|
|
return
|
|
|
|
}
|
2022-10-19 04:17:44 +00:00
|
|
|
go store.tickerProcessor(ctx)
|
2022-10-20 08:43:17 +00:00
|
|
|
stream.OnMarketTrade(store.handleMarketTrade)
|
2022-10-19 04:17:44 +00:00
|
|
|
} else {
|
|
|
|
stream.OnKLineClosed(store.handleKLineClosed)
|
|
|
|
}
|
2022-09-01 04:09:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (store *SerialMarketDataStore) handleKLineClosed(kline types.KLine) {
|
|
|
|
store.AddKLine(kline)
|
|
|
|
}
|
|
|
|
|
2022-10-19 04:17:44 +00:00
|
|
|
func (store *SerialMarketDataStore) handleMarketTrade(trade types.Trade) {
|
2022-10-19 05:42:34 +00:00
|
|
|
store.mu.Lock()
|
2022-10-19 04:17:44 +00:00
|
|
|
store.price = trade.Price
|
|
|
|
store.c = store.price
|
|
|
|
if store.price.Compare(store.h) > 0 {
|
|
|
|
store.h = store.price
|
|
|
|
}
|
|
|
|
if !store.l.IsZero() {
|
|
|
|
if store.price.Compare(store.l) < 0 {
|
|
|
|
store.l = store.price
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
store.l = store.price
|
|
|
|
}
|
|
|
|
if store.o.IsZero() {
|
|
|
|
store.o = store.price
|
|
|
|
}
|
2022-10-20 08:43:17 +00:00
|
|
|
store.v = store.v.Add(trade.Quantity)
|
|
|
|
store.qv = store.qv.Add(trade.QuoteQuantity)
|
2022-10-19 05:42:34 +00:00
|
|
|
store.mu.Unlock()
|
2022-10-19 04:17:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (store *SerialMarketDataStore) tickerProcessor(ctx context.Context) {
|
|
|
|
duration := store.MinInterval.Duration()
|
2022-10-19 08:59:00 +00:00
|
|
|
relativeTime := time.Now().UnixNano() % int64(duration)
|
|
|
|
waitTime := int64(duration) - relativeTime
|
2022-10-21 07:28:21 +00:00
|
|
|
select {
|
|
|
|
case <-time.After(time.Duration(waitTime)):
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
}
|
2022-10-19 04:17:44 +00:00
|
|
|
intervalCloseTicker := time.NewTicker(duration)
|
|
|
|
defer intervalCloseTicker.Stop()
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case time := <-intervalCloseTicker.C:
|
|
|
|
kline := types.KLine{
|
|
|
|
Symbol: store.Symbol,
|
2022-10-20 08:43:17 +00:00
|
|
|
StartTime: types.Time(time.Add(-1 * duration).Round(duration)),
|
2022-10-19 04:17:44 +00:00
|
|
|
EndTime: types.Time(time),
|
|
|
|
Interval: store.MinInterval,
|
|
|
|
Closed: true,
|
|
|
|
}
|
2022-10-19 05:42:34 +00:00
|
|
|
store.mu.Lock()
|
2022-10-19 04:17:44 +00:00
|
|
|
if store.c.IsZero() {
|
|
|
|
kline.Open = store.price
|
|
|
|
kline.Close = store.price
|
|
|
|
kline.High = store.price
|
|
|
|
kline.Low = store.price
|
|
|
|
kline.Volume = fixedpoint.Zero
|
|
|
|
kline.QuoteVolume = fixedpoint.Zero
|
|
|
|
} else {
|
|
|
|
kline.Open = store.o
|
|
|
|
kline.Close = store.c
|
|
|
|
kline.High = store.h
|
|
|
|
kline.Low = store.l
|
|
|
|
kline.Volume = store.v
|
|
|
|
kline.QuoteVolume = store.qv
|
|
|
|
store.o = fixedpoint.Zero
|
|
|
|
store.c = fixedpoint.Zero
|
|
|
|
store.h = fixedpoint.Zero
|
|
|
|
store.l = fixedpoint.Zero
|
|
|
|
store.v = fixedpoint.Zero
|
|
|
|
store.qv = fixedpoint.Zero
|
|
|
|
}
|
2022-10-19 05:42:34 +00:00
|
|
|
store.mu.Unlock()
|
2022-10-19 04:17:44 +00:00
|
|
|
store.AddKLine(kline, true)
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func (store *SerialMarketDataStore) AddKLine(kline types.KLine, async ...bool) {
|
2022-09-01 04:09:03 +00:00
|
|
|
if kline.Symbol != store.Symbol {
|
|
|
|
return
|
|
|
|
}
|
2022-10-19 04:17:44 +00:00
|
|
|
// only consumes MinInterval
|
|
|
|
if kline.Interval != store.MinInterval {
|
2022-09-01 04:09:03 +00:00
|
|
|
return
|
|
|
|
}
|
2022-10-19 04:17:44 +00:00
|
|
|
// endtime
|
|
|
|
duration := store.MinInterval.Duration()
|
2022-10-20 08:43:17 +00:00
|
|
|
timestamp := kline.StartTime.Time().Add(duration)
|
2022-09-01 04:09:03 +00:00
|
|
|
for _, val := range store.Subscription {
|
|
|
|
k, ok := store.KLines[val]
|
|
|
|
if !ok {
|
|
|
|
k = &types.KLine{}
|
|
|
|
k.Set(&kline)
|
|
|
|
k.Interval = val
|
|
|
|
k.Closed = false
|
|
|
|
store.KLines[val] = k
|
|
|
|
} else {
|
|
|
|
k.Merge(&kline)
|
|
|
|
k.Closed = false
|
|
|
|
}
|
2022-10-19 04:17:44 +00:00
|
|
|
if timestamp.Round(val.Duration()) == timestamp {
|
2022-09-01 04:09:03 +00:00
|
|
|
k.Closed = true
|
2022-10-19 04:17:44 +00:00
|
|
|
if len(async) > 0 && async[0] {
|
|
|
|
go store.MarketDataStore.AddKLine(*k)
|
|
|
|
} else {
|
|
|
|
store.MarketDataStore.AddKLine(*k)
|
|
|
|
}
|
2022-09-01 04:09:03 +00:00
|
|
|
delete(store.KLines, val)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|