Files
gocryptotrader/exchanges/gateio/gateio_websocket.go
Ryan O'Hara-Reid eb0571cc9b exchange: binance orderbook fix (#599)
* port orderbook binance management from draft singular asset (spot) processing add additional updates to buffer management

* integrate port

* shifted burden of proof to exchange and remove repairing techniques that obfuscate issues and could caause artifacts

* WIP

* Update exchanges, update tests, update configuration so we can default off on buffer util.

* Add buffer enabled switching to all exchanges and some that are missing, default to off.

* lbtc set not aggregate books

* Addr linter issues

* EOD wip

* optimization and bug fix pass

* clean before test and benchmarking

* add testing/benchmarks to sorting/reversing functions, dropped pointer to slice as we aren't changing slice len or cap

* Add tests and removed ptr for main book as we just ammend amount

* addr exchange test issues

* ci issues

* addr glorious issues

* Addr MCB nits, fixed funding rate book for bitfinex and fixed potential panic on nil book return

* addr linter issues

* updated mistakes

* Fix more tests

* revert bypass

* Addr mcb nits

* fix zero price bug caused by exchange. Filted out bid result rather then unsubscribing. Updated orderbook to L2 so there is no aggregation.

* Allow for zero bid and ask books to be loaded and warn if found.

* remove authentication subscription conflicts as they do not have a channel ID return

* WIP - Batching outbound requests for kraken as they do not give you the partial if you subscribe to do many things.

* finalised outbound request for kraken

* filter zero value due to invalid returned data from exchange, add in max subscription amount and increased outbound batch limit

* expand to max allowed book length & fix issue where they were sending a zero length ask side when we sent a depth of zero

* Updated function comments and added in more realistic book sizing for sort cases

* change map ordering

* amalgamate maps in buffer

* Rm ln

* fix kraken linter issues

* add in buffer initialisation

* increase timout by 30seconds

* Coinbene: Add websocket orderbook length check.

* Engine: Improve switch statement for orderbook summary dissplay.

* Binance: Added tests, remove deadlock

* Exchanges: Change orderbook field -> IsFundingRate

* Orderbook Buffer: Added method to orderbookHolder

* Kraken: removed superfluous integer for sleep

* Bitmex: fixed error return

* cmd/gctcli: force 8 decimal place usage for orderbook streaming

* Kraken: Add checksum and fix bug where we were dropping returned data which was causing artifacts

* Kraken: As per orderbook documentation added in maxdepth field to update to filter depth that goes beyond current scope

* Bitfinex: Tracking down bug on margin-funding, added sequence and checksum validation websocket config on connect (WIP)

* Bitfinex: Complete implementation of checksum

* Bitfinex: Fix funding book insertion and checksum - Dropped updates and deleting items not on book are continuously occuring from stream

* Bitfinex: Fix linter issues

* Bitfinex: Fix even more linter issues.

* Bitmex: Populate orderbook base identification fields to be passed back when error occurrs

* OkGroup: Populate orderbook base identification fields to be passed back when error occurrs

* BTSE: Change string check to 'connect success' to capture multiple user successful strings

* Bitfinex: Updated handling of funding tickers

* Bitfinex: Fix undocumented alignment bug for funding rates

* Bitfinex: Updated error return with more information

* Bitfinex: Change REST fetching to Raw book to keep it in line with websocket implementation. Fix woopsy.

* Localbitcoins: Had to impose a rate limiter to stop errors, fixed return for easier error identification.

* Exchanges: Update failing tests

* LocalBitcoins: Addr nit and bumped time by 1 second for fetching books

* Kraken: Dynamically scale precision based on str return for checksum calculations

* Kraken: Add pair and asset type to validateCRC32 error reponse

* BTSE: Filter out zero amount orderbook price levels in websocket return

* Exchanges: Update orderbook functions to return orderbook base to differentiate errors.

* BTSE: Fix spelling

* Bitmex: Fix error return string

* BTSE: Add orderbook filtering function

* Coinbene: Change wording

* BTSE: Add test for filtering

* Binance: Addr nits, added in variables for buffers and worker amounts and fixed error log messages

* GolangCI: Remove excess 0

* Binance: Reduces double ups on asset and pair in errors

* Binance: Fix error checking
2021-01-04 17:19:55 +11:00

684 lines
18 KiB
Go

package gateio
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
)
const (
gateioWebsocketEndpoint = "wss://ws.gateio.ws/v3/"
gateioWebsocketRateLimit = 120
)
// WsConnect initiates a websocket connection
func (g *Gateio) WsConnect() error {
if !g.Websocket.IsEnabled() || !g.IsEnabled() {
return errors.New(stream.WebsocketNotEnabled)
}
var dialer websocket.Dialer
err := g.Websocket.Conn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
go g.wsReadData()
if g.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
err = g.wsServerSignIn()
if err != nil {
g.Websocket.DataHandler <- err
g.Websocket.SetCanUseAuthenticatedEndpoints(false)
} else {
var authsubs []stream.ChannelSubscription
authsubs, err = g.GenerateAuthenticatedSubscriptions()
if err != nil {
g.Websocket.DataHandler <- err
g.Websocket.SetCanUseAuthenticatedEndpoints(false)
} else {
err = g.Websocket.SubscribeToChannels(authsubs)
if err != nil {
g.Websocket.DataHandler <- err
g.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
}
}
}
return nil
}
func (g *Gateio) wsServerSignIn() error {
nonce := int(time.Now().Unix() * 1000)
sigTemp := g.GenerateSignature(strconv.Itoa(nonce))
signature := crypto.Base64Encode(sigTemp)
signinWsRequest := WebsocketRequest{
ID: g.Websocket.Conn.GenerateMessageID(false),
Method: "server.sign",
Params: []interface{}{g.API.Credentials.Key, signature, nonce},
}
resp, err := g.Websocket.Conn.SendMessageReturnResponse(signinWsRequest.ID,
signinWsRequest)
if err != nil {
g.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
}
var response WebsocketAuthenticationResponse
err = json.Unmarshal(resp, &response)
if err != nil {
g.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
}
if response.Result.Status == "success" {
g.Websocket.SetCanUseAuthenticatedEndpoints(true)
return nil
}
return fmt.Errorf("%s cannot authenticate websocket connection: %s",
g.Name,
response.Result.Status)
}
// wsReadData receives and passes on websocket messages for processing
func (g *Gateio) wsReadData() {
g.Websocket.Wg.Add(1)
defer g.Websocket.Wg.Done()
for {
resp := g.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
return
}
err := g.wsHandleData(resp.Raw)
if err != nil {
g.Websocket.DataHandler <- err
}
}
}
func (g *Gateio) wsHandleData(respRaw []byte) error {
var result WebsocketResponse
err := json.Unmarshal(respRaw, &result)
if err != nil {
return err
}
if result.ID > 0 {
if g.Websocket.Match.IncomingWithData(result.ID, respRaw) {
return nil
}
}
if result.Error.Code != 0 {
if strings.Contains(result.Error.Message, "authentication") {
g.Websocket.SetCanUseAuthenticatedEndpoints(false)
return fmt.Errorf("%v - authentication failed: %v", g.Name, err)
}
return fmt.Errorf("%v error %s", g.Name, result.Error.Message)
}
switch {
case strings.Contains(result.Method, "ticker"):
var wsTicker WebsocketTicker
var c string
err = json.Unmarshal(result.Params[1], &wsTicker)
if err != nil {
return err
}
err = json.Unmarshal(result.Params[0], &c)
if err != nil {
return err
}
var p currency.Pair
p, err = currency.NewPairFromString(c)
if err != nil {
return err
}
g.Websocket.DataHandler <- &ticker.Price{
ExchangeName: g.Name,
Open: wsTicker.Open,
Close: wsTicker.Close,
Volume: wsTicker.BaseVolume,
QuoteVolume: wsTicker.QuoteVolume,
High: wsTicker.High,
Low: wsTicker.Low,
Last: wsTicker.Last,
AssetType: asset.Spot,
Pair: p,
}
case strings.Contains(result.Method, "trades"):
if !g.IsSaveTradeDataEnabled() {
return nil
}
var tradeData []WebsocketTrade
var c string
err = json.Unmarshal(result.Params[1], &tradeData)
if err != nil {
return err
}
err = json.Unmarshal(result.Params[0], &c)
if err != nil {
return err
}
var p currency.Pair
p, err = currency.NewPairFromString(c)
if err != nil {
return err
}
var trades []trade.Data
for i := range tradeData {
var tSide order.Side
tSide, err = order.StringToOrderSide(tradeData[i].Type)
if err != nil {
g.Websocket.DataHandler <- order.ClassificationError{
Exchange: g.Name,
Err: err,
}
}
trades = append(trades, trade.Data{
Timestamp: convert.TimeFromUnixTimestampDecimal(tradeData[i].Time),
CurrencyPair: p,
AssetType: asset.Spot,
Exchange: g.Name,
Price: tradeData[i].Price,
Amount: tradeData[i].Amount,
Side: tSide,
TID: strconv.FormatInt(tradeData[i].ID, 10),
})
}
return trade.AddTradesToBuffer(g.Name, trades...)
case strings.Contains(result.Method, "balance.update"):
var balance wsBalanceSubscription
err = json.Unmarshal(respRaw, &balance)
if err != nil {
return err
}
g.Websocket.DataHandler <- balance
case strings.Contains(result.Method, "order.update"):
var orderUpdate wsOrderUpdate
err = json.Unmarshal(respRaw, &orderUpdate)
if err != nil {
return err
}
invalidJSON := orderUpdate.Params[1].(map[string]interface{})
oStatus := order.UnknownStatus
oType := order.UnknownType
oSide := order.UnknownSide
switch orderUpdate.Params[0].(float64) {
case 1:
oStatus = order.New
case 2:
oStatus = order.PartiallyFilled
case 3:
oStatus = order.Filled
}
switch invalidJSON["orderType"].(float64) {
case 1:
oType = order.Limit
case 2:
oType = order.Market
}
switch invalidJSON["type"].(float64) {
case 1:
oSide = order.Sell
case 2:
oSide = order.Buy
}
var price, amount, filledTotal, left, fee float64
price, err = strconv.ParseFloat(invalidJSON["price"].(string), 64)
if err != nil {
return err
}
amount, err = strconv.ParseFloat(invalidJSON["amount"].(string), 64)
if err != nil {
return err
}
filledTotal, err = strconv.ParseFloat(invalidJSON["filledTotal"].(string), 64)
if err != nil {
return err
}
left, err = strconv.ParseFloat(invalidJSON["left"].(string), 64)
if err != nil {
return err
}
fee, err = strconv.ParseFloat(invalidJSON["dealFee"].(string), 64)
if err != nil {
return err
}
var p currency.Pair
p, err = currency.NewPairFromString(invalidJSON["market"].(string))
if err != nil {
return err
}
var a asset.Item
a, err = g.GetPairAssetType(p)
if err != nil {
return err
}
g.Websocket.DataHandler <- &order.Detail{
Price: price,
Amount: amount,
ExecutedAmount: filledTotal,
RemainingAmount: left,
Fee: fee,
Exchange: g.Name,
ID: strconv.FormatFloat(invalidJSON["id"].(float64), 'f', -1, 64),
Type: oType,
Side: oSide,
Status: oStatus,
AssetType: a,
Date: convert.TimeFromUnixTimestampDecimal(invalidJSON["ctime"].(float64)),
LastUpdated: convert.TimeFromUnixTimestampDecimal(invalidJSON["mtime"].(float64)),
Pair: p,
}
case strings.Contains(result.Method, "depth"):
var IsSnapshot bool
var c string
var data wsOrderbook
err = json.Unmarshal(result.Params[0], &IsSnapshot)
if err != nil {
return err
}
err = json.Unmarshal(result.Params[2], &c)
if err != nil {
return err
}
err = json.Unmarshal(result.Params[1], &data)
if err != nil {
return err
}
var asks, bids []orderbook.Item
var amount, price float64
for i := range data.Asks {
amount, err = strconv.ParseFloat(data.Asks[i][1], 64)
if err != nil {
return err
}
price, err = strconv.ParseFloat(data.Asks[i][0], 64)
if err != nil {
return err
}
asks = append(asks, orderbook.Item{Amount: amount, Price: price})
}
for i := range data.Bids {
amount, err = strconv.ParseFloat(data.Bids[i][1], 64)
if err != nil {
return err
}
price, err = strconv.ParseFloat(data.Bids[i][0], 64)
if err != nil {
return err
}
bids = append(bids, orderbook.Item{Amount: amount, Price: price})
}
var p currency.Pair
p, err = currency.NewPairFromString(c)
if err != nil {
return err
}
if IsSnapshot {
var newOrderBook orderbook.Base
newOrderBook.Asks = asks
newOrderBook.Bids = bids
newOrderBook.AssetType = asset.Spot
newOrderBook.Pair = p
newOrderBook.ExchangeName = g.Name
err = g.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
if err != nil {
return err
}
} else {
err = g.Websocket.Orderbook.Update(&buffer.Update{
Asks: asks,
Bids: bids,
Pair: p,
UpdateTime: time.Now(),
Asset: asset.Spot,
})
if err != nil {
return err
}
}
case strings.Contains(result.Method, "kline"):
var data []interface{}
err = json.Unmarshal(result.Params[0], &data)
if err != nil {
return err
}
open, err := strconv.ParseFloat(data[1].(string), 64)
if err != nil {
return err
}
closePrice, err := strconv.ParseFloat(data[2].(string), 64)
if err != nil {
return err
}
high, err := strconv.ParseFloat(data[3].(string), 64)
if err != nil {
return err
}
low, err := strconv.ParseFloat(data[4].(string), 64)
if err != nil {
return err
}
volume, err := strconv.ParseFloat(data[5].(string), 64)
if err != nil {
return err
}
p, err := currency.NewPairFromString(data[7].(string))
if err != nil {
return err
}
g.Websocket.DataHandler <- stream.KlineData{
Timestamp: time.Now(),
Pair: p,
AssetType: asset.Spot,
Exchange: g.Name,
OpenPrice: open,
ClosePrice: closePrice,
HighPrice: high,
LowPrice: low,
Volume: volume,
}
default:
g.Websocket.DataHandler <- stream.UnhandledMessageWarning{
Message: g.Name + stream.UnhandledMessage + string(respRaw),
}
return nil
}
return nil
}
// GenerateAuthenticatedSubscriptions returns authenticated subscriptions
func (g *Gateio) GenerateAuthenticatedSubscriptions() ([]stream.ChannelSubscription, error) {
if !g.Websocket.CanUseAuthenticatedEndpoints() {
return nil, nil
}
var channels = []string{"balance.subscribe", "order.subscribe"}
var subscriptions []stream.ChannelSubscription
enabledCurrencies, err := g.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
for i := range channels {
for j := range enabledCurrencies {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[i],
Currency: enabledCurrencies[j],
Asset: asset.Spot,
})
}
}
return subscriptions, nil
}
// GenerateDefaultSubscriptions returns default subscriptions
func (g *Gateio) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var channels = []string{"ticker.subscribe",
"trades.subscribe",
"depth.subscribe",
"kline.subscribe"}
var subscriptions []stream.ChannelSubscription
enabledCurrencies, err := g.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
for i := range channels {
for j := range enabledCurrencies {
params := make(map[string]interface{})
if strings.EqualFold(channels[i], "depth.subscribe") {
params["limit"] = 30
params["interval"] = "0.1"
} else if strings.EqualFold(channels[i], "kline.subscribe") {
params["interval"] = 1800
}
fpair, err := g.FormatExchangeCurrency(enabledCurrencies[j],
asset.Spot)
if err != nil {
return nil, err
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[i],
Currency: fpair.Upper(),
Params: params,
Asset: asset.Spot,
})
}
}
return subscriptions, nil
}
// Subscribe sends a websocket message to receive data from the channel
func (g *Gateio) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
payloads, err := g.generatePayload(channelsToSubscribe)
if err != nil {
return err
}
var errs common.Errors
for k := range payloads {
resp, err := g.Websocket.Conn.SendMessageReturnResponse(payloads[k].ID, payloads[k])
if err != nil {
errs = append(errs, err)
continue
}
var response WebsocketAuthenticationResponse
err = json.Unmarshal(resp, &response)
if err != nil {
errs = append(errs, err)
continue
}
if response.Result.Status != "success" {
errs = append(errs, fmt.Errorf("%v could not subscribe to %v",
g.Name,
payloads[k].Method))
continue
}
g.Websocket.AddSuccessfulSubscriptions(payloads[k].Channels...)
}
if errs != nil {
return errs
}
return nil
}
func (g *Gateio) generatePayload(channelsToSubscribe []stream.ChannelSubscription) ([]WebsocketRequest, error) {
if len(channelsToSubscribe) == 0 {
return nil, errors.New("cannot generate payload, no channels supplied")
}
var payloads []WebsocketRequest
channels:
for i := range channelsToSubscribe {
// Ensures params are in order
params := []interface{}{channelsToSubscribe[i].Currency}
if strings.EqualFold(channelsToSubscribe[i].Channel, "depth.subscribe") {
params = append(params,
channelsToSubscribe[i].Params["limit"],
channelsToSubscribe[i].Params["interval"])
} else if strings.EqualFold(channelsToSubscribe[i].Channel, "kline.subscribe") {
params = append(params, channelsToSubscribe[i].Params["interval"])
}
for j := range payloads {
if payloads[j].Method == channelsToSubscribe[i].Channel {
switch {
case strings.EqualFold(channelsToSubscribe[i].Channel, "depth.subscribe"):
if len(payloads[j].Params) == 3 {
// If more than one currency pair we need to send as
// matrix
_, ok := payloads[j].Params[0].(currency.Pair)
if ok {
var bucket = payloads[j].Params
payloads[j].Params = nil
payloads[j].Params = append(payloads[j].Params, bucket)
}
}
payloads[j].Params = append(payloads[j].Params, params)
case strings.EqualFold(channelsToSubscribe[i].Channel, "kline.subscribe"):
// Can only subscribe one market at the same time, market
// list is not supported currently. For multiple
// subscriptions, only the last one takes effect.
default:
payloads[j].Params = append(payloads[j].Params, params...)
}
payloads[j].Channels = append(payloads[j].Channels, channelsToSubscribe[i])
continue channels
}
}
payloads = append(payloads, WebsocketRequest{
ID: g.Websocket.Conn.GenerateMessageID(false),
Method: channelsToSubscribe[i].Channel,
Params: params,
Channels: []stream.ChannelSubscription{channelsToSubscribe[i]},
})
}
return payloads, nil
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (g *Gateio) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
// NOTE: This function does not take in parameters, it cannot unsubscribe a
// single item but a full channel. i.e. if you subscribe to ticker BTC_USDT
// & LTC_USDT this function will unsubscribe both. This function will be
// kept unlinked to the websocket subsystem and a full connection flush will
// occur when currency items are disabled.
var channelsThusFar []string
for i := range channelsToUnsubscribe {
if common.StringDataCompare(channelsThusFar,
channelsToUnsubscribe[i].Channel) {
continue
}
channelsThusFar = append(channelsThusFar,
channelsToUnsubscribe[i].Channel)
unsubscribeText := strings.Replace(channelsToUnsubscribe[i].Channel,
"subscribe",
"unsubscribe",
1)
unsubscribe := WebsocketRequest{
ID: g.Websocket.Conn.GenerateMessageID(false),
Method: unsubscribeText,
Params: []interface{}{channelsToUnsubscribe[i].Currency.String()},
}
resp, err := g.Websocket.Conn.SendMessageReturnResponse(unsubscribe.ID,
unsubscribe)
if err != nil {
return err
}
var response WebsocketAuthenticationResponse
err = json.Unmarshal(resp, &response)
if err != nil {
return err
}
if response.Result.Status != "success" {
return fmt.Errorf("%v could not subscribe to %v",
g.Name,
channelsToUnsubscribe[i].Channel)
}
}
return nil
}
func (g *Gateio) wsGetBalance(currencies []string) (*WsGetBalanceResponse, error) {
if !g.Websocket.CanUseAuthenticatedEndpoints() {
return nil, fmt.Errorf("%v not authorised to get balance", g.Name)
}
balanceWsRequest := wsGetBalanceRequest{
ID: g.Websocket.Conn.GenerateMessageID(false),
Method: "balance.query",
Params: currencies,
}
resp, err := g.Websocket.Conn.SendMessageReturnResponse(balanceWsRequest.ID, balanceWsRequest)
if err != nil {
return nil, err
}
var balance WsGetBalanceResponse
err = json.Unmarshal(resp, &balance)
if err != nil {
return &balance, err
}
if balance.Error.Message != "" {
return nil, fmt.Errorf("%s websocket error: %s",
g.Name,
balance.Error.Message)
}
return &balance, nil
}
func (g *Gateio) wsGetOrderInfo(market string, offset, limit int) (*WebSocketOrderQueryResult, error) {
if !g.Websocket.CanUseAuthenticatedEndpoints() {
return nil, fmt.Errorf("%v not authorised to get order info", g.Name)
}
ord := WebsocketRequest{
ID: g.Websocket.Conn.GenerateMessageID(false),
Method: "order.query",
Params: []interface{}{
market,
offset,
limit,
},
}
resp, err := g.Websocket.Conn.SendMessageReturnResponse(ord.ID, ord)
if err != nil {
return nil, err
}
var orderQuery WebSocketOrderQueryResult
err = json.Unmarshal(resp, &orderQuery)
if err != nil {
return &orderQuery, err
}
if orderQuery.Error.Message != "" {
return nil, fmt.Errorf("%s websocket error: %s",
g.Name,
orderQuery.Error.Message)
}
return &orderQuery, nil
}