bbgo_origin/pkg/exchange/ftx/stream.go

190 lines
4.6 KiB
Go
Raw Normal View History

package ftx
import (
"context"
2021-03-28 07:07:46 +00:00
"fmt"
"sync/atomic"
2021-03-27 10:07:35 +00:00
"time"
2021-03-28 07:07:46 +00:00
"github.com/gorilla/websocket"
"github.com/c9s/bbgo/pkg/service"
"github.com/c9s/bbgo/pkg/types"
)
2021-03-28 07:07:46 +00:00
const endpoint = "wss://ftx.com/ws/"
type Stream struct {
2021-03-02 14:18:41 +00:00
*types.StandardStream
ws *service.WebsocketClientBase
klineMessage chan types.KLine
exchange *Exchange
ctx context.Context
2021-03-28 07:07:46 +00:00
// publicOnly can only be configured before connecting
publicOnly int32
2021-03-28 07:07:46 +00:00
key string
secret string
subAccount string
2021-03-28 07:07:46 +00:00
2021-03-28 08:29:36 +00:00
// subscriptions are only accessed in single goroutine environment, so I don't use mutex to protect them
2021-03-28 07:07:46 +00:00
subscriptions []websocketRequest
}
func NewStream(key, secret string, subAccount string, e *Exchange) *Stream {
s := &Stream{
exchange: e,
2021-03-28 07:07:46 +00:00
key: key,
klineMessage: make(chan types.KLine),
2021-03-28 07:07:46 +00:00
secret: secret,
subAccount: subAccount,
2021-03-02 14:18:41 +00:00
StandardStream: &types.StandardStream{},
2021-03-28 07:07:46 +00:00
ws: service.NewWebsocketClientBase(endpoint, 3*time.Second),
}
2021-02-27 10:41:46 +00:00
2021-03-28 07:07:46 +00:00
s.ws.OnMessage((&messageHandler{StandardStream: s.StandardStream}).handleMessage)
s.ws.OnConnected(func(conn *websocket.Conn) {
subs := []websocketRequest{newLoginRequest(s.key, s.secret, time.Now(), s.subAccount)}
2021-03-28 08:29:36 +00:00
subs = append(subs, s.subscriptions...)
for _, sub := range subs {
2021-03-28 07:07:46 +00:00
if err := conn.WriteJSON(sub); err != nil {
s.ws.EmitError(fmt.Errorf("failed to send subscription: %+v", sub))
}
}
s.EmitConnect()
2021-03-28 07:07:46 +00:00
})
2021-03-28 08:29:36 +00:00
return s
}
func (s *Stream) Connect(ctx context.Context) error {
2021-03-27 08:58:51 +00:00
// If it's not public only, let's do the authentication.
if atomic.LoadInt32(&s.publicOnly) == 0 {
2021-03-27 11:02:19 +00:00
s.subscribePrivateEvents()
2021-03-27 10:07:35 +00:00
}
2021-03-28 07:07:46 +00:00
if err := s.ws.Connect(ctx); err != nil {
2021-03-27 10:07:35 +00:00
return err
2021-03-27 08:58:51 +00:00
}
s.ctx = ctx
s.EmitStart()
go s.handleChannelKlineMessage()
2021-03-27 08:58:51 +00:00
2021-03-28 08:29:36 +00:00
go func() {
// https://docs.ftx.com/?javascript#request-process
tk := time.NewTicker(15 * time.Second)
defer tk.Stop()
for {
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil {
logger.WithError(err).Errorf("websocket ping goroutine is terminated")
}
case <-tk.C:
if err := s.ws.Conn().WriteJSON(websocketRequest{
Operation: ping,
}); err != nil {
logger.WithError(err).Warnf("failed to ping, try in next tick")
}
}
}
}()
2021-03-27 08:58:51 +00:00
return nil
}
2021-03-27 11:02:19 +00:00
func (s *Stream) subscribePrivateEvents() {
2021-03-28 07:07:46 +00:00
s.addSubscription(websocketRequest{
2021-03-27 11:02:19 +00:00
Operation: subscribe,
Channel: privateOrdersChannel,
})
2021-03-28 07:07:46 +00:00
s.addSubscription(websocketRequest{
2021-03-28 06:42:54 +00:00
Operation: subscribe,
Channel: privateTradesChannel,
})
2021-03-27 11:02:19 +00:00
}
2021-03-28 07:07:46 +00:00
func (s *Stream) addSubscription(request websocketRequest) {
s.subscriptions = append(s.subscriptions, request)
}
func (s *Stream) SetPublicOnly() {
atomic.StoreInt32(&s.publicOnly, 1)
}
func (s *Stream) Subscribe(channel types.Channel, symbol string, option types.SubscribeOptions) {
if channel == types.BookChannel {
s.addSubscription(websocketRequest{
Operation: subscribe,
Channel: orderBookChannel,
Market: toLocalSymbol(TrimUpperString(symbol)),
})
} else if channel == types.KLineChannel {
// FTX does not support kline channel, do polling
go s.subscribeKLine(symbol, option)
} else {
2021-05-24 01:45:33 +00:00
panic("only support book/kline channel now")
2021-02-27 10:41:46 +00:00
}
}
func (s *Stream) handleChannelKlineMessage() {
for {
select {
case kline := <-s.klineMessage:
// FTX only returns closed kline
s.EmitKLineClosed(kline)
}
}
}
func (s *Stream) subscribeKLine(symbol string, option types.SubscribeOptions) {
interval := types.Interval(option.Interval)
if !isIntervalSupportedInKLine(interval) {
logger.Errorf("not supported kline interval %s", option.Interval)
return
}
2021-05-24 01:45:33 +00:00
ticker := time.NewTicker(interval.Duration())
defer ticker.Stop()
2021-05-24 01:45:33 +00:00
for {
select {
case <-s.ctx.Done():
if err := s.ctx.Err(); err != nil {
logger.WithError(err).Errorf("subscribeKLine goroutine is terminated")
}
case <-ticker.C:
klines := getLastKLine(s.exchange, s.ctx, symbol, interval)
if len(klines) >= 0 {
// handle mutiple klines, get the latest one
s.klineMessage <- klines[len(klines)-1]
}
}
2021-05-24 01:45:33 +00:00
}
}
2021-05-24 01:45:33 +00:00
func getLastKLine(e *Exchange, ctx context.Context, symbol string, interval types.Interval) []types.KLine {
// set since to more 30s ago to avoid getting no kline candle
since := time.Now().Add(time.Duration(-1*(interval.Minutes()*60+30)) * time.Second)
klines, err := e.QueryKLines(ctx, symbol, interval, types.KLineQueryOptions{
StartTime: &since,
})
if err != nil {
logger.WithError(err).Errorf("failed to get kline data")
return klines
}
2021-05-24 01:45:33 +00:00
return klines
}
2021-03-27 10:07:35 +00:00
2021-02-27 11:27:44 +00:00
func (s *Stream) Close() error {
2021-03-28 07:07:46 +00:00
s.subscriptions = nil
if s.ws != nil {
return s.ws.Conn().Close()
2021-03-27 01:54:12 +00:00
}
return nil
}