mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-15 23:16:48 +00:00
* Initial codes for a trade tracker * Moving everything in a broken fashion * Removes tradetracker. Removes some errors for subsystems * Cleans up some subsystems, renames stuttering types. Removes some global Bot usage * More basic subsystem renaming and file moving * Removes engine dependency from events,ntpserver,ordermanager,comms manager * Exports eventManager, fixes rpcserver. puts rpcserver back for now * Removes redundant error message, further removes engine dependencies * experimental end of day interface usage * adds ability to build the application * Withdraw and event manager handling * cleans up apiserver and communications manager * Cleans up some start/setup processes. Though should separate * More consistency with Setup Start Stop IsRunning funcs * Final consistency pass before testing phase * Fixes engine tests. Fixes stop nil issue * api server tests * Communications manager testing * Connection manager tests and nilsubsystem error * End of day currencypairsyncer tests * Adds databaseconnection/databaseconnection_test.go * Adds withdrawal manager tests * Deposit address testing. Moved orderbook sync first as its more important * Adds test for event manager * More full eventmanager testing * Adds testfile. Enables skipped test. * ntp manager tests * Adds ordermanager tests, Extracts a whole new subsystem from engine and fanangles import cycles * Adds websocket routine manager tests * Basic portfolio manager testing * Fixes issue with currency pair sync startup * Fixes issue with event manager startup * Starts the order manager before backtester starts * Fixes fee tests. Expands testing. Doesnt fix races * Fixes most test races * Resolves data races * Fixes subsystem test issues * currency pair syncer coverage tests * Refactors portfolio. Fixes tests. Withdraw validation Portfolio didn't need to exist with a portfolio manager. Now the porfolio manager is in charge how the portfolio is handled and all portfolio functions are attached to the base instead of just exported at the package level Withdrawal validation occurred at the exchange level when it can just be run at the withdrawal manager level. All withdrawal requests go through that endpoint * lint -fix * golang lint fixes * lints and comments everything * Updates GCT logo, adds documentation for some subsystems * More documentation and more logo updates * Fixes backtesting and apiserver errors encountered * Fixes errors and typos from reviewing * More minor fixes * Changes %h verb to %w * reverbs to %s * Humbly begins reverting to more flat engine package The main reasoning for this is that the subsystem split doesn't make sense in a golang environment. The subsystems are only meant to be used with engine and so by placing them in a non-engine area, it does not work and is inconsistent with the rest of the application's package layout. This will begin salvaging the changes made by reverting to a flat engine package, but maintaining the consistent designs introduced. Further, I will look to remove any TestMains and decrease the scope of testing to be more local and decrease the issues that have been caused from our style of testing. * Manages to re-flatten things. Everything is within its own file * mini fixes * Fixes tests and data races and lints * Updates docs tool for engine to create filename readmes * os -> ioutil * remove err * Appveyor version increase test * Removes tCleanup as its unsupported on appveyor * Adds stuff that I thought was in previous merge master commit * Removes cancel from test * Fixes really fun test-exclusive data race * minor nit fixes * niterinos * docs gen * rm;rf test * Remove typoline. expands startstop helper. Splits apiserver * Removes accidental folder * Uses update instead of replace for order upsert * addresses nits. Renames files. Regenerates documentation. * lint and removal of comments * Add new test for default scenario * Fixes typo * regen docs
1445 lines
42 KiB
Go
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("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("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("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("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("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
|
|
}
|
|
}
|