kucoin: fix kline parsing and subscription

This commit is contained in:
c9s 2021-12-23 17:32:56 +08:00
parent e2415857b0
commit e76dd1cbc4
9 changed files with 273 additions and 119 deletions

View File

@ -7,6 +7,7 @@ import (
"os/signal" "os/signal"
"time" "time"
"github.com/c9s/bbgo/pkg/exchange/kucoin"
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi" "github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -73,25 +74,33 @@ var websocketCmd = &cobra.Command{
defer c.Close() defer c.Close()
wsCmd := &kucoinapi.WebSocketCommand{ id := time.Now().UnixMilli()
Id: time.Now().UnixMilli(), wsCmds := []kucoin.WebSocketCommand{
Type: "subscribe", /*
Topic: "/market/ticker:ETH-USDT", {
PrivateChannel: false, Id: id+1,
Response: true, Type: "subscribe",
Topic: "/market/ticker:ETH-USDT",
PrivateChannel: false,
Response: true,
},
*/
{
Id: id+2,
Type: "subscribe",
Topic: "/market/candles:ETH-USDT_1min",
PrivateChannel: false,
Response: true,
},
} }
msg, err := wsCmd.JSON() for _, wsCmd := range wsCmds {
if err != nil { err = c.WriteJSON(wsCmd)
return err if err != nil {
return err
}
} }
err = c.WriteMessage(websocket.TextMessage, msg)
if err != nil {
return err
}
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
defer close(done) defer close(done)
@ -106,11 +115,23 @@ var websocketCmd = &cobra.Command{
} }
}() }()
pingTicker := time.NewTicker(bullet.PingInterval())
defer pingTicker.Stop()
for { for {
select { select {
case <-done: case <-done:
return nil return nil
case <-pingTicker.C:
if err := c.WriteJSON(kucoin.WebSocketCommand{
Id: time.Now().UnixMilli(),
Type: "ping",
}); err != nil {
logrus.WithError(err).Error("websocket ping error", err)
}
case <-interrupt: case <-interrupt:
logrus.Infof("interrupt") logrus.Infof("interrupt")

View File

@ -5,8 +5,10 @@ import (
"fmt" "fmt"
"syscall" "syscall"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/cmd/cmdutil" "github.com/c9s/bbgo/pkg/cmd/cmdutil"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -18,19 +20,23 @@ var klineCmd = &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background() ctx := context.Background()
exName, err := cmd.Flags().GetString("exchange") if userConfig == nil {
if err != nil { return errors.New("--config option or config file is missing")
return fmt.Errorf("can not get exchange from flags: %w", err)
} }
exchangeName, err := types.ValidExchangeName(exName) environ := bbgo.NewEnvironment()
if err := environ.ConfigureExchangeSessions(userConfig); err != nil {
return err
}
sessionName, err := cmd.Flags().GetString("session")
if err != nil { if err != nil {
return err return err
} }
ex, err := cmdutil.NewExchange(exchangeName) session, ok := environ.Session(sessionName)
if err != nil { if !ok {
return err return fmt.Errorf("session %s not found", sessionName)
} }
symbol, err := cmd.Flags().GetString("symbol") symbol, err := cmd.Flags().GetString("symbol")
@ -47,7 +53,7 @@ var klineCmd = &cobra.Command{
return err return err
} }
s := ex.NewStream() s := session.Exchange.NewStream()
s.SetPublicOnly() s.SetPublicOnly()
s.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{Interval: interval}) s.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{Interval: interval})
@ -61,9 +67,17 @@ var klineCmd = &cobra.Command{
log.Infof("connecting...") log.Infof("connecting...")
if err := s.Connect(ctx); err != nil { if err := s.Connect(ctx); err != nil {
return fmt.Errorf("failed to connect to %s", exchangeName) return err
} }
log.Infof("connected")
defer func() {
log.Infof("closing connection...")
if err := s.Close(); err != nil {
log.WithError(err).Errorf("connection close error")
}
}()
cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM)
return nil return nil
}, },
@ -71,7 +85,7 @@ var klineCmd = &cobra.Command{
func init() { func init() {
// since the public data does not require trading authentication, we use --exchange option here. // since the public data does not require trading authentication, we use --exchange option here.
klineCmd.Flags().String("exchange", "", "the exchange name") klineCmd.Flags().String("session", "", "session name")
klineCmd.Flags().String("symbol", "", "the trading pair. e.g, BTCUSDT, LTCUSDT...") klineCmd.Flags().String("symbol", "", "the trading pair. e.g, BTCUSDT, LTCUSDT...")
klineCmd.Flags().String("interval", "1m", "interval of the kline (candle), .e.g, 1m, 3m, 15m") klineCmd.Flags().String("interval", "1m", "interval of the kline (candle), .e.g, 1m, 3m, 15m")
RootCmd.AddCommand(klineCmd) RootCmd.AddCommand(klineCmd)

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"syscall" "syscall"
"time"
"github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/cmd/cmdutil" "github.com/c9s/bbgo/pkg/cmd/cmdutil"
@ -63,6 +64,7 @@ var userDataStreamCmd = &cobra.Command{
if err := s.Close(); err != nil { if err := s.Close(); err != nil {
log.WithError(err).Errorf("connection close error") log.WithError(err).Errorf("connection close error")
} }
time.Sleep(1 * time.Second)
}() }()
cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM)

View File

@ -67,11 +67,35 @@ func toGlobalTicker(s kucoinapi.Ticker24H) types.Ticker {
} }
} }
func toLocalInterval(i types.Interval) string {
switch i {
case types.Interval1m:
return "1min"
case types.Interval15m:
return "15min"
case types.Interval30m:
return "30min"
case types.Interval1h:
return "1hour"
case types.Interval2h:
return "2hour"
case types.Interval4h:
return "4hour"
}
return "1h"
}
// convertSubscriptions global subscription to local websocket command // convertSubscriptions global subscription to local websocket command
func convertSubscriptions(ss []types.Subscription) ([]kucoinapi.WebSocketCommand, error) { func convertSubscriptions(ss []types.Subscription) ([]WebSocketCommand, error) {
var id = time.Now().UnixMilli() var id = time.Now().UnixMilli()
var cmds []kucoinapi.WebSocketCommand var cmds []WebSocketCommand
for _, s := range ss { for _, s := range ss {
id++ id++
@ -82,15 +106,15 @@ func convertSubscriptions(ss []types.Subscription) ([]kucoinapi.WebSocketCommand
subscribeTopic = "/market/level2" + ":" + toLocalSymbol(s.Symbol) subscribeTopic = "/market/level2" + ":" + toLocalSymbol(s.Symbol)
case types.KLineChannel: case types.KLineChannel:
subscribeTopic = "/market/candles" + ":" + toLocalSymbol(s.Symbol) + "_" + s.Options.Interval subscribeTopic = "/market/candles" + ":" + toLocalSymbol(s.Symbol) + "_" + toLocalInterval(types.Interval(s.Options.Interval))
default: default:
return nil, fmt.Errorf("websocket channel %s is not supported by kucoin", s.Channel) return nil, fmt.Errorf("websocket channel %s is not supported by kucoin", s.Channel)
} }
cmds = append(cmds, kucoinapi.WebSocketCommand{ cmds = append(cmds, WebSocketCommand{
Id: id, Id: id,
Type: kucoinapi.WebSocketMessageTypeSubscribe, Type: WebSocketMessageTypeSubscribe,
Topic: subscribeTopic, Topic: subscribeTopic,
PrivateChannel: false, PrivateChannel: false,
Response: true, Response: true,

View File

@ -2,53 +2,61 @@ package kucoin
import ( import (
"encoding/json" "encoding/json"
"strings"
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi" "github.com/c9s/bbgo/pkg/types"
) )
func parseWebsocketPayload(in []byte) (*kucoinapi.WebSocketEvent, error) { func parseWebsocketPayload(in []byte) (*WebSocketEvent, error) {
var resp kucoinapi.WebSocketEvent var resp WebSocketEvent
var err = json.Unmarshal(in, &resp) var err = json.Unmarshal(in, &resp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
switch resp.Type { switch resp.Type {
case kucoinapi.WebSocketMessageTypeAck: case WebSocketMessageTypeAck:
return &resp, nil return &resp, nil
case kucoinapi.WebSocketMessageTypeMessage: case WebSocketMessageTypeError:
resp.Object = string(resp.Data)
return &resp, nil
case WebSocketMessageTypeMessage:
switch resp.Subject { switch resp.Subject {
case kucoinapi.WebSocketSubjectOrderChange: case WebSocketSubjectOrderChange:
var o kucoinapi.WebSocketPrivateOrder var o WebSocketPrivateOrderEvent
if err := json.Unmarshal(resp.Data, &o); err != nil { if err := json.Unmarshal(resp.Data, &o); err != nil {
return &resp, err return &resp, err
} }
resp.Object = &o resp.Object = &o
case kucoinapi.WebSocketSubjectAccountBalance: case WebSocketSubjectAccountBalance:
var o kucoinapi.WebSocketAccountBalance var o WebSocketAccountBalanceEvent
if err := json.Unmarshal(resp.Data, &o); err != nil { if err := json.Unmarshal(resp.Data, &o); err != nil {
return &resp, err return &resp, err
} }
resp.Object = &o resp.Object = &o
case kucoinapi.WebSocketSubjectTradeCandlesUpdate: case WebSocketSubjectTradeCandlesUpdate, WebSocketSubjectTradeCandlesAdd:
var o kucoinapi.WebSocketCandle var o WebSocketCandleEvent
if err := json.Unmarshal(resp.Data, &o); err != nil {
return &resp, err
}
o.Interval = extractIntervalFromTopic(resp.Topic)
o.Add = resp.Subject == WebSocketSubjectTradeCandlesAdd
resp.Object = &o
case WebSocketSubjectTradeL2Update:
var o WebSocketOrderBookL2Event
if err := json.Unmarshal(resp.Data, &o); err != nil { if err := json.Unmarshal(resp.Data, &o); err != nil {
return &resp, err return &resp, err
} }
resp.Object = &o resp.Object = &o
case kucoinapi.WebSocketSubjectTradeL2Update: case WebSocketSubjectTradeTicker:
var o kucoinapi.WebSocketOrderBookL2 var o WebSocketTickerEvent
if err := json.Unmarshal(resp.Data, &o); err != nil {
return &resp, err
}
resp.Object = &o
case kucoinapi.WebSocketSubjectTradeTicker:
var o kucoinapi.WebSocketTicker
if err := json.Unmarshal(resp.Data, &o); err != nil { if err := json.Unmarshal(resp.Data, &o); err != nil {
return &resp, err return &resp, err
} }
@ -62,3 +70,33 @@ func parseWebsocketPayload(in []byte) (*kucoinapi.WebSocketEvent, error) {
return &resp, nil return &resp, nil
} }
func extractIntervalFromTopic(topic string) types.Interval {
ta := strings.Split(topic, ":")
tb := strings.Split(ta[1], "_")
interval := tb[1]
return toGlobalInterval(interval)
}
func toGlobalInterval(a string) types.Interval {
switch a {
case "1min":
return types.Interval1m
case "15min":
return types.Interval15m
case "30min":
return types.Interval30m
case "1hour":
return types.Interval1h
case "2hour":
return types.Interval2h
case "4hour":
return types.Interval4h
case "6hour":
return types.Interval6h
case "12hour":
return types.Interval12h
}
return ""
}

View File

@ -14,18 +14,6 @@ import (
const readTimeout = 30 * time.Second const readTimeout = 30 * time.Second
type WebsocketOp struct {
Op string `json:"op"`
Args interface{} `json:"args"`
}
type WebsocketLogin struct {
Key string `json:"apiKey"`
Passphrase string `json:"passphrase"`
Timestamp string `json:"timestamp"`
Sign string `json:"sign"`
}
//go:generate callbackgen -type Stream -interface //go:generate callbackgen -type Stream -interface
type Stream struct { type Stream struct {
types.StandardStream types.StandardStream
@ -36,13 +24,15 @@ type Stream struct {
connCtx context.Context connCtx context.Context
connCancel context.CancelFunc connCancel context.CancelFunc
bullet *kucoinapi.Bullet bullet *kucoinapi.Bullet
candleEventCallbacks []func(e *kucoinapi.WebSocketCandle) candleEventCallbacks []func(candle *WebSocketCandleEvent, e *WebSocketEvent)
orderBookL2EventCallbacks []func(e *kucoinapi.WebSocketOrderBookL2) orderBookL2EventCallbacks []func(e *WebSocketOrderBookL2Event)
tickerEventCallbacks []func(e *kucoinapi.WebSocketTicker) tickerEventCallbacks []func(e *WebSocketTickerEvent)
accountBalanceEventCallbacks []func(e *kucoinapi.WebSocketAccountBalance) accountBalanceEventCallbacks []func(e *WebSocketAccountBalanceEvent)
privateOrderEventCallbacks []func(e *kucoinapi.WebSocketPrivateOrder) privateOrderEventCallbacks []func(e *WebSocketPrivateOrderEvent)
lastCandle map[string]types.KLine
} }
func NewStream(client *kucoinapi.RestClient) *Stream { func NewStream(client *kucoinapi.RestClient) *Stream {
@ -51,6 +41,7 @@ func NewStream(client *kucoinapi.RestClient) *Stream {
StandardStream: types.StandardStream{ StandardStream: types.StandardStream{
ReconnectC: make(chan struct{}, 1), ReconnectC: make(chan struct{}, 1),
}, },
lastCandle: make(map[string]types.KLine),
} }
stream.OnConnect(stream.handleConnect) stream.OnConnect(stream.handleConnect)
@ -58,16 +49,27 @@ func NewStream(client *kucoinapi.RestClient) *Stream {
stream.OnOrderBookL2Event(stream.handleOrderBookL2Event) stream.OnOrderBookL2Event(stream.handleOrderBookL2Event)
stream.OnTickerEvent(stream.handleTickerEvent) stream.OnTickerEvent(stream.handleTickerEvent)
stream.OnPrivateOrderEvent(stream.handlePrivateOrderEvent) stream.OnPrivateOrderEvent(stream.handlePrivateOrderEvent)
stream.OnAccountBalanceEvent(stream.handleAccountBalanceEvent)
return stream return stream
} }
func (s *Stream) handleCandleEvent(e *kucoinapi.WebSocketCandle) {} func (s *Stream) handleCandleEvent(candle *WebSocketCandleEvent, e *WebSocketEvent) {
kline := candle.KLine()
last, ok := s.lastCandle[e.Topic]
if ok && kline.StartTime.After(last.StartTime.Time()) || e.Subject == WebSocketSubjectTradeCandlesAdd {
last.Closed = true
s.EmitKLineClosed(last)
}
func (s *Stream) handleOrderBookL2Event(e *kucoinapi.WebSocketOrderBookL2) {} s.EmitKLine(kline)
s.lastCandle[e.Topic] = kline
}
func (s *Stream) handleTickerEvent(e *kucoinapi.WebSocketTicker) {} func (s *Stream) handleOrderBookL2Event(e *WebSocketOrderBookL2Event) {}
func (s *Stream) handleAccountBalanceEvent(e *kucoinapi.WebSocketAccountBalance) { func (s *Stream) handleTickerEvent(e *WebSocketTickerEvent) {}
func (s *Stream) handleAccountBalanceEvent(e *WebSocketAccountBalanceEvent) {
bm := types.BalanceMap{} bm := types.BalanceMap{}
bm[e.Currency] = types.Balance{ bm[e.Currency] = types.Balance{
Currency: e.Currency, Currency: e.Currency,
@ -77,7 +79,7 @@ func (s *Stream) handleAccountBalanceEvent(e *kucoinapi.WebSocketAccountBalance)
s.StandardStream.EmitBalanceUpdate(bm) s.StandardStream.EmitBalanceUpdate(bm)
} }
func (s *Stream) handlePrivateOrderEvent(e *kucoinapi.WebSocketPrivateOrder) { func (s *Stream) handlePrivateOrderEvent(e *WebSocketPrivateOrderEvent) {
if e.Type == "match" { if e.Type == "match" {
s.StandardStream.EmitTradeUpdate(types.Trade{ s.StandardStream.EmitTradeUpdate(types.Trade{
OrderID: hashStringID(e.OrderId), OrderID: hashStringID(e.OrderId),
@ -136,17 +138,17 @@ func (s *Stream) handleConnect() {
} }
} else { } else {
id := time.Now().UnixMilli() id := time.Now().UnixMilli()
cmds := []kucoinapi.WebSocketCommand{ cmds := []WebSocketCommand{
{ {
Id: id, Id: id,
Type: kucoinapi.WebSocketMessageTypeSubscribe, Type: WebSocketMessageTypeSubscribe,
Topic: "/spotMarket/tradeOrders", Topic: "/spotMarket/tradeOrders",
PrivateChannel: true, PrivateChannel: true,
Response: true, Response: true,
}, },
{ {
Id: id + 1, Id: id + 1,
Type: kucoinapi.WebSocketMessageTypeSubscribe, Type: WebSocketMessageTypeSubscribe,
Topic: "/account/balance", Topic: "/account/balance",
PrivateChannel: true, PrivateChannel: true,
Response: true, Response: true,
@ -160,7 +162,6 @@ func (s *Stream) handleConnect() {
} }
} }
func (s *Stream) Close() error { func (s *Stream) Close() error {
conn := s.Conn() conn := s.Conn()
return conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) return conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
@ -333,7 +334,7 @@ func (s *Stream) read(ctx context.Context) {
} }
// used for debugging // used for debugging
// fmt.Println(string(message)) // log.Println(string(message))
e, err := parseWebsocketPayload(message) e, err := parseWebsocketPayload(message)
if err != nil { if err != nil {
@ -352,22 +353,22 @@ func (s *Stream) read(ctx context.Context) {
} }
} }
func (s *Stream) dispatchEvent(e *kucoinapi.WebSocketEvent) { func (s *Stream) dispatchEvent(e *WebSocketEvent) {
switch et := e.Object.(type) { switch et := e.Object.(type) {
case *kucoinapi.WebSocketTicker: case *WebSocketTickerEvent:
s.EmitTickerEvent(et) s.EmitTickerEvent(et)
case *kucoinapi.WebSocketOrderBookL2: case *WebSocketOrderBookL2Event:
s.EmitOrderBookL2Event(et) s.EmitOrderBookL2Event(et)
case *kucoinapi.WebSocketCandle: case *WebSocketCandleEvent:
s.EmitCandleEvent(et) s.EmitCandleEvent(et, e)
case *kucoinapi.WebSocketAccountBalance: case *WebSocketAccountBalanceEvent:
s.EmitAccountBalanceEvent(et) s.EmitAccountBalanceEvent(et)
case *kucoinapi.WebSocketPrivateOrder: case *WebSocketPrivateOrderEvent:
s.EmitPrivateOrderEvent(et) s.EmitPrivateOrderEvent(et)
default: default:
@ -404,7 +405,7 @@ func ping(ctx context.Context, w WebSocketConnector, interval time.Duration) {
case <-pingTicker.C: case <-pingTicker.C:
conn := w.Conn() conn := w.Conn()
if err := conn.WriteJSON(kucoinapi.WebSocketCommand{ if err := conn.WriteJSON(WebSocketCommand{
Id: time.Now().UnixMilli(), Id: time.Now().UnixMilli(),
Type: "ping", Type: "ping",
}); err != nil { }); err != nil {

View File

@ -2,68 +2,64 @@
package kucoin package kucoin
import ( func (s *Stream) OnCandleEvent(cb func(candle *WebSocketCandleEvent, e *WebSocketEvent)) {
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
)
func (s *Stream) OnCandleEvent(cb func(c *kucoinapi.WebSocketCandle)) {
s.candleEventCallbacks = append(s.candleEventCallbacks, cb) s.candleEventCallbacks = append(s.candleEventCallbacks, cb)
} }
func (s *Stream) EmitCandleEvent(c *kucoinapi.WebSocketCandle) { func (s *Stream) EmitCandleEvent(candle *WebSocketCandleEvent, e *WebSocketEvent) {
for _, cb := range s.candleEventCallbacks { for _, cb := range s.candleEventCallbacks {
cb(c) cb(candle, e)
} }
} }
func (s *Stream) OnOrderBookL2Event(cb func(c *kucoinapi.WebSocketOrderBookL2)) { func (s *Stream) OnOrderBookL2Event(cb func(e *WebSocketOrderBookL2Event)) {
s.orderBookL2EventCallbacks = append(s.orderBookL2EventCallbacks, cb) s.orderBookL2EventCallbacks = append(s.orderBookL2EventCallbacks, cb)
} }
func (s *Stream) EmitOrderBookL2Event(c *kucoinapi.WebSocketOrderBookL2) { func (s *Stream) EmitOrderBookL2Event(e *WebSocketOrderBookL2Event) {
for _, cb := range s.orderBookL2EventCallbacks { for _, cb := range s.orderBookL2EventCallbacks {
cb(c) cb(e)
} }
} }
func (s *Stream) OnTickerEvent(cb func(c *kucoinapi.WebSocketTicker)) { func (s *Stream) OnTickerEvent(cb func(e *WebSocketTickerEvent)) {
s.tickerEventCallbacks = append(s.tickerEventCallbacks, cb) s.tickerEventCallbacks = append(s.tickerEventCallbacks, cb)
} }
func (s *Stream) EmitTickerEvent(c *kucoinapi.WebSocketTicker) { func (s *Stream) EmitTickerEvent(e *WebSocketTickerEvent) {
for _, cb := range s.tickerEventCallbacks { for _, cb := range s.tickerEventCallbacks {
cb(c) cb(e)
} }
} }
func (s *Stream) OnAccountBalanceEvent(cb func(c *kucoinapi.WebSocketAccountBalance)) { func (s *Stream) OnAccountBalanceEvent(cb func(e *WebSocketAccountBalanceEvent)) {
s.accountBalanceEventCallbacks = append(s.accountBalanceEventCallbacks, cb) s.accountBalanceEventCallbacks = append(s.accountBalanceEventCallbacks, cb)
} }
func (s *Stream) EmitAccountBalanceEvent(c *kucoinapi.WebSocketAccountBalance) { func (s *Stream) EmitAccountBalanceEvent(e *WebSocketAccountBalanceEvent) {
for _, cb := range s.accountBalanceEventCallbacks { for _, cb := range s.accountBalanceEventCallbacks {
cb(c) cb(e)
} }
} }
func (s *Stream) OnPrivateOrderEvent(cb func(c *kucoinapi.WebSocketPrivateOrder)) { func (s *Stream) OnPrivateOrderEvent(cb func(e *WebSocketPrivateOrderEvent)) {
s.privateOrderEventCallbacks = append(s.privateOrderEventCallbacks, cb) s.privateOrderEventCallbacks = append(s.privateOrderEventCallbacks, cb)
} }
func (s *Stream) EmitPrivateOrderEvent(c *kucoinapi.WebSocketPrivateOrder) { func (s *Stream) EmitPrivateOrderEvent(e *WebSocketPrivateOrderEvent) {
for _, cb := range s.privateOrderEventCallbacks { for _, cb := range s.privateOrderEventCallbacks {
cb(c) cb(e)
} }
} }
type StreamEventHub interface { type StreamEventHub interface {
OnCandleEvent(cb func(c *kucoinapi.WebSocketCandle)) OnCandleEvent(cb func(candle *WebSocketCandleEvent, e *WebSocketEvent))
OnOrderBookL2Event(cb func(c *kucoinapi.WebSocketOrderBookL2)) OnOrderBookL2Event(cb func(e *WebSocketOrderBookL2Event))
OnTickerEvent(cb func(c *kucoinapi.WebSocketTicker)) OnTickerEvent(cb func(e *WebSocketTickerEvent))
OnAccountBalanceEvent(cb func(c *kucoinapi.WebSocketAccountBalance)) OnAccountBalanceEvent(cb func(e *WebSocketAccountBalanceEvent))
OnPrivateOrderEvent(cb func(c *kucoinapi.WebSocketPrivateOrder)) OnPrivateOrderEvent(cb func(e *WebSocketPrivateOrderEvent))
} }

View File

@ -1,10 +1,12 @@
package kucoinapi package kucoin
import ( import (
"encoding/json" "encoding/json"
"time"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
) )
type WebSocketMessageType string type WebSocketMessageType string
@ -14,6 +16,7 @@ const (
WebSocketMessageTypeSubscribe WebSocketMessageType = "subscribe" WebSocketMessageTypeSubscribe WebSocketMessageType = "subscribe"
WebSocketMessageTypeUnsubscribe WebSocketMessageType = "unsubscribe" WebSocketMessageTypeUnsubscribe WebSocketMessageType = "unsubscribe"
WebSocketMessageTypeAck WebSocketMessageType = "ack" WebSocketMessageTypeAck WebSocketMessageType = "ack"
WebSocketMessageTypeError WebSocketMessageType = "error"
WebSocketMessageTypePong WebSocketMessageType = "pong" WebSocketMessageTypePong WebSocketMessageType = "pong"
WebSocketMessageTypeWelcome WebSocketMessageType = "welcome" WebSocketMessageTypeWelcome WebSocketMessageType = "welcome"
WebSocketMessageTypeMessage WebSocketMessageType = "message" WebSocketMessageTypeMessage WebSocketMessageType = "message"
@ -27,6 +30,7 @@ const (
WebSocketSubjectTradeL2Update WebSocketSubject = "trade.l2update" // order book L2 WebSocketSubjectTradeL2Update WebSocketSubject = "trade.l2update" // order book L2
WebSocketSubjectLevel2 WebSocketSubject = "level2" // level2 WebSocketSubjectLevel2 WebSocketSubject = "level2" // level2
WebSocketSubjectTradeCandlesUpdate WebSocketSubject = "trade.candles.update" WebSocketSubjectTradeCandlesUpdate WebSocketSubject = "trade.candles.update"
WebSocketSubjectTradeCandlesAdd WebSocketSubject = "trade.candles.add"
// private subjects // private subjects
WebSocketSubjectOrderChange WebSocketSubject = "orderChange" WebSocketSubjectOrderChange WebSocketSubject = "orderChange"
@ -53,12 +57,13 @@ type WebSocketEvent struct {
Topic string `json:"topic"` Topic string `json:"topic"`
Subject WebSocketSubject `json:"subject"` Subject WebSocketSubject `json:"subject"`
Data json.RawMessage `json:"data"` Data json.RawMessage `json:"data"`
Code int `json:"code"` // used in type error
// Object is used for storing the parsed Data // Object is used for storing the parsed Data
Object interface{} `json:"-"` Object interface{} `json:"-"`
} }
type WebSocketTicker struct { type WebSocketTickerEvent struct {
Sequence string `json:"sequence"` Sequence string `json:"sequence"`
Price fixedpoint.Value `json:"price"` Price fixedpoint.Value `json:"price"`
Size fixedpoint.Value `json:"size"` Size fixedpoint.Value `json:"size"`
@ -68,7 +73,7 @@ type WebSocketTicker struct {
BestBidSize fixedpoint.Value `json:"bestBidSize"` BestBidSize fixedpoint.Value `json:"bestBidSize"`
} }
type WebSocketOrderBookL2 struct { type WebSocketOrderBookL2Event struct {
SequenceStart int64 `json:"sequenceStart"` SequenceStart int64 `json:"sequenceStart"`
SequenceEnd int64 `json:"sequenceEnd"` SequenceEnd int64 `json:"sequenceEnd"`
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
@ -78,13 +83,44 @@ type WebSocketOrderBookL2 struct {
} `json:"changes"` } `json:"changes"`
} }
type WebSocketCandle struct { type WebSocketCandleEvent struct {
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
Candles []string `json:"candles"` Candles []string `json:"candles"`
Time int64 `json:"time"` Time types.MillisecondTimestamp `json:"time"`
// Interval is an injected field (not from the payload)
Interval types.Interval
// Is a new candle or not
Add bool
} }
type WebSocketPrivateOrder struct { func (e *WebSocketCandleEvent) KLine() types.KLine {
startTime := types.MustParseUnixTimestamp(e.Candles[0])
openPrice := util.MustParseFloat(e.Candles[1])
closePrice := util.MustParseFloat(e.Candles[2])
highPrice := util.MustParseFloat(e.Candles[3])
lowPrice := util.MustParseFloat(e.Candles[4])
volume := util.MustParseFloat(e.Candles[5])
quoteVolume := util.MustParseFloat(e.Candles[6])
kline := types.KLine{
Exchange: types.ExchangeKucoin,
Symbol: toGlobalSymbol(e.Symbol),
StartTime: types.Time(startTime),
EndTime: types.Time(startTime.Add(e.Interval.Duration() - time.Millisecond)),
Interval: e.Interval,
Open: openPrice,
Close: closePrice,
High: highPrice,
Low: lowPrice,
Volume: volume,
QuoteVolume: quoteVolume,
Closed: false,
}
return kline
}
type WebSocketPrivateOrderEvent struct {
OrderId string `json:"orderId"` OrderId string `json:"orderId"`
TradeId string `json:"tradeId"` TradeId string `json:"tradeId"`
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
@ -105,7 +141,7 @@ type WebSocketPrivateOrder struct {
Ts types.MillisecondTimestamp `json:"ts"` Ts types.MillisecondTimestamp `json:"ts"`
} }
type WebSocketAccountBalance struct { type WebSocketAccountBalanceEvent struct {
Total fixedpoint.Value `json:"total"` Total fixedpoint.Value `json:"total"`
Available fixedpoint.Value `json:"available"` Available fixedpoint.Value `json:"available"`
AvailableChange fixedpoint.Value `json:"availableChange"` AvailableChange fixedpoint.Value `json:"availableChange"`

View File

@ -10,6 +10,28 @@ import (
type MillisecondTimestamp time.Time type MillisecondTimestamp time.Time
func NewMillisecondTimestampFromInt(i int64) MillisecondTimestamp {
return MillisecondTimestamp(time.Unix(0, i * int64(time.Millisecond)))
}
func MustParseMillisecondTimestamp(a string) MillisecondTimestamp {
m, err := strconv.ParseInt(a, 10, 64) // startTime
if err != nil {
panic(fmt.Errorf("millisecond timestamp parse error %v", err))
}
return NewMillisecondTimestampFromInt(m)
}
func MustParseUnixTimestamp(a string) time.Time {
m, err := strconv.ParseInt(a, 10, 64) // startTime
if err != nil {
panic(fmt.Errorf("millisecond timestamp parse error %v", err))
}
return time.Unix(m, 0)
}
func (t MillisecondTimestamp) String() string { func (t MillisecondTimestamp) String() string {
return time.Time(t).String() return time.Time(t).String()
} }