package max import ( "strings" "time" "github.com/pkg/errors" "github.com/valyala/fastjson" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) var ErrIncorrectBookEntryElementLength = errors.New("incorrect book entry element length") const ( Buy = 1 Sell = -1 ) const ( BookEventSnapshot string = "snapshot" BookEventUpdate string = "update" ) var parserPool fastjson.ParserPool // ParseMessage accepts the raw messages from max public websocket channels and parses them into market data // Return types: *BookEvent, *PublicTradeEvent, *SubscriptionEvent, *ErrorEvent func ParseMessage(payload []byte) (interface{}, error) { parser := parserPool.Get() val, err := parser.ParseBytes(payload) if err != nil { return nil, errors.Wrap(err, "failed to parse payload: "+string(payload)) } if channel := string(val.GetStringBytes("c")); len(channel) > 0 { switch channel { case "kline": return parseKLineEvent(val) case "book": return parseBookEvent(val) case "trade": return parsePublicTradeEvent(val) case "user": return ParseUserEvent(val) } } eventType := string(val.GetStringBytes("e")) switch eventType { case "authenticated": return parseAuthEvent(val) case "error": return parseErrorEvent(val) case "subscribed", "unsubscribed": return parseSubscriptionEvent(val) } return nil, errors.Wrapf(ErrMessageTypeNotSupported, "payload %s", payload) } type TradeEntry struct { Trend string `json:"tr"` Price string `json:"p"` Volume string `json:"v"` Timestamp int64 `json:"T"` } func (e TradeEntry) Time() time.Time { return time.Unix(0, e.Timestamp*int64(time.Millisecond)) } // parseTradeEntry parse the trade content payload func parseTradeEntry(val *fastjson.Value) TradeEntry { return TradeEntry{ Trend: strings.ToLower(string(val.GetStringBytes("tr"))), Timestamp: val.GetInt64("T"), Price: string(val.GetStringBytes("p")), Volume: string(val.GetStringBytes("v")), } } type KLineEvent struct { Event string `json:"e"` Market string `json:"M"` Channel string `json:"c"` KLine KLine `json:"k"` Timestamp int64 `json:"T"` } /* { "c": "kline", "M": "btcusdt", "e": "update", "T": 1602999650179, "k": { "ST": 1602999900000, "ET": 1602999900000, "M": "btcusdt", "R": "5m", "O": "11417.21", "H": "11417.21", "L": "11417.21", "C": "11417.21", "v": "0", "ti": 0, "x": false } } */ type KLinePayload struct { StartTime int64 `json:"ST"` EndTime int64 `json:"ET"` Market string `json:"M"` Resolution string `json:"R"` Open string `json:"O"` High string `json:"H"` Low string `json:"L"` Close string `json:"C"` Volume string `json:"v"` LastTradeID int `json:"ti"` Closed bool `json:"x"` } func (k KLinePayload) KLine() types.KLine { return types.KLine{ StartTime: types.Time(time.Unix(0, k.StartTime*int64(time.Millisecond))), EndTime: types.Time(time.Unix(0, k.EndTime*int64(time.Millisecond))), Symbol: k.Market, Interval: types.Interval(k.Resolution), Open: fixedpoint.MustNewFromString(k.Open), Close: fixedpoint.MustNewFromString(k.Close), High: fixedpoint.MustNewFromString(k.High), Low: fixedpoint.MustNewFromString(k.Low), Volume: fixedpoint.MustNewFromString(k.Volume), QuoteVolume: fixedpoint.Zero, // TODO: add this from kingfisher LastTradeID: uint64(k.LastTradeID), NumberOfTrades: 0, // TODO: add this from kingfisher Closed: k.Closed, } } type PublicTradeEvent struct { Event string `json:"e"` Market string `json:"M"` Channel string `json:"c"` Trades []TradeEntry `json:"t"` Timestamp int64 `json:"T"` } func (e *PublicTradeEvent) Time() time.Time { return time.Unix(0, e.Timestamp*int64(time.Millisecond)) } func parsePublicTradeEvent(val *fastjson.Value) (*PublicTradeEvent, error) { event := PublicTradeEvent{ Event: string(val.GetStringBytes("e")), Market: string(val.GetStringBytes("M")), Channel: string(val.GetStringBytes("c")), Timestamp: val.GetInt64("T"), } for _, tradeValue := range val.GetArray("t") { event.Trades = append(event.Trades, parseTradeEntry(tradeValue)) } return &event, nil } type BookEvent struct { Event string `json:"e"` Market string `json:"M"` Channel string `json:"c"` Timestamp int64 `json:"t"` // Millisecond timestamp Bids types.PriceVolumeSlice Asks types.PriceVolumeSlice FirstUpdateID int64 `json:"fi"` LastUpdateID int64 `json:"li"` Version int64 `json:"v"` } func (e *BookEvent) Time() time.Time { return time.Unix(0, e.Timestamp*int64(time.Millisecond)) } func (e *BookEvent) OrderBook() (snapshot types.SliceOrderBook, err error) { snapshot.Symbol = strings.ToUpper(e.Market) snapshot.Time = e.Time() snapshot.Bids = e.Bids snapshot.Asks = e.Asks snapshot.LastUpdateId = e.LastUpdateID return snapshot, nil } func parseKLineEvent(val *fastjson.Value) (*KLineEvent, error) { event := KLineEvent{ Event: string(val.GetStringBytes("e")), Market: string(val.GetStringBytes("M")), Channel: string(val.GetStringBytes("c")), Timestamp: val.GetInt64("T"), } k := val.Get("k") event.KLine = KLine{ Symbol: string(k.GetStringBytes("M")), Interval: string(k.GetStringBytes("R")), StartTime: time.Unix(0, k.GetInt64("ST")*int64(time.Millisecond)), EndTime: time.Unix(0, k.GetInt64("ET")*int64(time.Millisecond)), Open: fixedpoint.MustNewFromBytes(k.GetStringBytes("O")), High: fixedpoint.MustNewFromBytes(k.GetStringBytes("H")), Low: fixedpoint.MustNewFromBytes(k.GetStringBytes("L")), Close: fixedpoint.MustNewFromBytes(k.GetStringBytes("C")), Volume: fixedpoint.MustNewFromBytes(k.GetStringBytes("v")), Closed: k.GetBool("x"), } return &event, nil } func parseBookEvent(val *fastjson.Value) (event *BookEvent, err error) { event = &BookEvent{ Event: string(val.GetStringBytes("e")), Market: string(val.GetStringBytes("M")), Channel: string(val.GetStringBytes("c")), Timestamp: val.GetInt64("T"), FirstUpdateID: val.GetInt64("fi"), LastUpdateID: val.GetInt64("li"), Version: val.GetInt64("v"), } // t := time.Unix(0, event.Timestamp*int64(time.Millisecond)) event.Asks, err = parseBookEntries2(val.GetArray("a")) if err != nil { return event, err } event.Bids, err = parseBookEntries2(val.GetArray("b")) return event, err } // parseBookEntries2 parses JSON struct like `[["233330", "0.33"], ....]` func parseBookEntries2(vals []*fastjson.Value) (entries types.PriceVolumeSlice, err error) { entries = make(types.PriceVolumeSlice, 0, 50) var arr []*fastjson.Value for _, entry := range vals { arr, err = entry.Array() if err != nil { return entries, err } if len(arr) < 2 { return entries, ErrIncorrectBookEntryElementLength } var pv types.PriceVolume pv.Price, err = fixedpoint.NewFromString(string(arr[0].GetStringBytes())) if err != nil { return entries, err } pv.Volume, err = fixedpoint.NewFromString(string(arr[1].GetStringBytes())) if err != nil { return entries, err } entries = append(entries, pv) } return entries, err } type ErrorEvent struct { Timestamp int64 Errors []string CommandID string } func (e ErrorEvent) Time() time.Time { return time.Unix(0, e.Timestamp*int64(time.Millisecond)) } func parseErrorEvent(val *fastjson.Value) (*ErrorEvent, error) { event := ErrorEvent{ Timestamp: val.GetInt64("T"), CommandID: string(val.GetStringBytes("i")), } for _, entry := range val.GetArray("E") { event.Errors = append(event.Errors, string(entry.GetStringBytes())) } return &event, nil } type SubscriptionEvent struct { Event string `json:"e"` Timestamp int64 `json:"T"` CommandID string `json:"i"` Subscriptions []Subscription `json:"s"` } func (e SubscriptionEvent) Time() time.Time { return time.Unix(0, e.Timestamp*int64(time.Millisecond)) } func parseSubscriptionEvent(val *fastjson.Value) (*SubscriptionEvent, error) { event := SubscriptionEvent{ Event: string(val.GetStringBytes("e")), Timestamp: val.GetInt64("T"), CommandID: string(val.GetStringBytes("i")), } for _, entry := range val.GetArray("s") { market := string(entry.GetStringBytes("market")) channel := string(entry.GetStringBytes("channel")) event.Subscriptions = append(event.Subscriptions, Subscription{ Market: market, Channel: channel, }) } return &event, nil }