Files
gocryptotrader/exchanges/coinbene/coinbene_websocket.go
Adam 504c2fad6d Feature: Implement funding rates, futures and coin margin (exchange API coverage) (#530)
* ALMOST THERE

* more api wips

* more api thingz

* testing n more api wipz

* more apiz

* more wips

* what is goin on

* more wips

* whip n testing

* testing

* testing

no keys

* remove log

* kraken is broken

ugh

* still broken

* fixing auth funcs + usdtm api docs

* wip

* api stuffs

* whip

* more wips

* whip

* more wip

* api wip n testing

* wip

* wip

* unsaved

* wip n testing

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* whip

* wrapper authenticated functions

* adding asset type and fixing dependencies

* wip

* binance auth wrapper start

* wrapper functionality

* wip

* wip

* wip

* wrapper cancel functions

* order submission for wrappers

* wip

* more error fixing and nits

* websocket beginning n error fix

* wip

* WOW

* glorious n shazzy nits

* useless nits

* wip

* fixing things

* merge stuffs

* crapveyor

* crapveyor rebuild

* probably broke more things than he fixed

* rm lns n other thangs

* hope

* please

* stop it

* done

* ofcourse

* rm vb

* fix lbank

* appveyor please

* float lev

* DONT ASK RYAN FOR HELP EVER

* wip

* wip

* endpoint upgrades continued

* path upgrade

* NeeeNeeeNeeeNeeeNING

* fix stuffs

* fixing time issue

* fixing broken funcs

* glorious nits

* shaz changes

* fixing errors for fundmon

* more error fixing for fundmon

* test running past 30s

* basic changes

* THX AGAIN SHAZBERT

* path system upgrade

* config upgrade

* unsaved stuffs

* broken wip config upgrade

* path system upgrade contd.

* path system upgrade contd

* path upgrade ready for review

* testing verbose removed

* linter stuffs

* appveyor stuffs

* appveyor stuff

* fixed?

* bugfix

* wip

* broken stuff

* fix test

* wierd hack fix

* appveyor pls stop

* error found

* more useless nits

* bitmex err

* broken wip

* broken wip path upgrade change to uint32

* changed url lookups to uint

* WOW

* ready4review

* config fixed HOPEFULLY

* config fix and glorious changes

* efficient way of getting orders and open orders

* binance wrapper logic fixing

* testing, adding tests and fixing lot of errrrrs

* merge master

* appveyor stuffs

* appveyor stuffs

* fmt

* test

* octalLiteral issue fix?

* octalLiteral fix?

* rm vb

* prnt ln to restart

* adding testz

* test fixzzz

* READY FOR REVIEW

* Actually ready now

* FORMATTING

* addressing shazzy n glorious nits

* crapveyor

* rm vb

* small change

* fixing err

* shazbert nits

* review changes

* requested changes

* more requested changes

* noo

* last nit fixes

* restart appveyor

* improving test cov

* Update .golangci.yml

* shazbert changes

* moving pair formatting

* format pair update wip

* path upgrade complete

* error fix

* appveyor linters

* more linters

* remove testexch

* more formatting changes

* changes

* shazbert changes

* checking older requested changes to ensure completion

* wip

* fixing broken code

* error fix

* all fixed

* additional changes

* more changes

* remove commented code

* ftx margin api

* appveyor fixes

* more appveyor issues + test addition

* more appveyor issues + test addition

* remove unnecessary

* testing

* testing, fixing okex api, error fix

* git merge fix

* go sum

* glorious changes and error fix

* rm vb

* more glorious changes and go mod tidy

* fixed now

* okex testing upgrade

* old config migration and batch fetching fix

* added test

* glorious requested changes WIP

* tested and fixed

* go fmted

* go fmt and test fix

* additional funcs and tests for fundingRates

* OKEX tested and fixed

* appveyor fixes

* ineff assign

* 1 glorious change

* error fix

* typo

* shazbert changes

* glorious code changes and path fixing huobi WIP

* adding assetType to accountinfo functions

* fixing panic

* panic fix and updating account info wrappers WIP

* updateaccountinfo updated

* testing WIP binance USDT n Coin Margined and Kraken Futures

* auth functions tested and fixed

* added test

* config reverted

* shazbert and glorious changes

* shazbert and glorious changes

* latest changes and portfolio update

* go fmt change:

* remove commented codes

* improved error checking

* index out of range fix

* rm ln

* critical nit

* glorious latest changes

* appveyor changes

* shazbert change

* easier readability

* latest glorious changes

* shadow dec

* assetstore updated

* last change

* another last change

* merge changes

* go mod tidy

* thrasher requested changes wip

* improving struct layouts

* appveyor go fmt

* remove unnecessary code

* shazbert changes

* small change

* oopsie

* tidy

* configtest reverted

* error fix

* oopsie

* for what

* test patch fix

* insecurities

* fixing tests

* fix config
2021-02-12 16:19:18 +11:00

513 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
}
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() {
c.Websocket.Wg.Add(1)
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.AssetType = assetType
newOB.Pair = newPair
newOB.ExchangeName = c.Name
newOB.LastUpdated = time.Unix(orderBook.Data[0].Timestamp, 0)
newOB.VerificationBypass = c.OrderbookVerificationBypass
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 {
maxSubsPerHour := 240
if 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 := crypto.GetHMAC(crypto.HashSHA256,
[]byte(signMsg),
[]byte(c.API.Credentials.Secret))
sign := crypto.HexEncodeToString(tempSign)
sub.Operation = "login"
sub.Arguments = []string{c.API.Credentials.Key, expTime, sign}
return c.Websocket.Conn.SendJSONMessage(sub)
}