From 22ff33cd54796e2b897a9191098c51c8341b82d8 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Mon, 4 Nov 2019 15:34:30 +1100 Subject: [PATCH] Engine QA (#367) * Improved error message when no config is set on startup * Change inccorect error wording * bump Bitfinex websocket orderbook return length to max * temporary fix of incorrect orderbook updates, limit to bid and ask len of 100, will be extended later if needed * Fixed issue in binance websocket that appended 0 volume bid/ask items * Fix panic when unmarshalling an empty pair from config * Add get pair asset method for exchange base Fix Bitmex orderbook stream Unbuffer Bitmex orderbook stream * force syncer to update ticker instead of fetch, which allows a stream * Fix websocket last price for coinbasepro * fix websocket ticker for coinut * Fix websocket orderbook stream Huobi * increase orderbook depth REST for Huobi * Fix websocket support and ensure data integrity * Fix time parsing issue after error checks * check error, only process enabled currency pairs, signal websocket data processing * expanded websocket functionality for okgroup * Add logic to not process zero length slice for orderbooks * fix websocket ticker only updating enabled and individual book updates * ZB fixes to order submission/retrieval/cancellation w/ general fixes * Quiet unnecessary warning * updated config entry values for REST and websocket (initial hack until I come up with a better solution for asset types) * Ch GetName function to field access modifyer & rm useless code * Add in error I missed * Nits addressed * some more fixes * Turned kraken default websocket to true and some small changes * fixes linter issues * Ensured okgroup books and sent update through to datahandler. Zb update as well. * Add test case to get asset type from pair * Add test for pairs unmarshal * Add testing and addressed nits * FIX linter issue * Addressed Gees nits * Thanks glorious spotter * more nitorinos * Addres even more nits * Add stringerino 4000 * Fix for panic cause by sort slice out of range, also nits addressed * fix linter issues * Changed from function to field access * Changed from function to field access * fix for orderbook update panic, removes quick fix - caused by sync item fetching through same protocol * Add new test and update random generator * pass in invalid string to future ob fetching, due to futures contract expire and a http 400 error is returned --- config/config.go | 3 +- currency/pairs.go | 5 + currency/pairs_test.go | 19 +- engine/syncer.go | 2 +- exchanges/binance/binance_types.go | 14 +- exchanges/binance/binance_websocket.go | 58 +- exchanges/binance/binance_wrapper.go | 10 +- exchanges/bitfinex/bitfinex_websocket.go | 9 +- exchanges/bitmex/bitmex_websocket.go | 47 +- exchanges/bitmex/bitmex_wrapper.go | 2 +- exchanges/bitstamp/bitstamp_websocket.go | 82 ++- exchanges/bitstamp/bitstamp_wrapper.go | 7 - exchanges/bittrex/bittrex_wrapper.go | 2 +- .../coinbasepro/coinbasepro_websocket.go | 13 +- exchanges/coinut/coinut_types.go | 25 +- exchanges/coinut/coinut_websocket.go | 17 +- exchanges/coinut/coinut_wrapper.go | 6 +- exchanges/exchange.go | 10 + exchanges/exchange_test.go | 26 + exchanges/gateio/gateio_websocket.go | 10 +- exchanges/gemini/gemini_websocket.go | 10 +- exchanges/hitbtc/hitbtc_websocket.go | 10 +- exchanges/huobi/huobi_types.go | 8 +- exchanges/huobi/huobi_websocket.go | 37 +- exchanges/huobi/huobi_wrapper.go | 2 +- exchanges/kraken/kraken_websocket.go | 332 +++++++---- exchanges/kraken/kraken_wrapper.go | 2 +- exchanges/lakebtc/lakebtc.go | 33 +- exchanges/lakebtc/lakebtc_websocket.go | 41 +- exchanges/lakebtc/lakebtc_wrapper.go | 27 +- exchanges/okcoin/okcoin_test.go | 12 - exchanges/okcoin/okcoin_wrapper.go | 19 +- exchanges/okex/okex.go | 73 +-- exchanges/okex/okex_test.go | 38 -- exchanges/okex/okex_wrapper.go | 253 ++++++--- exchanges/okgroup/okgroup.go | 35 +- exchanges/okgroup/okgroup_test.go | 78 +++ exchanges/okgroup/okgroup_types.go | 61 +- exchanges/okgroup/okgroup_websocket.go | 536 ++++++++++++------ exchanges/okgroup/okgroup_wrapper.go | 80 ++- exchanges/orderbook/orderbook.go | 12 +- exchanges/orderbook/orderbook_types.go | 4 + exchanges/poloniex/poloniex_test.go | 2 +- exchanges/poloniex/poloniex_websocket.go | 171 ++++-- exchanges/poloniex/poloniex_wrapper.go | 6 +- exchanges/request/request.go | 9 +- .../websocket/wsorderbook/wsorderbook.go | 163 +++--- .../websocket/wsorderbook/wsorderbook_test.go | 342 ++++++----- .../wsorderbook/wsorderbook_types.go | 14 +- exchanges/zb/zb_test.go | 13 +- exchanges/zb/zb_websocket.go | 14 +- exchanges/zb/zb_websocket_types.go | 18 +- exchanges/zb/zb_wrapper.go | 65 ++- 53 files changed, 1813 insertions(+), 1074 deletions(-) create mode 100644 exchanges/okgroup/okgroup_test.go diff --git a/config/config.go b/config/config.go index 52373774..d720beb4 100644 --- a/config/config.go +++ b/config/config.go @@ -1433,7 +1433,8 @@ func GetFilePath(file string) (string, error) { return newDirs[0], nil } - return "", errors.New("config default file path error") + return "", fmt.Errorf("config.json file not found in %s, please follow README.md in root dir for config generation", + newDir) } // ReadConfig verifies and checks for encryption and verifies the unencrypted diff --git a/currency/pairs.go b/currency/pairs.go index a6855e87..f383f99d 100644 --- a/currency/pairs.go +++ b/currency/pairs.go @@ -73,6 +73,11 @@ func (p *Pairs) UnmarshalJSON(d []byte) error { return err } + // If no pairs enabled in config just continue + if pairs == "" { + return nil + } + var allThePairs Pairs for _, data := range strings.Split(pairs, ",") { allThePairs = append(allThePairs, NewPairFromString(data)) diff --git a/currency/pairs_test.go b/currency/pairs_test.go index bd52814d..d58d57d1 100644 --- a/currency/pairs_test.go +++ b/currency/pairs_test.go @@ -68,13 +68,28 @@ func TestPairsFormat(t *testing.T) { func TestPairsUnmarshalJSON(t *testing.T) { var unmarshalHere Pairs - configPairs := "btc_usd,btc_aud,btc_ltc" - + configPairs := "" encoded, err := common.JSONEncode(configPairs) if err != nil { t.Fatal("Pairs UnmarshalJSON() error", err) } + err = common.JSONDecode([]byte{1, 3, 3, 7}, &unmarshalHere) + if err == nil { + t.Fatal("error cannot be nil") + } + + err = common.JSONDecode(encoded, &unmarshalHere) + if err != nil { + t.Fatal("Pairs UnmarshalJSON() error", err) + } + + configPairs = "btc_usd,btc_aud,btc_ltc" + encoded, err = common.JSONEncode(configPairs) + if err != nil { + t.Fatal("Pairs UnmarshalJSON() error", err) + } + err = common.JSONDecode(encoded, &unmarshalHere) if err != nil { t.Fatal("Pairs UnmarshalJSON() error", err) diff --git a/engine/syncer.go b/engine/syncer.go index 598661f5..8a944cb7 100644 --- a/engine/syncer.go +++ b/engine/syncer.go @@ -400,7 +400,7 @@ func (e *ExchangeCurrencyPairSyncer) worker() { result, err = Bot.Exchanges[x].FetchTicker(c.Pair, c.AssetType) } } else { - result, err = Bot.Exchanges[x].FetchTicker(c.Pair, c.AssetType) + result, err = Bot.Exchanges[x].UpdateTicker(c.Pair, c.AssetType) } printTickerSummary(&result, c.Pair, c.AssetType, exchangeName, err) if err == nil { diff --git a/exchanges/binance/binance_types.go b/exchanges/binance/binance_types.go index 30718199..e3bd8125 100644 --- a/exchanges/binance/binance_types.go +++ b/exchanges/binance/binance_types.go @@ -92,13 +92,13 @@ type DepthUpdateParams []struct { // WebsocketDepthStream is the difference for the update depth stream type WebsocketDepthStream struct { - Event string `json:"e"` - Timestamp int64 `json:"E"` - Pair string `json:"s"` - FirstUpdateID int64 `json:"U"` - LastUpdateID int64 `json:"u"` - UpdateBids []interface{} `json:"b"` - UpdateAsks []interface{} `json:"a"` + Event string `json:"e"` + Timestamp int64 `json:"E"` + Pair string `json:"s"` + FirstUpdateID int64 `json:"U"` + LastUpdateID int64 `json:"u"` + UpdateBids [][]interface{} `json:"b"` + UpdateAsks [][]interface{} `json:"a"` } // RecentTradeRequestParams represents Klines request data. diff --git a/exchanges/binance/binance_websocket.go b/exchanges/binance/binance_websocket.go index 72715f16..9b2ab70b 100644 --- a/exchanges/binance/binance_websocket.go +++ b/exchanges/binance/binance_websocket.go @@ -236,12 +236,16 @@ func (b *Binance) SeedLocalCache(p currency.Pair) error { } for i := range orderbookNew.Bids { - newOrderBook.Bids = append(newOrderBook.Bids, - orderbook.Item{Amount: orderbookNew.Bids[i].Quantity, Price: orderbookNew.Bids[i].Price}) + newOrderBook.Bids = append(newOrderBook.Bids, orderbook.Item{ + Amount: orderbookNew.Bids[i].Quantity, + Price: orderbookNew.Bids[i].Price, + }) } for i := range orderbookNew.Asks { - newOrderBook.Asks = append(newOrderBook.Asks, - orderbook.Item{Amount: orderbookNew.Asks[i].Quantity, Price: orderbookNew.Asks[i].Price}) + newOrderBook.Asks = append(newOrderBook.Asks, orderbook.Item{ + Amount: orderbookNew.Asks[i].Quantity, + Price: orderbookNew.Asks[i].Price, + }) } newOrderBook.LastUpdated = time.Unix(orderbookNew.LastUpdateID, 0) @@ -256,38 +260,38 @@ func (b *Binance) SeedLocalCache(p currency.Pair) error { func (b *Binance) UpdateLocalCache(wsdp *WebsocketDepthStream) error { var updateBid, updateAsk []orderbook.Item for i := range wsdp.UpdateBids { - var priceToBeUpdated orderbook.Item - for i, bids := range wsdp.UpdateBids[i].([]interface{}) { - switch i { - case 0: - priceToBeUpdated.Price, _ = strconv.ParseFloat(bids.(string), 64) - case 1: - priceToBeUpdated.Amount, _ = strconv.ParseFloat(bids.(string), 64) - } + p, err := strconv.ParseFloat(wsdp.UpdateBids[i][0].(string), 64) + if err != nil { + return err } - updateBid = append(updateBid, priceToBeUpdated) + a, err := strconv.ParseFloat(wsdp.UpdateBids[i][1].(string), 64) + if err != nil { + return err + } + + updateBid = append(updateBid, orderbook.Item{Price: p, Amount: a}) } for i := range wsdp.UpdateAsks { - var priceToBeUpdated orderbook.Item - for i, asks := range wsdp.UpdateAsks[i].([]interface{}) { - switch i { - case 0: - priceToBeUpdated.Price, _ = strconv.ParseFloat(asks.(string), 64) - case 1: - priceToBeUpdated.Amount, _ = strconv.ParseFloat(asks.(string), 64) - } + p, err := strconv.ParseFloat(wsdp.UpdateAsks[i][0].(string), 64) + if err != nil { + return err } - updateAsk = append(updateAsk, priceToBeUpdated) + a, err := strconv.ParseFloat(wsdp.UpdateAsks[i][1].(string), 64) + if err != nil { + return err + } + + updateAsk = append(updateAsk, orderbook.Item{Price: p, Amount: a}) } currencyPair := currency.NewPairFromFormattedPairs(wsdp.Pair, b.GetEnabledPairs(asset.Spot), b.GetPairFormat(asset.Spot, true)) return b.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{ - Bids: updateBid, - Asks: updateAsk, - CurrencyPair: currencyPair, - UpdateID: wsdp.LastUpdateID, - AssetType: asset.Spot, + Bids: updateBid, + Asks: updateAsk, + Pair: currencyPair, + UpdateID: wsdp.LastUpdateID, + Asset: asset.Spot, }) } diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index e30234ac..65c1d515 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -162,7 +162,7 @@ func (b *Binance) Setup(exch *config.ExchangeConfig) error { b.Websocket.Orderbook.Setup( exch.WebsocketOrderbookBufferLimit, - true, + false, true, true, false, @@ -202,7 +202,8 @@ func (b *Binance) Run() { if err != nil { log.Errorf(log.ExchangeSys, "%s failed to update currencies. Err: %s\n", - b.Name, err) + b.Name, + err) } } @@ -247,7 +248,10 @@ func (b *Binance) UpdateTradablePairs(forceUpdate bool) error { return err } - return b.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) + return b.UpdatePairs(currency.NewPairsFromStrings(pairs), + asset.Spot, + false, + forceUpdate) } // UpdateTicker updates and returns the ticker for a currency pair diff --git a/exchanges/bitfinex/bitfinex_websocket.go b/exchanges/bitfinex/bitfinex_websocket.go index b0381e36..80b3541d 100644 --- a/exchanges/bitfinex/bitfinex_websocket.go +++ b/exchanges/bitfinex/bitfinex_websocket.go @@ -498,10 +498,10 @@ func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType asset.Item, books // orderbook sides func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book []WebsocketBook) error { orderbookUpdate := wsorderbook.WebsocketOrderbookUpdate{ - Asks: []orderbook.Item{}, - Bids: []orderbook.Item{}, - AssetType: assetType, - CurrencyPair: p, + Asks: []orderbook.Item{}, + Bids: []orderbook.Item{}, + Asset: assetType, + Pair: p, } for i := 0; i < len(book); i++ { @@ -548,6 +548,7 @@ func (b *Bitfinex) GenerateDefaultSubscriptions() { params := make(map[string]interface{}) if channels[i] == "book" { params["prec"] = "P0" + params["len"] = "100" } subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: channels[i], diff --git a/exchanges/bitmex/bitmex_websocket.go b/exchanges/bitmex/bitmex_websocket.go index bb77a528..603cbe46 100644 --- a/exchanges/bitmex/bitmex_websocket.go +++ b/exchanges/bitmex/bitmex_websocket.go @@ -206,8 +206,17 @@ func (b *Bitmex) wsHandleIncomingData() { } p := currency.NewPairFromString(orderbooks.Data[0].Symbol) - // TODO: update this to support multiple asset types - err = b.processOrderbook(orderbooks.Data, orderbooks.Action, p, "CONTRACT") + var a asset.Item + a, err = b.GetPairAssetType(p) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + + err = b.processOrderbook(orderbooks.Data, + orderbooks.Action, + p, + a) if err != nil { b.Websocket.DataHandler <- err continue @@ -345,12 +354,14 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai asks = append(asks, orderbook.Item{ Price: data[i].Price, Amount: float64(data[i].Size), + ID: data[i].ID, }) continue } bids = append(bids, orderbook.Item{ Price: data[i].Price, Amount: float64(data[i].Size), + ID: data[i].ID, }) } @@ -379,24 +390,23 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai for i := range data { if strings.EqualFold(data[i].Side, "Sell") { asks = append(asks, orderbook.Item{ - Price: data[i].Price, Amount: float64(data[i].Size), + ID: data[i].ID, }) continue } bids = append(bids, orderbook.Item{ - Price: data[i].Price, Amount: float64(data[i].Size), + ID: data[i].ID, }) } err := b.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: currencyPair, - UpdateTime: time.Now(), - AssetType: assetType, - Action: action, + Bids: bids, + Asks: asks, + Pair: currencyPair, + Asset: assetType, + Action: action, }) if err != nil { return err @@ -413,7 +423,16 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (b *Bitmex) GenerateDefaultSubscriptions() { - contracts := b.GetEnabledPairs(asset.PerpetualContract) + assets := b.GetAssetTypes() + var allPairs currency.Pairs + + for x := range assets { + contracts := b.GetEnabledPairs(assets[x]) + for y := range contracts { + allPairs = allPairs.Add(contracts[y]) + } + } + channels := []string{bitmexWSOrderbookL2, bitmexWSTrade} subscriptions := []wshandler.WebsocketChannelSubscription{ { @@ -422,10 +441,10 @@ func (b *Bitmex) GenerateDefaultSubscriptions() { } for i := range channels { - for j := range contracts { + for j := range allPairs { subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ - Channel: fmt.Sprintf("%v:%v", channels[i], contracts[j].String()), - Currency: contracts[j], + Channel: fmt.Sprintf("%v:%v", channels[i], allPairs[j].String()), + Currency: allPairs[j], }) } } diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index d7968829..3f44db1a 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -188,7 +188,7 @@ func (b *Bitmex) Setup(exch *config.ExchangeConfig) error { b.Websocket.Orderbook.Setup( exch.WebsocketOrderbookBufferLimit, - true, + false, false, false, true, diff --git a/exchanges/bitstamp/bitstamp_websocket.go b/exchanges/bitstamp/bitstamp_websocket.go index 67d6c619..9cccbb62 100644 --- a/exchanges/bitstamp/bitstamp_websocket.go +++ b/exchanges/bitstamp/bitstamp_websocket.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" "strings" + "time" "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/common" @@ -13,7 +14,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -32,7 +32,7 @@ func (b *Bitstamp) WsConnect() error { return err } if b.Verbose { - log.Debugf(log.ExchangeSys, "%s Connected to Websocket.\n", b.GetName()) + log.Debugf(log.ExchangeSys, "%s Connected to Websocket.\n", b.Name) } err = b.seedOrderBook() @@ -76,7 +76,7 @@ func (b *Bitstamp) WsHandleData() { switch wsResponse.Event { case "bts:request_reconnect": if b.Verbose { - log.Debugf(log.ExchangeSys, "%v - Websocket reconnection request received", b.GetName()) + log.Debugf(log.ExchangeSys, "%v - Websocket reconnection request received", b.Name) } go b.Websocket.Shutdown() // Connection monitor will reconnect @@ -89,7 +89,7 @@ func (b *Bitstamp) WsHandleData() { } currencyPair := strings.Split(wsResponse.Channel, "_") - p := currency.NewPairFromString(strings.ToUpper(currencyPair[3])) + p := currency.NewPairFromString(strings.ToUpper(currencyPair[2])) err = b.wsUpdateOrderbook(wsOrderBookTemp.Data, p, asset.Spot) if err != nil { @@ -113,7 +113,7 @@ func (b *Bitstamp) WsHandleData() { Price: wsTradeTemp.Data.Price, Amount: wsTradeTemp.Data.Amount, CurrencyPair: p, - Exchange: b.GetName(), + Exchange: b.Name, AssetType: asset.Spot, } } @@ -122,7 +122,7 @@ func (b *Bitstamp) WsHandleData() { } func (b *Bitstamp) generateDefaultSubscriptions() { - var channels = []string{"live_trades_", "diff_order_book_"} + var channels = []string{"live_trades_", "order_book_"} enabledCurrencies := b.GetEnabledPairs(asset.Spot) var subscriptions []wshandler.WebsocketChannelSubscription for i := range channels { @@ -163,47 +163,45 @@ func (b *Bitstamp) wsUpdateOrderbook(update websocketOrderBook, p currency.Pair, } var asks, bids []orderbook.Item - if len(update.Asks) > 0 { - for i := range update.Asks { - target, err := strconv.ParseFloat(update.Asks[i][0], 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - amount, err := strconv.ParseFloat(update.Asks[i][1], 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - asks = append(asks, orderbook.Item{Price: target, Amount: amount}) + for i := range update.Asks { + target, err := strconv.ParseFloat(update.Asks[i][0], 64) + if err != nil { + b.Websocket.DataHandler <- err + continue } + + amount, err := strconv.ParseFloat(update.Asks[i][1], 64) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + + asks = append(asks, orderbook.Item{Price: target, Amount: amount}) } - if len(update.Bids) > 0 { - for i := range update.Bids { - target, err := strconv.ParseFloat(update.Bids[i][0], 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - amount, err := strconv.ParseFloat(update.Bids[i][1], 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - bids = append(bids, orderbook.Item{Price: target, Amount: amount}) + for i := range update.Bids { + target, err := strconv.ParseFloat(update.Bids[i][0], 64) + if err != nil { + b.Websocket.DataHandler <- err + continue } + + amount, err := strconv.ParseFloat(update.Bids[i][1], 64) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + + bids = append(bids, orderbook.Item{Price: target, Amount: amount}) } - err := b.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{ + + err := b.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{ Bids: bids, Asks: asks, - CurrencyPair: p, - UpdateID: update.Timestamp, + Pair: p, + LastUpdated: time.Unix(update.Timestamp, 0), AssetType: asset.Spot, + ExchangeName: b.Name, }) if err != nil { return err @@ -212,7 +210,7 @@ func (b *Bitstamp) wsUpdateOrderbook(update websocketOrderBook, p currency.Pair, b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: p, Asset: assetType, - Exchange: b.GetName(), + Exchange: b.Name, } return nil @@ -247,7 +245,7 @@ func (b *Bitstamp) seedOrderBook() error { newOrderBook.Bids = bids newOrderBook.Pair = p[x] newOrderBook.AssetType = asset.Spot - newOrderBook.ExchangeName = b.GetName() + newOrderBook.ExchangeName = b.Name err = b.Websocket.Orderbook.LoadSnapshot(&newOrderBook) if err != nil { @@ -257,7 +255,7 @@ func (b *Bitstamp) seedOrderBook() error { b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: p[x], Asset: asset.Spot, - Exchange: b.GetName(), + Exchange: b.Name, } } return nil diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index b04dc4d0..50a0fd69 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -159,13 +159,6 @@ func (b *Bitstamp) Setup(exch *config.ExchangeConfig) error { ResponseMaxLimit: exch.WebsocketResponseMaxLimit, } - b.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - true, - true, - true, - false, - exch.Name) return nil } diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index 45b1a5ea..07cc5f13 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -329,7 +329,7 @@ func (b *Bittrex) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { if s.OrderType != order.Limit { return submitOrderResponse, - errors.New("limit order not supported on exchange") + errors.New("limit orders only supported on exchange") } var response UUID diff --git a/exchanges/coinbasepro/coinbasepro_websocket.go b/exchanges/coinbasepro/coinbasepro_websocket.go index 9d151d10..a94f9cda 100644 --- a/exchanges/coinbasepro/coinbasepro_websocket.go +++ b/exchanges/coinbasepro/coinbasepro_websocket.go @@ -97,11 +97,10 @@ func (c *CoinbasePro) WsHandleData() { Open: ticker.Open24H, High: ticker.High24H, Low: ticker.Low24H, - Close: ticker.Price, + Last: ticker.Price, Volume: ticker.Volume24H, Bid: ticker.BestBid, Ask: ticker.BestAsk, - Last: ticker.LastSize, } case "snapshot": @@ -258,11 +257,11 @@ func (c *CoinbasePro) ProcessUpdate(update WebsocketL2Update) error { return err } err = c.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: p, - UpdateTime: timestamp, - AssetType: asset.Spot, + Bids: bids, + Asks: asks, + Pair: p, + UpdateTime: timestamp, + Asset: asset.Spot, }) if err != nil { return err diff --git a/exchanges/coinut/coinut_types.go b/exchanges/coinut/coinut_types.go index 2540e6a1..b42d3425 100644 --- a/exchanges/coinut/coinut_types.go +++ b/exchanges/coinut/coinut_types.go @@ -31,15 +31,22 @@ type Instruments struct { // Ticker holds ticker information type Ticker struct { - HighestBuy float64 `json:"highest_buy,string"` - InstrumentID int `json:"inst_id"` - Last float64 `json:"last,string"` - LowestSell float64 `json:"lowest_sell,string"` - OpenInterest float64 `json:"open_interest,string"` - Timestamp int64 `json:"timestamp"` - TransID int64 `json:"trans_id"` - Volume float64 `json:"volume,string"` - Volume24 float64 `json:"volume24,string"` + High24 float64 `json:"high24,string"` + HighestBuy float64 `json:"highest_buy,string"` + InstrumentID int `json:"inst_id"` + Last float64 `json:"last,string"` + Low24 float64 `json:"low24,string"` + LowestSell float64 `json:"lowest_sell,string"` + PrevTransID int64 `json:"prev_trans_id"` + PriceChange24 float64 `json:"price_change_24,string"` + Reply string `json:"reply"` + OpenInterest float64 `json:"open_interest,string"` + Timestamp int64 `json:"timestamp"` + TransID int64 `json:"trans_id"` + Volume float64 `json:"volume,string"` + Volume24 float64 `json:"volume24,string"` + Volume24Quote float64 `json:"volume24_quote,string"` + VolumeQuote float64 `json:"volume_quote,string"` } // OrderbookBase is a sub-type holding price and quantity diff --git a/exchanges/coinut/coinut_websocket.go b/exchanges/coinut/coinut_websocket.go index ff30766a..977e41c2 100644 --- a/exchanges/coinut/coinut_websocket.go +++ b/exchanges/coinut/coinut_websocket.go @@ -136,13 +136,16 @@ func (c *COINUT) wsProcessResponse(resp []byte) { c.Websocket.DataHandler <- err return } + currencyPair := wsInstrumentMap.LookupInstrument(ticker.InstID) c.Websocket.DataHandler <- wshandler.TickerData{ Exchange: c.Name, - Volume: ticker.Volume, - QuoteVolume: ticker.VolumeQuote, - High: ticker.HighestBuy, - Low: ticker.LowestSell, + Volume: ticker.Volume24, + QuoteVolume: ticker.Volume24Quote, + Bid: ticker.HighestBuy, + Ask: ticker.LowestSell, + High: ticker.High24, + Low: ticker.Low24, Last: ticker.Last, Timestamp: time.Unix(0, ticker.Timestamp), AssetType: asset.Spot, @@ -302,9 +305,9 @@ func (c *COINUT) WsProcessOrderbookUpdate(update *WsOrderbookUpdate) error { c.GetPairFormat(asset.Spot, true), ) bufferUpdate := &wsorderbook.WebsocketOrderbookUpdate{ - CurrencyPair: p, - UpdateID: update.TransID, - AssetType: asset.Spot, + Pair: p, + UpdateID: update.TransID, + Asset: asset.Spot, } if strings.EqualFold(update.Side, order.Buy.Lower()) { bufferUpdate.Bids = []orderbook.Item{{Price: update.Price, Amount: update.Volume}} diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index 14c94edc..501000d2 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -342,8 +342,10 @@ func (c *COINUT) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Pri } tickerPrice = ticker.Price{ Last: tick.Last, - High: tick.HighestBuy, - Low: tick.LowestSell, + High: tick.High24, + Low: tick.Low24, + Bid: tick.HighestBuy, + Ask: tick.LowestSell, Volume: tick.Volume24, Pair: p, LastUpdated: time.Unix(0, tick.Timestamp), diff --git a/exchanges/exchange.go b/exchanges/exchange.go index a5376011..37eabdc7 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -230,6 +230,16 @@ func (e *Base) GetAssetTypes() asset.Items { return e.CurrencyPairs.AssetTypes } +// GetPairAssetType returns the associated asset type for the currency pair +func (e *Base) GetPairAssetType(c currency.Pair) (asset.Item, error) { + for i := range e.GetAssetTypes() { + if e.GetEnabledPairs(e.GetAssetTypes()[i]).Contains(c, true) { + return e.GetAssetTypes()[i], nil + } + } + return "", errors.New("asset type not associated with currency pair") +} + // GetClientBankAccounts returns banking details associated with // a client for withdrawal purposes func (e *Base) GetClientBankAccounts(exchangeName, withdrawalCurrency string) (config.BankAccount, error) { diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index f9a9ad55..ae1c53ea 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -1357,3 +1357,29 @@ func TestGetBase(t *testing.T) { t.Error("name should be rawr") } } + +func TestGetAssetType(t *testing.T) { + var b Base + p := currency.NewPair(currency.BTC, currency.USD) + _, err := b.GetPairAssetType(p) + if err == nil { + t.Fatal("error cannot be nil") + } + b.CurrencyPairs.AssetTypes = asset.Items{asset.Spot} + b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) + b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ + Enabled: currency.Pairs{ + currency.NewPair(currency.BTC, currency.USD), + }, + ConfigFormat: ¤cy.PairFormat{Delimiter: "-"}, + } + + a, err := b.GetPairAssetType(p) + if err != nil { + t.Fatal(err) + } + + if a != asset.Spot { + t.Error("should be spot but is", a) + } +} diff --git a/exchanges/gateio/gateio_websocket.go b/exchanges/gateio/gateio_websocket.go index fe8d5e83..49d4221e 100644 --- a/exchanges/gateio/gateio_websocket.go +++ b/exchanges/gateio/gateio_websocket.go @@ -246,11 +246,11 @@ func (g *Gateio) WsHandleData() { } else { err = g.Websocket.Orderbook.Update( &wsorderbook.WebsocketOrderbookUpdate{ - Asks: asks, - Bids: bids, - CurrencyPair: currency.NewPairFromString(c), - UpdateTime: time.Now(), - AssetType: asset.Spot, + Asks: asks, + Bids: bids, + Pair: currency.NewPairFromString(c), + UpdateTime: time.Now(), + Asset: asset.Spot, }) if err != nil { g.Websocket.DataHandler <- err diff --git a/exchanges/gemini/gemini_websocket.go b/exchanges/gemini/gemini_websocket.go index 089f9fff..681d839c 100644 --- a/exchanges/gemini/gemini_websocket.go +++ b/exchanges/gemini/gemini_websocket.go @@ -319,11 +319,11 @@ func (g *Gemini) wsProcessUpdate(result WsMarketUpdateResponse, pair currency.Pa } } err := g.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{ - Asks: asks, - Bids: bids, - CurrencyPair: pair, - UpdateTime: time.Unix(0, result.TimestampMS), - AssetType: asset.Spot, + Asks: asks, + Bids: bids, + Pair: pair, + UpdateTime: time.Unix(0, result.TimestampMS), + Asset: asset.Spot, }) if err != nil { g.Websocket.DataHandler <- fmt.Errorf("%v %v", g.Name, err) diff --git a/exchanges/hitbtc/hitbtc_websocket.go b/exchanges/hitbtc/hitbtc_websocket.go index f8cfe0c3..5726ca8a 100644 --- a/exchanges/hitbtc/hitbtc_websocket.go +++ b/exchanges/hitbtc/hitbtc_websocket.go @@ -284,11 +284,11 @@ func (h *HitBTC) WsProcessOrderbookUpdate(update WsOrderbook) error { p := currency.NewPairFromFormattedPairs(update.Params.Symbol, h.GetEnabledPairs(asset.Spot), h.GetPairFormat(asset.Spot, true)) err := h.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{ - Asks: asks, - Bids: bids, - CurrencyPair: p, - UpdateID: update.Params.Sequence, - AssetType: asset.Spot, + Asks: asks, + Bids: bids, + Pair: p, + UpdateID: update.Params.Sequence, + Asset: asset.Spot, }) if err != nil { return err diff --git a/exchanges/huobi/huobi_types.go b/exchanges/huobi/huobi_types.go index 7dd12b22..b7ea87db 100644 --- a/exchanges/huobi/huobi_types.go +++ b/exchanges/huobi/huobi_types.go @@ -320,10 +320,10 @@ type WsDepth struct { Channel string `json:"ch"` Timestamp int64 `json:"ts"` Tick struct { - Bids []interface{} `json:"bids"` - Asks []interface{} `json:"asks"` - Timestamp int64 `json:"ts"` - Version int64 `json:"version"` + Bids [][]interface{} `json:"bids"` + Asks [][]interface{} `json:"asks"` + Timestamp int64 `json:"ts"` + Version int64 `json:"version"` } `json:"tick"` } diff --git a/exchanges/huobi/huobi_websocket.go b/exchanges/huobi/huobi_websocket.go index fb5897ec..e6675735 100644 --- a/exchanges/huobi/huobi_websocket.go +++ b/exchanges/huobi/huobi_websocket.go @@ -233,8 +233,14 @@ func (h *HUOBI) wsHandleMarketData(resp WsMessage) { h.Websocket.DataHandler <- err return } + data := strings.Split(depth.Channel, ".") - h.WsProcessOrderbook(&depth, data[1]) + err = h.WsProcessOrderbook(&depth, data[1]) + if err != nil { + h.Websocket.DataHandler <- err + return + } + case strings.Contains(init.Channel, "kline"): var kline WsKline err := common.JSONDecode(resp.Raw, &kline) @@ -297,32 +303,41 @@ func (h *HUOBI) wsHandleMarketData(resp WsMessage) { // WsProcessOrderbook processes new orderbook data func (h *HUOBI) WsProcessOrderbook(update *WsDepth, symbol string) error { p := currency.NewPairFromFormattedPairs(symbol, - h.GetEnabledPairs(asset.Spot), h.GetPairFormat(asset.Spot, true)) + h.GetEnabledPairs(asset.Spot), + h.GetPairFormat(asset.Spot, true)) + var bids, asks []orderbook.Item - for i := 0; i < len(update.Tick.Bids); i++ { - bidLevel := update.Tick.Bids[i].([]interface{}) - bids = append(bids, orderbook.Item{Price: bidLevel[0].(float64), - Amount: bidLevel[0].(float64)}) + for i := range update.Tick.Bids { + bids = append(bids, orderbook.Item{ + Price: update.Tick.Bids[i][0].(float64), + Amount: update.Tick.Bids[i][1].(float64), + }) } - for i := 0; i < len(update.Tick.Asks); i++ { - askLevel := update.Tick.Asks[i].([]interface{}) - asks = append(asks, orderbook.Item{Price: askLevel[0].(float64), - Amount: askLevel[0].(float64)}) + + for i := range update.Tick.Asks { + asks = append(asks, orderbook.Item{ + Price: update.Tick.Asks[i][0].(float64), + Amount: update.Tick.Asks[i][1].(float64), + }) } + var newOrderBook orderbook.Base newOrderBook.Asks = asks newOrderBook.Bids = bids newOrderBook.Pair = p + newOrderBook.AssetType = asset.Spot + newOrderBook.ExchangeName = h.Name + err := h.Websocket.Orderbook.LoadSnapshot(&newOrderBook) if err != nil { return err } + h.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: p, Exchange: h.GetName(), Asset: asset.Spot, } - return nil } diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index b14483a0..8c68e079 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -343,7 +343,7 @@ func (h *HUOBI) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderboo var orderBook orderbook.Base orderbookNew, err := h.GetDepth(OrderBookDataRequestParams{ Symbol: h.FormatExchangeCurrency(p, assetType).String(), - Type: OrderBookDataRequestParamsTypeStep1, + Type: OrderBookDataRequestParamsTypeStep0, }) if err != nil { return orderBook, err diff --git a/exchanges/kraken/kraken_websocket.go b/exchanges/kraken/kraken_websocket.go index 025d5aea..f4c2ceb9 100644 --- a/exchanges/kraken/kraken_websocket.go +++ b/exchanges/kraken/kraken_websocket.go @@ -143,31 +143,31 @@ func (k *Kraken) WsHandleDataResponse(response WebsocketDataResponse) { log.Debugf(log.ExchangeSys, "%v Websocket ticker data received", k.Name) } - k.wsProcessTickers(&channelData, response[1]) + k.wsProcessTickers(&channelData, response[1].(map[string]interface{})) case krakenWsOHLC: if k.Verbose { log.Debugf(log.ExchangeSys, "%v Websocket OHLC data received", k.Name) } - k.wsProcessCandles(&channelData, response[1]) + k.wsProcessCandles(&channelData, response[1].([]interface{})) case krakenWsOrderbook: if k.Verbose { log.Debugf(log.ExchangeSys, "%v Websocket Orderbook data received", k.Name) } - k.wsProcessOrderBook(&channelData, response[1]) + k.wsProcessOrderBook(&channelData, response[1].(map[string]interface{})) case krakenWsSpread: if k.Verbose { log.Debugf(log.ExchangeSys, "%v Websocket Spread data received", k.Name) } - k.wsProcessSpread(&channelData, response[1]) + k.wsProcessSpread(&channelData, response[1].([]interface{})) case krakenWsTrade: if k.Verbose { log.Debugf(log.ExchangeSys, "%v Websocket Trade data received", k.Name) } - k.wsProcessTrades(&channelData, response[1]) + k.wsProcessTrades(&channelData, response[1].([]interface{})) default: log.Errorf(log.ExchangeSys, "%v Unidentified websocket data received: %v", k.Name, @@ -238,22 +238,48 @@ func getSubscriptionChannelData(id int64) WebsocketChannelData { } // wsProcessTickers converts ticker data and sends it to the datahandler -func (k *Kraken) wsProcessTickers(channelData *WebsocketChannelData, data interface{}) { - tickerData := data.(map[string]interface{}) - askData := tickerData["a"].([]interface{}) - bidData := tickerData["b"].([]interface{}) - closeData := tickerData["c"].([]interface{}) - openData := tickerData["o"].([]interface{}) - lowData := tickerData["l"].([]interface{}) - highData := tickerData["h"].([]interface{}) - volumeData := tickerData["v"].([]interface{}) - closePrice, _ := strconv.ParseFloat(closeData[0].(string), 64) - openPrice, _ := strconv.ParseFloat(openData[0].(string), 64) - highPrice, _ := strconv.ParseFloat(highData[0].(string), 64) - lowPrice, _ := strconv.ParseFloat(lowData[0].(string), 64) - quantity, _ := strconv.ParseFloat(volumeData[0].(string), 64) - ask, _ := strconv.ParseFloat(askData[0].(string), 64) - bid, _ := strconv.ParseFloat(bidData[0].(string), 64) +func (k *Kraken) wsProcessTickers(channelData *WebsocketChannelData, data map[string]interface{}) { + closePrice, err := strconv.ParseFloat(data["c"].([]interface{})[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + openPrice, err := strconv.ParseFloat(data["o"].([]interface{})[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + highPrice, err := strconv.ParseFloat(data["h"].([]interface{})[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + lowPrice, err := strconv.ParseFloat(data["l"].([]interface{})[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + quantity, err := strconv.ParseFloat(data["v"].([]interface{})[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + ask, err := strconv.ParseFloat(data["a"].([]interface{})[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + bid, err := strconv.ParseFloat(data["b"].([]interface{})[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } k.Websocket.DataHandler <- wshandler.TickerData{ Exchange: k.Name, @@ -271,13 +297,17 @@ func (k *Kraken) wsProcessTickers(channelData *WebsocketChannelData, data interf } // wsProcessTickers converts ticker data and sends it to the datahandler -func (k *Kraken) wsProcessSpread(channelData *WebsocketChannelData, data interface{}) { - spreadData := data.([]interface{}) - bestBid := spreadData[0].(string) - bestAsk := spreadData[1].(string) - timeData, _ := strconv.ParseFloat(spreadData[2].(string), 64) - bidVolume := spreadData[3].(string) - askVolume := spreadData[4].(string) +func (k *Kraken) wsProcessSpread(channelData *WebsocketChannelData, data []interface{}) { + bestBid := data[0].(string) + bestAsk := data[1].(string) + timeData, err := strconv.ParseFloat(data[2].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + bidVolume := data[3].(string) + askVolume := data[4].(string) sec, dec := math.Modf(timeData) spreadTimestamp := time.Unix(int64(sec), int64(dec*(1e9))) if k.Verbose { @@ -294,14 +324,28 @@ func (k *Kraken) wsProcessSpread(channelData *WebsocketChannelData, data interfa } // wsProcessTrades converts trade data and sends it to the datahandler -func (k *Kraken) wsProcessTrades(channelData *WebsocketChannelData, data interface{}) { - tradeData := data.([]interface{}) - for i := range tradeData { - trade := tradeData[i].([]interface{}) - timeData, _ := strconv.ParseInt(trade[2].(string), 10, 64) - timeUnix := time.Unix(timeData, 0) - price, _ := strconv.ParseFloat(trade[0].(string), 64) - amount, _ := strconv.ParseFloat(trade[1].(string), 64) +func (k *Kraken) wsProcessTrades(channelData *WebsocketChannelData, data []interface{}) { + for i := range data { + trade := data[i].([]interface{}) + timeData, err := strconv.ParseFloat(trade[2].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + sec, dec := math.Modf(timeData) + timeUnix := time.Unix(int64(sec), int64(dec*(1e9))) + + price, err := strconv.ParseFloat(trade[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + amount, err := strconv.ParseFloat(trade[1].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } k.Websocket.DataHandler <- wshandler.TradeData{ AssetType: asset.Spot, @@ -318,17 +362,17 @@ func (k *Kraken) wsProcessTrades(channelData *WebsocketChannelData, data interfa // wsProcessOrderBook determines if the orderbook data is partial or update // Then sends to appropriate fun -func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data interface{}) { - obData := data.(map[string]interface{}) - if _, ok := obData["as"]; ok { - k.wsProcessOrderBookPartial(channelData, obData) +func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data map[string]interface{}) { + if fullAsk, ok := data["as"].([]interface{}); ok { + fullBids := data["as"].([]interface{}) + k.wsProcessOrderBookPartial(channelData, fullAsk, fullBids) } else { - _, asksExist := obData["a"] - _, bidsExist := obData["b"] + askData, asksExist := data["a"].([]interface{}) + bidData, bidsExist := data["b"].([]interface{}) if asksExist || bidsExist { k.wsRequestMtx.Lock() defer k.wsRequestMtx.Unlock() - err := k.wsProcessOrderBookUpdate(channelData, obData) + err := k.wsProcessOrderBookUpdate(channelData, askData, bidData) if err != nil { subscriptionToRemove := wshandler.WebsocketChannelSubscription{ Channel: krakenWsOrderbook, @@ -341,40 +385,64 @@ func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data inte } // wsProcessOrderBookPartial creates a new orderbook entry for a given currency pair -func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, obData map[string]interface{}) { +func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, askData, bidData []interface{}) { base := orderbook.Base{ Pair: channelData.Pair, AssetType: asset.Spot, } - // Kraken ob data is timestamped per price, GCT orderbook data is timestamped per entry - // Using the highest last update time, we can attempt to respect both within a reasonable degree + // Kraken ob data is timestamped per price, GCT orderbook data is + // timestamped per entry using the highest last update time, we can attempt + // to respect both within a reasonable degree var highestLastUpdate time.Time - askData := obData["as"].([]interface{}) for i := range askData { asks := askData[i].([]interface{}) - price, _ := strconv.ParseFloat(asks[0].(string), 64) - amount, _ := strconv.ParseFloat(asks[1].(string), 64) + price, err := strconv.ParseFloat(asks[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + amount, err := strconv.ParseFloat(asks[1].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } base.Asks = append(base.Asks, orderbook.Item{ Amount: amount, Price: price, }) - timeData, _ := strconv.ParseFloat(asks[2].(string), 64) + timeData, err := strconv.ParseFloat(asks[2].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } sec, dec := math.Modf(timeData) askUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9))) if highestLastUpdate.Before(askUpdatedTime) { highestLastUpdate = askUpdatedTime } } - bidData := obData["bs"].([]interface{}) + for i := range bidData { bids := bidData[i].([]interface{}) - price, _ := strconv.ParseFloat(bids[0].(string), 64) - amount, _ := strconv.ParseFloat(bids[1].(string), 64) + price, err := strconv.ParseFloat(bids[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + amount, err := strconv.ParseFloat(bids[1].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } base.Bids = append(base.Bids, orderbook.Item{ Amount: amount, Price: price, }) - timeData, _ := strconv.ParseFloat(bids[2].(string), 64) + timeData, err := strconv.ParseFloat(bids[2].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } sec, dec := math.Modf(timeData) bidUpdateTime := time.Unix(int64(sec), int64(dec*(1e9))) if highestLastUpdate.Before(bidUpdateTime) { @@ -382,6 +450,7 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, ob } } base.LastUpdated = highestLastUpdate + base.ExchangeName = k.Name err := k.Websocket.Orderbook.LoadSnapshot(&base) if err != nil { k.Websocket.DataHandler <- err @@ -395,48 +464,68 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, ob } // wsProcessOrderBookUpdate updates an orderbook entry for a given currency pair -func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, obData map[string]interface{}) error { +func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, askData, bidData []interface{}) error { update := wsorderbook.WebsocketOrderbookUpdate{ - AssetType: asset.Spot, - CurrencyPair: channelData.Pair, + Asset: asset.Spot, + Pair: channelData.Pair, } + var highestLastUpdate time.Time // Ask data is not always sent - if _, ok := obData["a"]; ok { - askData := obData["a"].([]interface{}) - for i := range askData { - asks := askData[i].([]interface{}) - price, _ := strconv.ParseFloat(asks[0].(string), 64) - amount, _ := strconv.ParseFloat(asks[1].(string), 64) - update.Asks = append(update.Asks, orderbook.Item{ - Amount: amount, - Price: price, - }) - timeData, _ := strconv.ParseFloat(asks[2].(string), 64) - sec, dec := math.Modf(timeData) - askUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9))) - if highestLastUpdate.Before(askUpdatedTime) { - highestLastUpdate = askUpdatedTime - } + for i := range askData { + asks := askData[i].([]interface{}) + price, err := strconv.ParseFloat(asks[0].(string), 64) + if err != nil { + return err + } + + amount, err := strconv.ParseFloat(asks[1].(string), 64) + if err != nil { + return err + } + + update.Asks = append(update.Asks, orderbook.Item{ + Amount: amount, + Price: price, + }) + timeData, err := strconv.ParseFloat(asks[2].(string), 64) + if err != nil { + return err + } + + sec, dec := math.Modf(timeData) + askUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9))) + if highestLastUpdate.Before(askUpdatedTime) { + highestLastUpdate = askUpdatedTime } } + // Bid data is not always sent - if _, ok := obData["b"]; ok { - bidData := obData["b"].([]interface{}) - for i := range bidData { - bids := bidData[i].([]interface{}) - price, _ := strconv.ParseFloat(bids[0].(string), 64) - amount, _ := strconv.ParseFloat(bids[1].(string), 64) - update.Bids = append(update.Bids, orderbook.Item{ - Amount: amount, - Price: price, - }) - timeData, _ := strconv.ParseFloat(bids[2].(string), 64) - sec, dec := math.Modf(timeData) - bidUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9))) - if highestLastUpdate.Before(bidUpdatedTime) { - highestLastUpdate = bidUpdatedTime - } + for i := range bidData { + bids := bidData[i].([]interface{}) + price, err := strconv.ParseFloat(bids[0].(string), 64) + if err != nil { + return err + } + + amount, err := strconv.ParseFloat(bids[1].(string), 64) + if err != nil { + return err + } + + update.Bids = append(update.Bids, orderbook.Item{ + Amount: amount, + Price: price, + }) + timeData, err := strconv.ParseFloat(bids[2].(string), 64) + if err != nil { + return err + } + + sec, dec := math.Modf(timeData) + bidUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9))) + if highestLastUpdate.Before(bidUpdatedTime) { + highestLastUpdate = bidUpdatedTime } } update.UpdateTime = highestLastUpdate @@ -454,17 +543,52 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, obD } // wsProcessCandles converts candle data and sends it to the data handler -func (k *Kraken) wsProcessCandles(channelData *WebsocketChannelData, data interface{}) { - candleData := data.([]interface{}) - startTimeData, _ := strconv.ParseInt(candleData[0].(string), 10, 64) - startTimeUnix := time.Unix(startTimeData, 0) - endTimeData, _ := strconv.ParseInt(candleData[1].(string), 10, 64) - endTimeUnix := time.Unix(endTimeData, 0) - openPrice, _ := strconv.ParseFloat(candleData[2].(string), 64) - highPrice, _ := strconv.ParseFloat(candleData[3].(string), 64) - lowPrice, _ := strconv.ParseFloat(candleData[4].(string), 64) - closePrice, _ := strconv.ParseFloat(candleData[5].(string), 64) - volume, _ := strconv.ParseFloat(candleData[7].(string), 64) +func (k *Kraken) wsProcessCandles(channelData *WebsocketChannelData, data []interface{}) { + startTime, err := strconv.ParseFloat(data[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + sec, dec := math.Modf(startTime) + startTimeUnix := time.Unix(int64(sec), int64(dec*(1e9))) + + endTime, err := strconv.ParseFloat(data[1].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + sec, dec = math.Modf(endTime) + endTimeUnix := time.Unix(int64(sec), int64(dec*(1e9))) + + openPrice, err := strconv.ParseFloat(data[2].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + highPrice, err := strconv.ParseFloat(data[3].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + lowPrice, err := strconv.ParseFloat(data[4].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + closePrice, err := strconv.ParseFloat(data[5].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + volume, err := strconv.ParseFloat(data[7].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } k.Websocket.DataHandler <- wshandler.KlineData{ AssetType: asset.Spot, @@ -501,11 +625,17 @@ func (k *Kraken) GenerateDefaultSubscriptions() { // Subscribe sends a websocket message to receive data from the channel func (k *Kraken) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { + var depth int64 + if channelToSubscribe.Channel == "book" { + depth = 1000 + } + resp := WebsocketSubscriptionEventRequest{ Event: krakenWsSubscribe, Pairs: []string{channelToSubscribe.Currency.String()}, Subscription: WebsocketSubscriptionData{ - Name: channelToSubscribe.Channel, + Name: channelToSubscribe.Channel, + Depth: depth, }, RequestID: k.WebsocketConn.GenerateMessageID(false), } diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index f28691d2..22f6cf75 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -74,7 +74,7 @@ func (k *Kraken) SetDefaults() { k.Features = exchange.Features{ Supports: exchange.FeaturesSupported{ REST: true, - Websocket: false, + Websocket: true, RESTCapabilities: protocol.Features{ TickerBatching: true, TickerFetching: true, diff --git a/exchanges/lakebtc/lakebtc.go b/exchanges/lakebtc/lakebtc.go index 12d09c24..498e88df 100644 --- a/exchanges/lakebtc/lakebtc.go +++ b/exchanges/lakebtc/lakebtc.go @@ -45,7 +45,8 @@ func (l *LakeBTC) GetTicker() (map[string]Ticker, error) { response := make(map[string]TickerResponse) path := fmt.Sprintf("%s/%s", l.API.Endpoints.URL, lakeBTCTicker) - if err := l.SendHTTPRequest(path, &response); err != nil { + err := l.SendHTTPRequest(path, &response) + if err != nil { return nil, err } @@ -55,22 +56,40 @@ func (l *LakeBTC) GetTicker() (map[string]Ticker, error) { var tick Ticker key := strings.ToUpper(k) if v.Ask != nil { - tick.Ask, _ = strconv.ParseFloat(v.Ask.(string), 64) + tick.Ask, err = strconv.ParseFloat(v.Ask.(string), 64) + if err != nil { + return nil, err + } } if v.Bid != nil { - tick.Bid, _ = strconv.ParseFloat(v.Bid.(string), 64) + tick.Bid, err = strconv.ParseFloat(v.Bid.(string), 64) + if err != nil { + return nil, err + } } if v.High != nil { - tick.High, _ = strconv.ParseFloat(v.High.(string), 64) + tick.High, err = strconv.ParseFloat(v.High.(string), 64) + if err != nil { + return nil, err + } } if v.Last != nil { - tick.Last, _ = strconv.ParseFloat(v.Last.(string), 64) + tick.Last, err = strconv.ParseFloat(v.Last.(string), 64) + if err != nil { + return nil, err + } } if v.Low != nil { - tick.Low, _ = strconv.ParseFloat(v.Low.(string), 64) + tick.Low, err = strconv.ParseFloat(v.Low.(string), 64) + if err != nil { + return nil, err + } } if v.Volume != nil { - tick.Volume, _ = strconv.ParseFloat(v.Volume.(string), 64) + tick.Volume, err = strconv.ParseFloat(v.Volume.(string), 64) + if err != nil { + return nil, err + } } result[key] = tick } diff --git a/exchanges/lakebtc/lakebtc_websocket.go b/exchanges/lakebtc/lakebtc_websocket.go index 3b1abae5..1e3003ba 100644 --- a/exchanges/lakebtc/lakebtc_websocket.go +++ b/exchanges/lakebtc/lakebtc_websocket.go @@ -74,9 +74,14 @@ func (l *LakeBTC) listenToEndpoints() error { func (l *LakeBTC) GenerateDefaultSubscriptions() { var subscriptions []wshandler.WebsocketChannelSubscription enabledCurrencies := l.GetEnabledPairs(asset.Spot) + for j := range enabledCurrencies { enabledCurrencies[j].Delimiter = "" - channel := fmt.Sprintf("%v%v%v", marketSubstring, enabledCurrencies[j].Lower(), globalSubstring) + channel := fmt.Sprintf("%v%v%v", + marketSubstring, + enabledCurrencies[j].Lower(), + globalSubstring) + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: channel, Currency: enabledCurrencies[j], @@ -164,8 +169,11 @@ func (l *LakeBTC) processOrderbook(obUpdate, channel string) error { if err != nil { return err } + + p := l.getCurrencyFromChannel(channel) + book := orderbook.Base{ - Pair: l.getCurrencyFromChannel(channel), + Pair: p, LastUpdated: time.Now(), AssetType: asset.Spot, ExchangeName: l.Name, @@ -188,6 +196,7 @@ func (l *LakeBTC) processOrderbook(obUpdate, channel string) error { Price: price, }) } + for i := 0; i < len(update.Bids); i++ { var amount, price float64 amount, err = strconv.ParseFloat(update.Bids[i][1], 64) @@ -205,7 +214,19 @@ func (l *LakeBTC) processOrderbook(obUpdate, channel string) error { Price: price, }) } - return l.Websocket.Orderbook.LoadSnapshot(&book) + + err = l.Websocket.Orderbook.LoadSnapshot(&book) + if err != nil { + return err + } + + l.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ + Pair: p, + Asset: asset.Spot, + Exchange: l.Name, + } + + return nil } func (l *LakeBTC) getCurrencyFromChannel(channel string) currency.Pair { @@ -221,7 +242,14 @@ func (l *LakeBTC) processTicker(ticker string) error { l.Websocket.DataHandler <- err return err } + + enabled := l.GetEnabledPairs(asset.Spot) for k, v := range tUpdate { + returnCurrency := currency.NewPairFromString(k) + if !enabled.Contains(returnCurrency, true) { + continue + } + tickerData := v.(map[string]interface{}) processTickerItem := func(tick map[string]interface{}, item string) float64 { if tick[item] == nil { @@ -230,7 +258,10 @@ func (l *LakeBTC) processTicker(ticker string) error { p, err := strconv.ParseFloat(tick[item].(string), 64) if err != nil { - l.Websocket.DataHandler <- fmt.Errorf("%s error parsing ticker data '%s' %v", l.Name, item, tickerData) + l.Websocket.DataHandler <- fmt.Errorf("%s error parsing ticker data '%s' %v", + l.Name, + item, + tickerData) return 0 } @@ -246,7 +277,7 @@ func (l *LakeBTC) processTicker(ticker string) error { Ask: processTickerItem(tickerData, tickerSellString), Volume: processTickerItem(tickerData, tickerVolumeString), AssetType: asset.Spot, - Pair: currency.NewPairFromString(k), + Pair: returnCurrency, } } return nil diff --git a/exchanges/lakebtc/lakebtc_wrapper.go b/exchanges/lakebtc/lakebtc_wrapper.go index 17649b40..9912f15b 100644 --- a/exchanges/lakebtc/lakebtc_wrapper.go +++ b/exchanges/lakebtc/lakebtc_wrapper.go @@ -214,20 +214,21 @@ func (l *LakeBTC) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Pr pairs := l.GetEnabledPairs(assetType) for i := range pairs { - currency := l.FormatExchangeCurrency(pairs[i], assetType).String() - if _, ok := ticks[currency]; !ok { + c, ok := ticks[l.FormatExchangeCurrency(pairs[i], assetType).String()] + if !ok { continue } + var tickerPrice ticker.Price tickerPrice.Pair = pairs[i] - tickerPrice.Ask = ticks[currency].Ask - tickerPrice.Bid = ticks[currency].Bid - tickerPrice.Volume = ticks[currency].Volume - tickerPrice.High = ticks[currency].High - tickerPrice.Low = ticks[currency].Low - tickerPrice.Last = ticks[currency].Last + tickerPrice.Ask = c.Ask + tickerPrice.Bid = c.Bid + tickerPrice.Volume = c.Volume + tickerPrice.High = c.High + tickerPrice.Low = c.Low + tickerPrice.Last = c.Last - err = ticker.ProcessTicker(l.GetName(), &tickerPrice, assetType) + err = ticker.ProcessTicker(l.Name, &tickerPrice, assetType) if err != nil { log.Error(log.Ticker, err) } @@ -237,7 +238,7 @@ func (l *LakeBTC) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Pr // FetchTicker returns the ticker for a currency pair func (l *LakeBTC) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(l.GetName(), p, assetType) + tickerNew, err := ticker.GetTicker(l.Name, p, assetType) if err != nil { return l.UpdateTicker(p, assetType) } @@ -246,7 +247,7 @@ func (l *LakeBTC) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Pri // FetchOrderbook returns orderbook base on the currency pair func (l *LakeBTC) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { - ob, err := orderbook.Get(l.GetName(), p, assetType) + ob, err := orderbook.Get(l.Name, p, assetType) if err != nil { return l.UpdateOrderbook(p, assetType) } @@ -270,7 +271,7 @@ func (l *LakeBTC) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderb } orderBook.Pair = p - orderBook.ExchangeName = l.GetName() + orderBook.ExchangeName = l.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -285,7 +286,7 @@ func (l *LakeBTC) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderb // LakeBTC exchange func (l *LakeBTC) GetAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.Exchange = l.GetName() + response.Exchange = l.Name accountInfo, err := l.GetAccountInformation() if err != nil { return response, err diff --git a/exchanges/okcoin/okcoin_test.go b/exchanges/okcoin/okcoin_test.go index bdd9b24d..1d058432 100644 --- a/exchanges/okcoin/okcoin_test.go +++ b/exchanges/okcoin/okcoin_test.go @@ -488,18 +488,6 @@ func TestGetSpotTokenPairDetails(t *testing.T) { } } -// TestGetSpotOrderBook API endpoint test -func TestGetSpotOrderBook(t *testing.T) { - TestSetDefaults(t) - request := okgroup.GetSpotOrderBookRequest{ - InstrumentID: spotCurrency, - } - _, err := o.GetSpotOrderBook(request) - if err != nil { - t.Error(err) - } -} - // TestGetSpotAllTokenPairsInformation API endpoint test func TestGetSpotAllTokenPairsInformation(t *testing.T) { TestSetDefaults(t) diff --git a/exchanges/okcoin/okcoin_wrapper.go b/exchanges/okcoin/okcoin_wrapper.go index ce5a2a92..f5a80933 100644 --- a/exchanges/okcoin/okcoin_wrapper.go +++ b/exchanges/okcoin/okcoin_wrapper.go @@ -142,7 +142,11 @@ func (o *OKCoin) Start(wg *sync.WaitGroup) { // Run implements the OKEX wrapper func (o *OKCoin) Run() { if o.Verbose { - log.Debugf(log.ExchangeSys, "%s Websocket: %s. (url: %s).\n", o.GetName(), common.IsEnabled(o.Websocket.IsEnabled()), o.WebsocketURL) + log.Debugf(log.ExchangeSys, + "%s Websocket: %s. (url: %s).\n", + o.Name, + common.IsEnabled(o.Websocket.IsEnabled()), + o.WebsocketURL) } forceUpdate := false @@ -161,7 +165,9 @@ func (o *OKCoin) Run() { err := o.UpdatePairs(enabledPairs, asset.Spot, true, true) if err != nil { - log.Errorf(log.ExchangeSys, "%s failed to update currencies.\n", o.GetName()) + log.Errorf(log.ExchangeSys, + "%s failed to update currencies.\n", + o.Name) return } } @@ -172,7 +178,10 @@ func (o *OKCoin) Run() { err := o.UpdateTradablePairs(forceUpdate) if err != nil { - log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", o.Name, err) + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", + o.Name, + err) } } @@ -237,12 +246,12 @@ func (o *OKCoin) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Pri } } } - return ticker.GetTicker(o.GetName(), p, assetType) + return ticker.GetTicker(o.Name, p, assetType) } // FetchTicker returns the ticker for a currency pair func (o *OKCoin) FetchTicker(p currency.Pair, assetType asset.Item) (tickerData ticker.Price, err error) { - tickerData, err = ticker.GetTicker(o.GetName(), p, assetType) + tickerData, err = ticker.GetTicker(o.Name, p, assetType) if err != nil { return o.UpdateTicker(p, assetType) } diff --git a/exchanges/okex/okex.go b/exchanges/okex/okex.go index c452f490..51a4d2bc 100644 --- a/exchanges/okex/okex.go +++ b/exchanges/okex/okex.go @@ -3,8 +3,6 @@ package okex import ( "fmt" "net/http" - "strconv" - "time" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/exchanges/okgroup" @@ -18,7 +16,7 @@ const ( okExAPIVersion = "/v3/" okExExchangeName = "OKEX" // OkExWebsocketURL WebsocketURL - OkExWebsocketURL = "wss://real.okex.com:10442/ws/v3" + OkExWebsocketURL = "wss://real.okex.com:8443/ws/v3" // API subsections okGroupFuturesSubsection = "futures" okGroupSwapSubsection = "swap" @@ -141,69 +139,6 @@ func (o *OKEX) GetFuturesContractInformation() (resp []okgroup.GetFuturesContrac return resp, o.SendHTTPRequest(http.MethodGet, okGroupFuturesSubsection, okgroup.OKGroupInstruments, nil, &resp, false) } -// GetFuturesOrderBook List all contracts. This request does not support pagination. The full list will be returned for a request. -func (o *OKEX) GetFuturesOrderBook(request okgroup.GetFuturesOrderBookRequest) (resp okgroup.GetFuturesOrderBookResponse, err error) { - requestURL := fmt.Sprintf("%v/%v/%v%v", okgroup.OKGroupInstruments, request.InstrumentID, okgroup.OKGroupGetSpotOrderBook, okgroup.FormatParameters(request)) - - type tempOB struct { - Bids [][]string `json:"bids"` - Asks [][]string `json:"asks"` - Timestamp time.Time `json:"timestamp"` - } - - var tmpOB tempOB - err = o.SendHTTPRequest(http.MethodGet, okGroupFuturesSubsection, requestURL, nil, &tmpOB, false) - if err != nil { - return resp, err - } - - processOB := func(ob [][]string) ([]okgroup.FuturesOrderbookItem, error) { - var processedOB []okgroup.FuturesOrderbookItem - for x := range ob { - price, convErr := strconv.ParseFloat(ob[x][0], 64) - if err != nil { - return nil, convErr - } - - size, convErr := strconv.ParseInt(ob[x][1], 10, 64) - if err != nil { - return nil, convErr - } - - liqOrders, convErr := strconv.ParseInt(ob[x][2], 10, 64) - if err != nil { - return nil, convErr - } - - numOrders, convErr := strconv.ParseInt(ob[x][3], 10, 64) - if err != nil { - return nil, convErr - } - - processedOB = append(processedOB, okgroup.FuturesOrderbookItem{ - Price: price, - Size: size, - ForceLiquidatedOrders: liqOrders, - NumberOrders: numOrders, - }) - } - return processedOB, nil - } - - resp.Bids, err = processOB(tmpOB.Bids) - if err != nil { - return - } - - resp.Asks, err = processOB(tmpOB.Asks) - if err != nil { - return - } - - resp.Timestamp = tmpOB.Timestamp - return resp, nil -} - // GetAllFuturesTokenInfo Get the last traded price, best bid/ask price, 24 hour trading volume and more info of all contracts. func (o *OKEX) GetAllFuturesTokenInfo() (resp []okgroup.GetFuturesTokenInfoResponse, _ error) { requestURL := fmt.Sprintf("%v/%v", okgroup.OKGroupInstruments, okgroup.OKGroupTicker) @@ -373,12 +308,6 @@ func (o *OKEX) GetSwapContractInformation() (resp []okgroup.GetSwapContractInfor return resp, o.SendHTTPRequest(http.MethodGet, okGroupSwapSubsection, okgroup.OKGroupInstruments, nil, &resp, false) } -// GetSwapOrderBook Get the charts of the trading pairs. -func (o *OKEX) GetSwapOrderBook(request okgroup.GetSwapOrderBookRequest) (resp okgroup.GetSwapOrderBookResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v/%v%v", okgroup.OKGroupInstruments, request.InstrumentID, okGroupDepth, okgroup.FormatParameters(request)) - return resp, o.SendHTTPRequest(http.MethodGet, okGroupSwapSubsection, requestURL, nil, &resp, false) -} - // GetAllSwapTokensInformation Get the last traded price, best bid/ask price, 24 hour trading volume and more info of all contracts. func (o *OKEX) GetAllSwapTokensInformation() (resp []okgroup.GetAllSwapTokensInformationResponse, _ error) { requestURL := fmt.Sprintf("%v/%v", okgroup.OKGroupInstruments, okgroup.OKGroupTicker) diff --git a/exchanges/okex/okex_test.go b/exchanges/okex/okex_test.go index 11752506..485837e2 100644 --- a/exchanges/okex/okex_test.go +++ b/exchanges/okex/okex_test.go @@ -522,19 +522,6 @@ func TestGetSpotTokenPairDetails(t *testing.T) { } } -// TestGetSpotOrderBook API endpoint test -func TestGetSpotOrderBook(t *testing.T) { - TestSetDefaults(t) - t.Parallel() - request := okgroup.GetSpotOrderBookRequest{ - InstrumentID: spotCurrency, - } - _, err := o.GetSpotOrderBook(request) - if err != nil { - t.Error(err) - } -} - // TestGetSpotAllTokenPairsInformation API endpoint test func TestGetSpotAllTokenPairsInformation(t *testing.T) { TestSetDefaults(t) @@ -1035,18 +1022,6 @@ func TestGetFuturesContractInformation(t *testing.T) { } } -// TestGetFuturesContractInformation API endpoint test -func TestGetFuturesOrderBook(t *testing.T) { - TestSetDefaults(t) - _, err := o.GetFuturesOrderBook(okgroup.GetFuturesOrderBookRequest{ - InstrumentID: getFutureInstrumentID(), - Size: 10, - }) - if err != nil { - t.Error(err) - } -} - // TestGetAllFuturesTokenInfo API endpoint test func TestGetAllFuturesTokenInfo(t *testing.T) { TestSetDefaults(t) @@ -1315,19 +1290,6 @@ func TestGetSwapContractInformation(t *testing.T) { } } -// TestGetSwapOrderBook API endpoint test -func TestGetSwapOrderBook(t *testing.T) { - TestSetDefaults(t) - t.Parallel() - _, err := o.GetSwapOrderBook(okgroup.GetSwapOrderBookRequest{ - InstrumentID: fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD), - Size: 200, - }) - if err != nil { - t.Error(err) - } -} - // TestGetAllSwapTokensInformation API endpoint test func TestGetAllSwapTokensInformation(t *testing.T) { TestSetDefaults(t) diff --git a/exchanges/okex/okex_wrapper.go b/exchanges/okex/okex_wrapper.go index 3e1b96f2..8fb0c00b 100644 --- a/exchanges/okex/okex_wrapper.go +++ b/exchanges/okex/okex_wrapper.go @@ -1,7 +1,9 @@ package okex import ( + "errors" "fmt" + "strings" "sync" "time" @@ -17,6 +19,11 @@ import ( log "github.com/thrasher-corp/gocryptotrader/logger" ) +const ( + delimiterDash = "-" + delimiterUnderscore = "_" +) + // GetDefaultConfig returns a default exchange config func (o *OKEX) GetDefaultConfig() (*config.ExchangeConfig, error) { o.SetDefaults() @@ -64,28 +71,38 @@ func (o *OKEX) SetDefaults() { fmt1 := currency.PairStore{ RequestFormat: ¤cy.PairFormat{ Uppercase: true, - Delimiter: "-", + Delimiter: delimiterDash, }, ConfigFormat: ¤cy.PairFormat{ Uppercase: true, - Delimiter: "_", + Delimiter: delimiterUnderscore, }, } o.CurrencyPairs.Store(asset.PerpetualSwap, fmt1) o.CurrencyPairs.Store(asset.Futures, fmt1) - fmt2 := currency.PairStore{ + index := currency.PairStore{ RequestFormat: ¤cy.PairFormat{ Uppercase: true, - Delimiter: "-", + Delimiter: delimiterDash, }, ConfigFormat: ¤cy.PairFormat{ Uppercase: true, - Delimiter: "-", }, } - o.CurrencyPairs.Store(asset.Spot, fmt2) - o.CurrencyPairs.Store(asset.Index, fmt2) + + spot := currency.PairStore{ + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: delimiterDash, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: delimiterDash, + }, + } + o.CurrencyPairs.Store(asset.Spot, spot) + o.CurrencyPairs.Store(asset.Index, index) o.Features = exchange.Features{ Supports: exchange.FeaturesSupported{ @@ -159,19 +176,25 @@ func (o *OKEX) Start(wg *sync.WaitGroup) { // Run implements the OKEX wrapper func (o *OKEX) Run() { if o.Verbose { - log.Debugf(log.ExchangeSys, "%s Websocket: %s. (url: %s).\n", o.GetName(), common.IsEnabled(o.Websocket.IsEnabled()), o.API.Endpoints.WebsocketURL) + log.Debugf(log.ExchangeSys, + "%s Websocket: %s. (url: %s).\n", + o.Name, + common.IsEnabled(o.Websocket.IsEnabled()), + o.API.Endpoints.WebsocketURL) } - if o.Config.CurrencyPairs.Pairs[asset.Spot].ConfigFormat == nil || o.Config.CurrencyPairs.Pairs[asset.Spot].RequestFormat == nil || - o.Config.CurrencyPairs.Pairs[asset.Index].ConfigFormat == nil || o.Config.CurrencyPairs.Pairs[asset.Index].RequestFormat == nil { + if o.Config.CurrencyPairs.Pairs[asset.Spot].ConfigFormat == nil || + o.Config.CurrencyPairs.Pairs[asset.Spot].RequestFormat == nil || + o.Config.CurrencyPairs.Pairs[asset.Index].ConfigFormat == nil || + o.Config.CurrencyPairs.Pairs[asset.Index].RequestFormat == nil { currFmt := currency.PairStore{ RequestFormat: ¤cy.PairFormat{ Uppercase: true, - Delimiter: "-", + Delimiter: delimiterDash, }, ConfigFormat: ¤cy.PairFormat{ Uppercase: true, - Delimiter: "-", + Delimiter: delimiterDash, }, } o.CurrencyPairs.Store(asset.Spot, currFmt) @@ -180,16 +203,18 @@ func (o *OKEX) Run() { o.Config.CurrencyPairs.Store(asset.Index, currFmt) } - if o.Config.CurrencyPairs.Pairs[asset.Futures].ConfigFormat == nil || o.Config.CurrencyPairs.Pairs[asset.Futures].RequestFormat == nil || - o.Config.CurrencyPairs.Pairs[asset.PerpetualSwap].ConfigFormat == nil || o.Config.CurrencyPairs.Pairs[asset.PerpetualSwap].RequestFormat == nil { + if o.Config.CurrencyPairs.Pairs[asset.Futures].ConfigFormat == nil || + o.Config.CurrencyPairs.Pairs[asset.Futures].RequestFormat == nil || + o.Config.CurrencyPairs.Pairs[asset.PerpetualSwap].ConfigFormat == nil || + o.Config.CurrencyPairs.Pairs[asset.PerpetualSwap].RequestFormat == nil { currFmt := currency.PairStore{ RequestFormat: ¤cy.PairFormat{ Uppercase: true, - Delimiter: "-", + Delimiter: delimiterDash, }, ConfigFormat: ¤cy.PairFormat{ Uppercase: true, - Delimiter: "_", + Delimiter: delimiterUnderscore, }, } o.CurrencyPairs.Store(asset.Futures, currFmt) @@ -198,14 +223,18 @@ func (o *OKEX) Run() { o.Config.CurrencyPairs.Store(asset.PerpetualSwap, currFmt) } - if !common.StringDataContains(o.Config.CurrencyPairs.Pairs[asset.Spot].Enabled.Strings(), o.CurrencyPairs.Pairs[asset.Spot].RequestFormat.Delimiter) { + if !common.StringDataContains(o.Config.CurrencyPairs.Pairs[asset.Spot].Enabled.Strings(), + o.CurrencyPairs.Pairs[asset.Spot].RequestFormat.Delimiter) { enabledPairs := currency.NewPairsFromStrings([]string{"EOS-USDT"}) log.Warnf(log.ExchangeSys, - "Enabled pairs for %v reset due to config upgrade, please enable the ones you would like again.", o.Name) + "Enabled pairs for %v reset due to config upgrade, please enable the ones you would like again.", + o.Name) err := o.UpdatePairs(enabledPairs, asset.Spot, true, true) if err != nil { - log.Errorf(log.ExchangeSys, "%s failed to update currencies.\n", o.GetName()) + log.Errorf(log.ExchangeSys, + "%s failed to update currencies.\n", + o.Name) return } } @@ -216,7 +245,10 @@ func (o *OKEX) Run() { err := o.UpdateTradablePairs(false) if err != nil { - log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", o.Name, err) + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", + o.Name, + err) } } @@ -231,7 +263,10 @@ func (o *OKEX) FetchTradablePairs(i asset.Item) ([]string, error) { } for x := range prods { - pairs = append(pairs, fmt.Sprintf("%v%v%v", prods[x].BaseCurrency, o.GetPairFormat(i, false).Delimiter, prods[x].QuoteCurrency)) + pairs = append(pairs, + currency.NewPairWithDelimiter(prods[x].BaseCurrency, + prods[x].QuoteCurrency, + o.GetPairFormat(i, false).Delimiter).String()) } return pairs, nil case asset.Futures: @@ -240,9 +275,10 @@ func (o *OKEX) FetchTradablePairs(i asset.Item) ([]string, error) { return nil, err } - var pairs []string for x := range prods { - pairs = append(pairs, fmt.Sprintf("%v%v%v", prods[x].UnderlyingIndex+prods[x].QuoteCurrency, o.GetPairFormat(i, false).Delimiter, prods[x].Delivery)) + p := strings.Split(prods[x].InstrumentID, delimiterDash) + pairs = append(pairs, + p[0]+delimiterDash+p[1]+o.GetPairFormat(i, false).Delimiter+p[2]) } return pairs, nil @@ -252,13 +288,18 @@ func (o *OKEX) FetchTradablePairs(i asset.Item) ([]string, error) { return nil, err } - var pairs []string for x := range prods { - pairs = append(pairs, fmt.Sprintf("%v%v%v%vSWAP", prods[x].UnderlyingIndex, o.GetPairFormat(i, false).Delimiter, prods[x].QuoteCurrency, o.GetPairFormat(i, false).Delimiter)) + pairs = append(pairs, + prods[x].UnderlyingIndex+ + delimiterDash+ + prods[x].QuoteCurrency+ + o.GetPairFormat(i, false).Delimiter+ + "SWAP") } return pairs, nil case asset.Index: - return []string{fmt.Sprintf("BTC%vUSD", o.GetPairFormat(i, false).Delimiter)}, nil + // This is updated in futures index + return nil, errors.New("index updated in futures") } return nil, fmt.Errorf("%s invalid asset type", o.Name) @@ -268,13 +309,33 @@ func (o *OKEX) FetchTradablePairs(i asset.Item) ([]string, error) { // them in the exchanges config func (o *OKEX) UpdateTradablePairs(forceUpdate bool) error { for x := range o.CurrencyPairs.AssetTypes { - a := o.CurrencyPairs.AssetTypes[x] - pairs, err := o.FetchTradablePairs(a) + if o.CurrencyPairs.AssetTypes[x] == asset.Index { + // Update from futures + continue + } + + pairs, err := o.FetchTradablePairs(o.CurrencyPairs.AssetTypes[x]) if err != nil { return err } - err = o.UpdatePairs(currency.NewPairsFromStrings(pairs), a, false, forceUpdate) + if o.CurrencyPairs.AssetTypes[x] == asset.Futures { + var indexPairs []string + for i := range pairs { + indexPairs = append(indexPairs, + strings.Split(pairs[i], delimiterUnderscore)[0]) + } + err = o.UpdatePairs(currency.NewPairsFromStrings(indexPairs), + asset.Index, + false, + forceUpdate) + if err != nil { + return err + } + } + + err = o.UpdatePairs(currency.NewPairsFromStrings(pairs), + o.CurrencyPairs.AssetTypes[x], false, forceUpdate) if err != nil { return err } @@ -291,92 +352,98 @@ func (o *OKEX) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price if err != nil { return tickerData, err } - pairs := o.GetEnabledPairs(assetType) - for i := range pairs { - for j := range resp { - if !pairs[i].Equal(resp[j].InstrumentID) { - continue - } - tickerData = ticker.Price{ - Last: resp[j].Last, - High: resp[j].High24h, - Low: resp[j].Low24h, - Bid: resp[j].BestBid, - Ask: resp[j].BestAsk, - Volume: resp[j].BaseVolume24h, - QuoteVolume: resp[j].QuoteVolume24h, - Open: resp[j].Open24h, - Pair: pairs[i], - LastUpdated: resp[j].Timestamp, - } - err = ticker.ProcessTicker(o.Name, &tickerData, assetType) - if err != nil { - log.Error(log.Ticker, err) - } + for j := range resp { + if !o.GetEnabledPairs(assetType).Contains(resp[j].InstrumentID, true) { + continue + } + tickerData = ticker.Price{ + Last: resp[j].Last, + High: resp[j].High24h, + Low: resp[j].Low24h, + Bid: resp[j].BestBid, + Ask: resp[j].BestAsk, + Volume: resp[j].BaseVolume24h, + QuoteVolume: resp[j].QuoteVolume24h, + Open: resp[j].Open24h, + Pair: resp[j].InstrumentID, + LastUpdated: resp[j].Timestamp, + } + err = ticker.ProcessTicker(o.Name, &tickerData, assetType) + if err != nil { + log.Error(log.Ticker, err) } } + case asset.PerpetualSwap: resp, err := o.GetAllSwapTokensInformation() if err != nil { return tickerData, err } - pairs := o.GetEnabledPairs(assetType) - for i := range pairs { - for j := range resp { - if !pairs[i].Equal(resp[j].InstrumentID) { - continue - } - tickerData = ticker.Price{ - Last: resp[j].Last, - High: resp[j].High24H, - Low: resp[j].Low24H, - Bid: resp[j].BestBid, - Ask: resp[j].BestAsk, - Volume: resp[j].Volume24H, - Pair: resp[j].InstrumentID, - LastUpdated: resp[j].Timestamp, - } - err = ticker.ProcessTicker(o.Name, &tickerData, assetType) - if err != nil { - log.Error(log.Ticker, err) - } + + for j := range resp { + p := strings.Split(resp[j].InstrumentID, delimiterDash) + nC := currency.NewPairWithDelimiter(p[0]+delimiterDash+p[1], + p[2], + delimiterUnderscore) + if !o.GetEnabledPairs(assetType).Contains(nC, true) { + continue + } + tickerData = ticker.Price{ + Last: resp[j].Last, + High: resp[j].High24H, + Low: resp[j].Low24H, + Bid: resp[j].BestBid, + Ask: resp[j].BestAsk, + Volume: resp[j].Volume24H, + Pair: nC, + LastUpdated: resp[j].Timestamp, + } + err = ticker.ProcessTicker(o.Name, &tickerData, assetType) + if err != nil { + log.Error(log.Ticker, err) } } + case asset.Futures: resp, err := o.GetAllFuturesTokenInfo() if err != nil { return tickerData, err } - pairs := o.GetEnabledPairs(assetType) - for i := range pairs { - for j := range resp { - if !pairs[i].Equal(resp[j].InstrumentID) { - continue - } - tickerData = ticker.Price{ - Last: resp[j].Last, - High: resp[j].High24h, - Low: resp[j].Low24h, - Bid: resp[j].BestBid, - Ask: resp[j].BestAsk, - Volume: resp[j].Volume24h, - Pair: resp[j].InstrumentID, - LastUpdated: resp[j].Timestamp, - } - err = ticker.ProcessTicker(o.Name, &tickerData, assetType) - if err != nil { - log.Error(log.Ticker, err) - } + + for j := range resp { + p := strings.Split(resp[j].InstrumentID, delimiterDash) + nC := currency.NewPairWithDelimiter(p[0]+delimiterDash+p[1], + p[2], + delimiterUnderscore) + if !o.GetEnabledPairs(assetType).Contains(nC, true) { + continue + } + tickerData = ticker.Price{ + Last: resp[j].Last, + High: resp[j].High24h, + Low: resp[j].Low24h, + Bid: resp[j].BestBid, + Ask: resp[j].BestAsk, + Volume: resp[j].Volume24h, + Pair: nC, + LastUpdated: resp[j].Timestamp, + } + err = ticker.ProcessTicker(o.Name, &tickerData, assetType) + if err != nil { + log.Error(log.Ticker, err) } } } - return ticker.GetTicker(o.GetName(), p, assetType) + return ticker.GetTicker(o.Name, p, assetType) } // FetchTicker returns the ticker for a currency pair func (o *OKEX) FetchTicker(p currency.Pair, assetType asset.Item) (tickerData ticker.Price, err error) { - tickerData, err = ticker.GetTicker(o.GetName(), p, assetType) + if assetType == asset.Index { + return tickerData, errors.New("ticker fetching not supported for index") + } + tickerData, err = ticker.GetTicker(o.Name, p, assetType) if err != nil { return o.UpdateTicker(p, assetType) } diff --git a/exchanges/okgroup/okgroup.go b/exchanges/okgroup/okgroup.go index 478139bd..491cecec 100644 --- a/exchanges/okgroup/okgroup.go +++ b/exchanges/okgroup/okgroup.go @@ -16,6 +16,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/crypto" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -307,11 +308,35 @@ func (o *OKGroup) GetSpotTokenPairDetails() (resp []GetSpotTokenPairDetailsRespo return resp, o.SendHTTPRequest(http.MethodGet, okGroupTokenSubsection, OKGroupInstruments, nil, &resp, false) } -// GetSpotOrderBook Getting the order book of a trading pair. Pagination is not supported here. -// The whole book will be returned for one request. Websocket is recommended here. -func (o *OKGroup) GetSpotOrderBook(request GetSpotOrderBookRequest) (resp GetSpotOrderBookResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v/%v%v", OKGroupInstruments, request.InstrumentID, OKGroupGetSpotOrderBook, FormatParameters(request)) - return resp, o.SendHTTPRequest(http.MethodGet, okGroupTokenSubsection, requestURL, nil, &resp, false) +// GetOrderBook Getting the order book of a trading pair. Pagination is not +// supported here. The whole book will be returned for one request. Websocket is +// recommended here. +func (o *OKGroup) GetOrderBook(request GetOrderBookRequest, a asset.Item) (resp GetOrderBookResponse, _ error) { + var requestType, endpoint string + switch a { + case asset.Spot: + endpoint = OKGroupGetSpotOrderBook + requestType = okGroupTokenSubsection + case asset.Futures: + endpoint = OKGroupGetSpotOrderBook + requestType = "futures" + case asset.PerpetualSwap: + endpoint = "depth" + requestType = "swap" + default: + return resp, errors.New("unhandled asset type") + } + requestURL := fmt.Sprintf("%v/%v/%v/%v", + OKGroupInstruments, + request.InstrumentID, + endpoint, + FormatParameters(request)) + return resp, o.SendHTTPRequest(http.MethodGet, + requestType, + requestURL, + nil, + &resp, + false) } // GetSpotAllTokenPairsInformation Get the last traded price, best bid/ask price, 24 hour trading volume and more info of all trading pairs. diff --git a/exchanges/okgroup/okgroup_test.go b/exchanges/okgroup/okgroup_test.go new file mode 100644 index 00000000..edcbf618 --- /dev/null +++ b/exchanges/okgroup/okgroup_test.go @@ -0,0 +1,78 @@ +package okgroup + +import ( + "log" + "os" + "testing" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" +) + +const ( + apiKey = "" + apiSecret = "" + + testAPIURL = "https://www.okex.com/api/" + testAPIVersion = "/v3/" +) + +var o OKGroup + +func TestMain(m *testing.M) { + cfg := config.GetConfig() + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("okgroup load config error", err) + } + okgroup, err := cfg.GetExchangeConfig("Okex") + if err != nil { + log.Fatal("okgroup Setup() init error", err) + } + + okgroup.API.AuthenticatedSupport = true + okgroup.API.Credentials.Key = apiKey + okgroup.API.Credentials.Secret = apiSecret + o.API.Endpoints.URL = testAPIURL + o.APIVersion = testAPIVersion + + o.Requester = request.New("okgroup_test_things", + request.NewRateLimit(time.Second, 10), + request.NewRateLimit(time.Second, 10), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout), + ) + o.Websocket = wshandler.New() + + err = o.Setup(okgroup) + if err != nil { + log.Fatal("okgroup setup error", err) + } + os.Exit(m.Run()) +} + +func TestGetOrderbook(t *testing.T) { + t.Parallel() + _, err := o.GetOrderBook(GetOrderBookRequest{InstrumentID: "BTC-USDT"}, + asset.Spot) + if err != nil { + t.Error(err) + } + + // futures expire and break test, will need to mock this in the future + _, err = o.GetOrderBook(GetOrderBookRequest{InstrumentID: "Payload"}, + asset.Futures) + if err == nil { + t.Error("error cannot be nil") + } + + _, err = o.GetOrderBook(GetOrderBookRequest{InstrumentID: "BTC-USD-SWAP"}, + asset.PerpetualSwap) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/okgroup/okgroup_types.go b/exchanges/okgroup/okgroup_types.go index 4eb8493e..266e36eb 100644 --- a/exchanges/okgroup/okgroup_types.go +++ b/exchanges/okgroup/okgroup_types.go @@ -274,15 +274,15 @@ type GetSpotTokenPairDetailsResponse struct { TickSize string `json:"tick_size"` } -// GetSpotOrderBookRequest request data for GetSpotOrderBook -type GetSpotOrderBookRequest struct { +// GetOrderBookRequest request data for GetOrderBook +type GetOrderBookRequest struct { Size int64 `url:"size,string,omitempty"` // [optional] number of results per request. Maximum 200 Depth float64 `url:"depth,string,omitempty"` // [optional] the aggregation of the book. e.g . 0.1,0.001 InstrumentID string `url:"-"` // [required] trading pairs } -// GetSpotOrderBookResponse response data for GetSpotOrderBook -type GetSpotOrderBookResponse struct { +// GetOrderBookResponse response data +type GetOrderBookResponse struct { Timestamp time.Time `json:"timestamp"` Asks [][]string `json:"asks"` // [[0]: "Price", [1]: "Size", [2]: "Num_orders"], ... Bids [][]string `json:"bids"` // [[0]: "Price", [1]: "Size", [2]: "Num_orders"], ... @@ -678,37 +678,16 @@ type GetFuturesContractInformationResponse struct { UnderlyingIndex string `json:"underlying_index"` } -// GetFuturesOrderBookRequest request data for GetFuturesOrderBook -type GetFuturesOrderBookRequest struct { - InstrumentID string `url:"-"` // [required] Contract ID, e.g. "BTC-USD-180213" - Size int64 `url:"size,omitempty"` // [optional] The size of the price range (max: 200) -} - -// FuturesOrderbookItem stores an individual futures orderbook item -type FuturesOrderbookItem struct { - Price float64 - Size int64 - ForceLiquidatedOrders int64 // Number of force liquidated orders - NumberOrders int64 // Number of orders on the price -} - -// GetFuturesOrderBookResponse response data for GetFuturesOrderBook -type GetFuturesOrderBookResponse struct { - Asks []FuturesOrderbookItem - Bids []FuturesOrderbookItem - Timestamp time.Time -} - // GetFuturesTokenInfoResponse response data for GetFuturesOrderBook type GetFuturesTokenInfoResponse struct { - BestAsk float64 `json:"best_ask,string"` - BestBid float64 `json:"best_bid,string"` - High24h float64 `json:"high_24h,string"` - InstrumentID currency.Pair `json:"instrument_id"` - Last float64 `json:"last,string"` - Low24h float64 `json:"low_24h,string"` - Timestamp time.Time `json:"timestamp"` - Volume24h float64 `json:"volume_24h,string"` + BestAsk float64 `json:"best_ask,string"` + BestBid float64 `json:"best_bid,string"` + High24h float64 `json:"high_24h,string"` + InstrumentID string `json:"instrument_id"` + Last float64 `json:"last,string"` + Low24h float64 `json:"low_24h,string"` + Timestamp time.Time `json:"timestamp"` + Volume24h float64 `json:"volume_24h,string"` } // GetFuturesFilledOrderRequest request data for GetFuturesFilledOrder @@ -1059,14 +1038,14 @@ type GetSwapOrderBookResponse struct { // GetAllSwapTokensInformationResponse response data for GetAllSwapTokensInformation type GetAllSwapTokensInformationResponse struct { - InstrumentID currency.Pair `json:"instrument_id"` - Last float64 `json:"last,string"` - High24H float64 `json:"high_24h,string"` - Low24H float64 `json:"low_24h,string"` - BestBid float64 `json:"best_bid,string"` - BestAsk float64 `json:"best_ask,string"` - Volume24H float64 `json:"volume_24h,string"` - Timestamp time.Time `json:"timestamp"` + InstrumentID string `json:"instrument_id"` + Last float64 `json:"last,string"` + High24H float64 `json:"high_24h,string"` + Low24H float64 `json:"low_24h,string"` + BestBid float64 `json:"best_bid,string"` + BestAsk float64 `json:"best_ask,string"` + Volume24H float64 `json:"volume_24h,string"` + Timestamp time.Time `json:"timestamp"` } // GetSwapFilledOrdersDataRequest request data for GetSwapFilledOrdersData diff --git a/exchanges/okgroup/okgroup_websocket.go b/exchanges/okgroup/okgroup_websocket.go index fe10fee8..b1c794ba 100644 --- a/exchanges/okgroup/okgroup_websocket.go +++ b/exchanges/okgroup/okgroup_websocket.go @@ -140,11 +140,36 @@ const ( okGroupWsFuturesOrder = okGroupWsFuturesSubsection + okGroupWsOrder okGroupWsRateLimit = 30 + + allowableIterations = 25 + delimiterColon = ":" + delimiterDash = "-" + delimiterUnderscore = "_" ) -// orderbookMutex Ensures if two entries arrive at once, only one can be processed at a time +// orderbookMutex Ensures if two entries arrive at once, only one can be +// processed at a time var orderbookMutex sync.Mutex -var defaultSubscribedChannels = []string{okGroupWsSpotDepth, okGroupWsSpotCandle300s, okGroupWsSpotTicker, okGroupWsSpotTrade} + +var defaultSpotSubscribedChannels = []string{okGroupWsSpotDepth, + okGroupWsSpotCandle300s, + okGroupWsSpotTicker, + okGroupWsSpotTrade} + +var defaultFuturesSubscribedChannels = []string{okGroupWsFuturesDepth, + okGroupWsFuturesCandle300s, + okGroupWsFuturesTicker, + okGroupWsFuturesTrade} + +var defaultIndexSubscribedChannels = []string{okGroupWsIndexCandle300s, + okGroupWsIndexTicker} + +var defaultSwapSubscribedChannels = []string{okGroupWsSwapDepth, + okGroupWsSwapCandle300s, + okGroupWsSwapTicker, + okGroupWsSwapTrade, + okGroupWsSwapFundingRate, + okGroupWsSwapMarkPrice} // WsConnect initiates a websocket connection func (o *OKGroup) WsConnect() error { @@ -161,13 +186,15 @@ func (o *OKGroup) WsConnect() error { o.Websocket.GetWebsocketURL()) } wg := sync.WaitGroup{} - wg.Add(2) + wg.Add(1) go o.WsHandleData(&wg) - go o.wsPingHandler(&wg) if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { err = o.WsLogin() if err != nil { - log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", o.Name, err) + log.Errorf(log.ExchangeSys, + "%v - authentication failed: %v\n", + o.Name, + err) } } @@ -177,36 +204,6 @@ func (o *OKGroup) WsConnect() error { return nil } -// wsPingHandler sends a message "ping" every 27 to maintain the connection to the websocket -func (o *OKGroup) wsPingHandler(wg *sync.WaitGroup) { - o.Websocket.Wg.Add(1) - defer o.Websocket.Wg.Done() - - ticker := time.NewTicker(time.Second * 27) - defer ticker.Stop() - - wg.Done() - - for { - select { - case <-o.Websocket.ShutdownC: - return - - case <-ticker.C: - if !o.Websocket.IsConnected() { - continue - } - err := o.WebsocketConn.Connection.WriteMessage(websocket.TextMessage, []byte("ping")) - if o.Verbose { - log.Debugf(log.ExchangeSys, "%v sending ping", o.GetName()) - } - if err != nil { - o.Websocket.DataHandler <- err - } - } - } -} - // WsHandleData handles the read data from the websocket connection func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) { o.Websocket.Wg.Add(1) @@ -240,7 +237,11 @@ func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) { err = common.JSONDecode(resp.Raw, &errorResponse) if err == nil && errorResponse.ErrorCode > 0 { if o.Verbose { - log.Debugf(log.ExchangeSys, "WS Error Event: %v Message: %v", errorResponse.Event, errorResponse.Message) + log.Debugf(log.ExchangeSys, + "WS Error Event: %v Message: %v for %s", + errorResponse.Event, + errorResponse.Message, + o.Name) } o.WsHandleErrorResponse(errorResponse) continue @@ -252,10 +253,12 @@ func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) { o.Websocket.SetCanUseAuthenticatedEndpoints(eventResponse.Success) } if o.Verbose { - log.Debugf(log.ExchangeSys, "WS Event: %v on Channel: %v", eventResponse.Event, eventResponse.Channel) + log.Debugf(log.ExchangeSys, + "WS Event: %v on Channel: %v for %s", + eventResponse.Event, + eventResponse.Channel, + o.Name) } - o.Websocket.DataHandler <- eventResponse - continue } } } @@ -273,7 +276,10 @@ func (o *OKGroup) WsLogin() error { base64 := crypto.Base64Encode(hmac) request := WebsocketEventRequest{ Operation: "login", - Arguments: []string{o.API.Credentials.Key, o.API.Credentials.ClientID, fmt.Sprintf("%v", unixTime), base64}, + Arguments: []string{o.API.Credentials.Key, + o.API.Credentials.ClientID, + fmt.Sprintf("%v", unixTime), + base64}, } err := o.WebsocketConn.SendMessage(request) if err != nil { @@ -286,7 +292,9 @@ func (o *OKGroup) WsLogin() error { // WsHandleErrorResponse sends an error message to ws handler func (o *OKGroup) WsHandleErrorResponse(event WebsocketErrorResponse) { errorMessage := fmt.Sprintf("%v error - %v message: %s ", - o.GetName(), event.ErrorCode, event.Message) + o.Name, + event.ErrorCode, + event.Message) if o.Verbose { log.Error(log.ExchangeSys, errorMessage) } @@ -314,28 +322,54 @@ func (o *OKGroup) GetWsChannelWithoutOrderType(table string) string { // eg "spot/ticker:BTCUSD" results in "SPOT" func (o *OKGroup) GetAssetTypeFromTableName(table string) asset.Item { assetIndex := strings.Index(table, "/") - return asset.Item(table[:assetIndex]) + switch table[:assetIndex] { + case asset.Futures.String(): + return asset.Futures + case asset.Spot.String(): + return asset.Spot + case "swap": + return asset.PerpetualSwap + case asset.Index.String(): + return asset.Index + default: + log.Warnf(log.ExchangeSys, "%s unhandled asset type %s", + o.Name, + table[:assetIndex]) + return asset.Item(table[:assetIndex]) + } } // WsHandleDataResponse classifies the WS response and sends to appropriate handler func (o *OKGroup) WsHandleDataResponse(response *WebsocketDataResponse) { switch o.GetWsChannelWithoutOrderType(response.Table) { - - case okGroupWsCandle60s, okGroupWsCandle180s, okGroupWsCandle300s, okGroupWsCandle900s, - okGroupWsCandle1800s, okGroupWsCandle3600s, okGroupWsCandle7200s, okGroupWsCandle14400s, - okGroupWsCandle21600s, okGroupWsCandle43200s, okGroupWsCandle86400s, okGroupWsCandle604900s: + case okGroupWsCandle60s, okGroupWsCandle180s, okGroupWsCandle300s, + okGroupWsCandle900s, okGroupWsCandle1800s, okGroupWsCandle3600s, + okGroupWsCandle7200s, okGroupWsCandle14400s, okGroupWsCandle21600s, + okGroupWsCandle43200s, okGroupWsCandle86400s, okGroupWsCandle604900s: o.wsProcessCandles(response) case okGroupWsDepth, okGroupWsDepth5: // Locking, orderbooks cannot be processed out of order orderbookMutex.Lock() err := o.WsProcessOrderBook(response) if err != nil { - pair := currency.NewPairDelimiter(response.Data[0].InstrumentID, "-") - channelToResubscribe := wshandler.WebsocketChannelSubscription{ - Channel: response.Table, - Currency: pair, + for i := range response.Data { + a := o.GetAssetTypeFromTableName(response.Table) + var c currency.Pair + switch a { + case asset.Futures, asset.PerpetualSwap: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterDash) + default: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) + } + + channelToResubscribe := wshandler.WebsocketChannelSubscription{ + Channel: response.Table, + Currency: c, + } + o.Websocket.ResubscribeToChannel(channelToResubscribe) } - o.Websocket.ResubscribeToChannel(channelToResubscribe) } orderbookMutex.Unlock() case okGroupWsTicker: @@ -343,26 +377,37 @@ func (o *OKGroup) WsHandleDataResponse(response *WebsocketDataResponse) { case okGroupWsTrade: o.wsProcessTrades(response) default: - logDataResponse(response) + logDataResponse(response, o.Name) } } // logDataResponse will log the details of any websocket data event // where there is no websocket datahandler for it -func logDataResponse(response *WebsocketDataResponse) { +func logDataResponse(response *WebsocketDataResponse, exchangeName string) { for i := range response.Data { - log.Errorf(log.ExchangeSys, "Unhandled channel: '%v'. Instrument '%v' Timestamp '%v', Data '%v", + log.Warnf(log.ExchangeSys, + "%s Unhandled channel: '%v'. Instrument '%v' Timestamp '%v'", + exchangeName, response.Table, response.Data[i].InstrumentID, - response.Data[i].Timestamp, - response.Data[i]) + response.Data[i].Timestamp) } } // wsProcessTickers converts ticker data and sends it to the datahandler func (o *OKGroup) wsProcessTickers(response *WebsocketDataResponse) { for i := range response.Data { - instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-") + a := o.GetAssetTypeFromTableName(response.Table) + var c currency.Pair + switch a { + case asset.Futures, asset.PerpetualSwap: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore) + default: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) + } + o.Websocket.DataHandler <- wshandler.TickerData{ Exchange: o.Name, Open: response.Data[i].Open24h, @@ -376,7 +421,7 @@ func (o *OKGroup) wsProcessTickers(response *WebsocketDataResponse) { Last: response.Data[i].Last, Timestamp: response.Data[i].Timestamp, AssetType: o.GetAssetTypeFromTableName(response.Table), - Pair: instrument, + Pair: c, } } } @@ -384,11 +429,21 @@ func (o *OKGroup) wsProcessTickers(response *WebsocketDataResponse) { // wsProcessTrades converts trade data and sends it to the datahandler func (o *OKGroup) wsProcessTrades(response *WebsocketDataResponse) { for i := range response.Data { - instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-") + a := o.GetAssetTypeFromTableName(response.Table) + var c currency.Pair + switch a { + case asset.Futures, asset.PerpetualSwap: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore) + default: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) + } + o.Websocket.DataHandler <- wshandler.TradeData{ Amount: response.Data[i].Size, AssetType: o.GetAssetTypeFromTableName(response.Table), - CurrencyPair: instrument, + CurrencyPair: c, EventTime: time.Now().Unix(), Exchange: o.GetName(), Price: response.Data[i].WebsocketTradeResponse.Price, @@ -401,10 +456,24 @@ func (o *OKGroup) wsProcessTrades(response *WebsocketDataResponse) { // wsProcessCandles converts candle data and sends it to the data handler func (o *OKGroup) wsProcessCandles(response *WebsocketDataResponse) { for i := range response.Data { - instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-") - timeData, err := time.Parse(time.RFC3339Nano, response.Data[i].WebsocketCandleResponse.Candle[0]) + a := o.GetAssetTypeFromTableName(response.Table) + var c currency.Pair + switch a { + case asset.Futures, asset.PerpetualSwap: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore) + default: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) + } + + timeData, err := time.Parse(time.RFC3339Nano, + response.Data[i].WebsocketCandleResponse.Candle[0]) if err != nil { - log.Warnf(log.ExchangeSys, "%v Time data could not be parsed: %v", o.GetName(), response.Data[i].Candle[0]) + log.Warnf(log.ExchangeSys, + "%v Time data could not be parsed: %v", + o.Name, + response.Data[i].Candle[0]) } candleIndex := strings.LastIndex(response.Table, okGroupWsCandle) @@ -416,16 +485,36 @@ func (o *OKGroup) wsProcessCandles(response *WebsocketDataResponse) { klineData := wshandler.KlineData{ AssetType: o.GetAssetTypeFromTableName(response.Table), - Pair: instrument, + Pair: c, Exchange: o.GetName(), Timestamp: timeData, Interval: candleInterval, } - klineData.OpenPrice, _ = strconv.ParseFloat(response.Data[i].Candle[1], 64) - klineData.HighPrice, _ = strconv.ParseFloat(response.Data[i].Candle[2], 64) - klineData.LowPrice, _ = strconv.ParseFloat(response.Data[i].Candle[3], 64) - klineData.ClosePrice, _ = strconv.ParseFloat(response.Data[i].Candle[4], 64) - klineData.Volume, _ = strconv.ParseFloat(response.Data[i].Candle[5], 64) + klineData.OpenPrice, err = strconv.ParseFloat(response.Data[i].Candle[1], 64) + if err != nil { + o.Websocket.DataHandler <- err + continue + } + klineData.HighPrice, err = strconv.ParseFloat(response.Data[i].Candle[2], 64) + if err != nil { + o.Websocket.DataHandler <- err + continue + } + klineData.LowPrice, err = strconv.ParseFloat(response.Data[i].Candle[3], 64) + if err != nil { + o.Websocket.DataHandler <- err + continue + } + klineData.ClosePrice, err = strconv.ParseFloat(response.Data[i].Candle[4], 64) + if err != nil { + o.Websocket.DataHandler <- err + continue + } + klineData.Volume, err = strconv.ParseFloat(response.Data[i].Candle[5], 64) + if err != nil { + o.Websocket.DataHandler <- err + continue + } o.Websocket.DataHandler <- klineData } @@ -434,57 +523,96 @@ func (o *OKGroup) wsProcessCandles(response *WebsocketDataResponse) { // WsProcessOrderBook Validates the checksum and updates internal orderbook values func (o *OKGroup) WsProcessOrderBook(response *WebsocketDataResponse) (err error) { for i := range response.Data { - instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-") + a := o.GetAssetTypeFromTableName(response.Table) + var c currency.Pair + switch a { + case asset.Futures, asset.PerpetualSwap: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore) + default: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) + } + if response.Action == okGroupWsOrderbookPartial { - err = o.WsProcessPartialOrderBook(&response.Data[i], instrument, response.Table) + err = o.WsProcessPartialOrderBook(&response.Data[i], c, a) + if err != nil { + return + } } else if response.Action == okGroupWsOrderbookUpdate { - err = o.WsProcessUpdateOrderbook(&response.Data[i], instrument, response.Table) + if len(response.Data[i].Asks) == 0 && len(response.Data[i].Bids) == 0 { + continue + } + err = o.WsProcessUpdateOrderbook(&response.Data[i], c, a) + if err != nil { + return + } } } return } // AppendWsOrderbookItems adds websocket orderbook data bid/asks into an orderbook item array -func (o *OKGroup) AppendWsOrderbookItems(entries [][]interface{}) (orderbookItems []orderbook.Item) { +func (o *OKGroup) AppendWsOrderbookItems(entries [][]interface{}) ([]orderbook.Item, error) { + var items []orderbook.Item for j := range entries { - amount, _ := strconv.ParseFloat(entries[j][1].(string), 64) - price, _ := strconv.ParseFloat(entries[j][0].(string), 64) - orderbookItems = append(orderbookItems, orderbook.Item{ - Amount: amount, - Price: price, - }) + amount, err := strconv.ParseFloat(entries[j][1].(string), 64) + if err != nil { + return nil, err + } + price, err := strconv.ParseFloat(entries[j][0].(string), 64) + if err != nil { + return nil, err + } + items = append(items, orderbook.Item{Amount: amount, Price: price}) } - return + return items, nil } // WsProcessPartialOrderBook takes websocket orderbook data and creates an orderbook // Calculates checksum to ensure it is valid -func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketDataWrapper, instrument currency.Pair, tableName string) error { +func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketDataWrapper, instrument currency.Pair, a asset.Item) error { signedChecksum := o.CalculatePartialOrderbookChecksum(wsEventData) if signedChecksum != wsEventData.Checksum { - return fmt.Errorf("channel: %v. Orderbook partial for %v checksum invalid", tableName, instrument) + return fmt.Errorf("%s channel: %s. Orderbook partial for %v checksum invalid", + o.Name, + a, + instrument) } if o.Verbose { - log.Debug(log.ExchangeSys, "Passed checksum!") + log.Debugf(log.ExchangeSys, + "%s passed checksum for instrument %s", + o.Name, + instrument) } - asks := o.AppendWsOrderbookItems(wsEventData.Asks) - bids := o.AppendWsOrderbookItems(wsEventData.Bids) + + asks, err := o.AppendWsOrderbookItems(wsEventData.Asks) + if err != nil { + return err + } + + bids, err := o.AppendWsOrderbookItems(wsEventData.Bids) + if err != nil { + return err + } + newOrderBook := orderbook.Base{ Asks: asks, Bids: bids, - AssetType: o.GetAssetTypeFromTableName(tableName), + AssetType: a, LastUpdated: wsEventData.Timestamp, Pair: instrument, ExchangeName: o.GetName(), } - err := o.Websocket.Orderbook.LoadSnapshot(&newOrderBook) + err = o.Websocket.Orderbook.LoadSnapshot(&newOrderBook) if err != nil { return err } + o.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: o.GetName(), - Asset: o.GetAssetTypeFromTableName(tableName), + Asset: a, Pair: instrument, } return nil @@ -492,123 +620,209 @@ func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketDataWrapper, i // WsProcessUpdateOrderbook updates an existing orderbook using websocket data // After merging WS data, it will sort, validate and finally update the existing orderbook -func (o *OKGroup) WsProcessUpdateOrderbook(wsEventData *WebsocketDataWrapper, instrument currency.Pair, tableName string) error { +func (o *OKGroup) WsProcessUpdateOrderbook(wsEventData *WebsocketDataWrapper, instrument currency.Pair, a asset.Item) error { update := wsorderbook.WebsocketOrderbookUpdate{ - AssetType: asset.Spot, - CurrencyPair: instrument, - UpdateTime: wsEventData.Timestamp, + Asset: a, + Pair: instrument, + UpdateTime: wsEventData.Timestamp, } - update.Asks = o.AppendWsOrderbookItems(wsEventData.Asks) - update.Bids = o.AppendWsOrderbookItems(wsEventData.Bids) - err := o.Websocket.Orderbook.Update(&update) - if err != nil { - log.Error(log.ExchangeSys, err) - } - updatedOb := o.Websocket.Orderbook.GetOrderbook(instrument, asset.Spot) - checksum := o.CalculateUpdateOrderbookChecksum(updatedOb) - if checksum == wsEventData.Checksum { - if o.Verbose { - log.Debug(log.ExchangeSys, "Orderbook valid") - } - o.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Exchange: o.GetName(), - Asset: o.GetAssetTypeFromTableName(tableName), - Pair: instrument, - } - } else { - if o.Verbose { - log.Warnln(log.ExchangeSys, "Orderbook invalid") - } - return fmt.Errorf("channel: %v. Orderbook update for %v checksum invalid. Received %v Calculated %v", tableName, instrument, wsEventData.Checksum, checksum) + var err error + update.Asks, err = o.AppendWsOrderbookItems(wsEventData.Asks) + if err != nil { + return err } + update.Bids, err = o.AppendWsOrderbookItems(wsEventData.Bids) + if err != nil { + return err + } + + err = o.Websocket.Orderbook.Update(&update) + if err != nil { + return err + } + + updatedOb := o.Websocket.Orderbook.GetOrderbook(instrument, a) + checksum := o.CalculateUpdateOrderbookChecksum(updatedOb) + + if checksum != wsEventData.Checksum { + // re-sub + log.Warnf(log.ExchangeSys, "%s checksum failure for item %s", + o.Name, + wsEventData.InstrumentID) + return errors.New("checksum failed") + } + + o.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ + Exchange: o.GetName(), + Asset: a, + Pair: instrument, + } + return nil } -// CalculatePartialOrderbookChecksum alternates over the first 25 bid and ask entries from websocket data -// The checksum is made up of the price and the quantity with a semicolon (:) deliminating them -// This will also work when there are less than 25 entries (for whatever reason) +// CalculatePartialOrderbookChecksum alternates over the first 25 bid and ask +// entries from websocket data. The checksum is made up of the price and the +// quantity with a semicolon (:) deliminating them. This will also work when +// there are less than 25 entries (for whatever reason) // eg Bid:Ask:Bid:Ask:Ask:Ask func (o *OKGroup) CalculatePartialOrderbookChecksum(orderbookData *WebsocketDataWrapper) int32 { var checksum string - iterations := 25 - for i := 0; i < iterations; i++ { - bidsMessage := "" - askMessage := "" + for i := 0; i < allowableIterations; i++ { if len(orderbookData.Bids)-1 >= i { - bidsMessage = fmt.Sprintf("%v:%v:", orderbookData.Bids[i][0], orderbookData.Bids[i][1]) + checksum += orderbookData.Bids[i][0].(string) + + delimiterColon + + orderbookData.Bids[i][1].(string) + + delimiterColon } if len(orderbookData.Asks)-1 >= i { - askMessage = fmt.Sprintf("%v:%v:", orderbookData.Asks[i][0], orderbookData.Asks[i][1]) - - } - if checksum == "" { - checksum = fmt.Sprintf("%v%v", bidsMessage, askMessage) - } else { - checksum = fmt.Sprintf("%v%v%v", checksum, bidsMessage, askMessage) + checksum += orderbookData.Asks[i][0].(string) + + delimiterColon + + orderbookData.Asks[i][1].(string) + + delimiterColon } } - checksum = strings.TrimSuffix(checksum, ":") + checksum = strings.TrimSuffix(checksum, delimiterColon) return int32(crc32.ChecksumIEEE([]byte(checksum))) } -// CalculateUpdateOrderbookChecksum alternates over the first 25 bid and ask entries of a merged orderbook -// The checksum is made up of the price and the quantity with a semicolon (:) deliminating them -// This will also work when there are less than 25 entries (for whatever reason) +// CalculateUpdateOrderbookChecksum alternates over the first 25 bid and ask +// entries of a merged orderbook. The checksum is made up of the price and the +// quantity with a semicolon (:) deliminating them. This will also work when +// there are less than 25 entries (for whatever reason) // eg Bid:Ask:Bid:Ask:Ask:Ask func (o *OKGroup) CalculateUpdateOrderbookChecksum(orderbookData *orderbook.Base) int32 { var checksum string - iterations := 25 - for i := 0; i < iterations; i++ { - bidsMessage := "" - askMessage := "" + for i := 0; i < allowableIterations; i++ { if len(orderbookData.Bids)-1 >= i { price := strconv.FormatFloat(orderbookData.Bids[i].Price, 'f', -1, 64) amount := strconv.FormatFloat(orderbookData.Bids[i].Amount, 'f', -1, 64) - bidsMessage = fmt.Sprintf("%v:%v:", price, amount) + checksum += price + delimiterColon + amount + delimiterColon } if len(orderbookData.Asks)-1 >= i { price := strconv.FormatFloat(orderbookData.Asks[i].Price, 'f', -1, 64) amount := strconv.FormatFloat(orderbookData.Asks[i].Amount, 'f', -1, 64) - askMessage = fmt.Sprintf("%v:%v:", price, amount) - } - if checksum == "" { - checksum = fmt.Sprintf("%v%v", bidsMessage, askMessage) - } else { - checksum = fmt.Sprintf("%v%v%v", checksum, bidsMessage, askMessage) + checksum += price + delimiterColon + amount + delimiterColon } } - checksum = strings.TrimSuffix(checksum, ":") + checksum = strings.TrimSuffix(checksum, delimiterColon) return int32(crc32.ChecksumIEEE([]byte(checksum))) } -// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be +// handled by ManageSubscriptions() func (o *OKGroup) GenerateDefaultSubscriptions() { - enabledCurrencies := o.GetEnabledPairs(asset.Spot) var subscriptions []wshandler.WebsocketChannelSubscription - if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { - defaultSubscribedChannels = append(defaultSubscribedChannels, okGroupWsSpotMarginAccount, okGroupWsSpotAccount, okGroupWsSpotOrder) - } - for i := range defaultSubscribedChannels { - for j := range enabledCurrencies { - enabledCurrencies[j].Delimiter = "-" - subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ - Channel: defaultSubscribedChannels[i], - Currency: enabledCurrencies[j], - }) + assets := o.GetAssetTypes() + for x := range assets { + enabledCurrencies := o.GetEnabledPairs(assets[x]) + if len(enabledCurrencies) == 0 { + continue + } + + switch assets[x] { + case asset.Spot: + for i := range enabledCurrencies { + for y := range defaultSpotSubscribedChannels { + subscriptions = append(subscriptions, + wshandler.WebsocketChannelSubscription{ + Channel: defaultSpotSubscribedChannels[y], + Currency: o.FormatExchangeCurrency(enabledCurrencies[i], + asset.Spot), + }) + } + } + + if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + subscriptions = append(subscriptions, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsSpotMarginAccount, + }, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsSpotAccount, + }, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsSpotOrder, + }) + } + case asset.Futures: + for i := range enabledCurrencies { + for y := range defaultFuturesSubscribedChannels { + subscriptions = append(subscriptions, + wshandler.WebsocketChannelSubscription{ + Channel: defaultFuturesSubscribedChannels[y], + Currency: o.FormatExchangeCurrency(enabledCurrencies[i], + asset.Futures), + }) + } + } + + if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + subscriptions = append(subscriptions, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsFuturesAccount, + }, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsFuturesPosition, + }, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsFuturesOrder, + }) + } + case asset.PerpetualSwap: + for i := range enabledCurrencies { + for y := range defaultSwapSubscribedChannels { + subscriptions = append(subscriptions, + wshandler.WebsocketChannelSubscription{ + Channel: defaultSwapSubscribedChannels[y], + Currency: o.FormatExchangeCurrency(enabledCurrencies[i], + asset.PerpetualSwap), + }) + } + } + + if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + subscriptions = append(subscriptions, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsSwapAccount, + }, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsSwapPosition, + }, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsSwapOrder, + }) + } + case asset.Index: + for i := range enabledCurrencies { + for y := range defaultIndexSubscribedChannels { + subscriptions = append(subscriptions, + wshandler.WebsocketChannelSubscription{ + Channel: defaultIndexSubscribedChannels[y], + Currency: o.FormatExchangeCurrency(enabledCurrencies[i], asset.Index), + }) + } + } + default: + o.Websocket.DataHandler <- errors.New("unhandled asset type") } } + o.Websocket.SubscribeToChannels(subscriptions) } // Subscribe sends a websocket message to receive data from the channel func (o *OKGroup) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { + c := channelToSubscribe.Currency.String() request := WebsocketEventRequest{ Operation: "subscribe", - Arguments: []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.String())}, + Arguments: []string{channelToSubscribe.Channel + delimiterColon + c}, } if strings.EqualFold(channelToSubscribe.Channel, okGroupWsSpotAccount) { - request.Arguments = []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.Base.String())} + request.Arguments = []string{channelToSubscribe.Channel + + delimiterColon + + channelToSubscribe.Currency.Base.String()} } return o.WebsocketConn.SendMessage(request) @@ -618,7 +832,9 @@ func (o *OKGroup) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscri func (o *OKGroup) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { request := WebsocketEventRequest{ Operation: "unsubscribe", - Arguments: []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.String())}, + Arguments: []string{channelToSubscribe.Channel + + delimiterColon + + channelToSubscribe.Currency.String()}, } return o.WebsocketConn.SendMessage(request) } diff --git a/exchanges/okgroup/okgroup_wrapper.go b/exchanges/okgroup/okgroup_wrapper.go index 52673cc7..5f7a85f8 100644 --- a/exchanges/okgroup/okgroup_wrapper.go +++ b/exchanges/okgroup/okgroup_wrapper.go @@ -1,6 +1,7 @@ package okgroup import ( + "errors" "fmt" "strconv" "strings" @@ -79,62 +80,93 @@ func (o *OKGroup) FetchOrderbook(p currency.Pair, assetType asset.Item) (resp or } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (o *OKGroup) UpdateOrderbook(p currency.Pair, assetType asset.Item) (resp orderbook.Base, err error) { - orderbookNew, err := o.GetSpotOrderBook(GetSpotOrderBookRequest{ - InstrumentID: o.FormatExchangeCurrency(p, assetType).String(), - }) +func (o *OKGroup) UpdateOrderbook(p currency.Pair, a asset.Item) (orderbook.Base, error) { + var resp orderbook.Base + if a == asset.Index { + return resp, errors.New("no orderbooks for index") + } + + orderbookNew, err := o.GetOrderBook(GetOrderBookRequest{ + InstrumentID: o.FormatExchangeCurrency(p, a).String(), + }, a) if err != nil { - return + return resp, err } for x := range orderbookNew.Bids { amount, convErr := strconv.ParseFloat(orderbookNew.Bids[x][1], 64) if convErr != nil { - log.Errorf(log.ExchangeSys, - "Could not convert %v to float64", - orderbookNew.Bids[x][1]) + return resp, err } price, convErr := strconv.ParseFloat(orderbookNew.Bids[x][0], 64) if convErr != nil { - log.Errorf(log.ExchangeSys, - "Could not convert %v to float64", - orderbookNew.Bids[x][0]) + return resp, err } + + var liquidationOrders, orderCount int64 + // Contract specific variables + if len(orderbookNew.Bids[x]) == 4 { + liquidationOrders, convErr = strconv.ParseInt(orderbookNew.Bids[x][2], 10, 64) + if convErr != nil { + return resp, err + } + + orderCount, convErr = strconv.ParseInt(orderbookNew.Bids[x][3], 10, 64) + if convErr != nil { + return resp, err + } + } + resp.Bids = append(resp.Bids, orderbook.Item{ - Amount: amount, - Price: price, + Amount: amount, + Price: price, + LiquidationOrders: liquidationOrders, + OrderCount: orderCount, }) } for x := range orderbookNew.Asks { amount, convErr := strconv.ParseFloat(orderbookNew.Asks[x][1], 64) if convErr != nil { - log.Errorf(log.ExchangeSys, - "Could not convert %v to float64", - orderbookNew.Asks[x][1]) + return resp, err } price, convErr := strconv.ParseFloat(orderbookNew.Asks[x][0], 64) if convErr != nil { - log.Errorf(log.ExchangeSys, - "Could not convert %v to float64", - orderbookNew.Asks[x][0]) + return resp, err } + + var liquidationOrders, orderCount int64 + // Contract specific variables + if len(orderbookNew.Asks[x]) == 4 { + liquidationOrders, convErr = strconv.ParseInt(orderbookNew.Asks[x][2], 10, 64) + if convErr != nil { + return resp, err + } + + orderCount, convErr = strconv.ParseInt(orderbookNew.Asks[x][3], 10, 64) + if convErr != nil { + return resp, err + } + } + resp.Asks = append(resp.Asks, orderbook.Item{ - Amount: amount, - Price: price, + Amount: amount, + Price: price, + LiquidationOrders: liquidationOrders, + OrderCount: orderCount, }) } resp.Pair = p - resp.AssetType = assetType + resp.AssetType = a resp.ExchangeName = o.Name err = resp.Process() if err != nil { - return + return resp, err } - return orderbook.Get(o.Name, p, assetType) + return orderbook.Get(o.Name, p, a) } // GetAccountInfo retrieves balances for all enabled currencies diff --git a/exchanges/orderbook/orderbook.go b/exchanges/orderbook/orderbook.go index 6396148f..4d9392f6 100644 --- a/exchanges/orderbook/orderbook.go +++ b/exchanges/orderbook/orderbook.go @@ -117,7 +117,17 @@ func (s *Service) SetNewData(b *Base) error { return err } - s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item][b.AssetType] = &Book{b: b, + // Below instigates orderbook item separation so we can ensure, in the event + // of a simultaneous update via websocket/rest/fix, we don't affect package + // scoped orderbook data which could result in a potential panic + cpyBook := *b + cpyBook.Bids = make([]Item, len(b.Bids)) + copy(cpyBook.Bids, b.Bids) + cpyBook.Asks = make([]Item, len(b.Asks)) + copy(cpyBook.Asks, b.Asks) + + s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item][b.AssetType] = &Book{ + b: &cpyBook, Main: singleID, Assoc: ids} return nil diff --git a/exchanges/orderbook/orderbook_types.go b/exchanges/orderbook/orderbook_types.go index 56260dbe..3686adcc 100644 --- a/exchanges/orderbook/orderbook_types.go +++ b/exchanges/orderbook/orderbook_types.go @@ -50,6 +50,10 @@ type Item struct { Amount float64 Price float64 ID int64 + + // Contract variables + LiquidationOrders int64 + OrderCount int64 } // Base holds the fields for the orderbook base diff --git a/exchanges/poloniex/poloniex_test.go b/exchanges/poloniex/poloniex_test.go index b63036f1..3ce7d352 100644 --- a/exchanges/poloniex/poloniex_test.go +++ b/exchanges/poloniex/poloniex_test.go @@ -256,7 +256,7 @@ func TestSubmitOrder(t *testing.T) { var orderSubmission = &order.Submit{ Pair: currency.Pair{ - Delimiter: "_", + Delimiter: delimiterUnderscore, Base: currency.BTC, Quote: currency.LTC, }, diff --git a/exchanges/poloniex/poloniex_websocket.go b/exchanges/poloniex/poloniex_websocket.go index 79008dda..4a81a363 100644 --- a/exchanges/poloniex/poloniex_websocket.go +++ b/exchanges/poloniex/poloniex_websocket.go @@ -26,6 +26,7 @@ const ( wsTickerDataID = 1002 ws24HourExchangeVolumeID = 1003 wsHeartbeat = 1010 + delimiterUnderscore = "_" ) var ( @@ -107,10 +108,15 @@ func (p *Poloniex) WsHandleData() { if len(data) == 2 && chanID != wsHeartbeat { if checkSubscriptionSuccess(data) { if p.Verbose { - log.Debugf(log.ExchangeSys, "poloniex websocket subscribed to channel successfully. %d", chanID) + log.Debugf(log.ExchangeSys, + "%s websocket subscribed to channel successfully. %d", + p.Name, + chanID) } } else { - p.Websocket.DataHandler <- fmt.Errorf("poloniex websocket subscription to channel failed. %d", chanID) + p.Websocket.DataHandler <- fmt.Errorf("%s websocket subscription to channel failed. %d", + p.Name, + chanID) } continue } @@ -135,17 +141,20 @@ func (p *Poloniex) WsHandleData() { dataL3map := dataL3[1].(map[string]interface{}) currencyPair, ok := dataL3map["currencyPair"].(string) if !ok { - p.Websocket.DataHandler <- errors.New("poloniex.go error - could not find currency pair in map") + p.Websocket.DataHandler <- fmt.Errorf("%s websocket could not find currency pair in map", + p.Name) continue } orderbookData, ok := dataL3map["orderBook"].([]interface{}) if !ok { - p.Websocket.DataHandler <- errors.New("poloniex.go error - could not find orderbook data in map") + p.Websocket.DataHandler <- fmt.Errorf("%s websocket could not find orderbook data in map", + p.Name) continue } - err := p.WsProcessOrderbookSnapshot(orderbookData, currencyPair) + err = p.WsProcessOrderbookSnapshot(orderbookData, + currencyPair) if err != nil { p.Websocket.DataHandler <- err continue @@ -158,7 +167,9 @@ func (p *Poloniex) WsHandleData() { } case "o": currencyPair := currencyIDMap[chanID] - err := p.WsProcessOrderbookUpdate(int64(data[1].(float64)), dataL3, currencyPair) + err = p.WsProcessOrderbookUpdate(int64(data[1].(float64)), + dataL3, + currencyPair) if err != nil { p.Websocket.DataHandler <- err continue @@ -180,8 +191,16 @@ func (p *Poloniex) WsHandleData() { side = "sell" } trade.Side = side - trade.Volume, _ = strconv.ParseFloat(dataL3[3].(string), 64) - trade.Price, _ = strconv.ParseFloat(dataL3[4].(string), 64) + trade.Volume, err = strconv.ParseFloat(dataL3[3].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + continue + } + trade.Price, err = strconv.ParseFloat(dataL3[4].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + continue + } trade.Timestamp = int64(dataL3[5].(float64)) p.Websocket.DataHandler <- wshandler.TradeData{ @@ -203,16 +222,60 @@ func (p *Poloniex) WsHandleData() { func (p *Poloniex) wsHandleTickerData(data []interface{}) { tickerData := data[2].([]interface{}) var t WsTicker - currencyPair := currencyIDMap[int(tickerData[0].(float64))] - t.LastPrice, _ = strconv.ParseFloat(tickerData[1].(string), 64) - t.LowestAsk, _ = strconv.ParseFloat(tickerData[2].(string), 64) - t.HighestBid, _ = strconv.ParseFloat(tickerData[3].(string), 64) - t.PercentageChange, _ = strconv.ParseFloat(tickerData[4].(string), 64) - t.BaseCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[5].(string), 64) - t.QuoteCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[6].(string), 64) + currencyPair := currency.NewPairDelimiter(currencyIDMap[int(tickerData[0].(float64))], delimiterUnderscore) + if !p.GetEnabledPairs(asset.Spot).Contains(currencyPair, true) { + return + } + + var err error + t.LastPrice, err = strconv.ParseFloat(tickerData[1].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + t.LowestAsk, err = strconv.ParseFloat(tickerData[2].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + t.HighestBid, err = strconv.ParseFloat(tickerData[3].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + t.PercentageChange, err = strconv.ParseFloat(tickerData[4].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + t.BaseCurrencyVolume24H, err = strconv.ParseFloat(tickerData[5].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + t.QuoteCurrencyVolume24H, err = strconv.ParseFloat(tickerData[6].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + t.IsFrozen = tickerData[7].(float64) == 1 - t.HighestTradeIn24H, _ = strconv.ParseFloat(tickerData[8].(string), 64) - t.LowestTradePrice24H, _ = strconv.ParseFloat(tickerData[9].(string), 64) + t.HighestTradeIn24H, err = strconv.ParseFloat(tickerData[8].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + t.LowestTradePrice24H, err = strconv.ParseFloat(tickerData[9].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } p.Websocket.DataHandler <- wshandler.TickerData{ Exchange: p.Name, @@ -225,7 +288,7 @@ func (p *Poloniex) wsHandleTickerData(data []interface{}) { Last: t.LastPrice, Timestamp: time.Now(), AssetType: asset.Spot, - Pair: currency.NewPairDelimiter(currencyPair, "_"), + Pair: currencyPair, } } @@ -234,7 +297,12 @@ func (p *Poloniex) wsHandleAccountData(accountData [][]interface{}) { for i := range accountData { switch accountData[i][0].(string) { case "b": - amount, _ := strconv.ParseFloat(accountData[i][3].(string), 64) + amount, err := strconv.ParseFloat(accountData[i][3].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + response := WsAccountBalanceUpdateResponse{ currencyID: accountData[i][1].(float64), wallet: accountData[i][2].(string), @@ -242,9 +310,23 @@ func (p *Poloniex) wsHandleAccountData(accountData [][]interface{}) { } p.Websocket.DataHandler <- response case "n": - timeParse, _ := time.Parse("2006-01-02 15:04:05", accountData[i][6].(string)) - rate, _ := strconv.ParseFloat(accountData[i][4].(string), 64) - amount, _ := strconv.ParseFloat(accountData[i][5].(string), 64) + timeParse, err := time.Parse("2006-01-02 15:04:05", accountData[i][6].(string)) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + rate, err := strconv.ParseFloat(accountData[i][4].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + amount, err := strconv.ParseFloat(accountData[i][5].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } response := WsNewLimitOrderResponse{ currencyID: accountData[i][1].(float64), @@ -262,11 +344,35 @@ func (p *Poloniex) wsHandleAccountData(accountData [][]interface{}) { } p.Websocket.DataHandler <- response case "t": - timeParse, _ := time.Parse("2006-01-02 15:04:05", accountData[i][8].(string)) - rate, _ := strconv.ParseFloat(accountData[i][2].(string), 64) - amount, _ := strconv.ParseFloat(accountData[i][3].(string), 64) - feeMultiplier, _ := strconv.ParseFloat(accountData[i][4].(string), 64) - totalFee, _ := strconv.ParseFloat(accountData[i][7].(string), 64) + timeParse, err := time.Parse("2006-01-02 15:04:05", accountData[i][8].(string)) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + rate, err := strconv.ParseFloat(accountData[i][2].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + amount, err := strconv.ParseFloat(accountData[i][3].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + feeMultiplier, err := strconv.ParseFloat(accountData[i][4].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + totalFee, err := strconv.ParseFloat(accountData[i][7].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } response := WsTradeNotificationResponse{ TradeID: accountData[i][1].(float64), @@ -336,7 +442,6 @@ func (p *Poloniex) WsProcessOrderbookSnapshot(ob []interface{}, symbol string) e // WsProcessOrderbookUpdate processes new orderbook updates func (p *Poloniex) WsProcessOrderbookUpdate(sequenceNumber int64, target []interface{}, symbol string) error { - sideCheck := target[1].(float64) cP := currency.NewPairFromString(symbol) price, err := strconv.ParseFloat(target[2].(string), 64) if err != nil { @@ -347,11 +452,11 @@ func (p *Poloniex) WsProcessOrderbookUpdate(sequenceNumber int64, target []inter return err } update := &wsorderbook.WebsocketOrderbookUpdate{ - CurrencyPair: cP, - AssetType: asset.Spot, - UpdateID: sequenceNumber, + Pair: cP, + Asset: asset.Spot, + UpdateID: sequenceNumber, } - if sideCheck == 0 { + if target[1].(float64) == 1 { update.Bids = []orderbook.Item{{Price: price, Amount: volume}} } else { update.Asks = []orderbook.Item{{Price: price, Amount: volume}} @@ -374,7 +479,7 @@ func (p *Poloniex) GenerateDefaultSubscriptions() { enabledCurrencies := p.GetEnabledPairs(asset.Spot) for j := range enabledCurrencies { - enabledCurrencies[j].Delimiter = "_" + enabledCurrencies[j].Delimiter = delimiterUnderscore subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: "orderbook", Currency: enabledCurrencies[j], diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index 09144a30..f5844854 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -58,11 +58,11 @@ func (p *Poloniex) SetDefaults() { }, UseGlobalFormat: true, RequestFormat: ¤cy.PairFormat{ - Delimiter: "_", + Delimiter: delimiterUnderscore, Uppercase: true, }, ConfigFormat: ¤cy.PairFormat{ - Delimiter: "_", + Delimiter: delimiterUnderscore, Uppercase: true, }, } @@ -163,7 +163,7 @@ func (p *Poloniex) Setup(exch *config.ExchangeConfig) error { p.Websocket.Orderbook.Setup( exch.WebsocketOrderbookBufferLimit, - true, + false, true, true, false, diff --git a/exchanges/request/request.go b/exchanges/request/request.go index 66d0cae9..10141a36 100644 --- a/exchanges/request/request.go +++ b/exchanges/request/request.go @@ -277,8 +277,13 @@ func (r *Requester) DoRequest(req *http.Request, path string, body io.Reader, re reader = resp.Body default: - log.Warnf(log.ExchangeSys, "%s request response content type differs from JSON; received %v [path: %s]\n", - r.Name, resp.Header.Get("Content-Type"), path) + if verbose { + log.Warnf(log.ExchangeSys, + "%s request response content type differs from JSON; received %v [path: %s]\n", + r.Name, + resp.Header.Get("Content-Type"), + path) + } reader = resp.Body } } diff --git a/exchanges/websocket/wsorderbook/wsorderbook.go b/exchanges/websocket/wsorderbook/wsorderbook.go index e24a93ea..26c4de55 100644 --- a/exchanges/websocket/wsorderbook/wsorderbook.go +++ b/exchanges/websocket/wsorderbook/wsorderbook.go @@ -25,27 +25,28 @@ func (w *WebsocketOrderbookLocal) Setup(obBufferLimit int, bufferEnabled, sortBu // Volume == 0; deletion at price target // Price target not found; append of price target // Price target found; amend volume of price target -func (w *WebsocketOrderbookLocal) Update(orderbookUpdate *WebsocketOrderbookUpdate) error { - if (orderbookUpdate.Bids == nil && orderbookUpdate.Asks == nil) || - (len(orderbookUpdate.Bids) == 0 && len(orderbookUpdate.Asks) == 0) { - return fmt.Errorf("%v cannot have bids and ask targets both nil", w.exchangeName) +func (w *WebsocketOrderbookLocal) Update(u *WebsocketOrderbookUpdate) error { + if (u.Bids == nil && u.Asks == nil) || (len(u.Bids) == 0 && len(u.Asks) == 0) { + return fmt.Errorf("%v cannot have bids and ask targets both nil", + w.exchangeName) } w.m.Lock() defer w.m.Unlock() - obLookup, ok := w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType] + obLookup, ok := w.ob[u.Pair][u.Asset] if !ok { return fmt.Errorf("ob.Base could not be found for Exchange %s CurrencyPair: %s AssetType: %s", w.exchangeName, - orderbookUpdate.CurrencyPair.String(), - orderbookUpdate.AssetType) + u.Pair, + u.Asset) } + if w.bufferEnabled { - overBufferLimit := w.processBufferUpdate(obLookup, orderbookUpdate) + overBufferLimit := w.processBufferUpdate(obLookup, u) if !overBufferLimit { return nil } } else { - w.processObUpdate(obLookup, orderbookUpdate) + w.processObUpdate(obLookup, u) } err := obLookup.Process() if err != nil { @@ -53,23 +54,23 @@ func (w *WebsocketOrderbookLocal) Update(orderbookUpdate *WebsocketOrderbookUpda } if w.bufferEnabled { // Reset the buffer - w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType] = nil + w.buffer[u.Pair][u.Asset] = nil } return nil } -func (w *WebsocketOrderbookLocal) processBufferUpdate(o *orderbook.Base, orderbookUpdate *WebsocketOrderbookUpdate) bool { +func (w *WebsocketOrderbookLocal) processBufferUpdate(o *orderbook.Base, u *WebsocketOrderbookUpdate) bool { if w.buffer == nil { w.buffer = make(map[currency.Pair]map[asset.Item][]*WebsocketOrderbookUpdate) } - if w.buffer[orderbookUpdate.CurrencyPair] == nil { - w.buffer[orderbookUpdate.CurrencyPair] = make(map[asset.Item][]*WebsocketOrderbookUpdate) + if w.buffer[u.Pair] == nil { + w.buffer[u.Pair] = make(map[asset.Item][]*WebsocketOrderbookUpdate) } - bufferLookup := w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType] + bufferLookup := w.buffer[u.Pair][u.Asset] if len(bufferLookup) <= w.obBufferLimit { - bufferLookup = append(bufferLookup, orderbookUpdate) + bufferLookup = append(bufferLookup, u) if len(bufferLookup) < w.obBufferLimit { - w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType] = bufferLookup + w.buffer[u.Pair][u.Asset] = bufferLookup return false } } @@ -88,61 +89,59 @@ func (w *WebsocketOrderbookLocal) processBufferUpdate(o *orderbook.Base, orderbo for i := 0; i < len(bufferLookup); i++ { w.processObUpdate(o, bufferLookup[i]) } - w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType] = bufferLookup + w.buffer[u.Pair][u.Asset] = bufferLookup return true } -func (w *WebsocketOrderbookLocal) processObUpdate(o *orderbook.Base, orderbookUpdate *WebsocketOrderbookUpdate) { +func (w *WebsocketOrderbookLocal) processObUpdate(o *orderbook.Base, u *WebsocketOrderbookUpdate) { if w.updateEntriesByID { - w.updateByIDAndAction(o, orderbookUpdate) + w.updateByIDAndAction(o, u) } else { - w.updateAsksByPrice(o, orderbookUpdate) - w.updateBidsByPrice(o, orderbookUpdate) + w.updateAsksByPrice(o, u) + w.updateBidsByPrice(o, u) } } -func (w *WebsocketOrderbookLocal) updateAsksByPrice(o *orderbook.Base, base *WebsocketOrderbookUpdate) { - for j := 0; j < len(base.Asks); j++ { - found := false - for k := 0; k < len(o.Asks); k++ { - if o.Asks[k].Price == base.Asks[j].Price { - found = true - if base.Asks[j].Amount == 0 { - o.Asks = append(o.Asks[:k], - o.Asks[k+1:]...) - break +func (w *WebsocketOrderbookLocal) updateAsksByPrice(o *orderbook.Base, u *WebsocketOrderbookUpdate) { +updates: + for j := range u.Asks { + for k := range o.Asks { + if o.Asks[k].Price == u.Asks[j].Price { + if u.Asks[j].Amount <= 0 { + o.Asks = append(o.Asks[:k], o.Asks[k+1:]...) + continue updates } - o.Asks[k].Amount = base.Asks[j].Amount - break + o.Asks[k].Amount = u.Asks[j].Amount + continue updates } } - if !found { - o.Asks = append(o.Asks, base.Asks[j]) + if u.Asks[j].Amount == 0 { + continue } + o.Asks = append(o.Asks, u.Asks[j]) } sort.Slice(o.Asks, func(i, j int) bool { return o.Asks[i].Price < o.Asks[j].Price }) } -func (w *WebsocketOrderbookLocal) updateBidsByPrice(o *orderbook.Base, base *WebsocketOrderbookUpdate) { - for j := 0; j < len(base.Bids); j++ { - found := false - for k := 0; k < len(o.Bids); k++ { - if o.Bids[k].Price == base.Bids[j].Price { - found = true - if base.Bids[j].Amount == 0 { - o.Bids = append(o.Bids[:k], - o.Bids[k+1:]...) - break +func (w *WebsocketOrderbookLocal) updateBidsByPrice(o *orderbook.Base, u *WebsocketOrderbookUpdate) { +updates: + for j := range u.Bids { + for k := range o.Bids { + if o.Bids[k].Price == u.Bids[j].Price { + if u.Bids[j].Amount <= 0 { + o.Bids = append(o.Bids[:k], o.Bids[k+1:]...) + continue updates } - o.Bids[k].Amount = base.Bids[j].Amount - break + o.Bids[k].Amount = u.Bids[j].Amount + continue updates } } - if !found { - o.Bids = append(o.Bids, base.Bids[j]) + if u.Bids[j].Amount == 0 { + continue } + o.Bids = append(o.Bids, u.Bids[j]) } sort.Slice(o.Bids, func(i, j int) bool { return o.Bids[i].Price > o.Bids[j].Price @@ -151,49 +150,52 @@ func (w *WebsocketOrderbookLocal) updateBidsByPrice(o *orderbook.Base, base *Web // updateByIDAndAction will receive an action to execute against the orderbook // it will then match by IDs instead of price to perform the action -func (w *WebsocketOrderbookLocal) updateByIDAndAction(o *orderbook.Base, orderbookUpdate *WebsocketOrderbookUpdate) { - switch orderbookUpdate.Action { +func (w *WebsocketOrderbookLocal) updateByIDAndAction(o *orderbook.Base, u *WebsocketOrderbookUpdate) { + switch u.Action { case "update": - for _, target := range orderbookUpdate.Bids { - for i := range o.Bids { - if o.Bids[i].ID == target.ID { - o.Bids[i].Amount = target.Amount + for x := range u.Bids { + for y := range o.Bids { + if o.Bids[y].ID == u.Bids[x].ID { + o.Bids[y].Amount = u.Bids[x].Amount break } } } - for _, target := range orderbookUpdate.Asks { - for i := range o.Asks { - if o.Asks[i].ID == target.ID { - o.Asks[i].Amount = target.Amount + for x := range u.Asks { + for y := range o.Asks { + if o.Asks[y].ID == u.Asks[x].ID { + o.Asks[y].Amount = u.Asks[x].Amount break } } } case "delete": - for _, target := range orderbookUpdate.Bids { - for i := 0; i < len(o.Bids); i++ { - if o.Bids[i].ID == target.ID { - o.Bids = append(o.Bids[:i], - o.Bids[i+1:]...) - i-- + for x := range u.Bids { + for y := 0; y < len(o.Bids); y++ { + if o.Bids[y].ID == u.Bids[x].ID { + o.Bids = append(o.Bids[:y], o.Bids[y+1:]...) break } } } - for _, target := range orderbookUpdate.Asks { - for i := 0; i < len(o.Asks); i++ { - if o.Asks[i].ID == target.ID { - o.Asks = append(o.Asks[:i], - o.Asks[i+1:]...) - i-- + for x := range u.Asks { + for y := 0; y < len(o.Asks); y++ { + if o.Asks[y].ID == u.Asks[x].ID { + o.Asks = append(o.Asks[:y], o.Asks[y+1:]...) break } } } case "insert": - o.Bids = append(o.Bids, orderbookUpdate.Bids...) - o.Asks = append(o.Asks, orderbookUpdate.Asks...) + o.Bids = append(o.Bids, u.Bids...) + sort.Slice(o.Bids, func(i, j int) bool { + return o.Bids[i].Price > o.Bids[j].Price + }) + + o.Asks = append(o.Asks, u.Asks...) + sort.Slice(o.Asks, func(i, j int) bool { + return o.Asks[i].Price < o.Asks[j].Price + }) } } @@ -225,22 +227,17 @@ func (w *WebsocketOrderbookLocal) LoadSnapshot(newOrderbook *orderbook.Base) err if w.ob[newOrderbook.Pair] == nil { w.ob[newOrderbook.Pair] = make(map[asset.Item]*orderbook.Base) } - fullObLookup := w.ob[newOrderbook.Pair][newOrderbook.AssetType] - if fullObLookup != nil && - (len(fullObLookup.Asks) > 0 || - len(fullObLookup.Bids) > 0) { - fullObLookup = newOrderbook - return newOrderbook.Process() - } + w.ob[newOrderbook.Pair][newOrderbook.AssetType] = newOrderbook return newOrderbook.Process() } -// GetOrderbook use sparingly. Modifying anything here will ruin hash calculation and cause problems -func (w *WebsocketOrderbookLocal) GetOrderbook(p currency.Pair, assetType asset.Item) *orderbook.Base { +// GetOrderbook use sparingly. Modifying anything here will ruin hash +// calculation and cause problems +func (w *WebsocketOrderbookLocal) GetOrderbook(p currency.Pair, a asset.Item) *orderbook.Base { w.m.Lock() defer w.m.Unlock() - return w.ob[p][assetType] + return w.ob[p][a] } // FlushCache flushes w.ob data to be garbage collected and refreshed when a diff --git a/exchanges/websocket/wsorderbook/wsorderbook_test.go b/exchanges/websocket/wsorderbook/wsorderbook_test.go index 78e27cf7..b67461fe 100644 --- a/exchanges/websocket/wsorderbook/wsorderbook_test.go +++ b/exchanges/websocket/wsorderbook/wsorderbook_test.go @@ -20,14 +20,15 @@ var itemArray = [][]orderbook.Item{ {{Price: 5000, Amount: 1, ID: 5}}, } +var cp = currency.NewPairFromString("BTCUSD") + const ( exchangeName = "exchangeTest" ) -func createSnapshot() (obl *WebsocketOrderbookLocal, curr currency.Pair, asks, bids []orderbook.Item, err error) { +func createSnapshot() (obl *WebsocketOrderbookLocal, asks, bids []orderbook.Item, err error) { var snapShot1 orderbook.Base snapShot1.ExchangeName = exchangeName - curr = currency.NewPairFromString("BTCUSD") asks = []orderbook.Item{ {Price: 4000, Amount: 1, ID: 6}, } @@ -37,7 +38,7 @@ func createSnapshot() (obl *WebsocketOrderbookLocal, curr currency.Pair, asks, b snapShot1.Asks = asks snapShot1.Bids = bids snapShot1.AssetType = asset.Spot - snapShot1.Pair = curr + snapShot1.Pair = cp obl = &WebsocketOrderbookLocal{exchangeName: exchangeName} err = obl.LoadSnapshot(&snapShot1) return @@ -52,7 +53,7 @@ func bidAskGenerator() []orderbook.Item { price = 1 } response = append(response, orderbook.Item{ - Amount: float64(rand.Intn(1)), + Amount: float64(rand.Intn(10)), Price: price, ID: int64(i), }) @@ -61,7 +62,7 @@ func bidAskGenerator() []orderbook.Item { } func BenchmarkUpdateBidsByPrice(b *testing.B) { - ob, curr, _, _, err := createSnapshot() + ob, _, _, err := createSnapshot() if err != nil { b.Error(err) } @@ -69,18 +70,18 @@ func BenchmarkUpdateBidsByPrice(b *testing.B) { for i := 0; i < b.N; i++ { bidAsks := bidAskGenerator() update := &WebsocketOrderbookUpdate{ - Bids: bidAsks, - Asks: bidAsks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: asset.Spot, + Bids: bidAsks, + Asks: bidAsks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, } - ob.updateBidsByPrice(ob.ob[curr][asset.Spot], update) + ob.updateBidsByPrice(ob.ob[cp][asset.Spot], update) } } func BenchmarkUpdateAsksByPrice(b *testing.B) { - ob, curr, _, _, err := createSnapshot() + ob, _, _, err := createSnapshot() if err != nil { b.Error(err) } @@ -88,25 +89,24 @@ func BenchmarkUpdateAsksByPrice(b *testing.B) { for i := 0; i < b.N; i++ { bidAsks := bidAskGenerator() update := &WebsocketOrderbookUpdate{ - Bids: bidAsks, - Asks: bidAsks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: asset.Spot, + Bids: bidAsks, + Asks: bidAsks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, } - ob.updateAsksByPrice(ob.ob[curr][asset.Spot], update) + ob.updateAsksByPrice(ob.ob[cp][asset.Spot], update) } } // BenchmarkBufferPerformance demonstrates buffer more performant than multi // process calls func BenchmarkBufferPerformance(b *testing.B) { - obl, curr, asks, bids, err := createSnapshot() + obl, asks, bids, err := createSnapshot() if err != nil { b.Fatal(err) } obl.bufferEnabled = true - cp := currency.NewPairFromString("BTCUSD") // This is to ensure we do not send in zero orderbook info to our main book // in orderbook.go, orderbooks should not be zero even after an update. dummyItem := orderbook.Item{ @@ -116,11 +116,11 @@ func BenchmarkBufferPerformance(b *testing.B) { } obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem) update := &WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: asset.Spot, + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, } b.ResetTimer() for i := 0; i < b.N; i++ { @@ -136,13 +136,12 @@ func BenchmarkBufferPerformance(b *testing.B) { // BenchmarkBufferSortingPerformance benchmark func BenchmarkBufferSortingPerformance(b *testing.B) { - obl, curr, asks, bids, err := createSnapshot() + obl, asks, bids, err := createSnapshot() if err != nil { b.Fatal(err) } obl.bufferEnabled = true obl.sortBuffer = true - cp := currency.NewPairFromString("BTCUSD") // This is to ensure we do not send in zero orderbook info to our main book // in orderbook.go, orderbooks should not be zero even after an update. dummyItem := orderbook.Item{ @@ -152,11 +151,11 @@ func BenchmarkBufferSortingPerformance(b *testing.B) { } obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem) update := &WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: asset.Spot, + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, } b.ResetTimer() for i := 0; i < b.N; i++ { @@ -172,14 +171,13 @@ func BenchmarkBufferSortingPerformance(b *testing.B) { // BenchmarkBufferSortingPerformance benchmark func BenchmarkBufferSortingByIDPerformance(b *testing.B) { - obl, curr, asks, bids, err := createSnapshot() + obl, asks, bids, err := createSnapshot() if err != nil { b.Fatal(err) } obl.bufferEnabled = true obl.sortBuffer = true obl.sortBufferByUpdateIDs = true - cp := currency.NewPairFromString("BTCUSD") // This is to ensure we do not send in zero orderbook info to our main book // in orderbook.go, orderbooks should not be zero even after an update. dummyItem := orderbook.Item{ @@ -189,11 +187,11 @@ func BenchmarkBufferSortingByIDPerformance(b *testing.B) { } obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem) update := &WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: asset.Spot, + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, } b.ResetTimer() for i := 0; i < b.N; i++ { @@ -210,11 +208,10 @@ func BenchmarkBufferSortingByIDPerformance(b *testing.B) { // BenchmarkNoBufferPerformance demonstrates orderbook process less performant // than buffer func BenchmarkNoBufferPerformance(b *testing.B) { - obl, curr, asks, bids, err := createSnapshot() + obl, asks, bids, err := createSnapshot() if err != nil { b.Fatal(err) } - cp := currency.NewPairFromString("BTCUSD") // This is to ensure we do not send in zero orderbook info to our main book // in orderbook.go, orderbooks should not be zero even after an update. dummyItem := orderbook.Item{ @@ -224,11 +221,11 @@ func BenchmarkNoBufferPerformance(b *testing.B) { } obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem) update := &WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: asset.Spot, + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, } b.ResetTimer() for i := 0; i < b.N; i++ { @@ -243,41 +240,41 @@ func BenchmarkNoBufferPerformance(b *testing.B) { } func TestUpdates(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Error(err) } - obl.updateAsksByPrice(obl.ob[curr][asset.Spot], &WebsocketOrderbookUpdate{ - Bids: itemArray[5], - Asks: itemArray[5], - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: asset.Spot, + obl.updateAsksByPrice(obl.ob[cp][asset.Spot], &WebsocketOrderbookUpdate{ + Bids: itemArray[5], + Asks: itemArray[5], + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, }) if err != nil { t.Error(err) } - obl.updateAsksByPrice(obl.ob[curr][asset.Spot], &WebsocketOrderbookUpdate{ - Bids: itemArray[0], - Asks: itemArray[0], - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: asset.Spot, + obl.updateAsksByPrice(obl.ob[cp][asset.Spot], &WebsocketOrderbookUpdate{ + Bids: itemArray[0], + Asks: itemArray[0], + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, }) if err != nil { t.Error(err) } - if len(obl.ob[curr][asset.Spot].Asks) != 3 { + if len(obl.ob[cp][asset.Spot].Asks) != 3 { t.Error("Did not update") } } // TestHittingTheBuffer logic test func TestHittingTheBuffer(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } @@ -287,30 +284,30 @@ func TestHittingTheBuffer(t *testing.T) { asks := itemArray[i] bids := itemArray[i] err = obl.Update(&WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: asset.Spot, + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, }) if err != nil { t.Fatal(err) } } - if len(obl.ob[curr][asset.Spot].Asks) != 3 { - t.Log(obl.ob[curr][asset.Spot]) + if len(obl.ob[cp][asset.Spot].Asks) != 3 { + t.Log(obl.ob[cp][asset.Spot]) t.Errorf("expected 3 entries, received: %v", - len(obl.ob[curr][asset.Spot].Asks)) + len(obl.ob[cp][asset.Spot].Asks)) } - if len(obl.ob[curr][asset.Spot].Bids) != 3 { + if len(obl.ob[cp][asset.Spot].Bids) != 3 { t.Errorf("expected 3 entries, received: %v", - len(obl.ob[curr][asset.Spot].Bids)) + len(obl.ob[cp][asset.Spot].Bids)) } } // TestInsertWithIDs logic test func TestInsertWithIDs(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } @@ -321,30 +318,30 @@ func TestInsertWithIDs(t *testing.T) { asks := itemArray[i] bids := itemArray[i] err = obl.Update(&WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: asset.Spot, - Action: "insert", + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + Action: "insert", }) if err != nil { t.Fatal(err) } } - if len(obl.ob[curr][asset.Spot].Asks) != 6 { + if len(obl.ob[cp][asset.Spot].Asks) != 6 { t.Errorf("expected 6 entries, received: %v", - len(obl.ob[curr][asset.Spot].Asks)) + len(obl.ob[cp][asset.Spot].Asks)) } - if len(obl.ob[curr][asset.Spot].Bids) != 6 { + if len(obl.ob[cp][asset.Spot].Bids) != 6 { t.Errorf("expected 6 entries, received: %v", - len(obl.ob[curr][asset.Spot].Bids)) + len(obl.ob[cp][asset.Spot].Bids)) } } // TestSortIDs logic test func TestSortIDs(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } @@ -356,34 +353,33 @@ func TestSortIDs(t *testing.T) { asks := itemArray[i] bids := itemArray[i] err = obl.Update(&WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateID: int64(i), - AssetType: asset.Spot, + Bids: bids, + Asks: asks, + Pair: cp, + UpdateID: int64(i), + Asset: asset.Spot, }) if err != nil { t.Fatal(err) } } - if len(obl.ob[curr][asset.Spot].Asks) != 3 { + if len(obl.ob[cp][asset.Spot].Asks) != 3 { t.Errorf("expected 3 entries, received: %v", - len(obl.ob[curr][asset.Spot].Asks)) + len(obl.ob[cp][asset.Spot].Asks)) } - if len(obl.ob[curr][asset.Spot].Bids) != 3 { + if len(obl.ob[cp][asset.Spot].Bids) != 3 { t.Errorf("expected 3 entries, received: %v", - len(obl.ob[curr][asset.Spot].Bids)) + len(obl.ob[cp][asset.Spot].Bids)) } } // TestDeleteWithIDs logic test func TestDeleteWithIDs(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } - cp := currency.NewPairFromString("BTCUSD") // This is to ensure we do not send in zero orderbook info to our main book // in orderbook.go, orderbooks should not be zero even after an update. dummyItem := orderbook.Item{ @@ -392,35 +388,41 @@ func TestDeleteWithIDs(t *testing.T) { ID: 1337, } obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem) + obl.ob[cp][asset.Spot].Asks = append(obl.ob[cp][asset.Spot].Asks, + itemArray[2][0]) + obl.ob[cp][asset.Spot].Asks = append(obl.ob[cp][asset.Spot].Asks, + itemArray[1][0]) + obl.updateEntriesByID = true for i := 0; i < len(itemArray); i++ { asks := itemArray[i] bids := itemArray[i] err = obl.Update(&WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: asset.Spot, - Action: "delete", + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + Action: "delete", }) if err != nil { t.Fatal(err) } } - if len(obl.ob[curr][asset.Spot].Asks) != 0 { + + if len(obl.ob[cp][asset.Spot].Asks) != 0 { t.Errorf("expected 0 entries, received: %v", - len(obl.ob[curr][asset.Spot].Asks)) + len(obl.ob[cp][asset.Spot].Asks)) } - if len(obl.ob[curr][asset.Spot].Bids) != 1 { + if len(obl.ob[cp][asset.Spot].Bids) != 1 { t.Errorf("expected 1 entries, received: %v", - len(obl.ob[curr][asset.Spot].Bids)) + len(obl.ob[cp][asset.Spot].Bids)) } } // TestUpdateWithIDs logic test func TestUpdateWithIDs(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } @@ -429,35 +431,38 @@ func TestUpdateWithIDs(t *testing.T) { asks := itemArray[i] bids := itemArray[i] err = obl.Update(&WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: asset.Spot, - Action: "update", + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + Action: "update", }) if err != nil { t.Fatal(err) } } - if len(obl.ob[curr][asset.Spot].Asks) != 1 { - t.Log(obl.ob[curr][asset.Spot]) - t.Errorf("expected 1 entries, received: %v", len(obl.ob[curr][asset.Spot].Asks)) + if len(obl.ob[cp][asset.Spot].Asks) != 1 { + t.Log(obl.ob[cp][asset.Spot]) + t.Errorf("expected 1 entries, received: %v", + len(obl.ob[cp][asset.Spot].Asks)) } - if len(obl.ob[curr][asset.Spot].Bids) != 1 { - t.Errorf("expected 1 entries, received: %v", len(obl.ob[curr][asset.Spot].Bids)) + if len(obl.ob[cp][asset.Spot].Bids) != 1 { + t.Errorf("expected 1 entries, received: %v", + len(obl.ob[cp][asset.Spot].Bids)) } } // TestOutOfOrderIDs logic test func TestOutOfOrderIDs(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } outOFOrderIDs := []int64{2, 1, 5, 3, 4, 6, 7} if itemArray[0][0].Price != 1000 { - t.Errorf("expected sorted price to be 3000, received: %v", itemArray[1][0].Price) + t.Errorf("expected sorted price to be 3000, received: %v", + itemArray[1][0].Price) } obl.bufferEnabled = true obl.sortBuffer = true @@ -465,18 +470,19 @@ func TestOutOfOrderIDs(t *testing.T) { for i := 0; i < len(itemArray); i++ { asks := itemArray[i] err = obl.Update(&WebsocketOrderbookUpdate{ - Asks: asks, - CurrencyPair: curr, - UpdateID: outOFOrderIDs[i], - AssetType: asset.Spot, + Asks: asks, + Pair: cp, + UpdateID: outOFOrderIDs[i], + Asset: asset.Spot, }) if err != nil { t.Fatal(err) } } // Index 1 since index 0 is price 7000 - if obl.ob[curr][asset.Spot].Asks[1].Price != 2000 { - t.Errorf("expected sorted price to be 3000, received: %v", obl.ob[curr][asset.Spot].Asks[1].Price) + if obl.ob[cp][asset.Spot].Asks[1].Price != 2000 { + t.Errorf("expected sorted price to be 3000, received: %v", + obl.ob[cp][asset.Spot].Asks[1].Price) } } @@ -484,7 +490,6 @@ func TestOutOfOrderIDs(t *testing.T) { func TestRunUpdateWithoutSnapshot(t *testing.T) { var obl WebsocketOrderbookLocal var snapShot1 orderbook.Base - curr := currency.NewPairFromString("BTCUSD") asks := []orderbook.Item{ {Price: 4000, Amount: 1, ID: 8}, } @@ -495,14 +500,14 @@ func TestRunUpdateWithoutSnapshot(t *testing.T) { snapShot1.Asks = asks snapShot1.Bids = bids snapShot1.AssetType = asset.Spot - snapShot1.Pair = curr + snapShot1.Pair = cp obl.exchangeName = exchangeName err := obl.Update(&WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: asset.Spot, + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, }) if err == nil { t.Fatal("expected an error running update with no snapshot loaded") @@ -516,18 +521,17 @@ func TestRunUpdateWithoutSnapshot(t *testing.T) { func TestRunUpdateWithoutAnyUpdates(t *testing.T) { var obl WebsocketOrderbookLocal var snapShot1 orderbook.Base - curr := currency.NewPairFromString("BTCUSD") snapShot1.Asks = []orderbook.Item{} snapShot1.Bids = []orderbook.Item{} snapShot1.AssetType = asset.Spot - snapShot1.Pair = curr + snapShot1.Pair = cp obl.exchangeName = exchangeName err := obl.Update(&WebsocketOrderbookUpdate{ - Bids: snapShot1.Asks, - Asks: snapShot1.Bids, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: asset.Spot, + Bids: snapShot1.Asks, + Asks: snapShot1.Bids, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, }) if err == nil { t.Fatal("expected an error running update with no snapshot loaded") @@ -542,11 +546,10 @@ func TestRunUpdateWithoutAnyUpdates(t *testing.T) { func TestRunSnapshotWithNoData(t *testing.T) { var obl WebsocketOrderbookLocal var snapShot1 orderbook.Base - curr := currency.NewPairFromString("BTCUSD") snapShot1.Asks = []orderbook.Item{} snapShot1.Bids = []orderbook.Item{} snapShot1.AssetType = asset.Spot - snapShot1.Pair = curr + snapShot1.Pair = cp snapShot1.ExchangeName = "test" obl.exchangeName = "test" err := obl.LoadSnapshot(&snapShot1) @@ -563,7 +566,6 @@ func TestLoadSnapshot(t *testing.T) { var obl WebsocketOrderbookLocal var snapShot1 orderbook.Base snapShot1.ExchangeName = "SnapshotWithOverride" - curr := currency.NewPairFromString("BTCUSD") asks := []orderbook.Item{ {Price: 4000, Amount: 1, ID: 8}, } @@ -573,7 +575,7 @@ func TestLoadSnapshot(t *testing.T) { snapShot1.Asks = asks snapShot1.Bids = bids snapShot1.AssetType = asset.Spot - snapShot1.Pair = curr + snapShot1.Pair = cp err := obl.LoadSnapshot(&snapShot1) if err != nil { t.Error(err) @@ -582,15 +584,15 @@ func TestLoadSnapshot(t *testing.T) { // TestFlushCache logic test func TestFlushCache(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } - if obl.ob[curr][asset.Spot] == nil { + if obl.ob[cp][asset.Spot] == nil { t.Error("expected ob to have ask entries") } obl.FlushCache() - if obl.ob[curr][asset.Spot] != nil { + if obl.ob[cp][asset.Spot] != nil { t.Error("expected ob be flushed") } @@ -632,7 +634,7 @@ func TestInsertingSnapShots(t *testing.T) { snapShot1.Asks = asks snapShot1.Bids = bids snapShot1.AssetType = asset.Spot - snapShot1.Pair = currency.NewPairFromString("BTCUSD") + snapShot1.Pair = cp err := obl.LoadSnapshot(&snapShot1) if err != nil { t.Fatal(err) @@ -714,23 +716,29 @@ func TestInsertingSnapShots(t *testing.T) { t.Fatal(err) } if obl.ob[snapShot1.Pair][snapShot1.AssetType].Asks[0] != snapShot1.Asks[0] { - t.Errorf("loaded data mismatch. Expected %v, received %v", snapShot1.Asks[0], obl.ob[snapShot1.Pair][snapShot1.AssetType].Asks[0]) + t.Errorf("loaded data mismatch. Expected %v, received %v", + snapShot1.Asks[0], + obl.ob[snapShot1.Pair][snapShot1.AssetType].Asks[0]) } if obl.ob[snapShot2.Pair][snapShot2.AssetType].Asks[0] != snapShot2.Asks[0] { - t.Errorf("loaded data mismatch. Expected %v, received %v", snapShot2.Asks[0], obl.ob[snapShot2.Pair][snapShot2.AssetType].Asks[0]) + t.Errorf("loaded data mismatch. Expected %v, received %v", + snapShot2.Asks[0], + obl.ob[snapShot2.Pair][snapShot2.AssetType].Asks[0]) } if obl.ob[snapShot3.Pair][snapShot3.AssetType].Asks[0] != snapShot3.Asks[0] { - t.Errorf("loaded data mismatch. Expected %v, received %v", snapShot3.Asks[0], obl.ob[snapShot3.Pair][snapShot3.AssetType].Asks[0]) + t.Errorf("loaded data mismatch. Expected %v, received %v", + snapShot3.Asks[0], + obl.ob[snapShot3.Pair][snapShot3.AssetType].Asks[0]) } } func TestGetOrderbook(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } - ob := obl.GetOrderbook(curr, asset.Spot) - if obl.ob[curr][asset.Spot] != ob { + ob := obl.GetOrderbook(cp, asset.Spot) + if obl.ob[cp][asset.Spot] != ob { t.Error("Failed to get orderbook") } } @@ -738,7 +746,35 @@ func TestGetOrderbook(t *testing.T) { func TestSetup(t *testing.T) { w := WebsocketOrderbookLocal{} w.Setup(1, true, true, true, true, "hi") - if w.obBufferLimit != 1 || !w.bufferEnabled || !w.sortBuffer || !w.sortBufferByUpdateIDs || !w.updateEntriesByID || w.exchangeName != "hi" { + if w.obBufferLimit != 1 || + !w.bufferEnabled || + !w.sortBuffer || + !w.sortBufferByUpdateIDs || + !w.updateEntriesByID || + w.exchangeName != "hi" { t.Errorf("Setup incorrectly loaded %s", w.exchangeName) } } + +func TestEnsureMultipleUpdatesViaPrice(t *testing.T) { + obl, _, _, err := createSnapshot() + if err != nil { + t.Error(err) + } + + asks := bidAskGenerator() + obl.updateAsksByPrice(obl.ob[cp][asset.Spot], &WebsocketOrderbookUpdate{ + Bids: asks, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + }) + if err != nil { + t.Error(err) + } + + if len(obl.ob[cp][asset.Spot].Asks) <= 3 { + t.Errorf("Insufficient updates") + } +} diff --git a/exchanges/websocket/wsorderbook/wsorderbook_types.go b/exchanges/websocket/wsorderbook/wsorderbook_types.go index 6766c797..2ae77a29 100644 --- a/exchanges/websocket/wsorderbook/wsorderbook_types.go +++ b/exchanges/websocket/wsorderbook/wsorderbook_types.go @@ -25,11 +25,11 @@ type WebsocketOrderbookLocal struct { // WebsocketOrderbookUpdate stores orderbook updates and dictates what features to use when processing type WebsocketOrderbookUpdate struct { - UpdateID int64 // Used when no time is provided - UpdateTime time.Time - AssetType asset.Item - Action string // Used in conjunction with UpdateEntriesByID - Bids []orderbook.Item - Asks []orderbook.Item - CurrencyPair currency.Pair + UpdateID int64 // Used when no time is provided + UpdateTime time.Time + Asset asset.Item + Action string // Used in conjunction with UpdateEntriesByID + Bids []orderbook.Item + Asks []orderbook.Item + Pair currency.Pair } diff --git a/exchanges/zb/zb_test.go b/exchanges/zb/zb_test.go index 4c22f6a7..d097d406 100644 --- a/exchanges/zb/zb_test.go +++ b/exchanges/zb/zb_test.go @@ -287,8 +287,8 @@ func TestGetActiveOrders(t *testing.T) { var getOrdersRequest = order.GetOrdersRequest{ OrderType: order.AnyType, - Currencies: []currency.Pair{currency.NewPair(currency.LTC, - currency.BTC)}, + Currencies: []currency.Pair{currency.NewPair(currency.XRP, + currency.USDT)}, } _, err := z.GetActiveOrders(&getOrdersRequest) @@ -327,6 +327,7 @@ func areTestAPIKeysSet() bool { func TestSubmitOrder(t *testing.T) { z.SetDefaults() TestSetup(t) + if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip(fmt.Sprintf("ApiKey: %s. Can place orders: %v", z.API.Credentials.Key, @@ -336,8 +337,8 @@ func TestSubmitOrder(t *testing.T) { var orderSubmission = &order.Submit{ Pair: currency.Pair{ Delimiter: "_", - Base: currency.QTUM, - Quote: currency.USD, + Base: currency.XRP, + Quote: currency.USDT, }, OrderSide: order.Buy, OrderType: order.Limit, @@ -361,7 +362,7 @@ func TestCancelExchangeOrder(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - currencyPair := currency.NewPair(currency.LTC, currency.BTC) + currencyPair := currency.NewPair(currency.XRP, currency.USDT) var orderCancellation = &order.Cancel{ OrderID: "1", @@ -387,7 +388,7 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - currencyPair := currency.NewPair(currency.LTC, currency.BTC) + currencyPair := currency.NewPair(currency.XRP, currency.USDT) var orderCancellation = &order.Cancel{ OrderID: "1", diff --git a/exchanges/zb/zb_websocket.go b/exchanges/zb/zb_websocket.go index 0ac6d2ec..f85cfd9c 100644 --- a/exchanges/zb/zb_websocket.go +++ b/exchanges/zb/zb_websocket.go @@ -104,7 +104,7 @@ func (z *ZB) WsHandleData() { Last: ticker.Data.Last, Bid: ticker.Data.Buy, Ask: ticker.Data.Sell, - Timestamp: time.Unix(0, ticker.Date), + Timestamp: time.Unix(0, ticker.Date*int64(time.Millisecond)), AssetType: asset.Spot, Pair: currency.NewPairFromString(cPair[0]), } @@ -119,19 +119,17 @@ func (z *ZB) WsHandleData() { var asks []orderbook.Item for i := range depth.Asks { - ask := depth.Asks[i].([]interface{}) asks = append(asks, orderbook.Item{ - Amount: ask[1].(float64), - Price: ask[0].(float64), + Amount: depth.Asks[i][1].(float64), + Price: depth.Asks[i][0].(float64), }) } var bids []orderbook.Item for i := range depth.Bids { - bid := depth.Bids[i].([]interface{}) bids = append(bids, orderbook.Item{ - Amount: bid[1].(float64), - Price: bid[0].(float64), + Amount: depth.Bids[i][1].(float64), + Price: depth.Bids[i][0].(float64), }) } @@ -172,7 +170,7 @@ func (z *ZB) WsHandleData() { channelInfo := strings.Split(result.Channel, "_") cPair := currency.NewPairFromString(channelInfo[0]) z.Websocket.DataHandler <- wshandler.TradeData{ - Timestamp: time.Unix(0, t.Date), + Timestamp: time.Unix(0, t.Date*int64(time.Millisecond)), CurrencyPair: cPair, AssetType: asset.Spot, Exchange: z.GetName(), diff --git a/exchanges/zb/zb_websocket_types.go b/exchanges/zb/zb_websocket_types.go index 4faef0a0..983c92aa 100644 --- a/exchanges/zb/zb_websocket_types.go +++ b/exchanges/zb/zb_websocket_types.go @@ -43,20 +43,20 @@ type WsTicker struct { // WsDepth defines websocket orderbook data type WsDepth struct { - Timestamp int64 `json:"timestamp"` - Asks []interface{} `json:"asks"` - Bids []interface{} `json:"bids"` + Timestamp int64 `json:"timestamp"` + Asks [][]interface{} `json:"asks"` + Bids [][]interface{} `json:"bids"` } // WsTrades defines websocket trade data type WsTrades struct { Data []struct { - Amount float64 `json:"amount,string"` - Price float64 `json:"price,string"` - TID int64 `json:"tid"` - Date int64 `json:"date"` - Type string `json:"type"` - TradeType string `json:"trade_type"` + Amount float64 `json:"amount,string"` + Price float64 `json:"price,string"` + TID interface{} `json:"tid"` + Date int64 `json:"date"` + Type string `json:"type"` + TradeType string `json:"trade_type"` } `json:"data"` } diff --git a/exchanges/zb/zb_wrapper.go b/exchanges/zb/zb_wrapper.go index 59cede51..4ca4efd7 100644 --- a/exchanges/zb/zb_wrapper.go +++ b/exchanges/zb/zb_wrapper.go @@ -2,8 +2,8 @@ package zb import ( "errors" - "fmt" "strconv" + "strings" "sync" "time" @@ -221,15 +221,16 @@ func (z *ZB) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, return tickerPrice, err } - for _, x := range z.GetEnabledPairs(assetType) { + enabledPairs := z.GetEnabledPairs(assetType) + for x := range enabledPairs { // We can't use either pair format here, so format it to lower- // case and without any delimiter - curr := x.Format("", false).String() + curr := enabledPairs[x].Format("", false).String() if _, ok := result[curr]; !ok { continue } var tp ticker.Price - tp.Pair = x + tp.Pair = enabledPairs[x] tp.High = result[curr].High tp.Last = result[curr].Last tp.Ask = result[curr].Sell @@ -276,12 +277,14 @@ func (z *ZB) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.B for x := range orderbookNew.Bids { data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data[1], Price: data[0]}) + orderBook.Bids = append(orderBook.Bids, + orderbook.Item{Amount: data[1], Price: data[0]}) } for x := range orderbookNew.Asks { data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data[1], Price: data[0]}) + orderBook.Asks = append(orderBook.Asks, + orderbook.Item{Amount: data[1], Price: data[0]}) } orderBook.Pair = p @@ -366,7 +369,7 @@ func (z *ZB) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { } response, err := z.SpotNewOrder(params) if response > 0 { - submitOrderResponse.OrderID = fmt.Sprintf("%v", response) + submitOrderResponse.OrderID = strconv.FormatInt(response, 10) } if err == nil { submitOrderResponse.IsOrderPlaced = true @@ -398,13 +401,13 @@ func (z *ZB) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { var allOpenOrders []Order enabledPairs := z.GetEnabledPairs(asset.Spot) for x := range enabledPairs { - // Limiting to 10 pages - for pageNumber := int64(0); pageNumber < 11; pageNumber++ { - fCurr := z.FormatExchangeCurrency(enabledPairs[x], asset.Spot).String() - openOrders, err := z.GetUnfinishedOrdersIgnoreTradeType(fCurr, - pageNumber, - 10) + fPair := z.FormatExchangeCurrency(enabledPairs[x], asset.Spot).String() + for y := int64(1); ; y++ { + openOrders, err := z.GetUnfinishedOrdersIgnoreTradeType(fPair, y, 10) if err != nil { + if strings.Contains(err.Error(), "3001") { + break + } return cancelAllOrdersResponse, err } @@ -413,6 +416,10 @@ func (z *ZB) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { } allOpenOrders = append(allOpenOrders, openOrders...) + + if len(openOrders) != 10 { + break + } } } @@ -481,20 +488,25 @@ func (z *ZB) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { func (z *ZB) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { var allOrders []Order for x := range req.Currencies { - // Limiting to 10 pages - for pageNumber := int64(0); pageNumber < 11; pageNumber++ { - fCurr := z.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String() - resp, err := z.GetUnfinishedOrdersIgnoreTradeType(fCurr, - pageNumber, - 10) + for i := int64(1); ; i++ { + fPair := z.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String() + resp, err := z.GetUnfinishedOrdersIgnoreTradeType(fPair, i, 10) if err != nil { + if strings.Contains(err.Error(), "3001") { + break + } return nil, err } + if len(resp) == 0 { break } allOrders = append(allOrders, resp...) + + if len(resp) != 10 { + break + } } } @@ -505,7 +517,7 @@ func (z *ZB) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error orderDate := time.Unix(int64(allOrders[i].TradeDate), 0) orderSide := orderSideMap[allOrders[i].Type] orders = append(orders, order.Detail{ - ID: fmt.Sprintf("%d", allOrders[i].ID), + ID: strconv.FormatInt(allOrders[i].ID, 10), Amount: allOrders[i].TotalAmount, Exchange: z.Name, OrderDate: orderDate, @@ -536,10 +548,9 @@ func (z *ZB) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error } for x := range req.Currencies { - // Limiting to 10 pages - for pageNumber := int64(0); pageNumber < 11; pageNumber++ { - fCurr := z.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String() - resp, err := z.GetOrders(fCurr, pageNumber, side) + for y := int64(1); ; y++ { + fPair := z.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String() + resp, err := z.GetOrders(fPair, y, side) if err != nil { return nil, err } @@ -549,6 +560,10 @@ func (z *ZB) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error } allOrders = append(allOrders, resp...) + + if len(resp) != 10 { + break + } } } @@ -559,7 +574,7 @@ func (z *ZB) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error orderDate := time.Unix(int64(allOrders[i].TradeDate), 0) orderSide := orderSideMap[allOrders[i].Type] orders = append(orders, order.Detail{ - ID: fmt.Sprintf("%d", allOrders[i].ID), + ID: strconv.FormatInt(allOrders[i].ID, 10), Amount: allOrders[i].TotalAmount, Exchange: z.Name, OrderDate: orderDate,