Files
gocryptotrader/exchanges/bitfinex/bitfinex_websocket.go
Ryan O'Hara-Reid 7b718700f7 orderbook: Implement initial linked list (#643)
* Exchanges: Initial implementation after rebase of depth (WIP)

* orderbook/buffer: convert and couple orderbook interaction functionality from buffer to orderbook linked list - Use single point reference for orderbook depth

* buffer/orderbook: conversion continued (WIP)

* exchange: buffer/linkedlist handover (WIP)

* Added some tests for yesterday

* linkedList: added more testing and trying to figure out broken things

* Started tying everything in

* continuous integration and testing

* orderbook: expanded tests

* go mod tidy

* Add in different synchornisation levels for protocols
Add in timer for the streaming system to reduce updates to datahandler
Add in more test code as I integrate more exchanges

* Depth: Add tests, add length check to call linked list updating, add in constructor.
Linked List: Improve tests, add in checks for zero liquidity on books.
Node: Added in cleaner POC, add in contructor.
Buffer: Fixed tests, checked benchmarks.

* orderbook: reinstate dispatch calls

* Addr glorious & madcozbad nits

* fix functionality and add tests

* Address linterinos

* remove label

* expanded comment

* fix races and and bitmex test

* reinstate go routine for alerting changes

* rm line :D

* fix more tests

* Addr glorious nits

* rm glorious field

* depth: defer unlock to stop deadlock

* orderbook: remove unused vars

* buffer: fix test to what it should be

* nits: madcosbad addr

* nits: glorious nits

* linkedlist: remove unused params

* orderbook: shift time call to outside of push to inline, add in case for update inster price for zero liquidity, nits

* orderbook: nits addressed

* engine: change stream -> websocket convention and remove unused function

* nits: glorious nits

* Websocket Buffer: Add verbosity switch

* linked list: Add comment

* linked list: fix spelling

* nits: glorious nits

* orderbook: Adds in test and explicit time type with constructor, fix nits

* linter

* spelling: removed the dere fence

* depth: Update alerting mechanism to a more battle tested state

* depth: spelling

* nits: glorious nits

* linked list: match cases

* buffer: fix linter issue

* golangci: increase timeout by 30 seconds

* nodes: update atomic checks

* spelling: fix

* node: add in commentary

* exchanges/syncer: add function to switch over to REST when websocket functionality is not available for a specific asset type

* linter: exchange linter issues

* syncer: Add in warning

* nits: glorious nits

* AssetWebsocketSupport: unexport map

* Nits: Adrr

* rm letter

* exchanges: Orderbook verification change for naming, deprecate checksum bypass as it has the potential to obfuscate errors that are at the tail end of the book, add in verification for websocket stream updates

* general: fix spelling remove breakpoint

* nits: fix more glorious nits until more are found

* orderbook: fix tests

* orderbook: fix wait tests and add in more checks

* nits: addr

* orderbook: remove dispatch reference

* linkedlist: consolidate bid/ask functions

* linked lisdt: remove words

* fix spelling
2021-04-23 15:16:01 +10:00

1445 lines
42 KiB
Go

package bitfinex
import (
"encoding/json"
"errors"
"fmt"
"hash/crc32"
"net/http"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"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"
"github.com/thrasher-corp/gocryptotrader/log"
)
var comms = make(chan stream.Response)
type checksum struct {
Token int
Sequence int64
}
// checksumStore quick global for now
var checksumStore = make(map[int]*checksum)
var cMtx sync.Mutex
// WsConnect starts a new websocket connection
func (b *Bitfinex) WsConnect() error {
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
return errors.New(stream.WebsocketNotEnabled)
}
var dialer websocket.Dialer
err := b.Websocket.Conn.Dial(&dialer, http.Header{})
if err != nil {
return fmt.Errorf("%v unable to connect to Websocket. Error: %s",
b.Name,
err)
}
go b.wsReadData(b.Websocket.Conn)
if b.Websocket.CanUseAuthenticatedEndpoints() {
err = b.Websocket.AuthConn.Dial(&dialer, http.Header{})
if err != nil {
log.Errorf(log.ExchangeSys,
"%v unable to connect to authenticated Websocket. Error: %s",
b.Name,
err)
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
go b.wsReadData(b.Websocket.AuthConn)
err = b.WsSendAuth()
if err != nil {
log.Errorf(log.ExchangeSys,
"%v - authentication failed: %v\n",
b.Name,
err)
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
}
go b.WsDataHandler()
return nil
}
// wsReadData receives and passes on websocket messages for processing
func (b *Bitfinex) wsReadData(ws stream.Connection) {
b.Websocket.Wg.Add(1)
defer b.Websocket.Wg.Done()
for {
resp := ws.ReadMessage()
if resp.Raw == nil {
return
}
comms <- resp
}
}
// WsDataHandler handles data from wsReadData
func (b *Bitfinex) WsDataHandler() {
b.Websocket.Wg.Add(1)
defer b.Websocket.Wg.Done()
for {
select {
case resp := <-comms:
if resp.Type == websocket.TextMessage {
err := b.wsHandleData(resp.Raw)
if err != nil {
b.Websocket.DataHandler <- err
}
}
case <-b.Websocket.ShutdownC:
return
}
}
}
func (b *Bitfinex) wsHandleData(respRaw []byte) error {
var result interface{}
err := json.Unmarshal(respRaw, &result)
if err != nil {
return err
}
switch d := result.(type) {
case map[string]interface{}:
event := d["event"]
switch event {
case "subscribed":
if symbol, ok := d["symbol"].(string); ok {
b.WsAddSubscriptionChannel(int(d["chanId"].(float64)),
d["channel"].(string),
symbol,
)
} else if key, ok := d["key"].(string); ok {
// Capture trading subscriptions
contents := strings.Split(d["key"].(string), ":")
if len(contents) > 3 {
// Edge case to parse margin strings.
// map[chanId:139136 channel:candles event:subscribed key:trade:1m:tXAUTF0:USTF0]
if contents[2][0] == 't' {
key = contents[2] + ":" + contents[3]
}
}
b.WsAddSubscriptionChannel(int(d["chanId"].(float64)),
d["channel"].(string),
key,
)
}
case "auth":
status := d["status"].(string)
if status == "OK" {
b.Websocket.DataHandler <- d
b.WsAddSubscriptionChannel(0, "account", "N/A")
} else if status == "fail" {
return fmt.Errorf("bitfinex.go error - Websocket unable to AUTH. Error code: %s",
d["code"].(string))
}
}
case []interface{}:
chanF, ok := d[0].(float64)
if !ok {
return errors.New("channel ID type assertion failure")
}
chanID := int(chanF)
var datum string
if datum, ok = d[1].(string); ok {
// Capturing heart beat
if datum == "hb" {
return nil
}
// Capturing checksum and storing value
if datum == "cs" {
var tokenF float64
tokenF, ok = d[2].(float64)
if !ok {
return errors.New("checksum token type assertion failure")
}
var seqNoF float64
seqNoF, ok = d[3].(float64)
if !ok {
return errors.New("sequence number type assertion failure")
}
cMtx.Lock()
checksumStore[chanID] = &checksum{
Token: int(tokenF),
Sequence: int64(seqNoF),
}
cMtx.Unlock()
return nil
}
}
chanInfo, ok := b.WebsocketSubdChannels[chanID]
if !ok && chanID != 0 {
return fmt.Errorf("bitfinex.go error - Unable to locate chanID: %d",
chanID)
}
var chanAsset = asset.Spot
var pair currency.Pair
pairInfo := strings.Split(chanInfo.Pair, ":")
switch {
case len(pairInfo) >= 3:
newPair := pairInfo[2]
if newPair[0] == 'f' {
chanAsset = asset.MarginFunding
}
pair, err = currency.NewPairFromString(newPair[1:])
if err != nil {
return err
}
case len(pairInfo) == 1:
newPair := pairInfo[0]
if newPair[0] == 'f' {
chanAsset = asset.MarginFunding
}
pair, err = currency.NewPairFromString(newPair[1:])
if err != nil {
return err
}
case chanInfo.Pair != "":
if strings.Contains(chanInfo.Pair, ":") {
chanAsset = asset.Margin
}
pair, err = currency.NewPairFromString(chanInfo.Pair[1:])
if err != nil {
return err
}
}
switch chanInfo.Channel {
case wsBook:
var newOrderbook []WebsocketBook
obSnapBundle, ok := d[1].([]interface{})
if !ok {
return errors.New("orderbook interface cast failed")
}
if len(obSnapBundle) == 0 {
return errors.New("no data within orderbook snapshot")
}
sequenceNo, ok := d[2].(float64)
if !ok {
return errors.New("type assertion failure")
}
var fundingRate bool
switch id := obSnapBundle[0].(type) {
case []interface{}:
for i := range obSnapBundle {
data := obSnapBundle[i].([]interface{})
id, okAssert := data[0].(float64)
if !okAssert {
return errors.New("type assertion failed for orderbook item data")
}
pricePeriod, okAssert := data[1].(float64)
if !okAssert {
return errors.New("type assertion failed for orderbook item data")
}
rateAmount, okAssert := data[2].(float64)
if !okAssert {
return errors.New("type assertion failed for orderbook item data")
}
if len(data) == 4 {
fundingRate = true
amount, okFunding := data[3].(float64)
if !okFunding {
return errors.New("type assertion failed for orderbook item data")
}
newOrderbook = append(newOrderbook, WebsocketBook{
ID: int64(id),
Period: int64(pricePeriod),
Price: rateAmount,
Amount: amount})
} else {
newOrderbook = append(newOrderbook, WebsocketBook{
ID: int64(id),
Price: pricePeriod,
Amount: rateAmount})
}
}
err := b.WsInsertSnapshot(pair, chanAsset, newOrderbook, fundingRate)
if err != nil {
return fmt.Errorf("bitfinex_websocket.go inserting snapshot error: %s",
err)
}
case float64:
pricePeriod, okSnap := obSnapBundle[1].(float64)
if !okSnap {
return errors.New("type assertion failed for orderbook snapshot data")
}
amountRate, okSnap := obSnapBundle[2].(float64)
if !okSnap {
return errors.New("type assertion failed for orderbook snapshot data")
}
if len(obSnapBundle) == 4 {
fundingRate = true
var amount float64
amount, okSnap = obSnapBundle[3].(float64)
if !okSnap {
return errors.New("type assertion failed for orderbook snapshot data")
}
newOrderbook = append(newOrderbook, WebsocketBook{
ID: int64(id),
Period: int64(pricePeriod),
Price: amountRate,
Amount: amount})
} else {
newOrderbook = append(newOrderbook, WebsocketBook{
ID: int64(id),
Price: pricePeriod,
Amount: amountRate})
}
err := b.WsUpdateOrderbook(pair, chanAsset, newOrderbook, chanID, int64(sequenceNo), fundingRate)
if err != nil {
return fmt.Errorf("bitfinex_websocket.go updating orderbook error: %s",
err)
}
}
return nil
case wsCandles:
if candleBundle, ok := d[1].([]interface{}); ok {
if len(candleBundle) == 0 {
return nil
}
switch candleData := candleBundle[0].(type) {
case []interface{}:
for i := range candleBundle {
element := candleBundle[i].([]interface{})
b.Websocket.DataHandler <- stream.KlineData{
Timestamp: time.Unix(0, int64(element[0].(float64))*int64(time.Millisecond)),
Exchange: b.Name,
AssetType: chanAsset,
Pair: pair,
OpenPrice: element[1].(float64),
ClosePrice: element[2].(float64),
HighPrice: element[3].(float64),
LowPrice: element[4].(float64),
Volume: element[5].(float64),
}
}
case float64:
b.Websocket.DataHandler <- stream.KlineData{
Timestamp: time.Unix(0, int64(candleData)*int64(time.Millisecond)),
Exchange: b.Name,
AssetType: chanAsset,
Pair: pair,
OpenPrice: candleBundle[1].(float64),
ClosePrice: candleBundle[2].(float64),
HighPrice: candleBundle[3].(float64),
LowPrice: candleBundle[4].(float64),
Volume: candleBundle[5].(float64),
}
}
}
return nil
case wsTicker:
tickerData := d[1].([]interface{})
if len(tickerData) == 10 {
b.Websocket.DataHandler <- &ticker.Price{
ExchangeName: b.Name,
Bid: tickerData[0].(float64),
Ask: tickerData[2].(float64),
Last: tickerData[6].(float64),
Volume: tickerData[7].(float64),
High: tickerData[8].(float64),
Low: tickerData[9].(float64),
AssetType: chanAsset,
Pair: pair,
}
} else {
b.Websocket.DataHandler <- &ticker.Price{
ExchangeName: b.Name,
FlashReturnRate: tickerData[0].(float64),
Bid: tickerData[1].(float64),
BidPeriod: tickerData[2].(float64),
BidSize: tickerData[3].(float64),
Ask: tickerData[4].(float64),
AskPeriod: tickerData[5].(float64),
AskSize: tickerData[6].(float64),
Last: tickerData[9].(float64),
Volume: tickerData[10].(float64),
High: tickerData[11].(float64),
Low: tickerData[12].(float64),
FlashReturnRateAmount: tickerData[15].(float64),
AssetType: chanAsset,
Pair: pair,
}
}
return nil
case wsTrades:
if !b.IsSaveTradeDataEnabled() {
return nil
}
if chanAsset == asset.MarginFunding {
return nil
}
var tradeHolder []WebsocketTrade
switch len(d) {
case 2:
snapshot := d[1].([]interface{})
for i := range snapshot {
elem := snapshot[i].([]interface{})
if len(elem) == 5 {
tradeHolder = append(tradeHolder, WebsocketTrade{
ID: int64(elem[0].(float64)),
Timestamp: int64(elem[1].(float64)),
Amount: elem[2].(float64),
Rate: elem[3].(float64),
Period: int64(elem[4].(float64)),
})
} else {
tradeHolder = append(tradeHolder, WebsocketTrade{
ID: int64(elem[0].(float64)),
Timestamp: int64(elem[1].(float64)),
Amount: elem[2].(float64),
Price: elem[3].(float64),
})
}
}
case 3:
if d[1].(string) != wsFundingTradeUpdate &&
d[1].(string) != wsTradeExecutionUpdate {
return nil
}
data := d[2].([]interface{})
if len(data) == 5 {
tradeHolder = append(tradeHolder, WebsocketTrade{
ID: int64(data[0].(float64)),
Timestamp: int64(data[1].(float64)),
Amount: data[2].(float64),
Rate: data[3].(float64),
Period: int64(data[4].(float64)),
})
} else {
tradeHolder = append(tradeHolder, WebsocketTrade{
ID: int64(data[0].(float64)),
Timestamp: int64(data[1].(float64)),
Amount: data[2].(float64),
Price: data[3].(float64),
})
}
}
var trades []trade.Data
for i := range tradeHolder {
side := order.Buy
newAmount := tradeHolder[i].Amount
if newAmount < 0 {
side = order.Sell
newAmount *= -1
}
price := tradeHolder[i].Price
if price == 0 && tradeHolder[i].Rate > 0 {
price = tradeHolder[i].Rate
}
trades = append(trades, trade.Data{
TID: strconv.FormatInt(tradeHolder[i].ID, 10),
CurrencyPair: pair,
Timestamp: time.Unix(0, tradeHolder[i].Timestamp*int64(time.Millisecond)),
Price: price,
Amount: newAmount,
Exchange: b.Name,
AssetType: chanAsset,
Side: side,
})
}
return b.AddTradesToBuffer(trades...)
}
if authResp, ok := d[1].(string); ok {
switch authResp {
case wsHeartbeat, pong:
return nil
case wsNotification:
notification := d[2].([]interface{})
if data, ok := notification[4].([]interface{}); ok {
channelName := notification[1].(string)
switch {
case strings.Contains(channelName, wsFundingOrderNewRequest),
strings.Contains(channelName, wsFundingOrderUpdateRequest),
strings.Contains(channelName, wsFundingOrderCancelRequest):
if data[0] != nil && data[0].(float64) > 0 {
id := int64(data[0].(float64))
if b.Websocket.Match.IncomingWithData(id, respRaw) {
return nil
}
b.wsHandleFundingOffer(data)
}
case strings.Contains(channelName, wsOrderNewRequest),
strings.Contains(channelName, wsOrderUpdateRequest),
strings.Contains(channelName, wsOrderCancelRequest):
if data[2] != nil && data[2].(float64) > 0 {
id := int64(data[2].(float64))
if b.Websocket.Match.IncomingWithData(id, respRaw) {
return nil
}
b.wsHandleOrder(data)
}
default:
return fmt.Errorf("%s - Unexpected data returned %s",
b.Name,
respRaw)
}
}
if notification[5] != nil &&
strings.EqualFold(notification[5].(string), wsError) {
return fmt.Errorf("%s - Error %s",
b.Name,
notification[6].(string))
}
case wsOrderSnapshot:
if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 {
if _, ok := snapBundle[0].([]interface{}); ok {
for i := range snapBundle {
positionData := snapBundle[i].([]interface{})
b.wsHandleOrder(positionData)
}
}
}
case wsOrderCancel, wsOrderNew, wsOrderUpdate:
if oData, ok := d[2].([]interface{}); ok && len(oData) > 0 {
b.wsHandleOrder(oData)
}
case wsPositionSnapshot:
var snapshot []WebsocketPosition
if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 {
if _, ok := snapBundle[0].([]interface{}); ok {
for i := range snapBundle {
positionData := snapBundle[i].([]interface{})
position := WebsocketPosition{
Pair: positionData[0].(string),
Status: positionData[1].(string),
Amount: positionData[2].(float64),
Price: positionData[3].(float64),
MarginFunding: positionData[4].(float64),
MarginFundingType: int64(positionData[5].(float64)),
ProfitLoss: positionData[6].(float64),
ProfitLossPercent: positionData[7].(float64),
LiquidationPrice: positionData[8].(float64),
Leverage: positionData[9].(float64),
}
snapshot = append(snapshot, position)
}
b.Websocket.DataHandler <- snapshot
}
}
case wsPositionNew, wsPositionUpdate, wsPositionClose:
if positionData, ok := d[2].([]interface{}); ok && len(positionData) > 0 {
position := WebsocketPosition{
Pair: positionData[0].(string),
Status: positionData[1].(string),
Amount: positionData[2].(float64),
Price: positionData[3].(float64),
MarginFunding: positionData[4].(float64),
MarginFundingType: int64(positionData[5].(float64)),
ProfitLoss: positionData[6].(float64),
ProfitLossPercent: positionData[7].(float64),
LiquidationPrice: positionData[8].(float64),
Leverage: positionData[9].(float64),
}
b.Websocket.DataHandler <- position
}
case wsTradeExecuted, wsTradeExecutionUpdate:
if tradeData, ok := d[2].([]interface{}); ok && len(tradeData) > 4 {
b.Websocket.DataHandler <- WebsocketTradeData{
TradeID: int64(tradeData[0].(float64)),
Pair: tradeData[1].(string),
Timestamp: int64(tradeData[2].(float64)),
OrderID: int64(tradeData[3].(float64)),
AmountExecuted: tradeData[4].(float64),
PriceExecuted: tradeData[5].(float64),
OrderType: tradeData[6].(string),
OrderPrice: tradeData[7].(float64),
Maker: tradeData[8].(float64) == 1,
Fee: tradeData[9].(float64),
FeeCurrency: tradeData[10].(string),
}
}
case wsFundingOrderSnapshot:
var snapshot []WsFundingOffer
if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 {
if _, ok := snapBundle[0].([]interface{}); ok {
for i := range snapBundle {
data := snapBundle[i].([]interface{})
offer := WsFundingOffer{
ID: int64(data[0].(float64)),
Symbol: data[1].(string),
Created: int64(data[2].(float64)),
Updated: int64(data[3].(float64)),
Amount: data[4].(float64),
OriginalAmount: data[5].(float64),
Type: data[6].(string),
Flags: data[9].(float64),
Status: data[10].(string),
Rate: data[14].(float64),
Period: int64(data[15].(float64)),
Notify: data[16].(float64) == 1,
Hidden: data[17].(float64) == 1,
Insure: data[18].(float64) == 1,
Renew: data[19].(float64) == 1,
RateReal: data[20].(float64),
}
snapshot = append(snapshot, offer)
}
b.Websocket.DataHandler <- snapshot
}
}
case wsFundingOrderNew, wsFundingOrderUpdate, wsFundingOrderCancel:
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
b.wsHandleFundingOffer(data)
}
case wsFundingCreditSnapshot:
var snapshot []WsCredit
if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 {
if _, ok := snapBundle[0].([]interface{}); ok {
for i := range snapBundle {
data := snapBundle[i].([]interface{})
credit := WsCredit{
ID: int64(data[0].(float64)),
Symbol: data[1].(string),
Side: data[2].(string),
Created: int64(data[3].(float64)),
Updated: int64(data[4].(float64)),
Amount: data[5].(float64),
Flags: data[6].(string),
Status: data[7].(string),
Rate: data[11].(float64),
Period: int64(data[12].(float64)),
Opened: int64(data[13].(float64)),
LastPayout: int64(data[14].(float64)),
Notify: data[15].(float64) == 1,
Hidden: data[16].(float64) == 1,
Insure: data[17].(float64) == 1,
Renew: data[18].(float64) == 1,
RateReal: data[19].(float64),
NoClose: data[20].(float64) == 1,
PositionPair: data[21].(string),
}
snapshot = append(snapshot, credit)
}
b.Websocket.DataHandler <- snapshot
}
}
case wsFundingCreditNew, wsFundingCreditUpdate, wsFundingCreditCancel:
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
b.Websocket.DataHandler <- WsCredit{
ID: int64(data[0].(float64)),
Symbol: data[1].(string),
Side: data[2].(string),
Created: int64(data[3].(float64)),
Updated: int64(data[4].(float64)),
Amount: data[5].(float64),
Flags: data[6].(string),
Status: data[7].(string),
Rate: data[11].(float64),
Period: int64(data[12].(float64)),
Opened: int64(data[13].(float64)),
LastPayout: int64(data[14].(float64)),
Notify: data[15].(float64) == 1,
Hidden: data[16].(float64) == 1,
Insure: data[17].(float64) == 1,
Renew: data[18].(float64) == 1,
RateReal: data[19].(float64),
NoClose: data[20].(float64) == 1,
PositionPair: data[21].(string),
}
}
case wsFundingLoanSnapshot:
var snapshot []WsCredit
if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 {
if _, ok := snapBundle[0].([]interface{}); ok {
for i := range snapBundle {
data := snapBundle[i].([]interface{})
credit := WsCredit{
ID: int64(data[0].(float64)),
Symbol: data[1].(string),
Side: data[2].(string),
Created: int64(data[3].(float64)),
Updated: int64(data[4].(float64)),
Amount: data[5].(float64),
Flags: data[6].(string),
Status: data[7].(string),
Rate: data[11].(float64),
Period: int64(data[12].(float64)),
Opened: int64(data[13].(float64)),
LastPayout: int64(data[14].(float64)),
Notify: data[15].(float64) == 1,
Hidden: data[16].(float64) == 1,
Insure: data[17].(float64) == 1,
Renew: data[18].(float64) == 1,
RateReal: data[19].(float64),
NoClose: data[20].(float64) == 1,
}
snapshot = append(snapshot, credit)
}
b.Websocket.DataHandler <- snapshot
}
}
case wsFundingLoanNew, wsFundingLoanUpdate, wsFundingLoanCancel:
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
b.Websocket.DataHandler <- WsCredit{
ID: int64(data[0].(float64)),
Symbol: data[1].(string),
Side: data[2].(string),
Created: int64(data[3].(float64)),
Updated: int64(data[4].(float64)),
Amount: data[5].(float64),
Flags: data[6].(string),
Status: data[7].(string),
Rate: data[11].(float64),
Period: int64(data[12].(float64)),
Opened: int64(data[13].(float64)),
LastPayout: int64(data[14].(float64)),
Notify: data[15].(float64) == 1,
Hidden: data[16].(float64) == 1,
Insure: data[17].(float64) == 1,
Renew: data[18].(float64) == 1,
RateReal: data[19].(float64),
NoClose: data[20].(float64) == 1,
}
}
case wsWalletSnapshot:
var snapshot []WsWallet
if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 {
if _, ok := snapBundle[0].([]interface{}); ok {
for i := range snapBundle {
data := snapBundle[i].([]interface{})
var balanceAvailable float64
if _, ok := data[4].(float64); ok {
balanceAvailable = data[4].(float64)
}
wallet := WsWallet{
Type: data[0].(string),
Currency: data[1].(string),
Balance: data[2].(float64),
UnsettledInterest: data[3].(float64),
BalanceAvailable: balanceAvailable,
}
snapshot = append(snapshot, wallet)
}
b.Websocket.DataHandler <- snapshot
}
}
case wsWalletUpdate:
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
var balanceAvailable float64
if _, ok := data[4].(float64); ok {
balanceAvailable = data[4].(float64)
}
b.Websocket.DataHandler <- WsWallet{
Type: data[0].(string),
Currency: data[1].(string),
Balance: data[2].(float64),
UnsettledInterest: data[3].(float64),
BalanceAvailable: balanceAvailable,
}
}
case wsBalanceUpdate:
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
b.Websocket.DataHandler <- WsBalanceInfo{
TotalAssetsUnderManagement: data[0].(float64),
NetAssetsUnderManagement: data[1].(float64),
}
}
case wsMarginInfoUpdate:
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
if data[0].(string) == "base" {
if infoBase, ok := d[2].([]interface{}); ok && len(infoBase) > 0 {
baseData := data[1].([]interface{})
b.Websocket.DataHandler <- WsMarginInfoBase{
UserProfitLoss: baseData[0].(float64),
UserSwaps: baseData[1].(float64),
MarginBalance: baseData[2].(float64),
MarginNet: baseData[3].(float64),
}
}
}
}
case wsFundingInfoUpdate:
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
if data[0].(string) == "sym" {
symbolData := data[1].([]interface{})
b.Websocket.DataHandler <- WsFundingInfo{
YieldLoan: symbolData[0].(float64),
YieldLend: symbolData[1].(float64),
DurationLoan: symbolData[2].(float64),
DurationLend: symbolData[3].(float64),
}
}
}
case wsFundingTradeExecuted, wsFundingTradeUpdate:
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
b.Websocket.DataHandler <- WsFundingTrade{
ID: int64(data[0].(float64)),
Symbol: data[1].(string),
MTSCreated: int64(data[2].(float64)),
OfferID: int64(data[3].(float64)),
Amount: data[4].(float64),
Rate: data[5].(float64),
Period: int64(data[6].(float64)),
Maker: data[7].(float64) == 1,
}
}
default:
b.Websocket.DataHandler <- stream.UnhandledMessageWarning{
Message: b.Name + stream.UnhandledMessage + string(respRaw),
}
return nil
}
}
}
return nil
}
func (b *Bitfinex) wsHandleFundingOffer(data []interface{}) {
var fo WsFundingOffer
if data[0] != nil {
fo.ID = int64(data[0].(float64))
}
if data[1] != nil {
fo.Symbol = data[1].(string)[1:]
}
if data[2] != nil {
fo.Created = int64(data[2].(float64))
}
if data[3] != nil {
fo.Updated = int64(data[0].(float64))
}
if data[15] != nil {
fo.Period = int64(data[15].(float64))
}
if data[4] != nil {
fo.Amount = data[4].(float64)
}
if data[5] != nil {
fo.OriginalAmount = data[5].(float64)
}
if data[6] != nil {
fo.Type = data[6].(string)
}
if data[9] != nil {
fo.Flags = data[9].(float64)
}
if data[9] != nil {
fo.Status = data[10].(string)
}
if data[9] != nil {
fo.Rate = data[14].(float64)
}
if data[16] != nil {
fo.Notify = data[16].(float64) == 1
}
if data[17] != nil {
fo.Hidden = data[17].(float64) == 1
}
if data[18] != nil {
fo.Insure = data[18].(float64) == 1
}
if data[19] != nil {
fo.Renew = data[19].(float64) == 1
}
if data[20] != nil {
fo.RateReal = data[20].(float64)
}
b.Websocket.DataHandler <- fo
}
func (b *Bitfinex) wsHandleOrder(data []interface{}) {
var od order.Detail
var err error
od.Exchange = b.Name
if data[0] != nil {
od.ID = strconv.FormatFloat(data[0].(float64), 'f', -1, 64)
}
if data[16] != nil {
od.Price = data[16].(float64)
}
if data[7] != nil {
od.Amount = data[7].(float64)
}
if data[6] != nil {
od.RemainingAmount = data[6].(float64)
}
if data[7] != nil && data[6] != nil {
od.ExecutedAmount = data[7].(float64) - data[6].(float64)
}
if data[4] != nil {
od.Date = time.Unix(int64(data[4].(float64))*1000, 0)
}
if data[5] != nil {
od.LastUpdated = time.Unix(int64(data[5].(float64))*1000, 0)
}
if data[2] != nil {
od.Pair, od.AssetType, err = b.GetRequestFormattedPairAndAssetType(data[3].(string)[1:])
if err != nil {
b.Websocket.DataHandler <- err
return
}
}
if data[8] != nil {
oType, err := order.StringToOrderType(data[8].(string))
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: od.ID,
Err: err,
}
}
od.Type = oType
}
if data[13] != nil {
oStatus, err := order.StringToOrderStatus(data[13].(string))
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: od.ID,
Err: err,
}
}
od.Status = oStatus
}
b.Websocket.DataHandler <- &od
}
// WsInsertSnapshot add the initial orderbook snapshot when subscribed to a
// channel
func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType asset.Item, books []WebsocketBook, fundingRate bool) error {
if len(books) == 0 {
return errors.New("bitfinex.go error - no orderbooks submitted")
}
var book orderbook.Base
for i := range books {
item := orderbook.Item{
ID: books[i].ID,
Amount: books[i].Amount,
Price: books[i].Price,
Period: books[i].Period,
}
if fundingRate {
if item.Amount < 0 {
item.Amount *= -1
book.Bids = append(book.Bids, item)
} else {
book.Asks = append(book.Asks, item)
}
} else {
if books[i].Amount > 0 {
book.Bids = append(book.Bids, item)
} else {
item.Amount *= -1
book.Asks = append(book.Asks, item)
}
}
}
book.Asset = assetType
book.Pair = p
book.Exchange = b.Name
book.PriceDuplication = true
book.IsFundingRate = fundingRate
book.VerifyOrderbook = b.CanVerifyOrderbook
return b.Websocket.Orderbook.LoadSnapshot(&book)
}
// WsUpdateOrderbook updates the orderbook list, removing and adding to the
// orderbook sides
func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book []WebsocketBook, channelID int, sequenceNo int64, fundingRate bool) error {
orderbookUpdate := buffer.Update{Asset: assetType, Pair: p}
for i := range book {
item := orderbook.Item{
ID: book[i].ID,
Amount: book[i].Amount,
Price: book[i].Price,
Period: book[i].Period,
}
if book[i].Price > 0 {
orderbookUpdate.Action = buffer.UpdateInsert
if fundingRate {
if book[i].Amount < 0 {
item.Amount *= -1
orderbookUpdate.Bids = append(orderbookUpdate.Bids, item)
} else {
orderbookUpdate.Asks = append(orderbookUpdate.Asks, item)
}
} else {
if book[i].Amount > 0 {
orderbookUpdate.Bids = append(orderbookUpdate.Bids, item)
} else {
item.Amount *= -1
orderbookUpdate.Asks = append(orderbookUpdate.Asks, item)
}
}
} else {
orderbookUpdate.Action = buffer.Delete
if fundingRate {
if book[i].Amount == 1 {
// delete bid
orderbookUpdate.Asks = append(orderbookUpdate.Asks, item)
} else {
// delete ask
orderbookUpdate.Bids = append(orderbookUpdate.Bids, item)
}
} else {
if book[i].Amount == 1 {
// delete bid
orderbookUpdate.Bids = append(orderbookUpdate.Bids, item)
} else {
// delete ask
orderbookUpdate.Asks = append(orderbookUpdate.Asks, item)
}
}
}
}
cMtx.Lock()
checkme := checksumStore[channelID]
if checkme == nil {
cMtx.Unlock()
return b.Websocket.Orderbook.Update(&orderbookUpdate)
}
checksumStore[channelID] = nil
cMtx.Unlock()
if checkme.Sequence+1 == sequenceNo {
// Sequence numbers get dropped, if checksum is not in line with
// sequence, do not check.
ob, err := b.Websocket.Orderbook.GetOrderbook(p, assetType)
if err != nil {
return fmt.Errorf("cannot calculate websocket checksum: book not found for %s %s %w",
p,
assetType,
err)
}
err = validateCRC32(ob, checkme.Token)
if err != nil {
return err
}
}
return b.Websocket.Orderbook.Update(&orderbookUpdate)
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (b *Bitfinex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var channels = []string{
wsBook,
wsTrades,
wsTicker,
wsCandles,
}
var subscriptions []stream.ChannelSubscription
assets := b.GetAssetTypes()
for i := range assets {
enabledPairs, err := b.GetEnabledPairs(assets[i])
if err != nil {
return nil, err
}
for j := range channels {
for k := range enabledPairs {
params := make(map[string]interface{})
if channels[j] == wsBook {
params["prec"] = "R0"
params["len"] = "100"
}
if channels[j] == wsCandles {
// TODO: Add ability to select timescale && funding period
var fundingPeriod string
prefix := "t"
if assets[i] == asset.MarginFunding {
prefix = "f"
fundingPeriod = ":p30"
}
params["key"] = "trade:1m:" + prefix + enabledPairs[k].String() + fundingPeriod
} else {
params["symbol"] = enabledPairs[k].String()
}
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: channels[j],
Currency: enabledPairs[k],
Params: params,
Asset: assets[i],
})
}
}
}
return subscriptions, nil
}
// Subscribe sends a websocket message to receive data from the channel
func (b *Bitfinex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
var errs common.Errors
checksum := make(map[string]interface{})
checksum["event"] = "conf"
checksum["flags"] = bitfinexChecksumFlag + bitfinexWsSequenceFlag
err := b.Websocket.Conn.SendJSONMessage(checksum)
if err != nil {
return err
}
for i := range channelsToSubscribe {
req := make(map[string]interface{})
req["event"] = "subscribe"
req["channel"] = channelsToSubscribe[i].Channel
for k, v := range channelsToSubscribe[i].Params {
req[k] = v
}
err := b.Websocket.Conn.SendJSONMessage(req)
if err != nil {
errs = append(errs, err)
continue
}
b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[i])
}
if errs != nil {
return errs
}
return nil
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (b *Bitfinex) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
var errs common.Errors
for i := range channelsToUnsubscribe {
req := make(map[string]interface{})
req["event"] = "unsubscribe"
req["channel"] = channelsToUnsubscribe[i].Channel
for k, v := range channelsToUnsubscribe[i].Params {
req[k] = v
}
err := b.Websocket.Conn.SendJSONMessage(req)
if err != nil {
errs = append(errs, err)
continue
}
b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i])
}
if errs != nil {
return errs
}
return nil
}
// WsSendAuth sends a autheticated event payload
func (b *Bitfinex) WsSendAuth() error {
if !b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled",
b.Name)
}
nonce := strconv.FormatInt(time.Now().Unix(), 10)
payload := "AUTH" + nonce
request := WsAuthRequest{
Event: "auth",
APIKey: b.API.Credentials.Key,
AuthPayload: payload,
AuthSig: crypto.HexEncodeToString(crypto.GetHMAC(crypto.HashSHA512_384,
[]byte(payload),
[]byte(b.API.Credentials.Secret))),
AuthNonce: nonce,
DeadManSwitch: 0,
}
err := b.Websocket.AuthConn.SendJSONMessage(request)
if err != nil {
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
}
return nil
}
// WsAddSubscriptionChannel adds a new subscription channel to the
// WebsocketSubdChannels map in bitfinex.go (Bitfinex struct)
func (b *Bitfinex) WsAddSubscriptionChannel(chanID int, channel, pair string) {
chanInfo := WebsocketChanInfo{Pair: pair, Channel: channel}
b.WebsocketSubdChannels[chanID] = chanInfo
if b.Verbose {
log.Debugf(log.ExchangeSys,
"%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n",
b.Name,
channel,
pair,
chanID)
}
}
// WsNewOrder authenticated new order request
func (b *Bitfinex) WsNewOrder(data *WsNewOrderRequest) (string, error) {
data.CustomID = b.Websocket.AuthConn.GenerateMessageID(false)
request := makeRequestInterface(wsOrderNew, data)
resp, err := b.Websocket.AuthConn.SendMessageReturnResponse(data.CustomID, request)
if err != nil {
return "", err
}
if resp == nil {
return "", errors.New(b.Name + " - Order message not returned")
}
var respData []interface{}
err = json.Unmarshal(resp, &respData)
if err != nil {
return "", err
}
responseDataDetail := respData[2].([]interface{})
responseOrderDetail := responseDataDetail[4].([]interface{})
var orderID string
if responseOrderDetail[0] != nil && responseOrderDetail[0].(float64) > 0 {
orderID = strconv.FormatFloat(responseOrderDetail[0].(float64), 'f', -1, 64)
}
errCode := responseDataDetail[6].(string)
errorMessage := responseDataDetail[7].(string)
if strings.EqualFold(errCode, wsError) {
return orderID, errors.New(b.Name + " - " + errCode + ": " + errorMessage)
}
return orderID, nil
}
// WsModifyOrder authenticated modify order request
func (b *Bitfinex) WsModifyOrder(data *WsUpdateOrderRequest) error {
request := makeRequestInterface(wsOrderUpdate, data)
resp, err := b.Websocket.AuthConn.SendMessageReturnResponse(data.OrderID, request)
if err != nil {
return err
}
if resp == nil {
return errors.New(b.Name + " - Order message not returned")
}
var responseData []interface{}
err = json.Unmarshal(resp, &responseData)
if err != nil {
return err
}
responseOrderData := responseData[2].([]interface{})
errCode := responseOrderData[6].(string)
errorMessage := responseOrderData[7].(string)
if strings.EqualFold(errCode, wsError) {
return errors.New(b.Name + " - " + errCode + ": " + errorMessage)
}
return nil
}
// WsCancelMultiOrders authenticated cancel multi order request
func (b *Bitfinex) WsCancelMultiOrders(orderIDs []int64) error {
cancel := WsCancelGroupOrdersRequest{
OrderID: orderIDs,
}
request := makeRequestInterface(wsCancelMultipleOrders, cancel)
return b.Websocket.AuthConn.SendJSONMessage(request)
}
// WsCancelOrder authenticated cancel order request
func (b *Bitfinex) WsCancelOrder(orderID int64) error {
cancel := WsCancelOrderRequest{
OrderID: orderID,
}
request := makeRequestInterface(wsOrderCancel, cancel)
resp, err := b.Websocket.AuthConn.SendMessageReturnResponse(orderID, request)
if err != nil {
return err
}
if resp == nil {
return fmt.Errorf("%v - Order %v failed to cancel", b.Name, orderID)
}
var responseData []interface{}
err = json.Unmarshal(resp, &responseData)
if err != nil {
return err
}
responseOrderData := responseData[2].([]interface{})
errCode := responseOrderData[6].(string)
errorMessage := responseOrderData[7].(string)
if strings.EqualFold(errCode, wsError) {
return errors.New(b.Name + " - " + errCode + ": " + errorMessage)
}
return nil
}
// WsCancelAllOrders authenticated cancel all orders request
func (b *Bitfinex) WsCancelAllOrders() error {
cancelAll := WsCancelAllOrdersRequest{All: 1}
request := makeRequestInterface(wsCancelMultipleOrders, cancelAll)
return b.Websocket.AuthConn.SendJSONMessage(request)
}
// WsNewOffer authenticated new offer request
func (b *Bitfinex) WsNewOffer(data *WsNewOfferRequest) error {
request := makeRequestInterface(wsFundingOrderNew, data)
return b.Websocket.AuthConn.SendJSONMessage(request)
}
// WsCancelOffer authenticated cancel offer request
func (b *Bitfinex) WsCancelOffer(orderID int64) error {
cancel := WsCancelOrderRequest{
OrderID: orderID,
}
request := makeRequestInterface(wsFundingOrderCancel, cancel)
resp, err := b.Websocket.AuthConn.SendMessageReturnResponse(orderID, request)
if err != nil {
return err
}
if resp == nil {
return fmt.Errorf("%v - Order %v failed to cancel", b.Name, orderID)
}
var responseData []interface{}
err = json.Unmarshal(resp, &responseData)
if err != nil {
return err
}
responseOrderData := responseData[2].([]interface{})
errCode := responseOrderData[6].(string)
var errorMessage string
if responseOrderData[7] != nil {
errorMessage = responseOrderData[7].(string)
}
if strings.EqualFold(errCode, wsError) {
return errors.New(b.Name + " - " + errCode + ": " + errorMessage)
}
return nil
}
func makeRequestInterface(channelName string, data interface{}) []interface{} {
return []interface{}{0, channelName, nil, data}
}
func validateCRC32(book *orderbook.Base, token int) error {
// Order ID's need to be sub-sorted in ascending order, this needs to be
// done on the main book to ensure that we do not cut price levels out below
reOrderByID(book.Bids)
reOrderByID(book.Asks)
// RO precision calculation is based on order ID's and amount values
var bids, asks []orderbook.Item
for i := 0; i < 25; i++ {
if i < len(book.Bids) {
bids = append(bids, book.Bids[i])
}
if i < len(book.Asks) {
asks = append(asks, book.Asks[i])
}
}
// ensure '-' (negative amount) is passed back to string buffer as
// this is needed for calcs - These get swapped if funding rate
bidmod := float64(1)
if book.IsFundingRate {
bidmod = -1
}
askMod := float64(-1)
if book.IsFundingRate {
askMod = 1
}
var check strings.Builder
for i := 0; i < 25; i++ {
if i < len(bids) {
check.WriteString(strconv.FormatInt(bids[i].ID, 10))
check.WriteString(":")
check.WriteString(strconv.FormatFloat(bidmod*bids[i].Amount, 'f', -1, 64))
check.WriteString(":")
}
if i < len(asks) {
check.WriteString(strconv.FormatInt(asks[i].ID, 10))
check.WriteString(":")
check.WriteString(strconv.FormatFloat(askMod*asks[i].Amount, 'f', -1, 64))
check.WriteString(":")
}
}
checksumStr := strings.TrimSuffix(check.String(), ":")
checksum := crc32.ChecksumIEEE([]byte(checksumStr))
if checksum == uint32(token) {
return nil
}
return fmt.Errorf("invalid checksum for %s %s: calculated [%d] does not match [%d]",
book.Asset,
book.Pair,
checksum,
uint32(token))
}
// reOrderByID sub sorts orderbook items by its corresponding ID when price
// levels are the same. TODO: Deprecate and shift to buffer level insertion
// based off ascending ID.
func reOrderByID(depth []orderbook.Item) {
subSort:
for x := 0; x < len(depth); {
var subset []orderbook.Item
// Traverse forward elements
for y := x + 1; y < len(depth); y++ {
if depth[x].Price == depth[y].Price &&
// Period matching is for funding rates, this was undocumented
// but these need to be matched with price for the correct ID
// alignment
depth[x].Period == depth[y].Period {
// Append element to subset when price match occurs
subset = append(subset, depth[y])
// Traverse next
continue
}
if len(subset) != 0 {
// Append root element
subset = append(subset, depth[x])
// Sort IDs by ascending
sort.Slice(subset, func(i, j int) bool {
return subset[i].ID < subset[j].ID
})
// Re-align elements with sorted ID subset
for z := range subset {
depth[x+z] = subset[z]
}
}
// When price is not matching change checked element to root
x = y
continue subSort
}
break
}
}