mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-20 23:16:49 +00:00
* golangci-lint/CI: Bump versions
Fix remaining linter issues
* Specifically set AppVeyor version
* Fix the infamous typos 👀
* Add go env cmd to AppVeyor
* Add go version cmd to AppVeyor
* Specify AppVeyor image, adjust linters
* Update go get to go install due to deprecation
* Bump golangci-lint timeout time for AppVeyor
* Change NW contract to NQ
* Address nitters
* GetRandomPair -> Pair{}
* Address nits
* Address time nitterinos plus additional tweaks
* More time inception upgrades!
* Bending time and space
518 lines
14 KiB
Go
518 lines
14 KiB
Go
package coinbene
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"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 (
|
|
wsContractURL = "wss://ws.coinbene.com/stream/ws"
|
|
event = "event"
|
|
topic = "topic"
|
|
swapChannelPrefix = "btc/"
|
|
spotChannelPrefix = "spot/"
|
|
)
|
|
|
|
// WsConnect connects to websocket
|
|
func (c *Coinbene) WsConnect() error {
|
|
if !c.Websocket.IsEnabled() || !c.IsEnabled() {
|
|
return errors.New(stream.WebsocketNotEnabled)
|
|
}
|
|
var dialer websocket.Dialer
|
|
err := c.Websocket.Conn.Dial(&dialer, http.Header{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.Websocket.Wg.Add(1)
|
|
go c.wsReadData()
|
|
|
|
if c.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
|
|
err = c.Login()
|
|
if err != nil {
|
|
c.Websocket.DataHandler <- err
|
|
c.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GenerateDefaultSubscriptions generates stuff
|
|
func (c *Coinbene) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
|
|
var channels = []string{"orderBook.%s.100", "tradeList.%s", "ticker.%s", "kline.%s.1h"}
|
|
var subscriptions []stream.ChannelSubscription
|
|
perpetualPairs, err := c.GetEnabledPairs(asset.PerpetualSwap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var spotPairs currency.Pairs
|
|
spotPairs, err = c.GetEnabledPairs(asset.Spot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for x := range channels {
|
|
for y := range perpetualPairs {
|
|
perpetualPairs[y].Delimiter = ""
|
|
subscriptions = append(subscriptions, stream.ChannelSubscription{
|
|
Channel: swapChannelPrefix + fmt.Sprintf(channels[x], perpetualPairs[y]),
|
|
Currency: perpetualPairs[y],
|
|
Asset: asset.PerpetualSwap,
|
|
})
|
|
}
|
|
for z := range spotPairs {
|
|
spotPairs[z].Delimiter = ""
|
|
subscriptions = append(subscriptions, stream.ChannelSubscription{
|
|
Channel: spotChannelPrefix + fmt.Sprintf(channels[x], spotPairs[z]),
|
|
Currency: spotPairs[z],
|
|
Asset: asset.Spot,
|
|
})
|
|
}
|
|
}
|
|
|
|
return subscriptions, nil
|
|
}
|
|
|
|
// GenerateAuthSubs generates auth subs
|
|
func (c *Coinbene) GenerateAuthSubs() ([]stream.ChannelSubscription, error) {
|
|
var subscriptions []stream.ChannelSubscription
|
|
var sub stream.ChannelSubscription
|
|
var userChannels = []string{"user.account", "user.position", "user.order"}
|
|
for z := range userChannels {
|
|
sub.Channel = userChannels[z]
|
|
subscriptions = append(subscriptions, sub)
|
|
}
|
|
return subscriptions, nil
|
|
}
|
|
|
|
// wsReadData receives and passes on websocket messages for processing
|
|
func (c *Coinbene) wsReadData() {
|
|
defer c.Websocket.Wg.Done()
|
|
for {
|
|
resp := c.Websocket.Conn.ReadMessage()
|
|
if resp.Raw == nil {
|
|
return
|
|
}
|
|
err := c.wsHandleData(resp.Raw)
|
|
if err != nil {
|
|
c.Websocket.DataHandler <- err
|
|
}
|
|
}
|
|
}
|
|
|
|
func inferAssetFromTopic(topic string) asset.Item {
|
|
if strings.Contains(topic, "spot/") {
|
|
return asset.Spot
|
|
}
|
|
return asset.PerpetualSwap
|
|
}
|
|
|
|
func (c *Coinbene) wsHandleData(respRaw []byte) error {
|
|
if string(respRaw) == stream.Ping {
|
|
err := c.Websocket.Conn.SendRawMessage(websocket.TextMessage, []byte(stream.Pong))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
var result map[string]interface{}
|
|
err := json.Unmarshal(respRaw, &result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, ok := result[event]
|
|
switch {
|
|
case ok && (result[event].(string) == "subscribe" || result[event].(string) == "unsubscribe"):
|
|
return nil
|
|
case ok && result[event].(string) == "error":
|
|
return fmt.Errorf("message: %s. code: %v", result["message"], result["code"])
|
|
}
|
|
if ok && strings.Contains(result[event].(string), "login") {
|
|
if result["success"].(bool) {
|
|
c.Websocket.SetCanUseAuthenticatedEndpoints(true)
|
|
var authsubs []stream.ChannelSubscription
|
|
authsubs, err = c.GenerateAuthSubs()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.Websocket.SubscribeToChannels(authsubs)
|
|
}
|
|
c.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
return fmt.Errorf("message: %s. code: %v", result["message"], result["code"])
|
|
}
|
|
assetType := inferAssetFromTopic(result[topic].(string))
|
|
var newPair currency.Pair
|
|
switch {
|
|
case strings.Contains(result[topic].(string), "ticker"):
|
|
var wsTicker WsTicker
|
|
err = json.Unmarshal(respRaw, &wsTicker)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newPair, err = c.getCurrencyFromWsTopic(assetType, wsTicker.Topic)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for x := range wsTicker.Data {
|
|
c.Websocket.DataHandler <- &ticker.Price{
|
|
Volume: wsTicker.Data[x].Volume24h,
|
|
Last: wsTicker.Data[x].LastPrice,
|
|
High: wsTicker.Data[x].High24h,
|
|
Low: wsTicker.Data[x].Low24h,
|
|
Bid: wsTicker.Data[x].BestBidPrice,
|
|
Ask: wsTicker.Data[x].BestAskPrice,
|
|
Pair: newPair,
|
|
ExchangeName: c.Name,
|
|
AssetType: assetType,
|
|
LastUpdated: time.Unix(wsTicker.Data[x].Timestamp, 0),
|
|
}
|
|
}
|
|
case strings.Contains(result[topic].(string), "tradeList"):
|
|
if !c.IsSaveTradeDataEnabled() {
|
|
return nil
|
|
}
|
|
var tradeList WsTradeList
|
|
err = json.Unmarshal(respRaw, &tradeList)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var trades []trade.Data
|
|
for i := range tradeList.Data {
|
|
var price, amount float64
|
|
t := time.Unix(int64(tradeList.Data[i][3].(float64))/1000, 0)
|
|
price, err = strconv.ParseFloat(tradeList.Data[i][0].(string), 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
amount, err = strconv.ParseFloat(tradeList.Data[i][2].(string), 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var tSide = order.Buy
|
|
if tradeList.Data[i][1] == "s" {
|
|
tSide = order.Sell
|
|
}
|
|
|
|
newPair, err = c.getCurrencyFromWsTopic(assetType, tradeList.Topic)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
trades = append(trades, trade.Data{
|
|
Timestamp: t,
|
|
Exchange: c.Name,
|
|
CurrencyPair: newPair,
|
|
AssetType: assetType,
|
|
Price: price,
|
|
Amount: amount,
|
|
Side: tSide,
|
|
})
|
|
}
|
|
return trade.AddTradesToBuffer(c.Name, trades...)
|
|
case strings.Contains(result[topic].(string), "orderBook"):
|
|
var orderBook WsOrderbookData
|
|
err = json.Unmarshal(respRaw, &orderBook)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(orderBook.Data) != 1 {
|
|
return errors.New("incomplete orderbook data has been received")
|
|
}
|
|
|
|
newPair, err = c.getCurrencyFromWsTopic(assetType, orderBook.Topic)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var amount, price float64
|
|
var asks, bids []orderbook.Item
|
|
for i := range orderBook.Data[0].Asks {
|
|
amount, err = strconv.ParseFloat(orderBook.Data[0].Asks[i][1], 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
price, err = strconv.ParseFloat(orderBook.Data[0].Asks[i][0], 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
asks = append(asks, orderbook.Item{
|
|
Amount: amount,
|
|
Price: price,
|
|
})
|
|
}
|
|
for j := range orderBook.Data[0].Bids {
|
|
price, err = strconv.ParseFloat(orderBook.Data[0].Bids[j][0], 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if price == 0 {
|
|
// Last level is coming back as a float with not enough decimal
|
|
// places e.g. ["0.000","1001.95"]],
|
|
// This needs to be filtered out as this can skew orderbook
|
|
// calculations
|
|
continue
|
|
}
|
|
|
|
amount, err = strconv.ParseFloat(orderBook.Data[0].Bids[j][1], 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bids = append(bids, orderbook.Item{
|
|
Amount: amount,
|
|
Price: price,
|
|
})
|
|
}
|
|
if orderBook.Action == "insert" {
|
|
var newOB orderbook.Base
|
|
newOB.Asks = asks
|
|
newOB.Bids = bids
|
|
newOB.Asset = assetType
|
|
newOB.Pair = newPair
|
|
newOB.Exchange = c.Name
|
|
newOB.LastUpdated = time.Unix(orderBook.Data[0].Timestamp, 0)
|
|
newOB.VerifyOrderbook = c.CanVerifyOrderbook
|
|
err = c.Websocket.Orderbook.LoadSnapshot(&newOB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if orderBook.Action == "update" {
|
|
newOB := buffer.Update{
|
|
Asks: asks,
|
|
Bids: bids,
|
|
Asset: assetType,
|
|
Pair: newPair,
|
|
UpdateID: orderBook.Data[0].Version,
|
|
UpdateTime: time.Unix(orderBook.Data[0].Timestamp, 0),
|
|
}
|
|
err = c.Websocket.Orderbook.Update(&newOB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case strings.Contains(result[topic].(string), "kline"):
|
|
var candleData WsKline
|
|
err = json.Unmarshal(respRaw, &candleData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newPair, err = c.getCurrencyFromWsTopic(assetType, candleData.Topic)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := range candleData.Data {
|
|
c.Websocket.DataHandler <- stream.KlineData{
|
|
Pair: newPair,
|
|
AssetType: assetType,
|
|
Exchange: c.Name,
|
|
OpenPrice: candleData.Data[i].Open,
|
|
HighPrice: candleData.Data[i].High,
|
|
LowPrice: candleData.Data[i].Low,
|
|
ClosePrice: candleData.Data[i].Close,
|
|
Volume: candleData.Data[i].Volume,
|
|
Timestamp: time.Unix(candleData.Data[i].Timestamp, 0),
|
|
}
|
|
}
|
|
case strings.Contains(result[topic].(string), "user.account"):
|
|
var userInfo WsUserInfo
|
|
err = json.Unmarshal(respRaw, &userInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Websocket.DataHandler <- userInfo
|
|
case strings.Contains(result[topic].(string), "user.position"):
|
|
var position WsPosition
|
|
err = json.Unmarshal(respRaw, &position)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Websocket.DataHandler <- position
|
|
case strings.Contains(result[topic].(string), "user.order"):
|
|
var orders WsUserOrders
|
|
err = json.Unmarshal(respRaw, &orders)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
format, err := c.GetPairFormat(assetType, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pairs, err := c.GetEnabledPairs(assetType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := range orders.Data {
|
|
oType, err := order.StringToOrderType(orders.Data[i].OrderType)
|
|
if err != nil {
|
|
c.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: c.Name,
|
|
OrderID: orders.Data[i].OrderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
oStatus, err := order.StringToOrderStatus(orders.Data[i].Status)
|
|
if err != nil {
|
|
c.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: c.Name,
|
|
OrderID: orders.Data[i].OrderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
newPair, err = currency.NewPairFromFormattedPairs(orders.Data[i].Symbol,
|
|
pairs,
|
|
format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.Websocket.DataHandler <- &order.Detail{
|
|
Price: orders.Data[i].OrderPrice,
|
|
Amount: orders.Data[i].Quantity,
|
|
ExecutedAmount: orders.Data[i].FilledQuantity,
|
|
RemainingAmount: orders.Data[i].Quantity - orders.Data[i].FilledQuantity,
|
|
Fee: orders.Data[i].Fee,
|
|
Exchange: c.Name,
|
|
ID: orders.Data[i].OrderID,
|
|
Type: oType,
|
|
Status: oStatus,
|
|
AssetType: assetType,
|
|
Date: orders.Data[i].OrderTime,
|
|
Leverage: float64(orders.Data[i].Leverage),
|
|
Pair: newPair,
|
|
}
|
|
}
|
|
default:
|
|
c.Websocket.DataHandler <- stream.UnhandledMessageWarning{
|
|
Message: c.Name + stream.UnhandledMessage + string(respRaw),
|
|
}
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Coinbene) getCurrencyFromWsTopic(assetType asset.Item, channelTopic string) (cp currency.Pair, err error) {
|
|
var format currency.PairFormat
|
|
format, err = c.GetPairFormat(assetType, true)
|
|
if err != nil {
|
|
return cp, err
|
|
}
|
|
|
|
var pairs currency.Pairs
|
|
pairs, err = c.GetEnabledPairs(assetType)
|
|
if err != nil {
|
|
return cp, err
|
|
}
|
|
// channel topics are formatted as "spot/orderbook.BTCUSDT"
|
|
channelSplit := strings.Split(channelTopic, ".")
|
|
if len(channelSplit) == 1 {
|
|
return currency.Pair{}, errors.New("no currency found in topic " + channelTopic)
|
|
}
|
|
cp, err = currency.MatchPairsWithNoDelimiter(channelSplit[1], pairs, format)
|
|
if err != nil {
|
|
return cp, err
|
|
}
|
|
if !pairs.Contains(cp, true) {
|
|
return cp, fmt.Errorf("currency %s not found in enabled pairs", cp.String())
|
|
}
|
|
return cp, nil
|
|
}
|
|
|
|
// Subscribe sends a websocket message to receive data from the channel
|
|
func (c *Coinbene) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
|
|
if maxSubsPerHour := 240; len(channelsToSubscribe) > maxSubsPerHour {
|
|
return fmt.Errorf("channel subscriptions length %d exceeds coinbene's limit of %d, try reducing enabled pairs",
|
|
len(channelsToSubscribe),
|
|
maxSubsPerHour)
|
|
}
|
|
|
|
var sub WsSub
|
|
sub.Operation = "subscribe"
|
|
// enabling all currencies can lead to a message too large being sent
|
|
// and no subscriptions being made
|
|
chanLimit := 15
|
|
for i := range channelsToSubscribe {
|
|
if len(sub.Arguments) > chanLimit {
|
|
err := c.Websocket.Conn.SendJSONMessage(sub)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sub.Arguments = []string{}
|
|
}
|
|
sub.Arguments = append(sub.Arguments, channelsToSubscribe[i].Channel)
|
|
}
|
|
err := c.Websocket.Conn.SendJSONMessage(sub)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe...)
|
|
return nil
|
|
}
|
|
|
|
// Unsubscribe sends a websocket message to receive data from the channel
|
|
func (c *Coinbene) Unsubscribe(channelToUnsubscribe []stream.ChannelSubscription) error {
|
|
var unsub WsSub
|
|
unsub.Operation = "unsubscribe"
|
|
// enabling all currencies can lead to a message too large being sent
|
|
// and no unsubscribes being made
|
|
chanLimit := 15
|
|
for i := range channelToUnsubscribe {
|
|
if len(unsub.Arguments) > chanLimit {
|
|
err := c.Websocket.Conn.SendJSONMessage(unsub)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
unsub.Arguments = []string{}
|
|
}
|
|
unsub.Arguments = append(unsub.Arguments, channelToUnsubscribe[i].Channel)
|
|
}
|
|
err := c.Websocket.Conn.SendJSONMessage(unsub)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Websocket.RemoveSuccessfulUnsubscriptions(channelToUnsubscribe...)
|
|
return nil
|
|
}
|
|
|
|
// Login logs in
|
|
func (c *Coinbene) Login() error {
|
|
var sub WsSub
|
|
expTime := time.Now().Add(time.Minute * 10).Format("2006-01-02T15:04:05Z")
|
|
signMsg := expTime + http.MethodGet + "/login"
|
|
|
|
tempSign, err := crypto.GetHMAC(crypto.HashSHA256,
|
|
[]byte(signMsg),
|
|
[]byte(c.API.Credentials.Secret))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sign := crypto.HexEncodeToString(tempSign)
|
|
sub.Operation = "login"
|
|
sub.Arguments = []string{c.API.Credentials.Key, expTime, sign}
|
|
return c.Websocket.Conn.SendJSONMessage(sub)
|
|
}
|