2020-08-31 04:32:51 +00:00
|
|
|
package max
|
2020-10-03 10:35:28 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2022-01-02 04:00:06 +00:00
|
|
|
"crypto/hmac"
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
2021-03-22 07:47:05 +00:00
|
|
|
"os"
|
2020-10-19 14:23:49 +00:00
|
|
|
"strconv"
|
|
|
|
"time"
|
2020-10-03 10:35:28 +00:00
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
"github.com/google/uuid"
|
2020-11-07 04:38:57 +00:00
|
|
|
|
2020-10-11 08:46:15 +00:00
|
|
|
max "github.com/c9s/bbgo/pkg/exchange/max/maxapi"
|
2020-10-29 14:53:04 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
2020-10-11 08:46:15 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/types"
|
2020-10-29 14:53:04 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/util"
|
2020-10-03 10:35:28 +00:00
|
|
|
)
|
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
//go:generate callbackgen -type Stream
|
2020-10-03 10:35:28 +00:00
|
|
|
type Stream struct {
|
|
|
|
types.StandardStream
|
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
key, secret string
|
|
|
|
|
|
|
|
bookEventCallbacks []func(e max.BookEvent)
|
|
|
|
tradeEventCallbacks []func(e max.PublicTradeEvent)
|
|
|
|
kLineEventCallbacks []func(e max.KLineEvent)
|
|
|
|
errorEventCallbacks []func(e max.ErrorEvent)
|
|
|
|
subscriptionEventCallbacks []func(e max.SubscriptionEvent)
|
|
|
|
|
|
|
|
tradeUpdateEventCallbacks []func(e max.TradeUpdateEvent)
|
|
|
|
tradeSnapshotEventCallbacks []func(e max.TradeSnapshotEvent)
|
|
|
|
orderUpdateEventCallbacks []func(e max.OrderUpdateEvent)
|
|
|
|
orderSnapshotEventCallbacks []func(e max.OrderSnapshotEvent)
|
|
|
|
|
|
|
|
accountSnapshotEventCallbacks []func(e max.AccountSnapshotEvent)
|
|
|
|
accountUpdateEventCallbacks []func(e max.AccountUpdateEvent)
|
2020-10-03 10:35:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewStream(key, secret string) *Stream {
|
2022-01-02 04:00:06 +00:00
|
|
|
stream := &Stream{
|
|
|
|
StandardStream: types.NewStandardStream(),
|
|
|
|
key: key,
|
|
|
|
secret: secret,
|
|
|
|
}
|
|
|
|
stream.SetEndpointCreator(stream.getEndpoint)
|
|
|
|
stream.SetParser(max.ParseMessage)
|
|
|
|
stream.SetDispatcher(stream.dispatchEvent)
|
|
|
|
|
|
|
|
stream.OnConnect(stream.handleConnect)
|
|
|
|
stream.OnKLineEvent(stream.handleKLineEvent)
|
|
|
|
stream.OnOrderSnapshotEvent(stream.handleOrderSnapshotEvent)
|
|
|
|
stream.OnOrderUpdateEvent(stream.handleOrderUpdateEvent)
|
|
|
|
stream.OnTradeUpdateEvent(stream.handleTradeEvent)
|
|
|
|
stream.OnBookEvent(stream.handleBookEvent)
|
|
|
|
stream.OnAccountSnapshotEvent(stream.handleAccountSnapshotEvent)
|
|
|
|
stream.OnAccountUpdateEvent(stream.handleAccountUpdateEvent)
|
|
|
|
return stream
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Stream) getEndpoint(ctx context.Context) (string, error) {
|
2021-03-22 07:47:05 +00:00
|
|
|
url := os.Getenv("MAX_API_WS_URL")
|
|
|
|
if url == "" {
|
|
|
|
url = max.WebSocketURL
|
|
|
|
}
|
2022-01-02 04:00:06 +00:00
|
|
|
return url, nil
|
|
|
|
}
|
2020-10-03 11:14:15 +00:00
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
func (s *Stream) handleConnect() {
|
|
|
|
if s.PublicOnly {
|
|
|
|
cmd := &max.WebsocketCommand{
|
|
|
|
Action: "subscribe",
|
2020-11-05 07:04:56 +00:00
|
|
|
}
|
2022-01-02 04:00:06 +00:00
|
|
|
for _, sub := range s.Subscriptions {
|
|
|
|
var err error
|
|
|
|
var depth int
|
|
|
|
|
|
|
|
if len(sub.Options.Depth) > 0 {
|
|
|
|
depth, err = strconv.Atoi(sub.Options.Depth)
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Errorf("depth parse error, given %v", sub.Options.Depth)
|
|
|
|
continue
|
|
|
|
}
|
2020-10-29 14:53:04 +00:00
|
|
|
}
|
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
cmd.Subscriptions = append(cmd.Subscriptions, max.Subscription{
|
|
|
|
Channel: string(sub.Channel),
|
|
|
|
Market: toLocalSymbol(sub.Symbol),
|
|
|
|
Depth: depth,
|
|
|
|
Resolution: sub.Options.Interval,
|
|
|
|
})
|
2020-10-29 14:53:04 +00:00
|
|
|
}
|
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
s.Conn.WriteJSON(cmd)
|
|
|
|
} else {
|
|
|
|
nonce := time.Now().UnixNano() / int64(time.Millisecond)
|
|
|
|
auth := &max.AuthMessage{
|
|
|
|
Action: "auth",
|
|
|
|
APIKey: s.key,
|
|
|
|
Nonce: nonce,
|
|
|
|
Signature: signPayload(fmt.Sprintf("%d", nonce), s.secret),
|
|
|
|
ID: uuid.New().String(),
|
2020-10-29 14:53:04 +00:00
|
|
|
}
|
2022-01-02 04:00:06 +00:00
|
|
|
if err := s.Conn.WriteJSON(auth); err != nil {
|
|
|
|
log.WithError(err).Error("failed to send auth request")
|
2020-10-19 14:23:49 +00:00
|
|
|
}
|
2022-01-02 04:00:06 +00:00
|
|
|
}
|
|
|
|
}
|
2020-10-19 14:23:49 +00:00
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
func (s *Stream) handleKLineEvent(e max.KLineEvent) {
|
|
|
|
kline := e.KLine.KLine()
|
|
|
|
s.EmitKLine(kline)
|
|
|
|
if kline.Closed {
|
|
|
|
s.EmitKLineClosed(kline)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Stream) handleOrderSnapshotEvent(e max.OrderSnapshotEvent) {
|
|
|
|
for _, o := range e.Orders {
|
|
|
|
globalOrder, err := toGlobalOrderUpdate(o)
|
2020-10-03 10:35:28 +00:00
|
|
|
if err != nil {
|
2022-01-02 04:00:06 +00:00
|
|
|
log.WithError(err).Error("websocket order snapshot convert error")
|
|
|
|
continue
|
2020-10-03 10:35:28 +00:00
|
|
|
}
|
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
s.EmitOrderUpdate(*globalOrder)
|
|
|
|
}
|
|
|
|
}
|
2020-10-03 11:14:15 +00:00
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
func (s *Stream) handleOrderUpdateEvent(e max.OrderUpdateEvent) {
|
|
|
|
for _, o := range e.Orders {
|
|
|
|
globalOrder, err := toGlobalOrderUpdate(o)
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Error("websocket order update convert error")
|
|
|
|
continue
|
2020-10-03 11:14:15 +00:00
|
|
|
}
|
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
s.EmitOrderUpdate(*globalOrder)
|
|
|
|
}
|
|
|
|
}
|
2020-10-03 11:14:15 +00:00
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
func (s *Stream) handleTradeEvent(e max.TradeUpdateEvent) {
|
|
|
|
for _, tradeUpdate := range e.Trades {
|
|
|
|
trade, err := convertWebSocketTrade(tradeUpdate)
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Error("websocket trade update convert error")
|
|
|
|
return
|
2020-10-03 11:14:15 +00:00
|
|
|
}
|
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
s.EmitTradeUpdate(*trade)
|
|
|
|
}
|
|
|
|
}
|
2020-10-03 11:14:15 +00:00
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
func (s *Stream) handleBookEvent(e max.BookEvent) {
|
|
|
|
newBook, err := e.OrderBook()
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Error("book convert error")
|
|
|
|
return
|
|
|
|
}
|
2020-12-28 08:24:35 +00:00
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
newBook.Symbol = toGlobalSymbol(e.Market)
|
2020-10-03 10:35:28 +00:00
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
switch e.Event {
|
|
|
|
case "snapshot":
|
|
|
|
s.EmitBookSnapshot(newBook)
|
|
|
|
case "update":
|
|
|
|
s.EmitBookUpdate(newBook)
|
|
|
|
}
|
|
|
|
}
|
2021-02-22 09:36:23 +00:00
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
func (s *Stream) handleAccountSnapshotEvent(e max.AccountSnapshotEvent) {
|
|
|
|
snapshot := map[string]types.Balance{}
|
|
|
|
for _, bm := range e.Balances {
|
|
|
|
balance, err := bm.Balance()
|
2021-02-22 09:36:23 +00:00
|
|
|
if err != nil {
|
2022-01-02 04:00:06 +00:00
|
|
|
continue
|
2021-02-22 09:36:23 +00:00
|
|
|
}
|
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
snapshot[balance.Currency] = *balance
|
2021-02-22 09:36:23 +00:00
|
|
|
}
|
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
s.EmitBalanceSnapshot(snapshot)
|
2020-10-03 11:14:15 +00:00
|
|
|
}
|
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
func (s *Stream) handleAccountUpdateEvent(e max.AccountUpdateEvent) {
|
|
|
|
snapshot := map[string]types.Balance{}
|
|
|
|
for _, bm := range e.Balances {
|
|
|
|
balance, err := bm.Balance()
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
2021-03-15 17:32:27 +00:00
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
snapshot[toGlobalCurrency(balance.Currency)] = *balance
|
|
|
|
}
|
2020-10-03 10:35:28 +00:00
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
s.EmitBalanceUpdate(snapshot)
|
2020-10-03 10:35:28 +00:00
|
|
|
}
|
2020-10-19 14:23:49 +00:00
|
|
|
|
|
|
|
func convertWebSocketTrade(t max.TradeUpdate) (*types.Trade, error) {
|
|
|
|
// skip trade ID that is the same. however this should not happen
|
|
|
|
var side = toGlobalSideType(t.Side)
|
|
|
|
|
|
|
|
// trade time
|
|
|
|
mts := time.Unix(0, t.Timestamp*int64(time.Millisecond))
|
|
|
|
|
|
|
|
price, err := strconv.ParseFloat(t.Price, 64)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
quantity, err := strconv.ParseFloat(t.Volume, 64)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
quoteQuantity := price * quantity
|
|
|
|
|
|
|
|
fee, err := strconv.ParseFloat(t.Fee, 64)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &types.Trade{
|
2021-12-23 05:15:27 +00:00
|
|
|
ID: t.ID,
|
2020-11-17 04:46:55 +00:00
|
|
|
OrderID: t.OrderID,
|
2020-10-19 14:23:49 +00:00
|
|
|
Symbol: toGlobalSymbol(t.Market),
|
2021-05-16 09:02:23 +00:00
|
|
|
Exchange: types.ExchangeMax,
|
2020-11-17 07:48:18 +00:00
|
|
|
Price: price,
|
2020-10-19 14:23:49 +00:00
|
|
|
Quantity: quantity,
|
|
|
|
Side: side,
|
2021-02-07 06:54:06 +00:00
|
|
|
IsBuyer: side == types.SideTypeBuy,
|
2020-10-19 14:23:49 +00:00
|
|
|
IsMaker: t.Maker,
|
|
|
|
Fee: fee,
|
|
|
|
FeeCurrency: toGlobalCurrency(t.FeeCurrency),
|
|
|
|
QuoteQuantity: quoteQuantity,
|
2021-05-19 17:32:26 +00:00
|
|
|
Time: types.Time(mts),
|
2020-10-19 14:23:49 +00:00
|
|
|
}, nil
|
|
|
|
}
|
2020-10-29 14:53:04 +00:00
|
|
|
|
|
|
|
func toGlobalOrderUpdate(u max.OrderUpdate) (*types.Order, error) {
|
|
|
|
executedVolume, err := fixedpoint.NewFromString(u.ExecutedVolume)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
remainingVolume, err := fixedpoint.NewFromString(u.RemainingVolume)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &types.Order{
|
|
|
|
SubmitOrder: types.SubmitOrder{
|
|
|
|
ClientOrderID: u.ClientOID,
|
2020-11-07 04:19:57 +00:00
|
|
|
Symbol: toGlobalSymbol(u.Market),
|
2020-10-29 14:53:04 +00:00
|
|
|
Side: toGlobalSideType(u.Side),
|
|
|
|
Type: toGlobalOrderType(u.OrderType),
|
|
|
|
Quantity: util.MustParseFloat(u.Volume),
|
|
|
|
Price: util.MustParseFloat(u.Price),
|
|
|
|
StopPrice: util.MustParseFloat(u.StopPrice),
|
|
|
|
TimeInForce: "GTC", // MAX only supports GTC
|
2021-03-16 10:39:18 +00:00
|
|
|
GroupID: u.GroupID,
|
2020-10-29 14:53:04 +00:00
|
|
|
},
|
2021-05-16 09:02:23 +00:00
|
|
|
Exchange: types.ExchangeMax,
|
2020-10-29 14:53:04 +00:00
|
|
|
OrderID: u.ID,
|
|
|
|
Status: toGlobalOrderStatus(u.State, executedVolume, remainingVolume),
|
|
|
|
ExecutedQuantity: executedVolume.Float64(),
|
2021-05-19 17:32:26 +00:00
|
|
|
CreationTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))),
|
2020-10-29 14:53:04 +00:00
|
|
|
}, nil
|
|
|
|
}
|
2022-01-02 04:00:06 +00:00
|
|
|
|
|
|
|
func (s *Stream) dispatchEvent(e interface{}) {
|
|
|
|
switch e := e.(type) {
|
|
|
|
|
|
|
|
case *max.BookEvent:
|
|
|
|
s.EmitBookEvent(*e)
|
|
|
|
|
|
|
|
case *max.PublicTradeEvent:
|
|
|
|
s.EmitTradeEvent(*e)
|
|
|
|
|
|
|
|
case *max.KLineEvent:
|
|
|
|
s.EmitKLineEvent(*e)
|
|
|
|
|
|
|
|
case *max.ErrorEvent:
|
|
|
|
s.EmitErrorEvent(*e)
|
|
|
|
|
|
|
|
case *max.SubscriptionEvent:
|
|
|
|
s.EmitSubscriptionEvent(*e)
|
|
|
|
|
|
|
|
case *max.TradeSnapshotEvent:
|
|
|
|
s.EmitTradeSnapshotEvent(*e)
|
|
|
|
|
|
|
|
case *max.TradeUpdateEvent:
|
|
|
|
s.EmitTradeUpdateEvent(*e)
|
|
|
|
|
|
|
|
case *max.AccountSnapshotEvent:
|
|
|
|
s.EmitAccountSnapshotEvent(*e)
|
|
|
|
|
|
|
|
case *max.AccountUpdateEvent:
|
|
|
|
s.EmitAccountUpdateEvent(*e)
|
|
|
|
|
|
|
|
case *max.OrderSnapshotEvent:
|
|
|
|
s.EmitOrderSnapshotEvent(*e)
|
|
|
|
|
|
|
|
case *max.OrderUpdateEvent:
|
|
|
|
s.EmitOrderUpdateEvent(*e)
|
|
|
|
|
|
|
|
default:
|
|
|
|
log.Errorf("unsupported %T event: %+v", e, e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func signPayload(payload string, secret string) string {
|
|
|
|
var sig = hmac.New(sha256.New, []byte(secret))
|
|
|
|
_, err := sig.Write([]byte(payload))
|
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return hex.EncodeToString(sig.Sum(nil))
|
|
|
|
}
|