diff --git a/.golangci.yml b/.golangci.yml index 0eb61a2b..6528b87f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,5 @@ run: - timeout: 1m30s + timeout: 2m0s issues-exit-code: 1 tests: true skip-dirs: diff --git a/cmd/exchange_template/wrapper_file.tmpl b/cmd/exchange_template/wrapper_file.tmpl index 22e34d26..f1913003 100644 --- a/cmd/exchange_template/wrapper_file.tmpl +++ b/cmd/exchange_template/wrapper_file.tmpl @@ -272,37 +272,32 @@ func ({{.Variable}} *{{.CapitalName}}) FetchOrderbook(currency currency.Pair, as // UpdateOrderbook updates and returns the orderbook for a currency pair func ({{.Variable}} *{{.CapitalName}}) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { - orderBook := new(orderbook.Base) + book := &orderbook.Base{ExchangeName: {{.Variable}}.Name, Pair: p, AssetType: assetType} // NOTE: UPDATE ORDERBOOK EXAMPLE /* orderbookNew, err := {{.Variable}}.GetOrderBook(exchange.FormatExchangeCurrency({{.Variable}}.Name, p).String(), 1000) if err != nil { - return orderBook, err + return book, err } for x := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Amount: orderbookNew.Bids[x].Quantity, Price: orderbookNew.Bids[x].Price, }) } for x := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ - Amount: orderBook.Asks[x].Quantity, - Price: orderBook.Asks[x].Price, + book.Asks = append(book.Asks, orderbook.Item{ + Amount: orderBookNew.Asks[x].Quantity, + Price: orderBookNew.Asks[x].Price, }) } */ - - orderBook.Pair = p - orderBook.ExchangeName = {{.Variable}}.Name - orderBook.AssetType = assetType - - err := orderBook.Process() + err := book.Process() if err != nil { - return orderBook, err + return book, err } return orderbook.Get({{.Variable}}.Name, p, assetType) diff --git a/cmd/gctcli/commands.go b/cmd/gctcli/commands.go index fac9805b..e5832a73 100644 --- a/cmd/gctcli/commands.go +++ b/cmd/gctcli/commands.go @@ -3152,7 +3152,7 @@ func getOrderbookStream(c *cli.Context) error { askPrice = resp.Asks[i].Price } - fmt.Printf("%f %s @ %f %s\t\t%f %s @ %f %s\n", + fmt.Printf("%.8f %s @ %.8f %s\t\t%.8f %s @ %.8f %s\n", bidAmount, resp.Pair.Base, bidPrice, diff --git a/config/config_types.go b/config/config_types.go index 91a9bebb..973648fa 100644 --- a/config/config_types.go +++ b/config/config_types.go @@ -113,23 +113,24 @@ type ConnectionMonitorConfig struct { // ExchangeConfig holds all the information needed for each enabled Exchange. type ExchangeConfig struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - Verbose bool `json:"verbose"` - UseSandbox bool `json:"useSandbox,omitempty"` - HTTPTimeout time.Duration `json:"httpTimeout"` - HTTPUserAgent string `json:"httpUserAgent,omitempty"` - HTTPDebugging bool `json:"httpDebugging,omitempty"` - WebsocketResponseCheckTimeout time.Duration `json:"websocketResponseCheckTimeout"` - WebsocketResponseMaxLimit time.Duration `json:"websocketResponseMaxLimit"` - WebsocketTrafficTimeout time.Duration `json:"websocketTrafficTimeout"` - WebsocketOrderbookBufferLimit int `json:"websocketOrderbookBufferLimit"` - ProxyAddress string `json:"proxyAddress,omitempty"` - BaseCurrencies currency.Currencies `json:"baseCurrencies"` - CurrencyPairs *currency.PairsManager `json:"currencyPairs"` - API APIConfig `json:"api"` - Features *FeaturesConfig `json:"features"` - BankAccounts []banking.Account `json:"bankAccounts,omitempty"` + Name string `json:"name"` + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + UseSandbox bool `json:"useSandbox,omitempty"` + HTTPTimeout time.Duration `json:"httpTimeout"` + HTTPUserAgent string `json:"httpUserAgent,omitempty"` + HTTPDebugging bool `json:"httpDebugging,omitempty"` + WebsocketResponseCheckTimeout time.Duration `json:"websocketResponseCheckTimeout"` + WebsocketResponseMaxLimit time.Duration `json:"websocketResponseMaxLimit"` + WebsocketTrafficTimeout time.Duration `json:"websocketTrafficTimeout"` + WebsocketOrderbookBufferLimit int `json:"websocketOrderbookBufferLimit"` + WebsocketOrderbookBufferEnabled bool `json:"websocketOrderbookBufferEnabled"` + ProxyAddress string `json:"proxyAddress,omitempty"` + BaseCurrencies currency.Currencies `json:"baseCurrencies"` + CurrencyPairs *currency.PairsManager `json:"currencyPairs"` + API APIConfig `json:"api"` + Features *FeaturesConfig `json:"features"` + BankAccounts []banking.Account `json:"bankAccounts,omitempty"` // Deprecated settings which will be removed in a future update AvailablePairs *currency.Pairs `json:"availablePairs,omitempty"` diff --git a/engine/routines.go b/engine/routines.go index 97340807..46520372 100644 --- a/engine/routines.go +++ b/engine/routines.go @@ -3,6 +3,7 @@ package engine import ( "errors" "fmt" + "strconv" "strings" "sync" @@ -115,16 +116,32 @@ func printTickerSummary(result *ticker.Price, protocol string, err error) { } } +const ( + book = "%s %s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s\n" +) + func printOrderbookSummary(result *orderbook.Base, protocol string, err error) { if err != nil { - if err == common.ErrNotYetImplemented { - log.Warnf(log.Ticker, "Failed to get %s ticker. Error: %s\n", + if result == nil { + log.Errorf(log.OrderBook, "Failed to get %s orderbook. Error: %s\n", protocol, err) return } - log.Errorf(log.OrderBook, "Failed to get %s orderbook. Error: %s\n", + if err == common.ErrNotYetImplemented { + log.Warnf(log.OrderBook, "Failed to get %s orderbook for %s %s %s. Error: %s\n", + protocol, + result.ExchangeName, + result.Pair, + result.AssetType, + err) + return + } + log.Errorf(log.OrderBook, "Failed to get %s orderbook for %s %s %s. Error: %s\n", protocol, + result.ExchangeName, + result.Pair, + result.AssetType, err) return } @@ -132,57 +149,33 @@ func printOrderbookSummary(result *orderbook.Base, protocol string, err error) { bidsAmount, bidsValue := result.TotalBidsAmount() asksAmount, asksValue := result.TotalAsksAmount() - if result.Pair.Quote.IsFiatCurrency() && - result.Pair.Quote != Bot.Config.Currency.FiatDisplayCurrency { + var bidValueResult, askValueResult string + switch { + case result.Pair.Quote.IsFiatCurrency() && result.Pair.Quote != Bot.Config.Currency.FiatDisplayCurrency: origCurrency := result.Pair.Quote.Upper() - log.Infof(log.OrderBook, "%s %s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s\n", - result.ExchangeName, - protocol, - FormatCurrency(result.Pair), - strings.ToUpper(result.AssetType.String()), - len(result.Bids), - bidsAmount, - result.Pair.Base, - printConvertCurrencyFormat(origCurrency, bidsValue), - len(result.Asks), - asksAmount, - result.Pair.Base, - printConvertCurrencyFormat(origCurrency, asksValue), - ) - } else { - if result.Pair.Quote.IsFiatCurrency() && - result.Pair.Quote == Bot.Config.Currency.FiatDisplayCurrency { - log.Infof(log.OrderBook, "%s %s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s\n", - result.ExchangeName, - protocol, - FormatCurrency(result.Pair), - strings.ToUpper(result.AssetType.String()), - len(result.Bids), - bidsAmount, - result.Pair.Base, - printCurrencyFormat(bidsValue), - len(result.Asks), - asksAmount, - result.Pair.Base, - printCurrencyFormat(asksValue), - ) - } else { - log.Infof(log.OrderBook, "%s %s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %f Asks len: %d Amount: %f %s. Total value: %f\n", - result.ExchangeName, - protocol, - FormatCurrency(result.Pair), - strings.ToUpper(result.AssetType.String()), - len(result.Bids), - bidsAmount, - result.Pair.Base, - bidsValue, - len(result.Asks), - asksAmount, - result.Pair.Base, - asksValue, - ) - } + bidValueResult = printConvertCurrencyFormat(origCurrency, bidsValue) + askValueResult = printConvertCurrencyFormat(origCurrency, asksValue) + case result.Pair.Quote.IsFiatCurrency() && result.Pair.Quote == Bot.Config.Currency.FiatDisplayCurrency: + bidValueResult = printCurrencyFormat(bidsValue) + askValueResult = printCurrencyFormat(asksValue) + default: + bidValueResult = strconv.FormatFloat(bidsValue, 'f', -1, 64) + askValueResult = strconv.FormatFloat(asksValue, 'f', -1, 64) } + log.Infof(log.OrderBook, book, + result.ExchangeName, + protocol, + FormatCurrency(result.Pair), + strings.ToUpper(result.AssetType.String()), + len(result.Bids), + bidsAmount, + result.Pair.Base, + bidValueResult, + len(result.Asks), + asksAmount, + result.Pair.Base, + askValueResult, + ) } func relayWebsocketEvent(result interface{}, event, assetType, exchangeName string) { diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index 3ba1008b..87054d69 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -65,6 +65,8 @@ type Binance struct { // Valid string list that is required by the exchange validLimits []int + + obm *orderbookManager } // GetExchangeInfo returns exchange information. Check binance_types for more diff --git a/exchanges/binance/binance_live_test.go b/exchanges/binance/binance_live_test.go index b20f29e4..954f22cf 100644 --- a/exchanges/binance/binance_live_test.go +++ b/exchanges/binance/binance_live_test.go @@ -34,6 +34,7 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal("Binance setup error", err) } + b.setupOrderbookManager() b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() log.Printf(sharedtestvalues.LiveTesting, b.Name, b.API.Endpoints.URL) os.Exit(m.Run()) diff --git a/exchanges/binance/binance_mock_test.go b/exchanges/binance/binance_mock_test.go index 0bec281b..69504d24 100644 --- a/exchanges/binance/binance_mock_test.go +++ b/exchanges/binance/binance_mock_test.go @@ -39,6 +39,8 @@ func TestMain(m *testing.M) { log.Fatal("Binance setup error", err) } + b.setupOrderbookManager() + serverDetails, newClient, err := mock.NewVCRServer(mockfile) if err != nil { log.Fatalf("Mock server error %s", err) diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index 991874b2..1f6cfbd5 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -1,6 +1,7 @@ package binance import ( + "encoding/json" "testing" "time" @@ -63,7 +64,7 @@ func TestGetOrderBook(t *testing.T) { t.Parallel() _, err := b.GetOrderBook(OrderBookDataRequestParams{ Symbol: currency.NewPair(currency.BTC, currency.USDT), - Limit: 10, + Limit: 1000, }) if err != nil { @@ -911,6 +912,7 @@ func TestWsTradeUpdate(t *testing.T) { } func TestWsDepthUpdate(t *testing.T) { + b.setupOrderbookManager() seedLastUpdateID := int64(161) book := OrderBook{ Asks: []OrderbookItem{ @@ -1184,3 +1186,50 @@ func TestGetRecentTrades(t *testing.T) { t.Error(err) } } + +func TestSeedLocalCache(t *testing.T) { + t.Parallel() + err := b.SeedLocalCache(currency.NewPair(currency.BTC, currency.USDT)) + if err != nil { + t.Fatal(err) + } +} + +func TestGenerateSubscriptions(t *testing.T) { + t.Parallel() + subs, err := b.GenerateSubscriptions() + if err != nil { + t.Fatal(err) + } + + if len(subs) != 4 { + t.Fatal("unexpected subscription length") + } +} + +var websocketDepthUpdate = []byte(`{"E":1608001030784,"U":7145637266,"a":[["19455.19000000","0.59490200"],["19455.37000000","0.00000000"],["19456.11000000","0.00000000"],["19456.16000000","0.00000000"],["19458.67000000","0.06400000"],["19460.73000000","0.05139800"],["19461.43000000","0.00000000"],["19464.59000000","0.00000000"],["19466.03000000","0.45000000"],["19466.36000000","0.00000000"],["19508.67000000","0.00000000"],["19572.96000000","0.00217200"],["24386.00000000","0.00256600"]],"b":[["19455.18000000","2.94649200"],["19453.15000000","0.01233600"],["19451.18000000","0.00000000"],["19446.85000000","0.11427900"],["19446.74000000","0.00000000"],["19446.73000000","0.00000000"],["19444.45000000","0.14937800"],["19426.75000000","0.00000000"],["19416.36000000","0.36052100"]],"e":"depthUpdate","s":"BTCUSDT","u":7145637297}`) + +func TestProcessUpdate(t *testing.T) { + t.Parallel() + p := currency.NewPair(currency.BTC, currency.USDT) + var depth WebsocketDepthStream + err := json.Unmarshal(websocketDepthUpdate, &depth) + if err != nil { + t.Fatal(err) + } + + err = b.obm.stageWsUpdate(&depth, p, asset.Spot) + if err != nil { + t.Fatal(err) + } + + err = b.obm.fetchBookViaREST(p) + if err != nil { + t.Fatal(err) + } + + err = b.obm.cleanup(p) + if err != nil { + t.Fatal(err) + } +} diff --git a/exchanges/binance/binance_types.go b/exchanges/binance/binance_types.go index 8f260c11..1e9e3f14 100644 --- a/exchanges/binance/binance_types.go +++ b/exchanges/binance/binance_types.go @@ -1,9 +1,11 @@ package binance import ( + "sync" "time" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) // withdrawals status codes description @@ -107,13 +109,13 @@ type DepthUpdateParams []struct { // WebsocketDepthStream is the difference for the update depth stream type WebsocketDepthStream struct { - Event string `json:"e"` - Timestamp time.Time `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 time.Time `json:"E"` + Pair string `json:"s"` + FirstUpdateID int64 `json:"U"` + LastUpdateID int64 `json:"u"` + UpdateBids [][2]interface{} `json:"b"` + UpdateAsks [][2]interface{} `json:"a"` } // RecentTradeRequestParams represents Klines request data. @@ -746,3 +748,24 @@ type WsPayload struct { Params []string `json:"params"` ID int64 `json:"id"` } + +// orderbookManager defines a way of managing and maintaining synchronisation +// across connections and assets. +type orderbookManager struct { + state map[currency.Code]map[currency.Code]map[asset.Item]*update + sync.Mutex + + jobs chan job +} + +type update struct { + buffer chan *WebsocketDepthStream + fetchingBook bool + initialSync bool +} + +// job defines a synchonisation job that tells a go routine to fetch an +// orderbook via the REST protocol +type job struct { + Pair currency.Pair +} diff --git a/exchanges/binance/binance_websocket.go b/exchanges/binance/binance_websocket.go index 002c6456..a7135e3a 100644 --- a/exchanges/binance/binance_websocket.go +++ b/exchanges/binance/binance_websocket.go @@ -28,6 +28,18 @@ const ( var listenKey string +var ( + // maxWSUpdateBuffer defines max websocket updates to apply when an + // orderbook is initially fetched + maxWSUpdateBuffer = 100 + // maxWSOrderbookJobs defines max websocket orderbook jobs in queue to fetch + // an orderbook snapshot via REST + maxWSOrderbookJobs = 2000 + // maxWSOrderbookWorkers defines a max amount of workers allowed to execute + // jobs from the job channel + maxWSOrderbookWorkers = 10 +) + // WsConnect initiates a websocket connection func (b *Binance) WsConnect() error { if !b.Websocket.IsEnabled() || !b.IsEnabled() { @@ -85,10 +97,24 @@ func (b *Binance) WsConnect() error { } go b.wsReadData() - + b.setupOrderbookManager() return nil } +func (b *Binance) setupOrderbookManager() { + if b.obm == nil { + b.obm = &orderbookManager{ + state: make(map[currency.Code]map[currency.Code]map[asset.Item]*update), + jobs: make(chan job, maxWSOrderbookJobs), + } + + for i := 0; i < maxWSOrderbookWorkers; i++ { + // 10 workers for synchronising book + b.SynchroniseWebsocketOrderbook() + } + } +} + // KeepAuthKeyAlive will continuously send messages to // keep the WS auth key active func (b *Binance) KeepAuthKeyAlive() { @@ -410,7 +436,6 @@ func (b *Binance) SeedLocalCache(p currency.Pair) error { if err != nil { return err } - return b.SeedLocalCacheWithBook(p, &ob) } @@ -457,57 +482,33 @@ func (b *Binance) UpdateLocalBuffer(wsdp *WebsocketDepthStream) error { return err } - currentBook := b.Websocket.Orderbook.GetOrderbook(currencyPair, asset.Spot) - if currentBook == nil { - // Used when a pair/s is enabled while connected - err = b.SeedLocalCache(currencyPair) - if err != nil { - return err - } - currentBook = b.Websocket.Orderbook.GetOrderbook(currencyPair, asset.Spot) + err = b.obm.stageWsUpdate(wsdp, currencyPair, asset.Spot) + if err != nil { + return err } - // Drop any event where u is <= lastUpdateId in the snapshot. - // The first processed event should have U <= lastUpdateId+1 AND u >= lastUpdateId+1. - // While listening to the stream, each new event's U should be equal to the previous event's u+1. - if wsdp.LastUpdateID <= currentBook.LastUpdateID { - return nil + err = b.applyBufferUpdate(currencyPair) + if err != nil { + cleanupErr := b.Websocket.Orderbook.FlushOrderbook(currencyPair, asset.Spot) + if cleanupErr != nil { + log.Errorf(log.WebsocketMgr, + "%s flushing websocket error: %v", + b.Name, + cleanupErr) + } + + cleanupErr = b.obm.cleanup(currencyPair) + if cleanupErr != nil { + log.Errorf(log.WebsocketMgr, + "%s cleanup websocket orderbook error: %v", + b.Name, + cleanupErr) + } + + return err } - var updateBid, updateAsk []orderbook.Item - for i := range wsdp.UpdateBids { - p, err := strconv.ParseFloat(wsdp.UpdateBids[i][0].(string), 64) - if err != nil { - return err - } - 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 { - p, err := strconv.ParseFloat(wsdp.UpdateAsks[i][0].(string), 64) - if err != nil { - return err - } - a, err := strconv.ParseFloat(wsdp.UpdateAsks[i][1].(string), 64) - if err != nil { - return err - } - - updateAsk = append(updateAsk, orderbook.Item{Price: p, Amount: a}) - } - - return b.Websocket.Orderbook.Update(&buffer.Update{ - Bids: updateBid, - Asks: updateAsk, - Pair: currencyPair, - UpdateID: wsdp.LastUpdateID, - Asset: asset.Spot, - }) + return nil } // GenerateSubscriptions generates the default subscription set @@ -568,3 +569,337 @@ func (b *Binance) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe...) return nil } + +// ProcessUpdate processes the websocket orderbook update +func (b *Binance) ProcessUpdate(cp currency.Pair, a asset.Item, ws *WebsocketDepthStream) error { + var updateBid []orderbook.Item + for i := range ws.UpdateBids { + price, ok := ws.UpdateBids[i][0].(string) + if !ok { + return errors.New("type assertion failed for bid price") + } + p, err := strconv.ParseFloat(price, 64) + if err != nil { + return err + } + amount, ok := ws.UpdateBids[i][1].(string) + if !ok { + return errors.New("type assertion failed for bid amount") + } + a, err := strconv.ParseFloat(amount, 64) + if err != nil { + return err + } + updateBid = append(updateBid, orderbook.Item{Price: p, Amount: a}) + } + + var updateAsk []orderbook.Item + for i := range ws.UpdateAsks { + price, ok := ws.UpdateAsks[i][0].(string) + if !ok { + return errors.New("type assertion failed for ask price") + } + p, err := strconv.ParseFloat(price, 64) + if err != nil { + return err + } + amount, ok := ws.UpdateAsks[i][1].(string) + if !ok { + return errors.New("type assertion failed for ask amount") + } + a, err := strconv.ParseFloat(amount, 64) + if err != nil { + return err + } + updateAsk = append(updateAsk, orderbook.Item{Price: p, Amount: a}) + } + + return b.Websocket.Orderbook.Update(&buffer.Update{ + Bids: updateBid, + Asks: updateAsk, + Pair: cp, + UpdateID: ws.LastUpdateID, + Asset: a, + }) +} + +// applyBufferUpdate applies the buffer to the orderbook or initiates a new +// orderbook sync by the REST protocol which is off handed to go routine. +func (b *Binance) applyBufferUpdate(pair currency.Pair) error { + fetching, err := b.obm.checkIsFetchingBook(pair) + if err != nil { + return err + } + if fetching { + return nil + } + + recent := b.Websocket.Orderbook.GetOrderbook(pair, asset.Spot) + if recent == nil { + return b.obm.fetchBookViaREST(pair) + } + + return b.obm.checkAndProcessUpdate(b.ProcessUpdate, pair, recent) +} + +// SynchroniseWebsocketOrderbook synchronises full orderbook for currency pair +// asset +func (b *Binance) SynchroniseWebsocketOrderbook() { + b.Websocket.Wg.Add(1) + go func() { + defer b.Websocket.Wg.Done() + for { + select { + case j := <-b.obm.jobs: + err := b.processJob(j.Pair) + if err != nil { + log.Errorf(log.WebsocketMgr, + "%s processing websocket orderbook error %v", + b.Name, err) + } + case <-b.Websocket.ShutdownC: + return + } + } + }() +} + +// processJob fetches and processes orderbook updates +func (b *Binance) processJob(p currency.Pair) error { + err := b.SeedLocalCache(p) + if err != nil { + return fmt.Errorf("%s %s seeding local cache for orderbook error: %v", + p, asset.Spot, err) + } + + err = b.obm.stopFetchingBook(p) + if err != nil { + return err + } + + // Immediately apply the buffer updates so we don't wait for a + // new update to initiate this. + err = b.applyBufferUpdate(p) + if err != nil { + errClean := b.Websocket.Orderbook.FlushOrderbook(p, asset.Spot) + if errClean != nil { + log.Errorf(log.WebsocketMgr, + "%s flushing websocket error: %v", + b.Name, + errClean) + } + errClean = b.obm.cleanup(p) + if errClean != nil { + log.Errorf(log.WebsocketMgr, "%s cleanup websocket error: %v", + b.Name, + errClean) + } + return err + } + return nil +} + +// stageWsUpdate stages websocket update to roll through updates that need to +// be applied to a fetched orderbook via REST. +func (o *orderbookManager) stageWsUpdate(u *WebsocketDepthStream, pair currency.Pair, a asset.Item) error { + o.Lock() + defer o.Unlock() + m1, ok := o.state[pair.Base] + if !ok { + m1 = make(map[currency.Code]map[asset.Item]*update) + o.state[pair.Base] = m1 + } + + m2, ok := m1[pair.Quote] + if !ok { + m2 = make(map[asset.Item]*update) + m1[pair.Quote] = m2 + } + + state, ok := m2[a] + if !ok { + state = &update{ + // 100ms update assuming we might have up to a 10 second delay. + // There could be a potential 100 updates for the currency. + buffer: make(chan *WebsocketDepthStream, maxWSUpdateBuffer), + fetchingBook: false, + initialSync: true, + } + m2[a] = state + } + + select { + // Put update in the channel buffer to be processed + case state.buffer <- u: + return nil + default: + return fmt.Errorf("channel blockage for %s, asset %s and connection", + pair, a) + } +} + +// checkIsFetchingBook checks status if the book is currently being via the REST +// protocol. +func (o *orderbookManager) checkIsFetchingBook(pair currency.Pair) (bool, error) { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][asset.Spot] + if !ok { + return false, + fmt.Errorf("check is fetching book cannot match currency pair %s asset type %s", + pair, + asset.Spot) + } + return state.fetchingBook, nil +} + +// stopFetchingBook completes the book fetching. +func (o *orderbookManager) stopFetchingBook(pair currency.Pair) error { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][asset.Spot] + if !ok { + return fmt.Errorf("could not match pair %s and asset type %s in hash table", + pair, + asset.Spot) + } + if !state.fetchingBook { + return fmt.Errorf("fetching book already set to false for %s %s", + pair, + asset.Spot) + } + state.fetchingBook = false + return nil +} + +// completeInitialSync sets if an asset type has completed its initial sync +func (o *orderbookManager) completeInitialSync(pair currency.Pair) error { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][asset.Spot] + if !ok { + return fmt.Errorf("complete initial sync cannot match currency pair %s asset type %s", + pair, + asset.Spot) + } + if !state.initialSync { + return fmt.Errorf("initital sync already set to false for %s %s", + pair, + asset.Spot) + } + state.initialSync = false + return nil +} + +// fetchBookViaREST pushes a job of fetching the orderbook via the REST protocol +// to get an initial full book that we can apply our buffered updates too. +func (o *orderbookManager) fetchBookViaREST(pair currency.Pair) error { + o.Lock() + defer o.Unlock() + + state, ok := o.state[pair.Base][pair.Quote][asset.Spot] + if !ok { + return fmt.Errorf("fetch book via rest cannot match currency pair %s asset type %s", + pair, + asset.Spot) + } + + state.initialSync = true + state.fetchingBook = true + + select { + case o.jobs <- job{pair}: + return nil + default: + return fmt.Errorf("%s %s book synchronisation channel blocked up", + pair, + asset.Spot) + } +} + +func (o *orderbookManager) checkAndProcessUpdate(processor func(currency.Pair, asset.Item, *WebsocketDepthStream) error, pair currency.Pair, recent *orderbook.Base) error { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][asset.Spot] + if !ok { + return fmt.Errorf("could not match pair [%s] asset type [%s] in hash table to process websocket orderbook update", + pair, asset.Spot) + } + + // This will continuously remove updates from the buffered channel and + // apply them to the current orderbook. +buffer: + for { + select { + case d := <-state.buffer: + process, err := state.validate(d, recent) + if err != nil { + return err + } + if process { + err := processor(pair, asset.Spot, d) + if err != nil { + return fmt.Errorf("%s %s processing update error: %w", + pair, asset.Spot, err) + } + } + default: + break buffer + } + } + return nil +} + +// validate checks for correct update alignment +func (u *update) validate(updt *WebsocketDepthStream, recent *orderbook.Base) (bool, error) { + if updt.LastUpdateID <= recent.LastUpdateID { + // Drop any event where u is <= lastUpdateId in the snapshot. + return false, nil + } + + id := recent.LastUpdateID + 1 + if u.initialSync { + // The first processed event should have U <= lastUpdateId+1 AND + // u >= lastUpdateId+1. + if updt.FirstUpdateID > id && updt.LastUpdateID < id { + return false, fmt.Errorf("initial websocket orderbook sync failure for pair %s and asset %s", + recent.Pair, + asset.Spot) + } + u.initialSync = false + } else if updt.FirstUpdateID != id { + // While listening to the stream, each new event's U should be + // equal to the previous event's u+1. + return false, fmt.Errorf("websocket orderbook synchronisation failure for pair %s and asset %s", + recent.Pair, + asset.Spot) + } + return true, nil +} + +// cleanup cleans up buffer and reset fetch and init +func (o *orderbookManager) cleanup(pair currency.Pair) error { + o.Lock() + state, ok := o.state[pair.Base][pair.Quote][asset.Spot] + if !ok { + o.Unlock() + return fmt.Errorf("cleanup cannot match %s %s to hash table", + pair, + asset.Spot) + } + +bufferEmpty: + for { + select { + case <-state.buffer: + // bleed and discard buffer + default: + break bufferEmpty + } + } + o.Unlock() + // reset underlying bools + _ = o.stopFetchingBook(pair) + _ = o.completeInitialSync(pair) + return nil +} diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index aef6eb8a..50210b3b 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -183,6 +183,7 @@ func (b *Binance) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: b.GenerateSubscriptions, Features: &b.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, SortBuffer: true, SortBufferByUpdateIDs: true, }) @@ -400,37 +401,31 @@ func (b *Binance) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderb // UpdateOrderbook updates and returns the orderbook for a currency pair func (b *Binance) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ExchangeName: b.Name, Pair: p, AssetType: assetType} orderbookNew, err := b.GetOrderBook(OrderBookDataRequestParams{ Symbol: p, Limit: 1000}) if err != nil { - return nil, err + return book, err } - orderBook := new(orderbook.Base) for x := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, - orderbook.Item{ - Amount: orderbookNew.Bids[x].Quantity, - Price: orderbookNew.Bids[x].Price, - }) + book.Bids = append(book.Bids, orderbook.Item{ + Amount: orderbookNew.Bids[x].Quantity, + Price: orderbookNew.Bids[x].Price, + }) } for x := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, - orderbook.Item{ - Amount: orderbookNew.Asks[x].Quantity, - Price: orderbookNew.Asks[x].Price, - }) + book.Asks = append(book.Asks, orderbook.Item{ + Amount: orderbookNew.Asks[x].Quantity, + Price: orderbookNew.Asks[x].Price, + }) } - orderBook.Pair = p - orderBook.ExchangeName = b.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } return orderbook.Get(b.Name, p, assetType) diff --git a/exchanges/bitfinex/bitfinex.go b/exchanges/bitfinex/bitfinex.go index 687f1d9e..573e88a9 100644 --- a/exchanges/bitfinex/bitfinex.go +++ b/exchanges/bitfinex/bitfinex.go @@ -77,6 +77,9 @@ const ( // activity. Cancelling orders will be possible. bitfinexMaintenanceMode = 0 bitfinexOperativeMode = 1 + + bitfinexChecksumFlag = 131072 + bitfinexWsSequenceFlag = 65536 ) // Bitfinex is the overarching type across the bitfinex package diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index 951f8dbc..06106fd7 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -15,6 +15,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) @@ -1144,12 +1145,12 @@ func TestWsSubscribedResponse(t *testing.T) { func TestWsTradingPairSnapshot(t *testing.T) { b.WebsocketSubdChannels[23405] = WebsocketChanInfo{Pair: "BTCUSD", Channel: wsBook} - pressXToJSON := `[23405,[[38334303613,9348.8,0.53],[38334308111,9348.8,5.98979404],[38331335157,9344.1,1.28965787],[38334302803,9343.8,0.08230094],[38334279092,9343,0.8],[38334307036,9342.938663676,0.8],[38332749107,9342.9,0.2],[38332277330,9342.8,0.85],[38329406786,9342,0.1432012],[38332841570,9341.947288638,0.3],[38332163238,9341.7,0.3],[38334303384,9341.6,0.324],[38332464840,9341.4,0.5],[38331935870,9341.2,0.5],[38334312082,9340.9,0.02126899],[38334261292,9340.8,0.26763],[38334138680,9340.625455254,0.12],[38333896802,9339.8,0.85],[38331627527,9338.9,1.57863959],[38334186713,9338.9,0.26769],[38334305819,9338.8,2.999],[38334211180,9338.75285796,3.999],[38334310699,9337.8,0.10679883],[38334307414,9337.5,1],[38334179822,9337.1,0.26773],[38334306600,9336.659955102,1.79],[38334299667,9336.6,1.1],[38334306452,9336.6,0.13979771],[38325672859,9336.3,1.25],[38334311646,9336.2,1],[38334258509,9336.1,0.37],[38334310592,9336,1.79],[38334310378,9335.6,1.43],[38334132444,9335.2,0.26777],[38331367325,9335,0.07],[38334310703,9335,0.10680562],[38334298209,9334.7,0.08757301],[38334304857,9334.456899462,0.291],[38334309940,9334.088390727,0.0725],[38334310377,9333.7,1.2868],[38334297615,9333.607784,0.1108],[38334095188,9333.3,0.26785],[38334228913,9332.7,0.40861186],[38334300526,9332.363996604,0.3884],[38334310701,9332.2,0.10680562],[38334303548,9332.005382871,0.07],[38334311798,9331.8,0.41285228],[38334301012,9331.7,1.7952],[38334089877,9331.4,0.2679],[38321942150,9331.2,0.2],[38334310670,9330,1.069],[38334063096,9329.6,0.26796],[38334310700,9329.4,0.10680562],[38334310404,9329.3,1],[38334281630,9329.1,6.57150597],[38334036864,9327.7,0.26801],[38334310702,9326.6,0.10680562],[38334311799,9326.1,0.50220625],[38334164163,9326,0.219638],[38334309722,9326,1.5],[38333051682,9325.8,0.26807],[38334302027,9325.7,0.75],[38334203435,9325.366592,0.32397696],[38321967613,9325,0.05],[38334298787,9324.9,0.3],[38334301719,9324.8,3.6227592],[38331316716,9324.763454646,0.71442],[38334310698,9323.8,0.10680562],[38334035499,9323.7,0.23431017],[38334223472,9322.670551788,0.42150603],[38334163459,9322.560399006,0.143967],[38321825171,9320.8,2],[38334075805,9320.467496148,0.30772633],[38334075800,9319.916732238,0.61457592],[38333682302,9319.7,0.0011],[38331323088,9319.116771762,0.12913],[38333677480,9319,0.0199],[38334277797,9318.6,0.89],[38325235155,9318.041088,1.20249],[38334310910,9317.82382938,1.79],[38334311811,9317.2,0.61079138],[38334311812,9317.2,0.71937652],[38333298214,9317.1,50],[38334306359,9317,1.79],[38325531545,9316.382823951,0.21263],[38333727253,9316.3,0.02316372],[38333298213,9316.1,45],[38333836479,9316,2.135],[38324520465,9315.9,2.7681],[38334307411,9315.5,1],[38330313617,9315.3,0.84455],[38334077770,9315.294024,0.01248397],[38334286663,9315.294024,1],[38325533762,9315.290315394,2.40498],[38334310018,9315.2,3],[38333682617,9314.6,0.0011],[38334304794,9314.6,0.76364676],[38334304798,9314.3,0.69242113],[38332915733,9313.8,0.0199],[38334084411,9312.8,1],[38334311893,9350.1,-1.015],[38334302734,9350.3,-0.26737],[38334300732,9350.8,-5.2],[38333957619,9351,-0.90677089],[38334300521,9351,-1.6457],[38334301600,9351.012829557,-0.0523],[38334308878,9351.7,-2.5],[38334299570,9351.921544,-0.1015],[38334279367,9352.1,-0.26732],[38334299569,9352.411802928,-0.4036],[38334202773,9353.4,-0.02139404],[38333918472,9353.7,-1.96412776],[38334278782,9354,-0.26731],[38334278606,9355,-1.2785],[38334302105,9355.439221251,-0.79191542],[38313897370,9355.569409242,-0.43363],[38334292995,9355.584296,-0.0979],[38334216989,9355.8,-0.03686414],[38333894025,9355.9,-0.26721],[38334293798,9355.936691952,-0.4311],[38331159479,9356,-0.4204022],[38333918888,9356.1,-1.10885563],[38334298205,9356.4,-0.20124428],[38328427481,9356.5,-0.1],[38333343289,9356.6,-0.41034213],[38334297205,9356.6,-0.08835018],[38334277927,9356.741101161,-0.0737],[38334311645,9356.8,-0.5],[38334309002,9356.9,-5],[38334309736,9357,-0.10680107],[38334306448,9357.4,-0.18645275],[38333693302,9357.7,-0.2672],[38332815159,9357.8,-0.0011],[38331239824,9358.2,-0.02],[38334271608,9358.3,-2.999],[38334311971,9358.4,-0.55],[38333919260,9358.5,-1.9972841],[38334265365,9358.5,-1.7841],[38334277960,9359,-3],[38334274601,9359.020969848,-3],[38326848839,9359.1,-0.84],[38334291080,9359.247048,-0.16199869],[38326848844,9359.4,-1.84],[38333680200,9359.6,-0.26713],[38331326606,9359.8,-0.84454],[38334309738,9359.8,-0.10680107],[38331314707,9359.9,-0.2],[38333919803,9360.9,-1.41177599],[38323651149,9361.33417827,-0.71442],[38333656906,9361.5,-0.26705],[38334035500,9361.5,-0.40861586],[38334091886,9362.4,-6.85940815],[38334269617,9362.5,-4],[38323629409,9362.545858872,-2.40497],[38334309737,9362.7,-0.10680107],[38334312380,9362.7,-3],[38325280830,9362.8,-1.75123],[38326622800,9362.8,-1.05145],[38333175230,9363,-0.0011],[38326848745,9363.2,-0.79],[38334308960,9363.206775564,-0.12],[38333920234,9363.3,-1.25318113],[38326848843,9363.4,-1.29],[38331239823,9363.4,-0.02],[38333209613,9363.4,-0.26719],[38334299964,9364,-0.05583123],[38323470224,9364.161816648,-0.12912],[38334284711,9365,-0.21346019],[38334299594,9365,-2.6757062],[38323211816,9365.073132585,-0.21262],[38334312456,9365.1,-0.11167861],[38333209612,9365.2,-0.26719],[38327770474,9365.3,-0.0073],[38334298788,9365.3,-0.3],[38334075803,9365.409831204,-0.30772637],[38334309740,9365.5,-0.10680107],[38326608767,9365.7,-2.76809],[38333920657,9365.7,-1.25848083],[38329594226,9366.6,-0.02587],[38334311813,9366.7,-4.72290945],[38316386301,9367.39258128,-2.37581],[38334302026,9367.4,-4.5],[38334228915,9367.9,-0.81725458],[38333921381,9368.1,-1.72213641],[38333175678,9368.2,-0.0011],[38334301150,9368.2,-2.654604],[38334297208,9368.3,-0.78036466],[38334309739,9368.3,-0.10680107],[38331227515,9368.7,-0.02],[38331184470,9369,-0.003975],[38334203436,9369.319616,-0.32397695],[38334269964,9369.7,-0.5],[38328386732,9370,-4.11759935],[38332719555,9370,-0.025],[38333921935,9370.5,-1.2224398],[38334258511,9370.5,-0.35],[38326848842,9370.8,-0.34],[38333985038,9370.9,-0.8551502],[38334283018,9370.9,-1],[38326848744,9371,-1.34]]]` + pressXToJSON := `[23405,[[38334303613,9348.8,0.53],[38334308111,9348.8,5.98979404],[38331335157,9344.1,1.28965787],[38334302803,9343.8,0.08230094],[38334279092,9343,0.8],[38334307036,9342.938663676,0.8],[38332749107,9342.9,0.2],[38332277330,9342.8,0.85],[38329406786,9342,0.1432012],[38332841570,9341.947288638,0.3],[38332163238,9341.7,0.3],[38334303384,9341.6,0.324],[38332464840,9341.4,0.5],[38331935870,9341.2,0.5],[38334312082,9340.9,0.02126899],[38334261292,9340.8,0.26763],[38334138680,9340.625455254,0.12],[38333896802,9339.8,0.85],[38331627527,9338.9,1.57863959],[38334186713,9338.9,0.26769],[38334305819,9338.8,2.999],[38334211180,9338.75285796,3.999],[38334310699,9337.8,0.10679883],[38334307414,9337.5,1],[38334179822,9337.1,0.26773],[38334306600,9336.659955102,1.79],[38334299667,9336.6,1.1],[38334306452,9336.6,0.13979771],[38325672859,9336.3,1.25],[38334311646,9336.2,1],[38334258509,9336.1,0.37],[38334310592,9336,1.79],[38334310378,9335.6,1.43],[38334132444,9335.2,0.26777],[38331367325,9335,0.07],[38334310703,9335,0.10680562],[38334298209,9334.7,0.08757301],[38334304857,9334.456899462,0.291],[38334309940,9334.088390727,0.0725],[38334310377,9333.7,1.2868],[38334297615,9333.607784,0.1108],[38334095188,9333.3,0.26785],[38334228913,9332.7,0.40861186],[38334300526,9332.363996604,0.3884],[38334310701,9332.2,0.10680562],[38334303548,9332.005382871,0.07],[38334311798,9331.8,0.41285228],[38334301012,9331.7,1.7952],[38334089877,9331.4,0.2679],[38321942150,9331.2,0.2],[38334310670,9330,1.069],[38334063096,9329.6,0.26796],[38334310700,9329.4,0.10680562],[38334310404,9329.3,1],[38334281630,9329.1,6.57150597],[38334036864,9327.7,0.26801],[38334310702,9326.6,0.10680562],[38334311799,9326.1,0.50220625],[38334164163,9326,0.219638],[38334309722,9326,1.5],[38333051682,9325.8,0.26807],[38334302027,9325.7,0.75],[38334203435,9325.366592,0.32397696],[38321967613,9325,0.05],[38334298787,9324.9,0.3],[38334301719,9324.8,3.6227592],[38331316716,9324.763454646,0.71442],[38334310698,9323.8,0.10680562],[38334035499,9323.7,0.23431017],[38334223472,9322.670551788,0.42150603],[38334163459,9322.560399006,0.143967],[38321825171,9320.8,2],[38334075805,9320.467496148,0.30772633],[38334075800,9319.916732238,0.61457592],[38333682302,9319.7,0.0011],[38331323088,9319.116771762,0.12913],[38333677480,9319,0.0199],[38334277797,9318.6,0.89],[38325235155,9318.041088,1.20249],[38334310910,9317.82382938,1.79],[38334311811,9317.2,0.61079138],[38334311812,9317.2,0.71937652],[38333298214,9317.1,50],[38334306359,9317,1.79],[38325531545,9316.382823951,0.21263],[38333727253,9316.3,0.02316372],[38333298213,9316.1,45],[38333836479,9316,2.135],[38324520465,9315.9,2.7681],[38334307411,9315.5,1],[38330313617,9315.3,0.84455],[38334077770,9315.294024,0.01248397],[38334286663,9315.294024,1],[38325533762,9315.290315394,2.40498],[38334310018,9315.2,3],[38333682617,9314.6,0.0011],[38334304794,9314.6,0.76364676],[38334304798,9314.3,0.69242113],[38332915733,9313.8,0.0199],[38334084411,9312.8,1],[38334311893,9350.1,-1.015],[38334302734,9350.3,-0.26737],[38334300732,9350.8,-5.2],[38333957619,9351,-0.90677089],[38334300521,9351,-1.6457],[38334301600,9351.012829557,-0.0523],[38334308878,9351.7,-2.5],[38334299570,9351.921544,-0.1015],[38334279367,9352.1,-0.26732],[38334299569,9352.411802928,-0.4036],[38334202773,9353.4,-0.02139404],[38333918472,9353.7,-1.96412776],[38334278782,9354,-0.26731],[38334278606,9355,-1.2785],[38334302105,9355.439221251,-0.79191542],[38313897370,9355.569409242,-0.43363],[38334292995,9355.584296,-0.0979],[38334216989,9355.8,-0.03686414],[38333894025,9355.9,-0.26721],[38334293798,9355.936691952,-0.4311],[38331159479,9356,-0.4204022],[38333918888,9356.1,-1.10885563],[38334298205,9356.4,-0.20124428],[38328427481,9356.5,-0.1],[38333343289,9356.6,-0.41034213],[38334297205,9356.6,-0.08835018],[38334277927,9356.741101161,-0.0737],[38334311645,9356.8,-0.5],[38334309002,9356.9,-5],[38334309736,9357,-0.10680107],[38334306448,9357.4,-0.18645275],[38333693302,9357.7,-0.2672],[38332815159,9357.8,-0.0011],[38331239824,9358.2,-0.02],[38334271608,9358.3,-2.999],[38334311971,9358.4,-0.55],[38333919260,9358.5,-1.9972841],[38334265365,9358.5,-1.7841],[38334277960,9359,-3],[38334274601,9359.020969848,-3],[38326848839,9359.1,-0.84],[38334291080,9359.247048,-0.16199869],[38326848844,9359.4,-1.84],[38333680200,9359.6,-0.26713],[38331326606,9359.8,-0.84454],[38334309738,9359.8,-0.10680107],[38331314707,9359.9,-0.2],[38333919803,9360.9,-1.41177599],[38323651149,9361.33417827,-0.71442],[38333656906,9361.5,-0.26705],[38334035500,9361.5,-0.40861586],[38334091886,9362.4,-6.85940815],[38334269617,9362.5,-4],[38323629409,9362.545858872,-2.40497],[38334309737,9362.7,-0.10680107],[38334312380,9362.7,-3],[38325280830,9362.8,-1.75123],[38326622800,9362.8,-1.05145],[38333175230,9363,-0.0011],[38326848745,9363.2,-0.79],[38334308960,9363.206775564,-0.12],[38333920234,9363.3,-1.25318113],[38326848843,9363.4,-1.29],[38331239823,9363.4,-0.02],[38333209613,9363.4,-0.26719],[38334299964,9364,-0.05583123],[38323470224,9364.161816648,-0.12912],[38334284711,9365,-0.21346019],[38334299594,9365,-2.6757062],[38323211816,9365.073132585,-0.21262],[38334312456,9365.1,-0.11167861],[38333209612,9365.2,-0.26719],[38327770474,9365.3,-0.0073],[38334298788,9365.3,-0.3],[38334075803,9365.409831204,-0.30772637],[38334309740,9365.5,-0.10680107],[38326608767,9365.7,-2.76809],[38333920657,9365.7,-1.25848083],[38329594226,9366.6,-0.02587],[38334311813,9366.7,-4.72290945],[38316386301,9367.39258128,-2.37581],[38334302026,9367.4,-4.5],[38334228915,9367.9,-0.81725458],[38333921381,9368.1,-1.72213641],[38333175678,9368.2,-0.0011],[38334301150,9368.2,-2.654604],[38334297208,9368.3,-0.78036466],[38334309739,9368.3,-0.10680107],[38331227515,9368.7,-0.02],[38331184470,9369,-0.003975],[38334203436,9369.319616,-0.32397695],[38334269964,9369.7,-0.5],[38328386732,9370,-4.11759935],[38332719555,9370,-0.025],[38333921935,9370.5,-1.2224398],[38334258511,9370.5,-0.35],[38326848842,9370.8,-0.34],[38333985038,9370.9,-0.8551502],[38334283018,9370.9,-1],[38326848744,9371,-1.34]],5]` err := b.wsHandleData([]byte(pressXToJSON)) if err != nil { t.Error(err) } - pressXToJSON = `[23405,[7617,52.98726298,7617.1,53.601795929999994,-550.9,-0.0674,7617,8318.92961981,8257.8,7500]]` + pressXToJSON = `[23405,[7617,52.98726298,7617.1,53.601795929999994,-550.9,-0.0674,7617,8318.92961981,8257.8,7500],6]` err = b.wsHandleData([]byte(pressXToJSON)) if err != nil { t.Error(err) @@ -1474,3 +1475,79 @@ func TestGetHistoricTrades(t *testing.T) { t.Error(err) } } + +var testOb = orderbook.Base{ + Asks: []orderbook.Item{ + {Price: 0.05005, Amount: 0.00000500}, + {Price: 0.05010, Amount: 0.00000500}, + {Price: 0.05015, Amount: 0.00000500}, + {Price: 0.05020, Amount: 0.00000500}, + {Price: 0.05025, Amount: 0.00000500}, + {Price: 0.05030, Amount: 0.00000500}, + {Price: 0.05035, Amount: 0.00000500}, + {Price: 0.05040, Amount: 0.00000500}, + {Price: 0.05045, Amount: 0.00000500}, + {Price: 0.05050, Amount: 0.00000500}, + }, + Bids: []orderbook.Item{ + {Price: 0.05000, Amount: 0.00000500}, + {Price: 0.04995, Amount: 0.00000500}, + {Price: 0.04990, Amount: 0.00000500}, + {Price: 0.04980, Amount: 0.00000500}, + {Price: 0.04975, Amount: 0.00000500}, + {Price: 0.04970, Amount: 0.00000500}, + {Price: 0.04965, Amount: 0.00000500}, + {Price: 0.04960, Amount: 0.00000500}, + {Price: 0.04955, Amount: 0.00000500}, + {Price: 0.04950, Amount: 0.00000500}, + }, +} + +func TestChecksum(t *testing.T) { + err := validateCRC32(&testOb, 190468240) + if err != nil { + t.Fatal(err) + } +} + +func TestReOrderbyID(t *testing.T) { + asks := []orderbook.Item{ + {ID: 4, Price: 100, Amount: 0.00000500}, + {ID: 3, Price: 100, Amount: 0.00000500}, + {ID: 2, Price: 100, Amount: 0.00000500}, + {ID: 1, Price: 100, Amount: 0.00000500}, + {ID: 5, Price: 101, Amount: 0.00000500}, + {ID: 6, Price: 102, Amount: 0.00000500}, + {ID: 8, Price: 103, Amount: 0.00000500}, + {ID: 7, Price: 103, Amount: 0.00000500}, + {ID: 9, Price: 104, Amount: 0.00000500}, + {ID: 10, Price: 105, Amount: 0.00000500}, + } + reOrderByID(asks) + + for i := range asks { + if asks[i].ID != int64(i+1) { + t.Fatal("order by ID failure") + } + } + + bids := []orderbook.Item{ + {ID: 4, Price: 100, Amount: 0.00000500}, + {ID: 3, Price: 100, Amount: 0.00000500}, + {ID: 2, Price: 100, Amount: 0.00000500}, + {ID: 1, Price: 100, Amount: 0.00000500}, + {ID: 5, Price: 99, Amount: 0.00000500}, + {ID: 6, Price: 98, Amount: 0.00000500}, + {ID: 8, Price: 97, Amount: 0.00000500}, + {ID: 7, Price: 97, Amount: 0.00000500}, + {ID: 9, Price: 96, Amount: 0.00000500}, + {ID: 10, Price: 95, Amount: 0.00000500}, + } + reOrderByID(bids) + + for i := range bids { + if bids[i].ID != int64(i+1) { + t.Fatal("order by ID failure") + } + } +} diff --git a/exchanges/bitfinex/bitfinex_types.go b/exchanges/bitfinex/bitfinex_types.go index 2c68ef21..3e4783ee 100644 --- a/exchanges/bitfinex/bitfinex_types.go +++ b/exchanges/bitfinex/bitfinex_types.go @@ -6,8 +6,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) -// AcceptedOrderType defines the accepted market types, exchange strings denote -// non-contract order types. +// AcceptedOrderType defines the accepted market types, exchange strings denote non-contract order types. var AcceptedOrderType = []string{"market", "limit", "stop", "trailing-stop", "fill-or-kill", "exchange market", "exchange limit", "exchange stop", "exchange trailing-stop", "exchange fill-or-kill"} @@ -377,7 +376,6 @@ type WebsocketBook struct { ID int64 Price float64 Amount float64 - Rate float64 Period int64 } diff --git a/exchanges/bitfinex/bitfinex_websocket.go b/exchanges/bitfinex/bitfinex_websocket.go index c3bf546d..4a9b2d7b 100644 --- a/exchanges/bitfinex/bitfinex_websocket.go +++ b/exchanges/bitfinex/bitfinex_websocket.go @@ -4,9 +4,12 @@ import ( "encoding/json" "errors" "fmt" + "hash/crc32" "net/http" + "sort" "strconv" "strings" + "sync" "time" "github.com/gorilla/websocket" @@ -26,6 +29,15 @@ import ( var comms = make(chan stream.Response) +type checksum struct { + Token int + Sequence int64 +} + +// checksumStore quick global for now +var checksumStore = make(map[int]*checksum) +var cMtx sync.Mutex + // WsConnect starts a new websocket connection func (b *Bitfinex) WsConnect() error { if !b.Websocket.IsEnabled() || !b.IsEnabled() { @@ -39,6 +51,7 @@ func (b *Bitfinex) WsConnect() error { b.Name, err) } + go b.wsReadData(b.Websocket.Conn) if b.Websocket.CanUseAuthenticatedEndpoints() { @@ -139,14 +152,42 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error { } } case []interface{}: - if hb, ok := d[1].(string); ok { + chanF, ok := d[0].(float64) + if !ok { + return errors.New("channel ID type assertion failure") + } + + chanID := int(chanF) + var datum string + if datum, ok = d[1].(string); ok { // Capturing heart beat - if hb == "hb" { + if datum == "hb" { + return nil + } + + // Capturing checksum and storing value + if datum == "cs" { + var tokenF float64 + tokenF, ok = d[2].(float64) + if !ok { + return errors.New("checksum token type assertion failure") + } + var seqNoF float64 + seqNoF, ok = d[3].(float64) + if !ok { + return errors.New("sequence number type assertion failure") + } + + cMtx.Lock() + checksumStore[chanID] = &checksum{ + Token: int(tokenF), + Sequence: int64(seqNoF), + } + cMtx.Unlock() return nil } } - chanID := int(d[0].(float64)) chanInfo, ok := b.WebsocketSubdChannels[chanID] if !ok && chanID != 0 { return fmt.Errorf("bitfinex.go error - Unable to locate chanID: %d", @@ -198,43 +239,81 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error { if len(obSnapBundle) == 0 { return errors.New("no data within orderbook snapshot") } + + sequenceNo, ok := d[2].(float64) + if !ok { + return errors.New("type assertion failure") + } + + var fundingRate bool switch id := obSnapBundle[0].(type) { case []interface{}: for i := range obSnapBundle { data := obSnapBundle[i].([]interface{}) + id, okAssert := data[0].(float64) + if !okAssert { + return errors.New("type assertion failed for orderbook item data") + } + pricePeriod, okAssert := data[1].(float64) + if !okAssert { + return errors.New("type assertion failed for orderbook item data") + } + rateAmount, okAssert := data[2].(float64) + if !okAssert { + return errors.New("type assertion failed for orderbook item data") + } if len(data) == 4 { + fundingRate = true + amount, okFunding := data[3].(float64) + if !okFunding { + return errors.New("type assertion failed for orderbook item data") + } newOrderbook = append(newOrderbook, WebsocketBook{ - ID: int64(data[0].(float64)), - Period: int64(data[1].(float64)), - Rate: data[2].(float64), - Amount: data[3].(float64)}) + ID: int64(id), + Period: int64(pricePeriod), + Price: rateAmount, + Amount: amount}) } else { newOrderbook = append(newOrderbook, WebsocketBook{ - ID: int64(data[0].(float64)), - Price: data[1].(float64), - Amount: data[2].(float64)}) + ID: int64(id), + Price: pricePeriod, + Amount: rateAmount}) } } - err := b.WsInsertSnapshot(pair, chanAsset, newOrderbook) + err := b.WsInsertSnapshot(pair, chanAsset, newOrderbook, fundingRate) if err != nil { return fmt.Errorf("bitfinex_websocket.go inserting snapshot error: %s", err) } case float64: + pricePeriod, okSnap := obSnapBundle[1].(float64) + if !okSnap { + return errors.New("type assertion failed for orderbook snapshot data") + } + amountRate, okSnap := obSnapBundle[2].(float64) + if !okSnap { + return errors.New("type assertion failed for orderbook snapshot data") + } if len(obSnapBundle) == 4 { + fundingRate = true + var amount float64 + amount, okSnap = obSnapBundle[3].(float64) + if !okSnap { + return errors.New("type assertion failed for orderbook snapshot data") + } newOrderbook = append(newOrderbook, WebsocketBook{ ID: int64(id), - Period: int64(obSnapBundle[1].(float64)), - Rate: obSnapBundle[2].(float64), - Amount: obSnapBundle[3].(float64)}) + Period: int64(pricePeriod), + Price: amountRate, + Amount: amount}) } else { newOrderbook = append(newOrderbook, WebsocketBook{ ID: int64(id), - Price: obSnapBundle[1].(float64), - Amount: obSnapBundle[2].(float64)}) + Price: pricePeriod, + Amount: amountRate}) } - err := b.WsUpdateOrderbook(pair, chanAsset, newOrderbook) + err := b.WsUpdateOrderbook(pair, chanAsset, newOrderbook, chanID, int64(sequenceNo), fundingRate) if err != nil { return fmt.Errorf("bitfinex_websocket.go updating orderbook error: %s", err) @@ -282,17 +361,38 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error { return nil case wsTicker: tickerData := d[1].([]interface{}) - b.Websocket.DataHandler <- &ticker.Price{ - ExchangeName: b.Name, - Bid: tickerData[0].(float64), - Ask: tickerData[2].(float64), - Last: tickerData[6].(float64), - Volume: tickerData[7].(float64), - High: tickerData[8].(float64), - Low: tickerData[9].(float64), - AssetType: chanAsset, - Pair: pair, + if len(tickerData) == 10 { + b.Websocket.DataHandler <- &ticker.Price{ + ExchangeName: b.Name, + Bid: tickerData[0].(float64), + Ask: tickerData[2].(float64), + Last: tickerData[6].(float64), + Volume: tickerData[7].(float64), + High: tickerData[8].(float64), + Low: tickerData[9].(float64), + AssetType: chanAsset, + Pair: pair, + } + } else { + b.Websocket.DataHandler <- &ticker.Price{ + ExchangeName: b.Name, + FlashReturnRate: tickerData[0].(float64), + Bid: tickerData[1].(float64), + BidPeriod: tickerData[2].(float64), + BidSize: tickerData[3].(float64), + Ask: tickerData[4].(float64), + AskPeriod: tickerData[5].(float64), + AskSize: tickerData[6].(float64), + Last: tickerData[9].(float64), + Volume: tickerData[10].(float64), + High: tickerData[11].(float64), + Low: tickerData[12].(float64), + FlashReturnRateAmount: tickerData[15].(float64), + AssetType: chanAsset, + Pair: pair, + } } + return nil case wsTrades: if !b.IsSaveTradeDataEnabled() { @@ -833,77 +933,120 @@ func (b *Bitfinex) wsHandleOrder(data []interface{}) { // WsInsertSnapshot add the initial orderbook snapshot when subscribed to a // channel -func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType asset.Item, books []WebsocketBook) error { +func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType asset.Item, books []WebsocketBook, fundingRate bool) error { if len(books) == 0 { return errors.New("bitfinex.go error - no orderbooks submitted") } - var bid, ask []orderbook.Item + var book orderbook.Base for i := range books { - if books[i].Amount > 0 { - bid = append(bid, orderbook.Item{ - ID: books[i].ID, - Amount: books[i].Amount, - Price: books[i].Price}) + item := orderbook.Item{ + ID: books[i].ID, + Amount: books[i].Amount, + Price: books[i].Price, + Period: books[i].Period, + } + if fundingRate { + if item.Amount < 0 { + item.Amount *= -1 + book.Bids = append(book.Bids, item) + } else { + book.Asks = append(book.Asks, item) + } } else { - ask = append(ask, orderbook.Item{ - ID: books[i].ID, - Amount: books[i].Amount * -1, - Price: books[i].Price}) + if books[i].Amount > 0 { + book.Bids = append(book.Bids, item) + } else { + item.Amount *= -1 + book.Asks = append(book.Asks, item) + } } } - var newOrderBook orderbook.Base - newOrderBook.Asks = ask - newOrderBook.AssetType = assetType - newOrderBook.Bids = bid - newOrderBook.Pair = p - newOrderBook.ExchangeName = b.Name - - return b.Websocket.Orderbook.LoadSnapshot(&newOrderBook) + book.AssetType = assetType + book.Pair = p + book.ExchangeName = b.Name + book.NotAggregated = true + book.IsFundingRate = fundingRate + return b.Websocket.Orderbook.LoadSnapshot(&book) } // WsUpdateOrderbook updates the orderbook list, removing and adding to the // orderbook sides -func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book []WebsocketBook) error { - orderbookUpdate := buffer.Update{ - Asset: assetType, - Pair: p, - } +func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book []WebsocketBook, channelID int, sequenceNo int64, fundingRate bool) error { + orderbookUpdate := buffer.Update{Asset: assetType, Pair: p} for i := range book { - switch { - case book[i].Price > 0: - orderbookUpdate.Action = "update/insert" - if book[i].Amount > 0 { - // update bid - orderbookUpdate.Bids = append(orderbookUpdate.Bids, - orderbook.Item{ - ID: book[i].ID, - Amount: book[i].Amount, - Price: book[i].Price}) - } else if book[i].Amount < 0 { - // update ask - orderbookUpdate.Asks = append(orderbookUpdate.Asks, - orderbook.Item{ - ID: book[i].ID, - Amount: book[i].Amount * -1, - Price: book[i].Price}) + item := orderbook.Item{ + ID: book[i].ID, + Amount: book[i].Amount, + Price: book[i].Price, + Period: book[i].Period, + } + + if book[i].Price > 0 { + orderbookUpdate.Action = buffer.UpdateInsert + if fundingRate { + if book[i].Amount < 0 { + item.Amount *= -1 + orderbookUpdate.Bids = append(orderbookUpdate.Bids, item) + } else { + orderbookUpdate.Asks = append(orderbookUpdate.Asks, item) + } + } else { + if book[i].Amount > 0 { + orderbookUpdate.Bids = append(orderbookUpdate.Bids, item) + } else { + item.Amount *= -1 + orderbookUpdate.Asks = append(orderbookUpdate.Asks, item) + } } - case book[i].Price == 0: - orderbookUpdate.Action = "delete" - if book[i].Amount == 1 { - // delete bid - orderbookUpdate.Bids = append(orderbookUpdate.Bids, - orderbook.Item{ - ID: book[i].ID}) - } else if book[i].Amount == -1 { - // delete ask - orderbookUpdate.Asks = append(orderbookUpdate.Asks, - orderbook.Item{ - ID: book[i].ID}) + } else { + orderbookUpdate.Action = buffer.Delete + if fundingRate { + if book[i].Amount == 1 { + // delete bid + orderbookUpdate.Asks = append(orderbookUpdate.Asks, item) + } else { + // delete ask + orderbookUpdate.Bids = append(orderbookUpdate.Bids, item) + } + } else { + if book[i].Amount == 1 { + // delete bid + orderbookUpdate.Bids = append(orderbookUpdate.Bids, item) + } else { + // delete ask + orderbookUpdate.Asks = append(orderbookUpdate.Asks, item) + } } } } + + cMtx.Lock() + checkme := checksumStore[channelID] + if checkme == nil { + cMtx.Unlock() + return b.Websocket.Orderbook.Update(&orderbookUpdate) + } + checksumStore[channelID] = nil + cMtx.Unlock() + + if checkme.Sequence+1 == sequenceNo { + // Sequence numbers get dropped, if checksum is not in line with + // sequence, do not check. + ob := b.Websocket.Orderbook.GetOrderbook(p, assetType) + if ob == nil { + return fmt.Errorf("cannot calculate websocket checksum: book not found for %s %s", + p, + assetType) + } + + err := validateCRC32(ob, checkme.Token) + if err != nil { + return err + } + } + return b.Websocket.Orderbook.Update(&orderbookUpdate) } @@ -961,6 +1104,14 @@ func (b *Bitfinex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, // Subscribe sends a websocket message to receive data from the channel func (b *Bitfinex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error { var errs common.Errors + checksum := make(map[string]interface{}) + checksum["event"] = "conf" + checksum["flags"] = bitfinexChecksumFlag + bitfinexWsSequenceFlag + err := b.Websocket.Conn.SendJSONMessage(checksum) + if err != nil { + return err + } + for i := range channelsToSubscribe { req := make(map[string]interface{}) req["event"] = "subscribe" @@ -1192,3 +1343,100 @@ func (b *Bitfinex) WsCancelOffer(orderID int64) error { func makeRequestInterface(channelName string, data interface{}) []interface{} { return []interface{}{0, channelName, nil, data} } + +func validateCRC32(book *orderbook.Base, token int) error { + // Order ID's need to be sub-sorted in ascending order, this needs to be + // done on the main book to ensure that we do not cut price levels out below + reOrderByID(book.Bids) + reOrderByID(book.Asks) + + // RO precision calculation is based on order ID's and amount values + var bids, asks []orderbook.Item + for i := 0; i < 25; i++ { + if i < len(book.Bids) { + bids = append(bids, book.Bids[i]) + } + if i < len(book.Asks) { + asks = append(asks, book.Asks[i]) + } + } + + // ensure '-' (negative amount) is passed back to string buffer as + // this is needed for calcs - These get swapped if funding rate + bidmod := float64(1) + if book.IsFundingRate { + bidmod = -1 + } + + askMod := float64(-1) + if book.IsFundingRate { + askMod = 1 + } + + var check strings.Builder + for i := 0; i < 25; i++ { + if i < len(bids) { + check.WriteString(strconv.FormatInt(bids[i].ID, 10)) + check.WriteString(":") + check.WriteString(strconv.FormatFloat(bidmod*bids[i].Amount, 'f', -1, 64)) + check.WriteString(":") + } + + if i < len(asks) { + check.WriteString(strconv.FormatInt(asks[i].ID, 10)) + check.WriteString(":") + check.WriteString(strconv.FormatFloat(askMod*asks[i].Amount, 'f', -1, 64)) + check.WriteString(":") + } + } + + checksumStr := strings.TrimSuffix(check.String(), ":") + checksum := crc32.ChecksumIEEE([]byte(checksumStr)) + if checksum == uint32(token) { + return nil + } + return fmt.Errorf("invalid checksum for %s %s: calculated [%d] does not match [%d]", + book.AssetType, + book.Pair, + checksum, + uint32(token)) +} + +// reOrderByID sub sorts orderbook items by its corresponding ID when price +// levels are the same. TODO: Deprecate and shift to buffer level insertion +// based off ascending ID. +func reOrderByID(depth []orderbook.Item) { +subSort: + for x := 0; x < len(depth); { + var subset []orderbook.Item + // Traverse forward elements + for y := x + 1; y < len(depth); y++ { + if depth[x].Price == depth[y].Price && + // Period matching is for funding rates, this was undocumented + // but these need to be matched with price for the correct ID + // alignment + depth[x].Period == depth[y].Period { + // Append element to subset when price match occurs + subset = append(subset, depth[y]) + // Traverse next + continue + } + if len(subset) != 0 { + // Append root element + subset = append(subset, depth[x]) + // Sort IDs by ascending + sort.Slice(subset, func(i, j int) bool { + return subset[i].ID < subset[j].ID + }) + // Re-align elements with sorted ID subset + for z := range subset { + depth[x+z] = subset[z] + } + } + // When price is not matching change checked element to root + x = y + continue subSort + } + break + } +} diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index 7e3e1cb7..8dcaa44e 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -199,6 +199,7 @@ func (b *Bitfinex) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: b.GenerateDefaultSubscriptions, Features: &b.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, UpdateEntriesByID: true, }) if err != nil { @@ -385,9 +386,15 @@ func (b *Bitfinex) FetchOrderbook(p currency.Pair, assetType asset.Item) (*order // UpdateOrderbook updates and returns the orderbook for a currency pair func (b *Bitfinex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + o := &orderbook.Base{ + ExchangeName: b.Name, + Pair: p, + AssetType: assetType, + NotAggregated: true} + fPair, err := b.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return o, err } b.appendOptionalDelimiter(&fPair) var prefix = "t" @@ -395,30 +402,46 @@ func (b *Bitfinex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orde prefix = "f" } - orderbookNew, err := b.GetOrderbook(prefix+fPair.String(), "P0", 100) + orderbookNew, err := b.GetOrderbook(prefix+fPair.String(), "R0", 100) if err != nil { - return nil, err + return o, err } - var o orderbook.Base - for x := range orderbookNew.Asks { - o.Asks = append(o.Asks, orderbook.Item{ - Price: orderbookNew.Asks[x].Price, - Amount: orderbookNew.Asks[x].Amount, - }) + if assetType == asset.MarginFunding { + o.IsFundingRate = true + for x := range orderbookNew.Asks { + o.Asks = append(o.Asks, orderbook.Item{ + ID: orderbookNew.Asks[x].OrderID, + Price: orderbookNew.Asks[x].Rate, + Amount: orderbookNew.Asks[x].Amount, + Period: int64(orderbookNew.Asks[x].Period), + }) + } + for x := range orderbookNew.Bids { + o.Bids = append(o.Bids, orderbook.Item{ + ID: orderbookNew.Bids[x].OrderID, + Price: orderbookNew.Bids[x].Rate, + Amount: orderbookNew.Bids[x].Amount, + Period: int64(orderbookNew.Bids[x].Period), + }) + } + } else { + for x := range orderbookNew.Asks { + o.Asks = append(o.Asks, orderbook.Item{ + ID: orderbookNew.Asks[x].OrderID, + Price: orderbookNew.Asks[x].Price, + Amount: orderbookNew.Asks[x].Amount, + }) + } + for x := range orderbookNew.Bids { + o.Bids = append(o.Bids, orderbook.Item{ + ID: orderbookNew.Bids[x].OrderID, + Price: orderbookNew.Bids[x].Price, + Amount: orderbookNew.Bids[x].Amount, + }) + } } - for x := range orderbookNew.Bids { - o.Bids = append(o.Bids, orderbook.Item{ - Price: orderbookNew.Bids[x].Price, - Amount: orderbookNew.Bids[x].Amount, - }) - } - - o.Pair = fPair - o.ExchangeName = b.Name - o.AssetType = assetType - err = o.Process() if err != nil { return nil, err diff --git a/exchanges/bitflyer/bitflyer_wrapper.go b/exchanges/bitflyer/bitflyer_wrapper.go index dee62eb1..8f33e8e8 100644 --- a/exchanges/bitflyer/bitflyer_wrapper.go +++ b/exchanges/bitflyer/bitflyer_wrapper.go @@ -250,33 +250,33 @@ func (b *Bitflyer) FetchOrderbook(p currency.Pair, assetType asset.Item) (*order // UpdateOrderbook updates and returns the orderbook for a currency pair func (b *Bitflyer) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ExchangeName: b.Name, Pair: p, AssetType: assetType} + fPair, err := b.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } - orderBook := new(orderbook.Base) - orderbookNew, err := b.GetOrderBook(b.CheckFXString(fPair).String()) if err != nil { - return orderBook, err + return book, err } for x := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Price: orderbookNew.Asks[x].Price, Amount: orderbookNew.Asks[x].Size}) + book.Asks = append(book.Asks, orderbook.Item{ + Price: orderbookNew.Asks[x].Price, + Amount: orderbookNew.Asks[x].Size}) } for x := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Price: orderbookNew.Bids[x].Price, Amount: orderbookNew.Bids[x].Size}) + book.Bids = append(book.Bids, orderbook.Item{ + Price: orderbookNew.Bids[x].Price, + Amount: orderbookNew.Bids[x].Size}) } - orderBook.Pair = fPair - orderBook.ExchangeName = b.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } return orderbook.Get(b.Name, fPair, assetType) diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index b7dbe7c0..76a2c69f 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -242,16 +242,16 @@ func (b *Bithumb) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderb // UpdateOrderbook updates and returns the orderbook for a currency pair func (b *Bithumb) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { - orderBook := new(orderbook.Base) + book := &orderbook.Base{ExchangeName: b.Name, Pair: p, AssetType: assetType} curr := p.Base.String() orderbookNew, err := b.GetOrderBook(curr) if err != nil { - return orderBook, err + return book, err } for i := range orderbookNew.Data.Bids { - orderBook.Bids = append(orderBook.Bids, + book.Bids = append(book.Bids, orderbook.Item{ Amount: orderbookNew.Data.Bids[i].Quantity, Price: orderbookNew.Data.Bids[i].Price, @@ -259,22 +259,17 @@ func (b *Bithumb) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*order } for i := range orderbookNew.Data.Asks { - orderBook.Asks = append(orderBook.Asks, + book.Asks = append(book.Asks, orderbook.Item{ Amount: orderbookNew.Data.Asks[i].Quantity, Price: orderbookNew.Data.Asks[i].Price, }) } - orderBook.Pair = p - orderBook.ExchangeName = b.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(b.Name, p, assetType) } diff --git a/exchanges/bitmex/bitmex_test.go b/exchanges/bitmex/bitmex_test.go index 828e4c9a..17a83676 100644 --- a/exchanges/bitmex/bitmex_test.go +++ b/exchanges/bitmex/bitmex_test.go @@ -868,9 +868,12 @@ func TestWSOrderbookHandling(t *testing.T) { ] }`) err = b.wsHandleData(pressXToJSON) - if err != nil { + if err != nil && err.Error() != "perpetualcontract ETHUSD update cannot be deleted id: 17999995000 not found" { t.Error(err) } + if err == nil { + t.Error("expecting error") + } } func TestWSDeleveragePositionUpdateHandling(t *testing.T) { diff --git a/exchanges/bitmex/bitmex_websocket.go b/exchanges/bitmex/bitmex_websocket.go index e56931a1..3b0909c6 100644 --- a/exchanges/bitmex/bitmex_websocket.go +++ b/exchanges/bitmex/bitmex_websocket.go @@ -491,27 +491,29 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, p currency. switch action { case bitmexActionInitialData: - var newOrderBook orderbook.Base + var book orderbook.Base for i := range data { - if strings.EqualFold(data[i].Side, order.Sell.String()) { - newOrderBook.Asks = append(newOrderBook.Asks, orderbook.Item{ - Price: data[i].Price, - Amount: float64(data[i].Size), - ID: data[i].ID, - }) - continue - } - newOrderBook.Bids = append(newOrderBook.Bids, orderbook.Item{ + item := orderbook.Item{ Price: data[i].Price, Amount: float64(data[i].Size), ID: data[i].ID, - }) + } + switch { + case strings.EqualFold(data[i].Side, order.Sell.String()): + book.Asks = append(book.Asks, item) + case strings.EqualFold(data[i].Side, order.Buy.String()): + book.Bids = append(book.Bids, item) + default: + return fmt.Errorf("could not process websocket orderbook update, order side could not be matched for %s", + data[i].Side) + } } - newOrderBook.AssetType = a - newOrderBook.Pair = p - newOrderBook.ExchangeName = b.Name + orderbook.Reverse(book.Asks) // Reverse asks for correct alignment + book.AssetType = a + book.Pair = p + book.ExchangeName = b.Name - err := b.Websocket.Orderbook.LoadSnapshot(&newOrderBook) + err := b.Websocket.Orderbook.LoadSnapshot(&book) if err != nil { return fmt.Errorf("bitmex_websocket.go process orderbook error - %s", err) @@ -519,17 +521,16 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, p currency. default: var asks, bids []orderbook.Item for i := range data { - if strings.EqualFold(data[i].Side, "Sell") { - asks = append(asks, orderbook.Item{ - Amount: float64(data[i].Size), - ID: data[i].ID, - }) - continue - } - bids = append(bids, orderbook.Item{ + nItem := orderbook.Item{ + Price: data[i].Price, Amount: float64(data[i].Size), ID: data[i].ID, - }) + } + if strings.EqualFold(data[i].Side, "Sell") { + asks = append(asks, nItem) + continue + } + bids = append(bids, nItem) } err := b.Websocket.Orderbook.Update(&buffer.Update{ @@ -537,7 +538,7 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, p currency. Asks: asks, Pair: p, Asset: a, - Action: action, + Action: buffer.Action(action), }) if err != nil { return err @@ -548,24 +549,6 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, p currency. // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (b *Bitmex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) { - assets := b.GetAssetTypes() - var allPairs currency.Pairs - var associatedAssets []asset.Item - for x := range assets { - contracts, err := b.GetEnabledPairs(assets[x]) - if err != nil { - return nil, err - } - for y := range contracts { - allPairs = allPairs.Add(contracts[y]) - associatedAssets = append(associatedAssets, assets[x]) - } - } - - if len(allPairs) != len(associatedAssets) { - return nil, fmt.Errorf("%s generate default subscriptions: pair and asset type len mismatch", b.Name) - } - channels := []string{bitmexWSOrderbookL2, bitmexWSTrade} subscriptions := []stream.ChannelSubscription{ { @@ -573,13 +556,24 @@ func (b *Bitmex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, e }, } - for i := range channels { - for j := range allPairs { - subscriptions = append(subscriptions, stream.ChannelSubscription{ - Channel: channels[i] + ":" + allPairs[j].String(), - Currency: allPairs[j], - Asset: associatedAssets[j], - }) + assets := b.GetAssetTypes() + for x := range assets { + contracts, err := b.GetEnabledPairs(assets[x]) + if err != nil { + return nil, err + } + for y := range contracts { + for z := range channels { + if assets[x] == asset.Index && channels[z] == bitmexWSOrderbookL2 { + // There are no L2 orderbook for index assets + continue + } + subscriptions = append(subscriptions, stream.ChannelSubscription{ + Channel: channels[z] + ":" + contracts[y].String(), + Currency: contracts[y], + Asset: assets[x], + }) + } } } return subscriptions, nil diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index e103158d..b0a0e3f5 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -155,6 +155,7 @@ func (b *Bitmex) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: b.GenerateDefaultSubscriptions, Features: &b.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, UpdateEntriesByID: true, }) if err != nil { @@ -332,46 +333,50 @@ func (b *Bitmex) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbo // UpdateOrderbook updates and returns the orderbook for a currency pair func (b *Bitmex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ + ExchangeName: b.Name, + Pair: p, + AssetType: assetType, + } + if assetType == asset.Index { - return nil, common.ErrFunctionNotSupported + return book, common.ErrFunctionNotSupported } fpair, err := b.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } orderbookNew, err := b.GetOrderbook(OrderBookGetL2Params{ Symbol: fpair.String(), Depth: 500}) if err != nil { - return nil, err + return book, err } - orderBook := new(orderbook.Base) for i := range orderbookNew { - if strings.EqualFold(orderbookNew[i].Side, order.Sell.String()) { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + switch { + case strings.EqualFold(orderbookNew[i].Side, order.Sell.String()): + book.Asks = append(book.Asks, orderbook.Item{ Amount: float64(orderbookNew[i].Size), Price: orderbookNew[i].Price}) - continue - } - if strings.EqualFold(orderbookNew[i].Side, order.Buy.String()) { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + case strings.EqualFold(orderbookNew[i].Side, order.Buy.String()): + book.Bids = append(book.Bids, orderbook.Item{ Amount: float64(orderbookNew[i].Size), Price: orderbookNew[i].Price}) + default: + return book, + fmt.Errorf("could not process orderbook, order side [%s] could not be matched", + orderbookNew[i].Side) } } + orderbook.Reverse(book.Asks) - orderBook.Pair = p - orderBook.ExchangeName = b.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(b.Name, p, assetType) } diff --git a/exchanges/bitstamp/bitstamp_test.go b/exchanges/bitstamp/bitstamp_test.go index 389164e2..e9b7bf19 100644 --- a/exchanges/bitstamp/bitstamp_test.go +++ b/exchanges/bitstamp/bitstamp_test.go @@ -639,7 +639,7 @@ func TestWsOrderbook(t *testing.T) { } func TestWsOrderbook2(t *testing.T) { - pressXToJSON := []byte(`{"data": {"timestamp": "1580336904", "microtimestamp": "1580336904228758", "bids": [["9317.09", "2.67910000"], ["9296.14", "0.00000000"], ["9294.36", "0.04967421"]], "asks": [["9333.85", "0.00000000"], ["9339.20", "0.20000000"], ["9339.66", "0.00000000"], ["9342.63", "4.90000000"], ["9343.66", "0.00000000"], ["9343.76", "2.87275947"]]}, "event": "data", "channel": "diff_order_book_btcusd"}`) + pressXToJSON := []byte(`{"data":{"timestamp":"1606965727","microtimestamp":"1606965727403931","bids":[["19133.97","0.01000000"],["19131.58","0.39200000"],["19131.18","0.69581810"],["19131.17","0.48139054"],["19129.72","0.48164130"],["19129.71","0.65400000"],["19128.80","1.04500000"],["19128.59","0.65400000"],["19128.12","0.00259236"],["19127.81","0.19784245"],["19126.66","1.04500000"],["19125.74","0.26020000"],["19124.68","0.22000000"],["19122.01","0.39777840"],["19122.00","1.04600000"],["19121.27","0.16741000"],["19121.10","1.56390000"],["19119.90","1.60000000"],["19119.58","0.15593238"],["19117.70","1.14600000"],["19115.36","2.61300000"],["19114.60","1.19570000"],["19113.88","0.07500000"],["19113.86","0.15668522"],["19113.70","1.00000000"],["19113.69","1.60000000"],["19112.27","0.00166667"],["19111.00","0.15464628"],["19108.80","0.70000000"],["19108.77","0.16300000"],["19108.38","1.10000000"],["19107.53","0.10000000"],["19106.83","0.21377991"],["19106.78","3.45938881"],["19104.24","1.30000000"],["19100.81","0.00166667"],["19100.21","0.49770000"],["19099.54","2.40971961"],["19099.53","0.51223189"],["19097.40","1.55000000"],["19095.55","2.61300000"],["19092.94","0.27402906"],["19092.20","1.60000000"],["19089.36","0.00166667"],["19086.32","1.62000000"],["19085.23","1.65670000"],["19080.88","1.40000000"],["19075.45","1.16000000"],["19071.24","1.20000000"],["19065.09","1.51000000"],["19059.38","1.57000000"],["19058.11","0.37393556"],["19052.98","0.01000000"],["19052.90","0.33000000"],["19049.55","6.89000000"],["19047.61","6.03623432"],["19030.16","16.60260000"],["19026.76","23.90800000"],["19024.78","2.16656212"],["19022.11","0.02628500"],["19020.37","6.03000000"],["19000.00","0.00132020"],["18993.52","2.22000000"],["18979.21","6.03240000"],["18970.20","0.01500000"],["18969.14","7.42000000"],["18956.46","6.03240000"],["18950.22","42.37500000"],["18950.00","0.00132019"],["18949.94","0.52650000"],["18946.00","0.00791700"],["18933.74","6.03240000"],["18932.21","8.21000000"],["18926.99","0.00150000"],["18926.98","0.02641500"],["18925.00","0.02000000"],["18909.99","0.00133000"],["18908.47","7.15000000"],["18905.99","0.00133000"],["18905.20","0.00190000"],["18901.00","0.10000000"],["18900.67","0.24430000"],["18900.00","7.56529933"],["18895.99","0.00178450"],["18890.00","0.10000000"],["18889.90","0.10580000"],["18888.00","0.00362564"],["18887.00","4.00000000"],["18881.62","0.20583403"],["18880.08","5.72198740"],["18880.05","8.33480000"],["18879.09","7.33000000"],["18875.99","0.00132450"],["18875.00","0.02000000"],["18873.47","0.25934200"],["18871.99","0.00132600"],["18870.93","0.36463225"],["18864.10","43.56800000"],["18853.11","0.00540000"],["18850.01","0.38925549"]],"asks":[["19141.75","0.39300000"],["19141.78","0.10204700"],["19143.05","1.99685100"],["19143.08","0.05777900"],["19143.09","1.60700800"],["19143.10","0.48282909"],["19143.36","0.11250000"],["19144.06","0.26040000"],["19145.97","0.65400000"],["19146.02","0.22000000"],["19146.56","0.45061841"],["19147.45","0.15877831"],["19148.92","0.70431840"],["19148.93","0.78400000"],["19150.32","0.78400000"],["19151.55","0.07500000"],["19152.64","3.11400000"],["19153.32","1.04600000"],["19153.84","0.15626630"],["19155.57","3.10000000"],["19156.40","0.13438213"],["19156.92","0.16300000"],["19157.54","1.38970000"],["19158.18","0.00166667"],["19158.41","0.15317000"],["19158.78","0.15888798"],["19160.14","0.10000000"],["19160.34","1.60000000"],["19160.70","1.21590000"],["19162.17","0.00352761"],["19162.67","1.04500000"],["19163.61","0.15000000"],["19163.80","1.18050000"],["19164.62","0.86919692"],["19165.36","0.15674424"],["19166.75","1.40000000"],["19167.47","2.61300000"],["19169.68","0.00166667"],["19171.08","0.15452025"],["19171.69","0.54308236"],["19172.12","0.49000000"],["19173.47","1.34000000"],["19174.49","1.07436448"],["19175.37","0.01200000"],["19178.25","1.50000000"],["19178.80","0.49770000"],["19181.18","0.00166667"],["19182.75","1.77297176"],["19182.76","2.61099999"],["19183.03","1.20000000"],["19185.17","6.00352761"],["19189.56","0.05797137"],["19189.72","1.17000000"],["19193.94","1.60000000"],["19197.15","0.26961100"],["19200.00","0.03107838"],["19200.06","1.29000000"],["19202.73","1.65670000"],["19206.06","1.30000000"],["19208.19","6.00352761"],["19209.00","0.00132021"],["19210.70","1.20000000"],["19213.77","0.02615500"],["19217.40","8.50000000"],["19217.57","1.29000000"],["19222.61","1.19000000"],["19230.00","0.00193480"],["19231.24","6.00000000"],["19237.91","6.89152278"],["19240.13","6.90000000"],["19242.16","0.00336000"],["19243.38","0.00299103"],["19244.48","14.79300000"],["19248.25","0.01300000"],["19250.00","1.95802492"],["19251.00","0.45000000"],["19254.20","0.00366102"],["19254.32","6.00000000"],["19259.00","0.00131022"],["19266.43","0.00917191"],["19267.63","0.05000000"],["19267.79","7.10000000"],["19268.72","16.60260000"],["19277.42","6.00000000"],["19286.64","0.00916230"],["19295.49","7.77000000"],["19300.00","0.19668172"],["19306.00","0.06000000"],["19307.00","3.00000000"],["19307.40","0.19000000"],["19309.00","0.00262046"],["19310.33","0.02602500"],["19319.33","0.00213688"],["19320.00","0.00171242"],["19321.02","48.47300000"],["19322.74","0.00250000"],["19324.00","0.36983571"],["19325.54","0.02314521"],["19325.73","7.22000000"],["19326.50","0.00915272"]]},"channel":"order_book_btcusd","event":"data"}`) err := b.wsHandleData(pressXToJSON) if err != nil { t.Error(err) diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index 550cfe65..8ee4e6c0 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -165,6 +165,7 @@ func (b *Bitstamp) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: b.generateDefaultSubscriptions, Features: &b.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, }) if err != nil { return err @@ -315,40 +316,34 @@ func (b *Bitstamp) FetchOrderbook(p currency.Pair, assetType asset.Item) (*order // UpdateOrderbook updates and returns the orderbook for a currency pair func (b *Bitstamp) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ExchangeName: b.Name, Pair: p, AssetType: assetType} fPair, err := b.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } - orderBook := new(orderbook.Base) orderbookNew, err := b.GetOrderbook(fPair.String()) if err != nil { - return orderBook, err + return book, err } for x := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Amount: orderbookNew.Bids[x].Amount, Price: orderbookNew.Bids[x].Price, }) } for x := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + book.Asks = append(book.Asks, orderbook.Item{ Amount: orderbookNew.Asks[x].Amount, Price: orderbookNew.Asks[x].Price, }) } - - orderBook.Pair = fPair - orderBook.ExchangeName = b.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(b.Name, fPair, assetType) } diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index c3801c9c..e48baf91 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -328,9 +328,10 @@ func (b *Bittrex) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderb // UpdateOrderbook updates and returns the orderbook for a currency pair func (b *Bittrex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ExchangeName: b.Name, Pair: p, AssetType: assetType} fpair, err := b.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } orderbookNew, err := b.GetOrderbook(fpair.String()) @@ -338,9 +339,8 @@ func (b *Bittrex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*order return nil, err } - orderBook := new(orderbook.Base) for x := range orderbookNew.Result.Buy { - orderBook.Bids = append(orderBook.Bids, + book.Bids = append(book.Bids, orderbook.Item{ Amount: orderbookNew.Result.Buy[x].Quantity, Price: orderbookNew.Result.Buy[x].Rate, @@ -349,23 +349,17 @@ func (b *Bittrex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*order } for x := range orderbookNew.Result.Sell { - orderBook.Asks = append(orderBook.Asks, + book.Asks = append(book.Asks, orderbook.Item{ Amount: orderbookNew.Result.Sell[x].Quantity, Price: orderbookNew.Result.Sell[x].Rate, }, ) } - - orderBook.Pair = p - orderBook.ExchangeName = b.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(b.Name, p, assetType) } diff --git a/exchanges/btcmarkets/btcmarkets.go b/exchanges/btcmarkets/btcmarkets.go index 4e6b213b..e108befb 100644 --- a/exchanges/btcmarkets/btcmarkets.go +++ b/exchanges/btcmarkets/btcmarkets.go @@ -670,7 +670,7 @@ func (b *BTCMarkets) GetBatchTrades(ids []string) (BatchTradeResponse, error) { request.Auth) } -// CancelBatchOrders cancels given ids +// CancelBatch cancels given ids func (b *BTCMarkets) CancelBatch(ids []string) (BatchCancelResponse, error) { var resp BatchCancelResponse marketIDs := strings.Join(ids, ",") diff --git a/exchanges/btcmarkets/btcmarkets_websocket.go b/exchanges/btcmarkets/btcmarkets_websocket.go index f3afa7c6..4c79f837 100644 --- a/exchanges/btcmarkets/btcmarkets_websocket.go +++ b/exchanges/btcmarkets/btcmarkets_websocket.go @@ -119,7 +119,7 @@ func (b *BTCMarkets) wsHandleData(respRaw []byte) error { if ob.Snapshot { err = b.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{ Pair: p, - Bids: bids, + Bids: orderbook.SortBids(bids), // Alignment completely out sort is needed Asks: asks, LastUpdated: ob.Timestamp, AssetType: asset.Spot, diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index d7d15b16..2979d903 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -156,7 +156,7 @@ func (b *BTCMarkets) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: b.generateDefaultSubscriptions, Features: &b.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, - BufferEnabled: true, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, SortBuffer: true, }) if err != nil { @@ -351,33 +351,34 @@ func (b *BTCMarkets) FetchOrderbook(p currency.Pair, assetType asset.Item) (*ord // UpdateOrderbook updates and returns the orderbook for a currency pair func (b *BTCMarkets) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ + ExchangeName: b.Name, + Pair: p, + AssetType: assetType, NotAggregated: true} + fpair, err := b.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } tempResp, err := b.GetOrderbook(fpair.String(), 2) if err != nil { - return nil, err + return book, err } - orderBook := new(orderbook.Base) for x := range tempResp.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Amount: tempResp.Bids[x].Volume, Price: tempResp.Bids[x].Price}) } for y := range tempResp.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + book.Asks = append(book.Asks, orderbook.Item{ Amount: tempResp.Asks[y].Volume, Price: tempResp.Asks[y].Price}) } - orderBook.Pair = p - orderBook.ExchangeName = b.Name - orderBook.AssetType = assetType - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } return orderbook.Get(b.Name, p, assetType) } diff --git a/exchanges/btse/btse_test.go b/exchanges/btse/btse_test.go index f9111d4c..34b86a08 100644 --- a/exchanges/btse/btse_test.go +++ b/exchanges/btse/btse_test.go @@ -849,3 +849,19 @@ func TestGetHistoricTrades(t *testing.T) { t.Error("unexpected error") } } + +func TestOrderbookFilter(t *testing.T) { + t.Parallel() + if !b.orderbookFilter(0, 1) { + t.Fatal("incorrect filtering") + } + if !b.orderbookFilter(1, 0) { + t.Fatal("incorrect filtering") + } + if !b.orderbookFilter(0, 0) { + t.Fatal("incorrect filtering") + } + if b.orderbookFilter(1, 1) { + t.Fatal("incorrect filtering") + } +} diff --git a/exchanges/btse/btse_websocket.go b/exchanges/btse/btse_websocket.go index a62b19c7..3f937005 100644 --- a/exchanges/btse/btse_websocket.go +++ b/exchanges/btse/btse_websocket.go @@ -111,7 +111,7 @@ func (b *BTSE) wsHandleData(respRaw []byte) error { var result Result err := json.Unmarshal(respRaw, &result) if err != nil { - if strings.Contains(string(respRaw), "UNLOGIN_USER connect success") || + if strings.Contains(string(respRaw), "connect success") || strings.Contains(string(respRaw), "authenticated successfully") { return nil } else if strings.Contains(string(respRaw), "AUTHENTICATE ERROR") { @@ -223,7 +223,7 @@ func (b *BTSE) wsHandleData(respRaw []byte) error { }) } return trade.AddTradesToBuffer(b.Name, trades...) - case strings.Contains(result["topic"].(string), "orderBookApi"): + case strings.Contains(result["topic"].(string), "orderBookL2Api"): var t wsOrderBook err = json.Unmarshal(respRaw, &t) if err != nil { @@ -242,6 +242,9 @@ func (b *BTSE) wsHandleData(respRaw []byte) error { if err != nil { return err } + if b.orderbookFilter(price, amount) { + continue + } newOB.Asks = append(newOB.Asks, orderbook.Item{ Price: price, Amount: amount, @@ -258,6 +261,9 @@ func (b *BTSE) wsHandleData(respRaw []byte) error { if err != nil { return err } + if b.orderbookFilter(price, amount) { + continue + } newOB.Bids = append(newOB.Bids, orderbook.Item{ Price: price, Amount: amount, @@ -275,6 +281,7 @@ func (b *BTSE) wsHandleData(respRaw []byte) error { newOB.Pair = p newOB.AssetType = a newOB.ExchangeName = b.Name + orderbook.Reverse(newOB.Asks) // Reverse asks for correct alignment err = b.Websocket.Orderbook.LoadSnapshot(&newOB) if err != nil { return err @@ -286,9 +293,24 @@ func (b *BTSE) wsHandleData(respRaw []byte) error { return nil } +// orderbookFilter is needed on book levels from this exchange as their data +// is incorrect +func (b *BTSE) orderbookFilter(price, amount float64) bool { + // Amount filtering occurs when the amount exceeds the decimal returned. + // e.g. {"price":"1.37","size":"0.00"} currency: SFI-ETH + // Opted to not round up to 0.01 as this might skew calculations + // more than removing from the books completely. + + // Price filtering occurs when we are deep in the bid book and there are + // prices that are less than 4 decimal places + // e.g. {"price":"0.0000","size":"14219"} currency: TRX-PAX + // We cannot load a zero price and this will ruin calculations + return price == 0 || amount == 0 +} + // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (b *BTSE) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) { - var channels = []string{"orderBookApi:%s_0", "tradeHistory:%s"} + var channels = []string{"orderBookL2Api:%s_0", "tradeHistory:%s"} pairs, err := b.GetEnabledPairs(asset.Spot) if err != nil { return nil, err diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index 98649b46..99b620dc 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -185,6 +185,7 @@ func (b *BTSE) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: b.GenerateDefaultSubscriptions, Features: &b.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, }) if err != nil { return err @@ -318,32 +319,33 @@ func (b *BTSE) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbook // UpdateOrderbook updates and returns the orderbook for a currency pair func (b *BTSE) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ExchangeName: b.Name, Pair: p, AssetType: assetType} fPair, err := b.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } a, err := b.FetchOrderBook(fPair.String(), 0, 0, 0, assetType == asset.Spot) if err != nil { - return nil, err + return book, err } - orderBook := new(orderbook.Base) for x := range a.BuyQuote { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Price: a.BuyQuote[x].Price, Amount: a.BuyQuote[x].Size}) } for x := range a.SellQuote { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + book.Asks = append(book.Asks, orderbook.Item{ Price: a.SellQuote[x].Price, Amount: a.SellQuote[x].Size}) } - orderBook.Pair = p - orderBook.ExchangeName = b.Name - orderBook.AssetType = assetType - err = orderBook.Process() + orderbook.Reverse(book.Asks) // Reverse asks for correct alignment + book.Pair = p + book.ExchangeName = b.Name + book.AssetType = assetType + err = book.Process() if err != nil { - return orderBook, err + return book, err } return orderbook.Get(b.Name, p, assetType) } diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index b743e53b..e6a0778c 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -167,7 +167,7 @@ func (c *CoinbasePro) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: c.GenerateDefaultSubscriptions, Features: &c.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, - BufferEnabled: true, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, SortBuffer: true, }) if err != nil { @@ -400,35 +400,33 @@ func (c *CoinbasePro) FetchOrderbook(p currency.Pair, assetType asset.Item) (*or // UpdateOrderbook updates and returns the orderbook for a currency pair func (c *CoinbasePro) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ExchangeName: c.Name, Pair: p, AssetType: assetType} fpair, err := c.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } orderbookNew, err := c.GetOrderbook(fpair.String(), 2) if err != nil { - return nil, err + return book, err } obNew := orderbookNew.(OrderbookL1L2) - orderBook := new(orderbook.Base) for x := range obNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: obNew.Bids[x].Amount, Price: obNew.Bids[x].Price}) + book.Bids = append(book.Bids, orderbook.Item{ + Amount: obNew.Bids[x].Amount, + Price: obNew.Bids[x].Price}) } for x := range obNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: obNew.Asks[x].Amount, Price: obNew.Asks[x].Price}) + book.Asks = append(book.Asks, orderbook.Item{ + Amount: obNew.Asks[x].Amount, + Price: obNew.Asks[x].Price}) } - - orderBook.Pair = p - orderBook.ExchangeName = c.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(c.Name, p, assetType) } diff --git a/exchanges/coinbene/coinbene_types.go b/exchanges/coinbene/coinbene_types.go index dfa8c72e..1595549f 100644 --- a/exchanges/coinbene/coinbene_types.go +++ b/exchanges/coinbene/coinbene_types.go @@ -220,10 +220,10 @@ type WsOrderbookData struct { Topic string `json:"topic"` Action string `json:"action"` Data []struct { - Bids [][]string `json:"bids"` - Asks [][]string `json:"asks"` - Version int64 `json:"version"` - Timestamp int64 `json:"timestamp"` + Bids [][2]string `json:"bids"` + Asks [][2]string `json:"asks"` + Version int64 `json:"version"` + Timestamp int64 `json:"timestamp"` } `json:"data"` } diff --git a/exchanges/coinbene/coinbene_websocket.go b/exchanges/coinbene/coinbene_websocket.go index e7560842..bac636f6 100644 --- a/exchanges/coinbene/coinbene_websocket.go +++ b/exchanges/coinbene/coinbene_websocket.go @@ -233,6 +233,10 @@ func (c *Coinbene) wsHandleData(respRaw []byte) error { return err } + if len(orderBook.Data) != 1 { + return errors.New("incomplete orderbook data has been received") + } + newPair, err = c.getCurrencyFromWsTopic(assetType, orderBook.Topic) if err != nil { return err @@ -254,14 +258,24 @@ func (c *Coinbene) wsHandleData(respRaw []byte) error { }) } for j := range orderBook.Data[0].Bids { - amount, err = strconv.ParseFloat(orderBook.Data[0].Bids[j][1], 64) - if err != nil { - return err - } price, err = strconv.ParseFloat(orderBook.Data[0].Bids[j][0], 64) if err != nil { return err } + + if price == 0 { + // Last level is coming back as a float with not enough decimal + // places e.g. ["0.000","1001.95"]], + // This needs to be filtered out as this can skew orderbook + // calculations + continue + } + + amount, err = strconv.ParseFloat(orderBook.Data[0].Bids[j][1], 64) + if err != nil { + return err + } + bids = append(bids, orderbook.Item{ Amount: amount, Price: price, @@ -427,11 +441,18 @@ func (c *Coinbene) getCurrencyFromWsTopic(assetType asset.Item, channelTopic str // Subscribe sends a websocket message to receive data from the channel func (c *Coinbene) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error { + maxSubsPerHour := 240 + if len(channelsToSubscribe) > maxSubsPerHour { + return fmt.Errorf("channel subscriptions length %d exceeds coinbene's limit of %d, try reducing enabled pairs", + len(channelsToSubscribe), + maxSubsPerHour) + } + var sub WsSub sub.Operation = "subscribe" // enabling all currencies can lead to a message too large being sent // and no subscriptions being made - chanLimit := 10 + chanLimit := 15 for i := range channelsToSubscribe { if len(sub.Arguments) > chanLimit { err := c.Websocket.Conn.SendJSONMessage(sub) @@ -456,7 +477,7 @@ func (c *Coinbene) Unsubscribe(channelToUnsubscribe []stream.ChannelSubscription unsub.Operation = "unsubscribe" // enabling all currencies can lead to a message too large being sent // and no unsubscribes being made - chanLimit := 10 + chanLimit := 15 for i := range channelToUnsubscribe { if len(unsub.Arguments) > chanLimit { err := c.Websocket.Conn.SendJSONMessage(unsub) diff --git a/exchanges/coinbene/coinbene_wrapper.go b/exchanges/coinbene/coinbene_wrapper.go index c75b2ec3..63a87971 100644 --- a/exchanges/coinbene/coinbene_wrapper.go +++ b/exchanges/coinbene/coinbene_wrapper.go @@ -182,7 +182,7 @@ func (c *Coinbene) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: c.GenerateDefaultSubscriptions, Features: &c.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, - BufferEnabled: true, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, SortBuffer: true, }) if err != nil { @@ -405,15 +405,15 @@ func (c *Coinbene) FetchOrderbook(p currency.Pair, assetType asset.Item) (*order // UpdateOrderbook updates and returns the orderbook for a currency pair func (c *Coinbene) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { - resp := new(orderbook.Base) + book := &orderbook.Base{ExchangeName: c.Name, Pair: p, AssetType: assetType} if !c.SupportsAsset(assetType) { - return nil, + return book, fmt.Errorf("%s does not support asset type %s", c.Name, assetType) } fpair, err := c.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } var tempResp Orderbook @@ -428,11 +428,8 @@ func (c *Coinbene) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orde ) } if err != nil { - return nil, err + return book, err } - resp.ExchangeName = c.Name - resp.Pair = p - resp.AssetType = assetType for x := range tempResp.Asks { item := orderbook.Item{ Price: tempResp.Asks[x].Price, @@ -441,7 +438,7 @@ func (c *Coinbene) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orde if assetType == asset.PerpetualSwap { item.OrderCount = tempResp.Asks[x].Count } - resp.Asks = append(resp.Asks, item) + book.Asks = append(book.Asks, item) } for x := range tempResp.Bids { item := orderbook.Item{ @@ -451,11 +448,11 @@ func (c *Coinbene) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orde if assetType == asset.PerpetualSwap { item.OrderCount = tempResp.Bids[x].Count } - resp.Bids = append(resp.Bids, item) + book.Bids = append(book.Bids, item) } - err = resp.Process() + err = book.Process() if err != nil { - return nil, err + return book, err } return orderbook.Get(c.Name, p, assetType) } diff --git a/exchanges/coinut/coinut_test.go b/exchanges/coinut/coinut_test.go index f944aeb3..9a31fa35 100644 --- a/exchanges/coinut/coinut_test.go +++ b/exchanges/coinut/coinut_test.go @@ -650,9 +650,9 @@ func TestGetNonce(t *testing.T) { func TestWsOrderbook(t *testing.T) { pressXToJSON := []byte(`{ "buy": - [ { "count": 7, "price": "750.00000000", "qty": "0.07000000" }, - { "count": 1, "price": "751.00000000", "qty": "0.01000000" }, - { "count": 1, "price": "751.34500000", "qty": "0.01000000" } ], + [ { "count": 1, "price": "751.34500000", "qty": "0.01000000" }, + { "count": 1, "price": "751.00000000", "qty": "0.01000000" }, + { "count": 7, "price": "750.00000000", "qty": "0.07000000" } ], "sell": [ { "count": 6, "price": "750.58100000", "qty": "0.06000000" }, { "count": 1, "price": "750.58200000", "qty": "0.01000000" }, diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index 81a67c62..3fd1278f 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -150,7 +150,7 @@ func (c *COINUT) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: c.GenerateDefaultSubscriptions, Features: &c.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, - BufferEnabled: true, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, SortBuffer: true, SortBufferByUpdateIDs: true, }) @@ -453,44 +453,42 @@ func (c *COINUT) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbo // UpdateOrderbook updates and returns the orderbook for a currency pair func (c *COINUT) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { - orderBook := new(orderbook.Base) + book := &orderbook.Base{ExchangeName: c.Name, Pair: p, AssetType: assetType} err := c.loadInstrumentsIfNotLoaded() if err != nil { - return orderBook, err + return book, err } fpair, err := c.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } instID := c.instrumentMap.LookupID(fpair.String()) if instID == 0 { - return orderBook, errLookupInstrumentID + return book, errLookupInstrumentID } orderbookNew, err := c.GetInstrumentOrderbook(instID, 200) if err != nil { - return orderBook, err + return book, err } for x := range orderbookNew.Buy { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: orderbookNew.Buy[x].Quantity, Price: orderbookNew.Buy[x].Price}) + book.Bids = append(book.Bids, orderbook.Item{ + Amount: orderbookNew.Buy[x].Quantity, + Price: orderbookNew.Buy[x].Price}) } for x := range orderbookNew.Sell { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: orderbookNew.Sell[x].Quantity, Price: orderbookNew.Sell[x].Price}) + book.Asks = append(book.Asks, orderbook.Item{ + Amount: orderbookNew.Sell[x].Quantity, + Price: orderbookNew.Sell[x].Price}) } - - orderBook.Pair = p - orderBook.ExchangeName = c.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(c.Name, p, assetType) } diff --git a/exchanges/exmo/exmo_wrapper.go b/exchanges/exmo/exmo_wrapper.go index 0b423f9f..322fa746 100644 --- a/exchanges/exmo/exmo_wrapper.go +++ b/exchanges/exmo/exmo_wrapper.go @@ -235,25 +235,31 @@ func (e *EXMO) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbook // UpdateOrderbook updates and returns the orderbook for a currency pair func (e *EXMO) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + callingBook := &orderbook.Base{ExchangeName: e.Name, Pair: p, AssetType: assetType} enabledPairs, err := e.GetEnabledPairs(assetType) if err != nil { - return nil, err + return callingBook, err } pairsCollated, err := e.FormatExchangeCurrencies(enabledPairs, assetType) if err != nil { - return nil, err + return callingBook, err } result, err := e.GetOrderbook(pairsCollated) if err != nil { - return nil, err + return callingBook, err } for i := range enabledPairs { + book := &orderbook.Base{ + ExchangeName: e.Name, + Pair: enabledPairs[i], + AssetType: assetType} + curr, err := e.FormatExchangeCurrency(enabledPairs[i], assetType) if err != nil { - return nil, err + return callingBook, err } data, ok := result[curr.String()] @@ -261,20 +267,19 @@ func (e *EXMO) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderboo continue } - orderBook := new(orderbook.Base) for y := range data.Ask { var price, amount float64 price, err = strconv.ParseFloat(data.Ask[y][0], 64) if err != nil { - return orderBook, err + return book, err } amount, err = strconv.ParseFloat(data.Ask[y][1], 64) if err != nil { - return orderBook, err + return book, err } - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + book.Asks = append(book.Asks, orderbook.Item{ Price: price, Amount: amount, }) @@ -284,27 +289,23 @@ func (e *EXMO) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderboo var price, amount float64 price, err = strconv.ParseFloat(data.Bid[y][0], 64) if err != nil { - return orderBook, err + return book, err } amount, err = strconv.ParseFloat(data.Bid[y][1], 64) if err != nil { - return orderBook, err + return book, err } - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Price: price, Amount: amount, }) } - orderBook.Pair = enabledPairs[i] - orderBook.ExchangeName = e.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } } return orderbook.Get(e.Name, p, assetType) diff --git a/exchanges/ftx/ftx.go b/exchanges/ftx/ftx.go index f170801f..a04e18cb 100644 --- a/exchanges/ftx/ftx.go +++ b/exchanges/ftx/ftx.go @@ -123,7 +123,13 @@ func (f *FTX) GetOrderbook(marketName string, depth int64) (OrderbookData, error result := struct { Data TempOBData `json:"result"` }{} - strDepth := strconv.FormatInt(depth, 10) + + strDepth := "20" // If we send a zero value we get zero asks from the + // endpoint + if depth != 0 { + strDepth = strconv.FormatInt(depth, 10) + } + var resp OrderbookData err := f.SendHTTPRequest(fmt.Sprintf(ftxAPIURL+getOrderbook, marketName, strDepth), &result) if err != nil { @@ -131,13 +137,15 @@ func (f *FTX) GetOrderbook(marketName string, depth int64) (OrderbookData, error } resp.MarketName = marketName for x := range result.Data.Asks { - resp.Asks = append(resp.Asks, OData{Price: result.Data.Asks[x][0], - Size: result.Data.Asks[x][1], + resp.Asks = append(resp.Asks, OData{ + Price: result.Data.Asks[x][0], + Size: result.Data.Asks[x][1], }) } for y := range result.Data.Bids { - resp.Bids = append(resp.Bids, OData{Price: result.Data.Bids[y][0], - Size: result.Data.Bids[y][1], + resp.Bids = append(resp.Bids, OData{ + Price: result.Data.Bids[y][0], + Size: result.Data.Bids[y][1], }) } return resp, nil diff --git a/exchanges/ftx/ftx_wrapper.go b/exchanges/ftx/ftx_wrapper.go index 53b0d6df..afe8cf98 100644 --- a/exchanges/ftx/ftx_wrapper.go +++ b/exchanges/ftx/ftx_wrapper.go @@ -177,6 +177,7 @@ func (f *FTX) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: f.GenerateDefaultSubscriptions, Features: &f.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, }) if err != nil { return err @@ -332,31 +333,28 @@ func (f *FTX) FetchOrderbook(currency currency.Pair, assetType asset.Item) (*ord // UpdateOrderbook updates and returns the orderbook for a currency pair func (f *FTX) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { - orderBook := new(orderbook.Base) + book := &orderbook.Base{ExchangeName: f.Name, Pair: p, AssetType: assetType} formattedPair, err := f.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } - tempResp, err := f.GetOrderbook(formattedPair.String(), 0) + tempResp, err := f.GetOrderbook(formattedPair.String(), 100) if err != nil { - return orderBook, err + return book, err } for x := range tempResp.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Amount: tempResp.Bids[x].Size, Price: tempResp.Bids[x].Price}) } for y := range tempResp.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + book.Asks = append(book.Asks, orderbook.Item{ Amount: tempResp.Asks[y].Size, Price: tempResp.Asks[y].Price}) } - orderBook.Pair = p - orderBook.ExchangeName = f.Name - orderBook.AssetType = assetType - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } return orderbook.Get(f.Name, p, assetType) } diff --git a/exchanges/gateio/gateio_types.go b/exchanges/gateio/gateio_types.go index 8012dc4d..051ba8e7 100644 --- a/exchanges/gateio/gateio_types.go +++ b/exchanges/gateio/gateio_types.go @@ -525,3 +525,10 @@ type TradeHistoryEntry struct { TradeID string `json:"tradeID"` Type string `json:"type"` } + +// wsOrderbook defines a websocket orderbook +type wsOrderbook struct { + Asks [][]string `json:"asks"` + Bids [][]string `json:"bids"` + ID int64 `json:"id"` +} diff --git a/exchanges/gateio/gateio_websocket.go b/exchanges/gateio/gateio_websocket.go index 3ee4c8a7..2871bf4f 100644 --- a/exchanges/gateio/gateio_websocket.go +++ b/exchanges/gateio/gateio_websocket.go @@ -297,7 +297,8 @@ func (g *Gateio) wsHandleData(respRaw []byte) error { case strings.Contains(result.Method, "depth"): var IsSnapshot bool var c string - var data = make(map[string][][]string) + var data wsOrderbook + err = json.Unmarshal(result.Params[0], &IsSnapshot) if err != nil { return err @@ -314,42 +315,29 @@ func (g *Gateio) wsHandleData(respRaw []byte) error { } var asks, bids []orderbook.Item - askData, askOk := data["asks"] - for i := range askData { - var amount, price float64 - amount, err = strconv.ParseFloat(askData[i][1], 64) + var amount, price float64 + for i := range data.Asks { + amount, err = strconv.ParseFloat(data.Asks[i][1], 64) if err != nil { return err } - price, err = strconv.ParseFloat(askData[i][0], 64) + price, err = strconv.ParseFloat(data.Asks[i][0], 64) if err != nil { return err } - asks = append(asks, orderbook.Item{ - Amount: amount, - Price: price, - }) + asks = append(asks, orderbook.Item{Amount: amount, Price: price}) } - bidData, bidOk := data["bids"] - for i := range bidData { - var amount, price float64 - amount, err = strconv.ParseFloat(bidData[i][1], 64) + for i := range data.Bids { + amount, err = strconv.ParseFloat(data.Bids[i][1], 64) if err != nil { return err } - price, err = strconv.ParseFloat(bidData[i][0], 64) + price, err = strconv.ParseFloat(data.Bids[i][0], 64) if err != nil { return err } - bids = append(bids, orderbook.Item{ - Amount: amount, - Price: price, - }) - } - - if !askOk && !bidOk { - g.Websocket.DataHandler <- errors.New("gatio websocket error - cannot access ask or bid data") + bids = append(bids, orderbook.Item{Amount: amount, Price: price}) } var p currency.Pair @@ -359,14 +347,6 @@ func (g *Gateio) wsHandleData(respRaw []byte) error { } if IsSnapshot { - if !askOk { - g.Websocket.DataHandler <- errors.New("gatio websocket error - cannot access ask data") - } - - if !bidOk { - g.Websocket.DataHandler <- errors.New("gatio websocket error - cannot access bid data") - } - var newOrderBook orderbook.Base newOrderBook.Asks = asks newOrderBook.Bids = bids diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 9f6615f4..1973a73c 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -164,7 +164,7 @@ func (g *Gateio) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: g.GenerateDefaultSubscriptions, Features: &g.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, - BufferEnabled: true, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, }) if err != nil { return err @@ -278,40 +278,34 @@ func (g *Gateio) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbo // UpdateOrderbook updates and returns the orderbook for a currency pair func (g *Gateio) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { - orderBook := new(orderbook.Base) + book := &orderbook.Base{ExchangeName: g.Name, Pair: p, AssetType: assetType} curr, err := g.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } orderbookNew, err := g.GetOrderbook(curr.String()) if err != nil { - return orderBook, err + return book, err } for x := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Amount: orderbookNew.Bids[x].Amount, Price: orderbookNew.Bids[x].Price, }) } for x := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + book.Asks = append(book.Asks, orderbook.Item{ Amount: orderbookNew.Asks[x].Amount, Price: orderbookNew.Asks[x].Price, }) } - - orderBook.Pair = p - orderBook.ExchangeName = g.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(g.Name, p, assetType) } diff --git a/exchanges/gemini/gemini_websocket.go b/exchanges/gemini/gemini_websocket.go index 3953cc1b..157b4f8d 100644 --- a/exchanges/gemini/gemini_websocket.go +++ b/exchanges/gemini/gemini_websocket.go @@ -389,6 +389,8 @@ func (g *Gemini) wsProcessUpdate(result WsMarketUpdateResponse, pair currency.Pa }) } } + + orderbook.Reverse(bids) // Correct bid alignment var newOrderBook orderbook.Base newOrderBook.Asks = asks newOrderBook.Bids = bids diff --git a/exchanges/gemini/gemini_wrapper.go b/exchanges/gemini/gemini_wrapper.go index 194de7f7..ae9bde8c 100644 --- a/exchanges/gemini/gemini_wrapper.go +++ b/exchanges/gemini/gemini_wrapper.go @@ -143,7 +143,7 @@ func (g *Gemini) Setup(exch *config.ExchangeConfig) error { Connector: g.WsConnect, Features: &g.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, - BufferEnabled: true, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, SortBuffer: true, }) } @@ -294,34 +294,32 @@ func (g *Gemini) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbo // UpdateOrderbook updates and returns the orderbook for a currency pair func (g *Gemini) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ExchangeName: g.Name, Pair: p, AssetType: assetType} fPair, err := g.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } - orderBook := new(orderbook.Base) orderbookNew, err := g.GetOrderbook(fPair.String(), url.Values{}) if err != nil { - return orderBook, err + return book, err } for x := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: orderbookNew.Bids[x].Amount, Price: orderbookNew.Bids[x].Price}) + book.Bids = append(book.Bids, orderbook.Item{ + Amount: orderbookNew.Bids[x].Amount, + Price: orderbookNew.Bids[x].Price}) } for x := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: orderbookNew.Asks[x].Amount, Price: orderbookNew.Asks[x].Price}) + book.Asks = append(book.Asks, orderbook.Item{ + Amount: orderbookNew.Asks[x].Amount, + Price: orderbookNew.Asks[x].Price}) } - - orderBook.Pair = fPair - orderBook.ExchangeName = g.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(g.Name, fPair, assetType) } diff --git a/exchanges/hitbtc/hitbtc_test.go b/exchanges/hitbtc/hitbtc_test.go index 3e3001cc..07678938 100644 --- a/exchanges/hitbtc/hitbtc_test.go +++ b/exchanges/hitbtc/hitbtc_test.go @@ -661,7 +661,7 @@ func TestWsOrderbook(t *testing.T) { }, { "price": "0.054590", - "size": "0.000" + "size": "1.000" }, { "price": "0.054591", diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index 969ee536..d11111a5 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -166,7 +166,7 @@ func (h *HitBTC) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: h.GenerateDefaultSubscriptions, Features: &h.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, - BufferEnabled: true, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, SortBuffer: true, SortBufferByUpdateIDs: true, }) @@ -366,40 +366,34 @@ func (h *HitBTC) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbo // UpdateOrderbook updates and returns the orderbook for a currency pair func (h *HitBTC) UpdateOrderbook(c currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ExchangeName: h.Name, Pair: c, AssetType: assetType} fpair, err := h.FormatExchangeCurrency(c, assetType) if err != nil { - return nil, err + return book, err } orderbookNew, err := h.GetOrderbook(fpair.String(), 1000) if err != nil { - return nil, err + return book, err } - orderBook := new(orderbook.Base) for x := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Amount: orderbookNew.Bids[x].Amount, Price: orderbookNew.Bids[x].Price, }) } for x := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + book.Asks = append(book.Asks, orderbook.Item{ Amount: orderbookNew.Asks[x].Amount, Price: orderbookNew.Asks[x].Price, }) } - - orderBook.Pair = c - orderBook.ExchangeName = h.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(h.Name, c, assetType) } diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index f86d4ace..3dc9ab5f 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -164,6 +164,7 @@ func (h *HUOBI) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: h.GenerateDefaultSubscriptions, Features: &h.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, }) if err != nil { return err @@ -385,42 +386,36 @@ func (h *HUOBI) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderboo // UpdateOrderbook updates and returns the orderbook for a currency pair func (h *HUOBI) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ExchangeName: h.Name, Pair: p, AssetType: assetType} fpair, err := h.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } orderbookNew, err := h.GetDepth(OrderBookDataRequestParams{ Symbol: fpair.String(), Type: OrderBookDataRequestParamsTypeStep0, }) if err != nil { - return nil, err + return book, err } - orderBook := new(orderbook.Base) for x := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Amount: orderbookNew.Bids[x][1], Price: orderbookNew.Bids[x][0], }) } for x := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + book.Asks = append(book.Asks, orderbook.Item{ Amount: orderbookNew.Asks[x][1], Price: orderbookNew.Asks[x][0], }) } - - orderBook.Pair = p - orderBook.ExchangeName = h.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(h.Name, p, assetType) } diff --git a/exchanges/itbit/itbit_test.go b/exchanges/itbit/itbit_test.go index 3802f0a3..16595815 100644 --- a/exchanges/itbit/itbit_test.go +++ b/exchanges/itbit/itbit_test.go @@ -61,7 +61,7 @@ func TestGetTicker(t *testing.T) { func TestGetOrderbook(t *testing.T) { t.Parallel() - _, err := i.GetOrderbook("XBTSGD") + _, err := i.GetOrderbook("XBTUSD") if err != nil { t.Error("GetOrderbook() error", err) } diff --git a/exchanges/itbit/itbit_wrapper.go b/exchanges/itbit/itbit_wrapper.go index 321977c0..3e692f0b 100644 --- a/exchanges/itbit/itbit_wrapper.go +++ b/exchanges/itbit/itbit_wrapper.go @@ -186,9 +186,10 @@ func (i *ItBit) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderboo // UpdateOrderbook updates and returns the orderbook for a currency pair func (i *ItBit) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ExchangeName: i.Name, Pair: p, AssetType: assetType, NotAggregated: true} fpair, err := i.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } orderbookNew, err := i.GetOrderbook(fpair.String()) @@ -196,18 +197,17 @@ func (i *ItBit) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbo return nil, err } - orderBook := new(orderbook.Base) for x := range orderbookNew.Bids { var price, amount float64 price, err = strconv.ParseFloat(orderbookNew.Bids[x][0], 64) if err != nil { - return orderBook, err + return book, err } amount, err = strconv.ParseFloat(orderbookNew.Bids[x][1], 64) if err != nil { - return orderBook, err + return book, err } - orderBook.Bids = append(orderBook.Bids, + book.Bids = append(book.Bids, orderbook.Item{ Amount: amount, Price: price, @@ -218,28 +218,22 @@ func (i *ItBit) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbo var price, amount float64 price, err = strconv.ParseFloat(orderbookNew.Asks[x][0], 64) if err != nil { - return orderBook, err + return book, err } amount, err = strconv.ParseFloat(orderbookNew.Asks[x][1], 64) if err != nil { - return orderBook, err + return book, err } - orderBook.Asks = append(orderBook.Asks, + book.Asks = append(book.Asks, orderbook.Item{ Amount: amount, Price: price, }) } - - orderBook.Pair = p - orderBook.ExchangeName = i.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(i.Name, p, assetType) } diff --git a/exchanges/kraken/kraken.go b/exchanges/kraken/kraken.go index 1f9aca25..ae616672 100644 --- a/exchanges/kraken/kraken.go +++ b/exchanges/kraken/kraken.go @@ -57,7 +57,7 @@ const ( krakenRequestRate = 1 // Status consts - StatusOpen = "open" + statusOpen = "open" ) var ( diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index 5b072dc5..daa62856 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -18,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/stream" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" @@ -1084,7 +1085,7 @@ func TestWsOrdrbook(t *testing.T) { "channelID": 13333337, "channelName": "book", "event": "subscriptionStatus", - "pair": "XBT/EUR", + "pair": "XBT/USD", "status": "subscribed", "subscription": { "name": "book" @@ -1112,7 +1113,42 @@ func TestWsOrdrbook(t *testing.T) { "5542.70000", "0.64700000", "1534614244.654432" - ] + ], + [ + "5544.30000", + "2.50700000", + "1534614248.123678" + ], + [ + "5545.80000", + "0.33000000", + "1534614098.345543" + ], + [ + "5546.70000", + "0.64700000", + "1534614244.654432" + ], + [ + "5547.70000", + "0.64700000", + "1534614244.654432" + ], + [ + "5548.30000", + "2.50700000", + "1534614248.123678" + ], + [ + "5549.80000", + "0.33000000", + "1534614098.345543" + ], + [ + "5550.70000", + "0.64700000", + "1534614244.654432" + ] ], "bs": [ [ @@ -1129,7 +1165,42 @@ func TestWsOrdrbook(t *testing.T) { "5539.50000", "5.00000000", "1534613831.243486" - ] + ], + [ + "5538.20000", + "1.52900000", + "1534614248.765567" + ], + [ + "5537.90000", + "0.30000000", + "1534614241.769870" + ], + [ + "5536.50000", + "5.00000000", + "1534613831.243486" + ], + [ + "5535.20000", + "1.52900000", + "1534614248.765567" + ], + [ + "5534.90000", + "0.30000000", + "1534614241.769870" + ], + [ + "5533.50000", + "5.00000000", + "1534613831.243486" + ], + [ + "5532.50000", + "5.00000000", + "1534613831.243486" + ] ] }, "book-100", @@ -1153,7 +1224,8 @@ func TestWsOrdrbook(t *testing.T) { "0.40100000", "1534614248.456738" ] - ] + ], + "c": "4187525586" }, "book-10", "XBT/USD" @@ -1171,7 +1243,8 @@ func TestWsOrdrbook(t *testing.T) { "0.00000000", "1534614335.345903" ] - ] + ], + "c": "4187525586" }, "book-10", "XBT/USD" @@ -1529,3 +1602,49 @@ func TestGetHistoricTrades(t *testing.T) { t.Error(err) } } + +var testOb = orderbook.Base{ + Asks: []orderbook.Item{ + {Price: 0.05005, Amount: 0.00000500}, + {Price: 0.05010, Amount: 0.00000500}, + {Price: 0.05015, Amount: 0.00000500}, + {Price: 0.05020, Amount: 0.00000500}, + {Price: 0.05025, Amount: 0.00000500}, + {Price: 0.05030, Amount: 0.00000500}, + {Price: 0.05035, Amount: 0.00000500}, + {Price: 0.05040, Amount: 0.00000500}, + {Price: 0.05045, Amount: 0.00000500}, + {Price: 0.05050, Amount: 0.00000500}, + }, + Bids: []orderbook.Item{ + {Price: 0.05000, Amount: 0.00000500}, + {Price: 0.04995, Amount: 0.00000500}, + {Price: 0.04990, Amount: 0.00000500}, + {Price: 0.04980, Amount: 0.00000500}, + {Price: 0.04975, Amount: 0.00000500}, + {Price: 0.04970, Amount: 0.00000500}, + {Price: 0.04965, Amount: 0.00000500}, + {Price: 0.04960, Amount: 0.00000500}, + {Price: 0.04955, Amount: 0.00000500}, + {Price: 0.04950, Amount: 0.00000500}, + }, +} + +const krakenAPIDocChecksum = 974947235 + +func TestChecksumCalculation(t *testing.T) { + expected := "5005" + if v := trim("0.05005"); v != expected { + t.Fatalf("expected %s but received %s", expected, v) + } + + expected = "500" + if v := trim("0.00000500"); v != expected { + t.Fatalf("expected %s but received %s", expected, v) + } + + err := validateCRC32(&testOb, krakenAPIDocChecksum, 5, 8) + if err != nil { + t.Fatal(err) + } +} diff --git a/exchanges/kraken/kraken_types.go b/exchanges/kraken/kraken_types.go index b1633e0a..0d8bba97 100644 --- a/exchanges/kraken/kraken_types.go +++ b/exchanges/kraken/kraken_types.go @@ -465,7 +465,7 @@ type WebsocketErrorResponse struct { type WebsocketChannelData struct { Subscription string Pair currency.Pair - ChannelID int64 + ChannelID *int64 } // WsTokenResponse holds the WS auth token @@ -485,7 +485,7 @@ type wsSystemStatus struct { } type wsSubscription struct { - ChannelID int64 `json:"channelID"` + ChannelID *int64 `json:"channelID"` ChannelName string `json:"channelName"` ErrorMessage string `json:"errorMessage"` Event string `json:"event"` diff --git a/exchanges/kraken/kraken_websocket.go b/exchanges/kraken/kraken_websocket.go index b22d4131..235af73d 100644 --- a/exchanges/kraken/kraken_websocket.go +++ b/exchanges/kraken/kraken_websocket.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "hash/crc32" "net/http" "strconv" "strings" @@ -52,6 +53,7 @@ const ( krakenWsCancelAllOrderStatus = "cancelAllStatus" krakenWsRateLimit = 50 krakenWsPingDelay = time.Second * 27 + krakenWsOrderbookDepth = 1000 ) // orderbookMutex Ensures if two entries arrive at once, only one can be @@ -62,7 +64,8 @@ var pingRequest = WebsocketBaseEventRequest{Event: stream.Ping} // Channels require a topic and a currency // Format [[ticker,but-t4u],[orderbook,nce-btt]] -var defaultSubscribedChannels = []string{krakenWsTicker, +var defaultSubscribedChannels = []string{ + krakenWsTicker, krakenWsTrade, krakenWsOrderbook, krakenWsOHLC, @@ -346,9 +349,7 @@ func (k *Kraken) wsHandleData(respRaw []byte) error { } k.addNewSubscriptionChannelData(&sub) if sub.RequestID > 0 { - if k.Websocket.Match.IncomingWithData(sub.RequestID, respRaw) { - return nil - } + k.Websocket.Match.IncomingWithData(sub.RequestID, respRaw) } default: k.Websocket.DataHandler <- stream.UnhandledMessageWarning{ @@ -379,7 +380,10 @@ func (k *Kraken) wsPingHandler() error { func (k *Kraken) wsReadDataResponse(response WebsocketDataResponse) error { if cID, ok := response[0].(float64); ok { channelID := int64(cID) - channelData := getSubscriptionChannelData(channelID) + channelData, err := getSubscriptionChannelData(channelID) + if err != nil { + return err + } switch channelData.Subscription { case krakenWsTicker: t, ok := response[1].(map[string]interface{}) @@ -398,6 +402,21 @@ func (k *Kraken) wsReadDataResponse(response WebsocketDataResponse) error { if !ok { return errors.New("received invalid orderbook data") } + + if len(response) == 5 { + ob2, okob2 := response[2].(map[string]interface{}) + if !okob2 { + return errors.New("received invalid orderbook data") + } + + // Squish both maps together to process + for k, v := range ob2 { + if _, ok := ob[k]; ok { + return errors.New("cannot merge maps, conflict is present") + } + ob[k] = v + } + } return k.wsProcessOrderBook(&channelData, ob) case krakenWsSpread: s, ok := response[1].([]interface{}) @@ -412,8 +431,9 @@ func (k *Kraken) wsReadDataResponse(response WebsocketDataResponse) error { } return k.wsProcessTrades(&channelData, t) default: - return fmt.Errorf("%s received unidentified data: %+v", + return fmt.Errorf("%s received unidentified data for subscription %s: %+v", k.Name, + channelData.Subscription, response) } } @@ -597,13 +617,17 @@ func (k *Kraken) addNewSubscriptionChannelData(response *wsSubscription) { } // getSubscriptionChannelData retrieves WebsocketChannelData based on response ID -func getSubscriptionChannelData(id int64) WebsocketChannelData { +func getSubscriptionChannelData(id int64) (WebsocketChannelData, error) { for i := range subscriptionChannelPair { - if id == subscriptionChannelPair[i].ChannelID { - return subscriptionChannelPair[i] + if subscriptionChannelPair[i].ChannelID == nil { + continue + } + if id == *subscriptionChannelPair[i].ChannelID { + return subscriptionChannelPair[i], nil } } - return WebsocketChannelData{} + return WebsocketChannelData{}, + fmt.Errorf("could not get subscription data for id %d", id) } // wsProcessTickers converts ticker data and sends it to the datahandler @@ -733,17 +757,30 @@ func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data map[ } else { askData, asksExist := data["a"].([]interface{}) bidData, bidsExist := data["b"].([]interface{}) + checksum, ok := data["c"].(string) + if !ok { + return fmt.Errorf("could not process orderbook update checksum not found") + } if asksExist || bidsExist { k.wsRequestMtx.Lock() defer k.wsRequestMtx.Unlock() - err := k.wsProcessOrderBookUpdate(channelData, askData, bidData) + err := k.wsProcessOrderBookUpdate(channelData, askData, bidData, checksum) if err != nil { - subscriptionToRemove := &stream.ChannelSubscription{ + go func(resub *stream.ChannelSubscription) { + // This was locking the main websocket reader routine and a + // backlog occurred. So put this into it's own go routine. + errResub := k.Websocket.ResubscribeToChannel(resub) + if errResub != nil { + log.Errorf(log.WebsocketMgr, + "resubscription failure for %v: %v", + resub, + errResub) + } + }(&stream.ChannelSubscription{ Channel: krakenWsOrderbook, Currency: channelData.Pair, Asset: asset.Spot, - } - k.Websocket.ResubscribeToChannel(subscriptionToRemove) + }) return err } } @@ -814,22 +851,39 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, as } // wsProcessOrderBookUpdate updates an orderbook entry for a given currency pair -func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, askData, bidData []interface{}) error { +func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, askData, bidData []interface{}, checksum string) error { update := buffer.Update{ - Asset: asset.Spot, - Pair: channelData.Pair, + Asset: asset.Spot, + Pair: channelData.Pair, + MaxDepth: krakenWsOrderbookDepth, } + // Calculating checksum requires incoming decimal place checks for both + // price and amount as there is no set standard between currency pairs. This + // is calculated per update as opposed to snapshot because changes to + // decimal amounts could occur at any time. + var priceDP, amtDP int var highestLastUpdate time.Time // Ask data is not always sent for i := range askData { asks := askData[i].([]interface{}) - price, err := strconv.ParseFloat(asks[0].(string), 64) + + priceStr, ok := asks[0].(string) + if !ok { + return errors.New("price type assertion failure") + } + + price, err := strconv.ParseFloat(priceStr, 64) if err != nil { return err } - amount, err := strconv.ParseFloat(asks[1].(string), 64) + amountStr, ok := asks[1].(string) + if !ok { + return errors.New("amount type assertion failure") + } + + amount, err := strconv.ParseFloat(amountStr, 64) if err != nil { return err } @@ -838,7 +892,13 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, ask Amount: amount, Price: price, }) - timeData, err := strconv.ParseFloat(asks[2].(string), 64) + + timeStr, ok := asks[2].(string) + if !ok { + return errors.New("time type assertion failure") + } + + timeData, err := strconv.ParseFloat(timeStr, 64) if err != nil { return err } @@ -847,17 +907,43 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, ask if highestLastUpdate.Before(askUpdatedTime) { highestLastUpdate = askUpdatedTime } + + if i == len(askData)-1 { + pSplit := strings.Split(priceStr, ".") + if len(pSplit) != 2 { + return errors.New("incorrect decimal data returned for price") + } + + priceDP = len(pSplit[1]) + aSplit := strings.Split(amountStr, ".") + if len(aSplit) != 2 { + return errors.New("incorrect decimal data returned for amount") + } + + amtDP = len(aSplit[1]) + } } // Bid data is not always sent for i := range bidData { bids := bidData[i].([]interface{}) - price, err := strconv.ParseFloat(bids[0].(string), 64) + + priceStr, ok := bids[0].(string) + if !ok { + return errors.New("price type assertion failure") + } + + price, err := strconv.ParseFloat(priceStr, 64) if err != nil { return err } - amount, err := strconv.ParseFloat(bids[1].(string), 64) + amountStr, ok := bids[1].(string) + if !ok { + return errors.New("amount type assertion failure") + } + + amount, err := strconv.ParseFloat(amountStr, 64) if err != nil { return err } @@ -866,7 +952,13 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, ask Amount: amount, Price: price, }) - timeData, err := strconv.ParseFloat(bids[2].(string), 64) + + timeStr, ok := bids[2].(string) + if !ok { + return errors.New("time type assertion failure") + } + + timeData, err := strconv.ParseFloat(timeStr, 64) if err != nil { return err } @@ -875,9 +967,85 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, ask if highestLastUpdate.Before(bidUpdatedTime) { highestLastUpdate = bidUpdatedTime } + + if i == len(bidData)-1 { + pSplit := strings.Split(priceStr, ".") + if len(pSplit) != 2 { + return errors.New("incorrect decimal data returned for price") + } + + priceDP = len(pSplit[1]) + aSplit := strings.Split(amountStr, ".") + if len(aSplit) != 2 { + return errors.New("incorrect decimal data returned for amount") + } + + amtDP = len(aSplit[1]) + } } update.UpdateTime = highestLastUpdate - return k.Websocket.Orderbook.Update(&update) + err := k.Websocket.Orderbook.Update(&update) + if err != nil { + return err + } + + book := k.Websocket.Orderbook.GetOrderbook(channelData.Pair, asset.Spot) + if book == nil { + return fmt.Errorf("cannot calculate websocket checksum: book not found for %s %s", + channelData.Pair, + asset.Spot) + } + + token, err := strconv.ParseInt(checksum, 10, 64) + if err != nil { + return err + } + + return validateCRC32(book, uint32(token), priceDP, amtDP) +} + +func validateCRC32(b *orderbook.Base, token uint32, decPrice, decAmount int) error { + if len(b.Asks) < 10 || len(b.Bids) < 10 { + return fmt.Errorf("%s %s insufficient bid and asks to calculate checksum", + b.Pair, + b.AssetType) + } + + if decPrice == 0 || decAmount == 0 { + return fmt.Errorf("%s %s trailing decimal count not calculated", b.Pair, + b.AssetType) + } + + var checkStr strings.Builder + for i := 0; i < 10; i++ { + priceStr := trim(strconv.FormatFloat(b.Asks[i].Price, 'f', decPrice, 64)) + checkStr.WriteString(priceStr) + amountStr := trim(strconv.FormatFloat(b.Asks[i].Amount, 'f', decAmount, 64)) + checkStr.WriteString(amountStr) + } + + for i := 0; i < 10; i++ { + priceStr := trim(strconv.FormatFloat(b.Bids[i].Price, 'f', decPrice, 64)) + checkStr.WriteString(priceStr) + amountStr := trim(strconv.FormatFloat(b.Bids[i].Amount, 'f', decAmount, 64)) + checkStr.WriteString(amountStr) + } + + if check := crc32.ChecksumIEEE([]byte(checkStr.String())); check != token { + return fmt.Errorf("%s %s invalid checksum %d, expected %d", + b.Pair, + b.AssetType, + check, + token) + } + return nil +} + +// trim removes '.' and prefixed '0' from subsequent string +func trim(s string) string { + s = strings.Replace(s, ".", "", 1) + s = strings.TrimLeft(s, "0") + return s } // wsProcessCandles converts candle data and sends it to the data handler @@ -970,65 +1138,74 @@ func (k *Kraken) GenerateAuthenticatedSubscriptions() ([]stream.ChannelSubscript // Subscribe sends a websocket message to receive data from the channel func (k *Kraken) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error { - var subs []WebsocketSubscriptionEventRequest + var subscriptions = make(map[string]*[]WebsocketSubscriptionEventRequest) channels: - for x := range channelsToSubscribe { - for y := range subs { - if subs[y].Subscription.Name == channelsToSubscribe[x].Channel { - subs[y].Pairs = append(subs[y].Pairs, - channelsToSubscribe[x].Currency.String()) - subs[y].Channels = append(subs[y].Channels, channelsToSubscribe[x]) - continue channels + for i := range channelsToSubscribe { + s, ok := subscriptions[channelsToSubscribe[i].Channel] + if !ok { + s = &[]WebsocketSubscriptionEventRequest{} + subscriptions[channelsToSubscribe[i].Channel] = s + } + + for j := range *s { + if len((*s)[j].Channels) >= 20 { + // Batch outgoing subscriptions as there are limitations on the + // orderbook snapshots + continue } + (*s)[j].Pairs = append((*s)[j].Pairs, channelsToSubscribe[i].Currency.String()) + (*s)[j].Channels = append((*s)[j].Channels, channelsToSubscribe[i]) + continue channels } - var id int64 - if common.StringDataContains(authenticatedChannels, channelsToSubscribe[x].Channel) { - id = k.Websocket.AuthConn.GenerateMessageID(false) - } else { - id = k.Websocket.Conn.GenerateMessageID(false) - } - - resp := WebsocketSubscriptionEventRequest{ - Event: krakenWsSubscribe, - Subscription: WebsocketSubscriptionData{ - Name: channelsToSubscribe[x].Channel, - }, + id := k.Websocket.Conn.GenerateMessageID(false) + outbound := WebsocketSubscriptionEventRequest{ + Event: krakenWsSubscribe, RequestID: id, + Subscription: WebsocketSubscriptionData{ + Name: channelsToSubscribe[i].Channel, + }, } - if channelsToSubscribe[x].Channel == "book" { - // TODO: Add ability to make depth customisable - resp.Subscription.Depth = 1000 + if channelsToSubscribe[i].Channel == "book" { + outbound.Subscription.Depth = 1000 } - if !channelsToSubscribe[x].Currency.IsEmpty() { - resp.Pairs = []string{channelsToSubscribe[x].Currency.String()} + if !channelsToSubscribe[i].Currency.IsEmpty() { + outbound.Pairs = []string{channelsToSubscribe[i].Currency.String()} } - if channelsToSubscribe[x].Params != nil { - resp.Subscription.Token = authToken + if channelsToSubscribe[i].Params != nil { + outbound.Subscription.Token = authToken } - resp.Channels = append(resp.Channels, channelsToSubscribe[x]) - subs = append(subs, resp) + outbound.Channels = append(outbound.Channels, channelsToSubscribe[i]) + *s = append(*s, outbound) } var errs common.Errors - for i := range subs { - if common.StringDataContains(authenticatedChannels, subs[i].Subscription.Name) { - _, err := k.Websocket.AuthConn.SendMessageReturnResponse(subs[i].RequestID, subs[i]) + for subType, subs := range subscriptions { + for i := range *subs { + if common.StringDataContains(authenticatedChannels, (*subs)[i].Subscription.Name) { + _, err := k.Websocket.AuthConn.SendMessageReturnResponse((*subs)[i].RequestID, (*subs)[i]) + if err != nil { + errs = append(errs, err) + continue + } + k.Websocket.AddSuccessfulSubscriptions((*subs)[i].Channels...) + continue + } + if subType == "book" { + // There is an undocumented subscription limit that is present + // on websocket orderbooks, to subscribe to the channel while + // actually receiving the snapshots a rudimentary sleep is + // imposed and requests are batched in allotments of 20 items. + time.Sleep(time.Second) + } + _, err := k.Websocket.Conn.SendMessageReturnResponse((*subs)[i].RequestID, (*subs)[i]) if err != nil { errs = append(errs, err) continue } - k.Websocket.AddSuccessfulSubscriptions(subs[i].Channels...) - continue + k.Websocket.AddSuccessfulSubscriptions((*subs)[i].Channels...) } - - _, err := k.Websocket.Conn.SendMessageReturnResponse(subs[i].RequestID, subs[i]) - if err != nil { - errs = append(errs, err) - continue - } - k.Websocket.AddSuccessfulSubscriptions(subs[i].Channels...) } if errs != nil { return errs @@ -1052,8 +1229,7 @@ channels: } var depth int64 if channelsToUnsubscribe[x].Channel == "book" { - // TODO: Add ability to make depth customisable - depth = 1000 + depth = krakenWsOrderbookDepth } var id int64 diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 1788c873..38fbd0bc 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -187,7 +187,7 @@ func (k *Kraken) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: k.GenerateDefaultSubscriptions, Features: &k.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, - BufferEnabled: true, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, SortBuffer: true, }) if err != nil { @@ -425,40 +425,34 @@ func (k *Kraken) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbo // UpdateOrderbook updates and returns the orderbook for a currency pair func (k *Kraken) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ExchangeName: k.Name, Pair: p, AssetType: assetType} fpair, err := k.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } orderbookNew, err := k.GetDepth(fpair.String()) if err != nil { - return nil, err + return book, err } - var orderBook = new(orderbook.Base) for x := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Amount: orderbookNew.Bids[x].Amount, Price: orderbookNew.Bids[x].Price, }) } for x := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + book.Asks = append(book.Asks, orderbook.Item{ Amount: orderbookNew.Asks[x].Amount, Price: orderbookNew.Asks[x].Price, }) } - - orderBook.Pair = p - orderBook.ExchangeName = k.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(k.Name, p, assetType) } @@ -757,7 +751,7 @@ func (k *Kraken) GetOrderInfo(orderID string, pair currency.Pair, assetType asse } price := orderInfo.Price - if orderInfo.Status == StatusOpen { + if orderInfo.Status == statusOpen { price = orderInfo.Description.Price } diff --git a/exchanges/lakebtc/lakebtc_wrapper.go b/exchanges/lakebtc/lakebtc_wrapper.go index d5761088..52202f4e 100644 --- a/exchanges/lakebtc/lakebtc_wrapper.go +++ b/exchanges/lakebtc/lakebtc_wrapper.go @@ -138,6 +138,7 @@ func (l *LakeBTC) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: l.GenerateDefaultSubscriptions, Features: &l.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, }) } @@ -268,34 +269,32 @@ func (l *LakeBTC) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderb // UpdateOrderbook updates and returns the orderbook for a currency pair func (l *LakeBTC) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ExchangeName: l.Name, Pair: p, AssetType: assetType} fPair, err := l.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } - orderBook := new(orderbook.Base) orderbookNew, err := l.GetOrderBook(fPair.String()) if err != nil { - return orderBook, err + return book, err } for x := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: orderbookNew.Bids[x].Amount, Price: orderbookNew.Bids[x].Price}) + book.Bids = append(book.Bids, orderbook.Item{ + Amount: orderbookNew.Bids[x].Amount, + Price: orderbookNew.Bids[x].Price}) } for x := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: orderbookNew.Asks[x].Amount, Price: orderbookNew.Asks[x].Price}) + book.Asks = append(book.Asks, orderbook.Item{ + Amount: orderbookNew.Asks[x].Amount, + Price: orderbookNew.Asks[x].Price}) } - - orderBook.Pair = fPair - orderBook.ExchangeName = l.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(l.Name, fPair, assetType) } diff --git a/exchanges/lbank/lbank_wrapper.go b/exchanges/lbank/lbank_wrapper.go index 404e1aad..abd5e45e 100644 --- a/exchanges/lbank/lbank_wrapper.go +++ b/exchanges/lbank/lbank_wrapper.go @@ -243,26 +243,26 @@ func (l *Lbank) FetchOrderbook(currency currency.Pair, assetType asset.Item) (*o // UpdateOrderbook updates and returns the orderbook for a currency pair func (l *Lbank) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { - orderBook := new(orderbook.Base) + book := &orderbook.Base{ExchangeName: l.Name, Pair: p, AssetType: assetType} fpair, err := l.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } a, err := l.GetMarketDepths(fpair.String(), "60", "1") if err != nil { - return orderBook, err + return book, err } for i := range a.Asks { price, convErr := strconv.ParseFloat(a.Asks[i][0], 64) if convErr != nil { - return orderBook, convErr + return book, convErr } amount, convErr := strconv.ParseFloat(a.Asks[i][1], 64) if convErr != nil { - return orderBook, convErr + return book, convErr } - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + book.Asks = append(book.Asks, orderbook.Item{ Price: price, Amount: amount, }) @@ -270,25 +270,21 @@ func (l *Lbank) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbo for i := range a.Bids { price, convErr := strconv.ParseFloat(a.Bids[i][0], 64) if convErr != nil { - return orderBook, convErr + return book, convErr } amount, convErr := strconv.ParseFloat(a.Bids[i][1], 64) if convErr != nil { - return orderBook, convErr + return book, convErr } - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Price: price, Amount: amount, }) } - orderBook.Pair = p - orderBook.ExchangeName = l.Name - orderBook.AssetType = assetType - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(l.Name, p, assetType) } diff --git a/exchanges/localbitcoins/localbitcoins.go b/exchanges/localbitcoins/localbitcoins.go index 93e3964b..e1c5a9dd 100644 --- a/exchanges/localbitcoins/localbitcoins.go +++ b/exchanges/localbitcoins/localbitcoins.go @@ -128,7 +128,7 @@ func (l *LocalBitcoins) GetAccountInformation(username string, self bool) (Accou } } else { path := fmt.Sprintf("%s/%s/%s/", l.API.Endpoints.URL, localbitcoinsAPIAccountInfo, username) - err := l.SendHTTPRequest(path, &resp) + err := l.SendHTTPRequest(path, &resp, request.Unset) if err != nil { return resp.Data, err } @@ -335,14 +335,14 @@ func (l *LocalBitcoins) GetTradeInfo(contactID string) (dbi DashBoardInfo, err e // GetCountryCodes returns a list of valid and recognized countrycodes func (l *LocalBitcoins) GetCountryCodes() error { - return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPICountryCodes, nil) + return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPICountryCodes, nil, request.Unset) } // GetCurrencies returns a list of valid and recognized fiat currencies. Also // contains human readable name for every currency and boolean that tells if // currency is an altcoin. func (l *LocalBitcoins) GetCurrencies() error { - return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPICurrencies, nil) + return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPICurrencies, nil, request.Unset) } // GetDashboardInfo returns a list of trades on the data key contact_list. This @@ -470,13 +470,13 @@ func (l *LocalBitcoins) MarkNotifications() error { // and code for payment methods, and possible limitations in currencies and bank // name choices. func (l *LocalBitcoins) GetPaymentMethods() error { - return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPIPaymentMethods, nil) + return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPIPaymentMethods, nil, request.Unset) } // GetPaymentMethodsByCountry returns a list of valid payment methods filtered // by countrycodes. func (l *LocalBitcoins) GetPaymentMethodsByCountry(countryCode string) error { - return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPIPaymentMethods+countryCode, nil) + return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPIPaymentMethods+countryCode, nil, request.Unset) } // CheckPincode checks the given PIN code against the token owners currently @@ -511,7 +511,7 @@ func (l *LocalBitcoins) CheckPincode(pin int) (bool, error) { // sell listings for each. // TODO func (l *LocalBitcoins) GetPlaces() error { - return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPIPlaces, nil) + return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPIPlaces, nil, request.Unset) } // VerifyUsername returns list of real name verifiers for the user. Returns a @@ -639,20 +639,22 @@ func (l *LocalBitcoins) GetWalletAddress() (string, error) { // GetBitcoinsWithCashAd returns buy or sell as cash local advertisements. // TODO func (l *LocalBitcoins) GetBitcoinsWithCashAd() error { - return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPICashBuy, nil) + return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPICashBuy, nil, request.Unset) } // GetBitcoinsOnlineAd this API returns buy or sell Bitcoin online ads. // TODO func (l *LocalBitcoins) GetBitcoinsOnlineAd() error { - return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPIOnlineBuy, nil) + return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPIOnlineBuy, nil, request.Unset) } // GetTicker returns list of all completed trades. func (l *LocalBitcoins) GetTicker() (map[string]Ticker, error) { result := make(map[string]Ticker) - - return result, l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPITicker, &result) + return result, + l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPITicker, + &result, + tickerLimiter) } // GetTradableCurrencies returns a list of tradable fiat currencies @@ -673,9 +675,11 @@ func (l *LocalBitcoins) GetTradableCurrencies() ([]string, error) { // GetTrades returns all closed trades in online buy and online sell categories, // updated every 15 minutes. func (l *LocalBitcoins) GetTrades(currency string, values url.Values) ([]Trade, error) { - path := common.EncodeURLValues(fmt.Sprintf("%s%s/trades.json", l.API.Endpoints.URL+localbitcoinsAPIBitcoincharts, currency), values) + path := common.EncodeURLValues(fmt.Sprintf("%s%s/trades.json", + l.API.Endpoints.URL+localbitcoinsAPIBitcoincharts, currency), + values) var result []Trade - return result, l.SendHTTPRequest(path, &result) + return result, l.SendHTTPRequest(path, &result, request.Unset) } // GetOrderbook returns buy and sell bitcoin online advertisements. Amount is @@ -688,9 +692,9 @@ func (l *LocalBitcoins) GetOrderbook(currency string) (Orderbook, error) { Asks [][]string `json:"asks"` } - path := fmt.Sprintf("%s/%s/orderbook.json", l.API.Endpoints.URL+localbitcoinsAPIBitcoincharts, currency) + path := l.API.Endpoints.URL + localbitcoinsAPIBitcoincharts + currency + "/orderbook.json" resp := response{} - err := l.SendHTTPRequest(path, &resp) + err := l.SendHTTPRequest(path, &resp, orderBookLimiter) if err != nil { return Orderbook{}, err @@ -730,7 +734,7 @@ func (l *LocalBitcoins) GetOrderbook(currency string) (Orderbook, error) { } // SendHTTPRequest sends an unauthenticated HTTP request -func (l *LocalBitcoins) SendHTTPRequest(path string, result interface{}) error { +func (l *LocalBitcoins) SendHTTPRequest(path string, result interface{}, ep request.EndpointLimit) error { return l.SendPayload(context.Background(), &request.Item{ Method: http.MethodGet, Path: path, @@ -738,6 +742,7 @@ func (l *LocalBitcoins) SendHTTPRequest(path string, result interface{}) error { Verbose: l.Verbose, HTTPDebugging: l.HTTPDebugging, HTTPRecording: l.HTTPRecording, + Endpoint: ep, }) } diff --git a/exchanges/localbitcoins/localbitcoins_wrapper.go b/exchanges/localbitcoins/localbitcoins_wrapper.go index 07fdec92..d3ed063a 100644 --- a/exchanges/localbitcoins/localbitcoins_wrapper.go +++ b/exchanges/localbitcoins/localbitcoins_wrapper.go @@ -92,7 +92,8 @@ func (l *LocalBitcoins) SetDefaults() { } l.Requester = request.New(l.Name, - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout), + request.WithLimiter(SetRateLimit())) l.API.Endpoints.URLDefault = localbitcoinsAPIURL l.API.Endpoints.URL = l.API.Endpoints.URLDefault @@ -213,33 +214,30 @@ func (l *LocalBitcoins) FetchOrderbook(p currency.Pair, assetType asset.Item) (* // UpdateOrderbook updates and returns the orderbook for a currency pair func (l *LocalBitcoins) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { - orderBook := new(orderbook.Base) + book := &orderbook.Base{ExchangeName: l.Name, Pair: p, AssetType: assetType} orderbookNew, err := l.GetOrderbook(p.Quote.String()) if err != nil { - return orderBook, err + return book, err } for x := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Amount: orderbookNew.Bids[x].Amount / orderbookNew.Bids[x].Price, Price: orderbookNew.Bids[x].Price, }) } for x := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + book.Asks = append(book.Asks, orderbook.Item{ Amount: orderbookNew.Asks[x].Amount / orderbookNew.Asks[x].Price, Price: orderbookNew.Asks[x].Price, }) } - orderBook.Pair = p - orderBook.ExchangeName = l.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + book.NotAggregated = true + err = book.Process() if err != nil { - return orderBook, err + return book, err } return orderbook.Get(l.Name, p, assetType) diff --git a/exchanges/localbitcoins/rate_limit.go b/exchanges/localbitcoins/rate_limit.go new file mode 100644 index 00000000..edebb296 --- /dev/null +++ b/exchanges/localbitcoins/rate_limit.go @@ -0,0 +1,37 @@ +package localbitcoins + +import ( + "time" + + "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "golang.org/x/time/rate" +) + +const orderBookLimiter request.EndpointLimit = 1 +const tickerLimiter request.EndpointLimit = 2 + +// RateLimit define s custom rate limiter scoped for orderbook requests +type RateLimit struct { + Orderbook *rate.Limiter + Ticker *rate.Limiter +} + +// Limit executes rate limiting functionality for Binance +func (r *RateLimit) Limit(f request.EndpointLimit) error { + if f == orderBookLimiter { + time.Sleep(r.Orderbook.Reserve().Delay()) + } else if f == tickerLimiter { + time.Sleep(r.Ticker.Reserve().Delay()) + } + return nil +} + +// SetRateLimit returns the rate limit for the exchange +func SetRateLimit() *RateLimit { + return &RateLimit{ + // 4 seconds per book fetching is the best time frame to actually + // receive without retying. There is undocumentated rate limit. + Orderbook: request.NewRateLimit(4*time.Second, 1), + Ticker: request.NewRateLimit(time.Second, 1), + } +} diff --git a/exchanges/okgroup/okgroup_wrapper.go b/exchanges/okgroup/okgroup_wrapper.go index 5b727752..11e49b25 100644 --- a/exchanges/okgroup/okgroup_wrapper.go +++ b/exchanges/okgroup/okgroup_wrapper.go @@ -50,6 +50,7 @@ func (o *OKGroup) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: o.GenerateDefaultSubscriptions, Features: &o.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, }) if err != nil { return err @@ -77,9 +78,14 @@ func (o *OKGroup) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderb // UpdateOrderbook updates and returns the orderbook for a currency pair func (o *OKGroup) UpdateOrderbook(p currency.Pair, a asset.Item) (*orderbook.Base, error) { - orderBook := new(orderbook.Base) + book := &orderbook.Base{ + ExchangeName: o.Name, + Pair: p, + AssetType: a, + } + if a == asset.Index { - return orderBook, errors.New("no orderbooks for index") + return book, errors.New("no orderbooks for index") } fPair, err := o.FormatExchangeCurrency(p, a) @@ -91,17 +97,17 @@ func (o *OKGroup) UpdateOrderbook(p currency.Pair, a asset.Item) (*orderbook.Bas InstrumentID: fPair.String(), }, a) if err != nil { - return orderBook, err + return book, err } for x := range orderbookNew.Bids { amount, convErr := strconv.ParseFloat(orderbookNew.Bids[x][1], 64) if convErr != nil { - return orderBook, err + return book, err } price, convErr := strconv.ParseFloat(orderbookNew.Bids[x][0], 64) if convErr != nil { - return orderBook, err + return book, err } var liquidationOrders, orderCount int64 @@ -109,16 +115,16 @@ func (o *OKGroup) UpdateOrderbook(p currency.Pair, a asset.Item) (*orderbook.Bas if len(orderbookNew.Bids[x]) == 4 { liquidationOrders, convErr = strconv.ParseInt(orderbookNew.Bids[x][2], 10, 64) if convErr != nil { - return orderBook, err + return book, err } orderCount, convErr = strconv.ParseInt(orderbookNew.Bids[x][3], 10, 64) if convErr != nil { - return orderBook, err + return book, err } } - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Amount: amount, Price: price, LiquidationOrders: liquidationOrders, @@ -129,11 +135,11 @@ func (o *OKGroup) UpdateOrderbook(p currency.Pair, a asset.Item) (*orderbook.Bas for x := range orderbookNew.Asks { amount, convErr := strconv.ParseFloat(orderbookNew.Asks[x][1], 64) if convErr != nil { - return orderBook, err + return book, err } price, convErr := strconv.ParseFloat(orderbookNew.Asks[x][0], 64) if convErr != nil { - return orderBook, err + return book, err } var liquidationOrders, orderCount int64 @@ -141,16 +147,16 @@ func (o *OKGroup) UpdateOrderbook(p currency.Pair, a asset.Item) (*orderbook.Bas if len(orderbookNew.Asks[x]) == 4 { liquidationOrders, convErr = strconv.ParseInt(orderbookNew.Asks[x][2], 10, 64) if convErr != nil { - return orderBook, err + return book, err } orderCount, convErr = strconv.ParseInt(orderbookNew.Asks[x][3], 10, 64) if convErr != nil { - return orderBook, err + return book, err } } - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + book.Asks = append(book.Asks, orderbook.Item{ Amount: amount, Price: price, LiquidationOrders: liquidationOrders, @@ -158,13 +164,9 @@ func (o *OKGroup) UpdateOrderbook(p currency.Pair, a asset.Item) (*orderbook.Bas }) } - orderBook.Pair = p - orderBook.AssetType = a - orderBook.ExchangeName = o.Name - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } return orderbook.Get(o.Name, fPair, a) diff --git a/exchanges/orderbook/calculator_test.go b/exchanges/orderbook/calculator_test.go index 315a349e..410fa9f2 100644 --- a/exchanges/orderbook/calculator_test.go +++ b/exchanges/orderbook/calculator_test.go @@ -25,7 +25,7 @@ func TestWhaleBomb(t *testing.T) { t.Parallel() b := testSetup() - // invalid price amout + // invalid price amount _, err := b.WhaleBomb(-1, true) if err == nil { t.Error("unexpected result") diff --git a/exchanges/orderbook/orderbook.go b/exchanges/orderbook/orderbook.go index aedc36c0..22632e09 100644 --- a/exchanges/orderbook/orderbook.go +++ b/exchanges/orderbook/orderbook.go @@ -1,7 +1,6 @@ package orderbook import ( - "errors" "fmt" "sort" "strings" @@ -11,25 +10,21 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/dispatch" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/log" ) // Get checks and returns the orderbook given an exchange name and currency pair -// if it exists func Get(exchange string, p currency.Pair, a asset.Item) (*Base, error) { - o, err := service.Retrieve(exchange, p, a) - if err != nil { - return nil, err - } - return o, nil + return service.Retrieve(exchange, p, a) } // SubscribeOrderbook subcribes to an orderbook and returns a communication // channel to stream orderbook data updates func SubscribeOrderbook(exchange string, p currency.Pair, a asset.Item) (dispatch.Pipe, error) { exchange = strings.ToLower(exchange) - service.RLock() - defer service.RUnlock() - book, ok := service.Books[exchange][p.Base.Item][p.Quote.Item][a] + service.Lock() + defer service.Unlock() + book, ok := service.Books[exchange][a][p.Base.Item][p.Quote.Item] if !ok { return dispatch.Pipe{}, fmt.Errorf("orderbook item not found for %s %s %s", @@ -42,10 +37,9 @@ func SubscribeOrderbook(exchange string, p currency.Pair, a asset.Item) (dispatc // SubscribeToExchangeOrderbooks subcribes to all orderbooks on an exchange func SubscribeToExchangeOrderbooks(exchange string) (dispatch.Pipe, error) { - exchange = strings.ToLower(exchange) - service.RLock() - defer service.RUnlock() - id, ok := service.Exchange[exchange] + service.Lock() + defer service.Unlock() + id, ok := service.Exchange[strings.ToLower(exchange)] if !ok { return dispatch.Pipe{}, fmt.Errorf("%s exchange orderbooks not found", exchange) @@ -57,43 +51,49 @@ func SubscribeToExchangeOrderbooks(exchange string) (dispatch.Pipe, error) { func (s *Service) Update(b *Base) error { name := strings.ToLower(b.ExchangeName) s.Lock() - book, ok := s.Books[name][b.Pair.Base.Item][b.Pair.Quote.Item][b.AssetType] - if ok { - book.b.Bids = b.Bids - book.b.Asks = b.Asks - book.b.LastUpdated = b.LastUpdated - ids := append(book.Assoc, book.Main) - s.Unlock() - return s.mux.Publish(ids, b) + m1, ok := s.Books[name] + if !ok { + m1 = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*Book) + s.Books[name] = m1 } - switch { - case s.Books[name] == nil: - s.Books[name] = make(map[*currency.Item]map[*currency.Item]map[asset.Item]*Book) - fallthrough - case s.Books[name][b.Pair.Base.Item] == nil: - s.Books[name][b.Pair.Base.Item] = make(map[*currency.Item]map[asset.Item]*Book) - fallthrough - case s.Books[name][b.Pair.Base.Item][b.Pair.Quote.Item] == nil: - s.Books[name][b.Pair.Base.Item][b.Pair.Quote.Item] = make(map[asset.Item]*Book) + m2, ok := m1[b.AssetType] + if !ok { + m2 = make(map[*currency.Item]map[*currency.Item]*Book) + m1[b.AssetType] = m2 } - err := s.SetNewData(b, name) - if err != nil { + m3, ok := m2[b.Pair.Base.Item] + if !ok { + m3 = make(map[*currency.Item]*Book) + m2[b.Pair.Base.Item] = m3 + } + + book, ok := m3[b.Pair.Quote.Item] + if !ok { + book = new(Book) + m3[b.Pair.Quote.Item] = book + err := s.SetNewData(b, book, name) s.Unlock() return err } + + book.b.Bids = append(b.Bids[:0:0], b.Bids...) // nolint:gocritic // Short hand to not use make and copy + book.b.Asks = append(b.Asks[:0:0], b.Asks...) // nolint:gocritic // Short hand to not use make and copy + book.b.LastUpdated = b.LastUpdated + ids := append(book.Assoc, book.Main) s.Unlock() - return nil + return s.mux.Publish(ids, b) } // SetNewData sets new data -func (s *Service) SetNewData(b *Base, fmtName string) error { - ids, err := s.GetAssociations(b, fmtName) +func (s *Service) SetNewData(ob *Base, book *Book, exch string) error { + var err error + book.Assoc, err = s.getAssociations(strings.ToLower(exch)) if err != nil { return err } - singleID, err := s.mux.GetID() + book.Main, err = s.mux.GetID() if err != nil { return err } @@ -101,34 +101,24 @@ func (s *Service) SetNewData(b *Base, fmtName string) error { // 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[fmtName][b.Pair.Base.Item][b.Pair.Quote.Item][b.AssetType] = &Book{ - b: &cpyBook, - Main: singleID, - Assoc: ids} + cpy := *ob + cpy.Bids = append(cpy.Bids[:0:0], cpy.Bids...) + cpy.Asks = append(cpy.Asks[:0:0], cpy.Asks...) + book.b = &cpy return nil } // GetAssociations links a singular book with it's dispatch associations -func (s *Service) GetAssociations(b *Base, fmtName string) ([]uuid.UUID, error) { - if b == nil { - return nil, errors.New("orderbook is nil") - } - +func (s *Service) getAssociations(exch string) ([]uuid.UUID, error) { var ids []uuid.UUID - exchangeID, ok := s.Exchange[fmtName] + exchangeID, ok := s.Exchange[exch] if !ok { var err error exchangeID, err = s.mux.GetID() if err != nil { return nil, err } - s.Exchange[fmtName] = exchangeID + s.Exchange[exch] = exchangeID } ids = append(ids, exchangeID) @@ -137,45 +127,34 @@ func (s *Service) GetAssociations(b *Base, fmtName string) ([]uuid.UUID, error) // Retrieve gets orderbook data from the slice func (s *Service) Retrieve(exchange string, p currency.Pair, a asset.Item) (*Base, error) { - exchange = strings.ToLower(exchange) - s.RLock() - defer s.RUnlock() - if _, ok := s.Books[exchange]; !ok { + s.Lock() + defer s.Unlock() + m1, ok := s.Books[strings.ToLower(exchange)] + if !ok { return nil, fmt.Errorf("no orderbooks for %s exchange", exchange) } - if _, ok := s.Books[exchange][p.Base.Item]; !ok { - return nil, fmt.Errorf("no orderbooks associated with base currency %s", - p.Base) - } - - if _, ok := s.Books[exchange][p.Base.Item][p.Quote.Item]; !ok { - return nil, fmt.Errorf("no orderbooks associated with quote currency %s", - p.Quote) - } - - var liveOrderBook *Book - var ok bool - if liveOrderBook, ok = s.Books[exchange][p.Base.Item][p.Quote.Item][a]; !ok { + m2, ok := m1[a] + if !ok { return nil, fmt.Errorf("no orderbooks associated with asset type %s", a) } - localCopyOfAsks := make([]Item, len(s.Books[exchange][p.Base.Item][p.Quote.Item][a].b.Asks)) - localCopyOfBids := make([]Item, len(s.Books[exchange][p.Base.Item][p.Quote.Item][a].b.Bids)) - copy(localCopyOfBids, liveOrderBook.b.Bids) - copy(localCopyOfAsks, liveOrderBook.b.Asks) - - ob := Base{ - Pair: liveOrderBook.b.Pair, - Bids: localCopyOfBids, - Asks: localCopyOfAsks, - LastUpdated: liveOrderBook.b.LastUpdated, - LastUpdateID: liveOrderBook.b.LastUpdateID, - AssetType: liveOrderBook.b.AssetType, - ExchangeName: liveOrderBook.b.ExchangeName, + m3, ok := m2[p.Base.Item] + if !ok { + return nil, fmt.Errorf("no orderbooks associated with base currency %s", + p.Base) } + book, ok := m3[p.Quote.Item] + if !ok { + return nil, fmt.Errorf("no orderbooks associated with base currency %s", + p.Quote) + } + + ob := *book.b + ob.Bids = append(ob.Bids[:0:0], ob.Bids...) + ob.Asks = append(ob.Asks[:0:0], ob.Asks...) return &ob, nil } @@ -206,62 +185,131 @@ func (b *Base) Update(bids, asks []Item) { b.LastUpdated = time.Now() } -// Verify ensures that the orderbook items are correctly sorted +// Verify ensures that the orderbook items are correctly sorted prior to being +// set and will reject any book with incorrect values. // Bids should always go from a high price to a low price and -// asks should always go from a low price to a higher price -func (b *Base) Verify() { - var lastPrice float64 - var sortBids, sortAsks bool - for x := range b.Bids { - if lastPrice != 0 && b.Bids[x].Price >= lastPrice { - sortBids = true - break +// Asks should always go from a low price to a higher price +func (b *Base) Verify() error { + // Checking for both ask and bid lengths being zero has been removed and + // a warning has been put in place some exchanges e.g. LakeBTC return zero + // level books. In the event that there is a massive liquidity change where + // a book dries up, this will still update so we do not traverse potential + // incorrect old data. + if len(b.Asks) == 0 || len(b.Bids) == 0 { + log.Warnf(log.OrderBook, + bookLengthIssue, + b.ExchangeName, + b.Pair, + b.AssetType, + len(b.Bids), + len(b.Asks)) + } + for i := range b.Bids { + if b.Bids[i].Price == 0 { + return fmt.Errorf(bidLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errPriceNotSet) } - lastPrice = b.Bids[x].Price - } - - lastPrice = 0 - for x := range b.Asks { - if lastPrice != 0 && b.Asks[x].Price <= lastPrice { - sortAsks = true - break + if b.Bids[i].Amount <= 0 { + return fmt.Errorf(bidLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errAmountInvalid) + } + if b.IsFundingRate && b.Bids[i].Period == 0 { + return fmt.Errorf(bidLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errPeriodUnset) + } + if i != 0 { + if b.Bids[i].Price > b.Bids[i-1].Price { + return fmt.Errorf(bidLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errOutOfOrder) + } + + if !b.NotAggregated && b.Bids[i].Price == b.Bids[i-1].Price { + return fmt.Errorf(bidLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errDuplication) + } + + if b.Bids[i].ID != 0 && b.Bids[i].ID == b.Bids[i-1].ID { + return fmt.Errorf(bidLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errIDDuplication) + } } - lastPrice = b.Asks[x].Price } - if sortBids { - sort.Sort(sort.Reverse(byOBPrice(b.Bids))) - } + for i := range b.Asks { + if b.Asks[i].Price == 0 { + return fmt.Errorf(askLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errPriceNotSet) + } + if b.Asks[i].Amount <= 0 { + return fmt.Errorf(askLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errAmountInvalid) + } + if b.IsFundingRate && b.Asks[i].Period == 0 { + return fmt.Errorf(askLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errPeriodUnset) + } + if i != 0 { + if b.Asks[i].Price < b.Asks[i-1].Price { + return fmt.Errorf(askLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errOutOfOrder) + } - if sortAsks { - sort.Sort((byOBPrice(b.Asks))) + if !b.NotAggregated && b.Asks[i].Price == b.Asks[i-1].Price { + return fmt.Errorf(askLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errDuplication) + } + + if b.Asks[i].ID != 0 && b.Asks[i].ID == b.Asks[i-1].ID { + return fmt.Errorf(askLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errIDDuplication) + } + } } + return nil } // Process processes incoming orderbooks, creating or updating the orderbook // list func (b *Base) Process() error { if b.ExchangeName == "" { - return errors.New(errExchangeNameUnset) + return errExchangeNameUnset } if b.Pair.IsEmpty() { - return errors.New(errPairNotSet) + return errPairNotSet } if b.AssetType.String() == "" { - return errors.New(errAssetTypeNotSet) - } - - if len(b.Asks) == 0 && len(b.Bids) == 0 { - return errors.New(errNoOrderbook) + return errAssetTypeNotSet } if b.LastUpdated.IsZero() { b.LastUpdated = time.Now() } - b.Verify() + err := b.Verify() + if err != nil { + return err + } return service.Update(b) } + +// Reverse reverses the order of orderbook items; some bid/asks are +// returned in either ascending or descending order. One bid or ask slice +// depending on whats received can be reversed. This is usually faster than +// using a sort algorithm as the algorithm could be impeded by a worst case time +// complexity when elements are shifted as opposed to just swapping element +// values. +func Reverse(elem []Item) { + eLen := len(elem) + var target int + for i := eLen/2 - 1; i >= 0; i-- { + target = eLen - 1 - i + elem[i], elem[target] = elem[target], elem[i] + } +} + +// SortAsks sorts ask items to the correct ascending order if pricing values are +// scattered. If order from exchange is descending consider using the Reverse +// function. +func SortAsks(d []Item) []Item { + sort.Sort(byOBPrice(d)) + return d +} + +// SortBids sorts bid items to the correct descending order if pricing values +// are scattered. If order from exchange is ascending consider using the Reverse +// function. +func SortBids(d []Item) []Item { + sort.Sort(sort.Reverse(byOBPrice(d))) + return d +} diff --git a/exchanges/orderbook/orderbook_test.go b/exchanges/orderbook/orderbook_test.go index 4812537d..06eb5af5 100644 --- a/exchanges/orderbook/orderbook_test.go +++ b/exchanges/orderbook/orderbook_test.go @@ -1,6 +1,7 @@ package orderbook import ( + "errors" "log" "math/rand" "os" @@ -46,14 +47,7 @@ func TestSubscribeOrderbook(t *testing.T) { } b.ExchangeName = "SubscribeOBTest" - - err = b.Process() - if err == nil { - t.Error("error cannot be nil") - } - - b.Bids = []Item{{}} - + b.Bids = []Item{{Price: 100, Amount: 1}, {Price: 99, Amount: 1}} err = b.Process() if err != nil { t.Error("process error", err) @@ -61,7 +55,7 @@ func TestSubscribeOrderbook(t *testing.T) { _, err = SubscribeOrderbook("SubscribeOBTest", p, asset.Spot) if err != nil { - t.Error("error cannot be nil") + t.Error(err) } // process redundant update @@ -120,7 +114,7 @@ func TestSubscribeToExchangeOrderbooks(t *testing.T) { Pair: p, AssetType: asset.Spot, ExchangeName: "SubscribeToExchangeOrderbooks", - Bids: []Item{{}}, + Bids: []Item{{Price: 100, Amount: 1}, {Price: 99, Amount: 1}}, } err = b.Process() @@ -138,21 +132,87 @@ func TestVerify(t *testing.T) { t.Parallel() b := Base{ ExchangeName: "TestExchange", + AssetType: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD), - Bids: []Item{ - {Price: 100}, {Price: 101}, {Price: 99}, - }, - Asks: []Item{ - {Price: 100}, {Price: 99}, {Price: 101}, - }, } - b.Verify() - if r := b.Bids[1].Price; r != 100 { - t.Error("unexpected result") + err := b.Verify() + if err != nil { + t.Fatalf("expecting %v error but received %v", nil, err) } - if r := b.Asks[1].Price; r != 100 { - t.Error("unexpected result") + + b.Asks = []Item{{ID: 1337, Price: 99, Amount: 1}, {ID: 1337, Price: 100, Amount: 1}} + err = b.Verify() + if err == nil || !errors.Is(err, errIDDuplication) { + t.Fatalf("expecting %s error but received %v", errIDDuplication, err) + } + + b.Asks = []Item{{Price: 100, Amount: 1}, {Price: 100, Amount: 1}} + err = b.Verify() + if err == nil || !errors.Is(err, errDuplication) { + t.Fatalf("expecting %s error but received %v", errDuplication, err) + } + + b.Asks = []Item{{Price: 100, Amount: 1}, {Price: 99, Amount: 1}} + b.IsFundingRate = true + err = b.Verify() + if err == nil || !errors.Is(err, errPeriodUnset) { + t.Fatalf("expecting %s error but received %v", errPeriodUnset, err) + } + b.IsFundingRate = false + + err = b.Verify() + if err == nil || !errors.Is(err, errOutOfOrder) { + t.Fatalf("expecting %s error but received %v", errOutOfOrder, err) + } + + b.Asks = []Item{{Price: 100, Amount: 1}, {Price: 100, Amount: 0}} + err = b.Verify() + if err == nil || !errors.Is(err, errAmountInvalid) { + t.Fatalf("expecting %s error but received %v", errAmountInvalid, err) + } + + b.Asks = []Item{{Price: 100, Amount: 1}, {Price: 0, Amount: 100}} + err = b.Verify() + if err == nil || !errors.Is(err, errPriceNotSet) { + t.Fatalf("expecting %s error but received %v", errPriceNotSet, err) + } + + b.Bids = []Item{{ID: 1337, Price: 100, Amount: 1}, {ID: 1337, Price: 99, Amount: 1}} + err = b.Verify() + if err == nil || !errors.Is(err, errIDDuplication) { + t.Fatalf("expecting %s error but received %v", errIDDuplication, err) + } + + b.Bids = []Item{{Price: 100, Amount: 1}, {Price: 100, Amount: 1}} + err = b.Verify() + if err == nil || !errors.Is(err, errDuplication) { + t.Fatalf("expecting %s error but received %v", errDuplication, err) + } + + b.Bids = []Item{{Price: 99, Amount: 1}, {Price: 100, Amount: 1}} + b.IsFundingRate = true + err = b.Verify() + if err == nil || !errors.Is(err, errPeriodUnset) { + t.Fatalf("expecting %s error but received %v", errPeriodUnset, err) + } + b.IsFundingRate = false + + err = b.Verify() + if err == nil || !errors.Is(err, errOutOfOrder) { + t.Fatalf("expecting %s error but received %v", errOutOfOrder, err) + } + + b.Bids = []Item{{Price: 100, Amount: 1}, {Price: 100, Amount: 0}} + err = b.Verify() + if err == nil || !errors.Is(err, errAmountInvalid) { + t.Fatalf("expecting %s error but received %v", errAmountInvalid, err) + } + + b.Bids = []Item{{Price: 100, Amount: 1}, {Price: 0, Amount: 100}} + err = b.Verify() + if err == nil || !errors.Is(err, errPriceNotSet) { + t.Fatalf("expecting %s error but received %v", errPriceNotSet, err) } } @@ -517,16 +577,172 @@ func TestProcessOrderbook(t *testing.T) { wg.Wait() } -func TestSetNewData(t *testing.T) { - err := service.SetNewData(nil, "") +func deployUnorderedSlice() []Item { + var items []Item + rand.Seed(time.Now().UnixNano()) + for i := 0; i < 1000; i++ { + items = append(items, Item{Amount: 1, Price: rand.Float64(), ID: rand.Int63()}) // nolint:gosec // Not needed in tests + } + return items +} + +func TestSorting(t *testing.T) { + var b Base + + b.Asks = deployUnorderedSlice() + err := b.Verify() if err == nil { - t.Error("error cannot be nil") + t.Fatal("error cannot be nil") + } + + SortAsks(nil) + + SortAsks(b.Asks) + err = b.Verify() + if err != nil { + t.Fatal(err) + } + + b.Bids = deployUnorderedSlice() + err = b.Verify() + if err == nil { + t.Fatal("error cannot be nil") + } + + SortBids(nil) + + SortBids(b.Bids) + err = b.Verify() + if err != nil { + t.Fatal(err) } } -func TestGetAssociations(t *testing.T) { - _, err := service.GetAssociations(nil, "") +func deploySliceOrdered() []Item { + rand.Seed(time.Now().UnixNano()) + var items []Item + for i := 0; i < 1000; i++ { + items = append(items, Item{Amount: 1, Price: float64(i + 1), ID: rand.Int63()}) // nolint:gosec // Not needed in tests + } + return items +} + +func TestReverse(t *testing.T) { + var b Base + + length := 1000 + b.Bids = deploySliceOrdered() + if len(b.Bids) != length { + t.Fatal("incorrect length") + } + + err := b.Verify() if err == nil { - t.Error("error cannot be nil") + t.Fatal("error cannot be nil") + } + + Reverse(nil) + Reverse(b.Bids) + err = b.Verify() + if err != nil { + t.Fatal(err) + } + + b.Asks = append(b.Bids[:0:0], b.Bids...) // nolint:gocritic // Short hand + err = b.Verify() + if err == nil { + t.Fatal("error cannot be nil") + } + + Reverse(b.Asks) + err = b.Verify() + if err != nil { + t.Fatal(err) + } +} + +// 705985 1856 ns/op 0 B/op 0 allocs/op +func BenchmarkReverse(b *testing.B) { + length := 1000 + s := deploySliceOrdered() + if len(s) != length { + b.Fatal("incorrect length") + } + + for i := 0; i < b.N; i++ { + Reverse(s) + } +} + +// 20209 56385 ns/op 49189 B/op 2 allocs/op +func BenchmarkSortAsksDecending(b *testing.B) { + s := deploySliceOrdered() + for i := 0; i < b.N; i++ { + ts := append(s[:0:0], s...) + SortAsks(ts) + } +} + +// 14924 79199 ns/op 49206 B/op 3 allocs/op +func BenchmarkSortBidsAscending(b *testing.B) { + s := deploySliceOrdered() + Reverse(s) + for i := 0; i < b.N; i++ { + ts := append(s[:0:0], s...) + SortBids(ts) + } +} + +// 9842 133761 ns/op 49194 B/op 2 allocs/op +func BenchmarkSortAsksStandard(b *testing.B) { + s := deployUnorderedSlice() + for i := 0; i < b.N; i++ { + ts := append(s[:0:0], s...) + SortAsks(ts) + } +} + +// 7058 155057 ns/op 49214 B/op 3 allocs/op +func BenchmarkSortBidsStandard(b *testing.B) { + s := deployUnorderedSlice() + for i := 0; i < b.N; i++ { + ts := append(s[:0:0], s...) + SortBids(ts) + } +} + +// 20565 57001 ns/op 49188 B/op 2 allocs/op +func BenchmarkSortAsksAscending(b *testing.B) { + s := deploySliceOrdered() + for i := 0; i < b.N; i++ { + ts := append(s[:0:0], s...) + SortAsks(ts) + } +} + +// 12565 97257 ns/op 49208 B/op 3 allocs/op +func BenchmarkSortBidsDescending(b *testing.B) { + s := deploySliceOrdered() + Reverse(s) + for i := 0; i < b.N; i++ { + ts := append(s[:0:0], s...) + SortBids(ts) + } +} + +// 923154 1169 ns/op 4096 B/op 1 allocs/op +func BenchmarkDuplicatingSlice(b *testing.B) { + s := deploySliceOrdered() + for i := 0; i < b.N; i++ { + _ = append(s[:0:0], s...) + } +} + +// 705922 1546 ns/op 4096 B/op 1 allocs/op +func BenchmarkCopySlice(b *testing.B) { + s := deploySliceOrdered() + for i := 0; i < b.N; i++ { + cpy := make([]Item, len(s)) + copy(cpy, s) } } diff --git a/exchanges/orderbook/orderbook_types.go b/exchanges/orderbook/orderbook_types.go index 4cd72a2d..6b980a99 100644 --- a/exchanges/orderbook/orderbook_types.go +++ b/exchanges/orderbook/orderbook_types.go @@ -1,6 +1,7 @@ package orderbook import ( + "errors" "sync" "time" @@ -12,21 +13,31 @@ import ( // const values for orderbook package const ( - errExchangeNameUnset = "orderbook exchange name not set" - errPairNotSet = "orderbook currency pair not set" - errAssetTypeNotSet = "orderbook asset type not set" - errNoOrderbook = "orderbook bids and asks are empty" + bidLoadBookFailure = "cannot load book for exchange %s pair %s asset %s for Bids: %w" + askLoadBookFailure = "cannot load book for exchange %s pair %s asset %s for Asks: %w" + bookLengthIssue = "Potential book issue for exchange %s pair %s asset %s length Bids %d length Asks %d" ) // Vars for the orderbook package var ( service *Service + + errExchangeNameUnset = errors.New("orderbook exchange name not set") + errPairNotSet = errors.New("orderbook currency pair not set") + errAssetTypeNotSet = errors.New("orderbook asset type not set") + errNoOrderbook = errors.New("orderbook bids and asks are empty") + errPriceNotSet = errors.New("price cannot be zero") + errAmountInvalid = errors.New("amount cannot be less or equal to zero") + errOutOfOrder = errors.New("pricing out of order") + errDuplication = errors.New("price duplication") + errIDDuplication = errors.New("id duplication") + errPeriodUnset = errors.New("funding rate period is unset") ) func init() { service = new(Service) service.mux = dispatch.GetNewMux() - service.Books = make(map[string]map[*currency.Item]map[*currency.Item]map[asset.Item]*Book) + service.Books = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*Book) service.Exchange = make(map[string]uuid.UUID) } @@ -39,10 +50,10 @@ type Book struct { // Service holds orderbook information for each individual exchange type Service struct { - Books map[string]map[*currency.Item]map[*currency.Item]map[asset.Item]*Book + Books map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*Book Exchange map[string]uuid.UUID mux *dispatch.Mux - sync.RWMutex + sync.Mutex } // Item stores the amount and price values @@ -51,6 +62,9 @@ type Item struct { Price float64 ID int64 + // Funding rate field + Period int64 + // Contract variables LiquidationOrders int64 OrderCount int64 @@ -65,6 +79,10 @@ type Base struct { LastUpdateID int64 `json:"lastUpdateId"` AssetType asset.Item `json:"assetType"` ExchangeName string `json:"exchangeName"` + // NotAggregated defines whether an orderbook can contain duplicate prices + // in a payload + NotAggregated bool `json:"-"` + IsFundingRate bool `json:"fundingRate"` } type byOBPrice []Item diff --git a/exchanges/poloniex/poloniex_websocket.go b/exchanges/poloniex/poloniex_websocket.go index 6819dd8d..6037570d 100644 --- a/exchanges/poloniex/poloniex_websocket.go +++ b/exchanges/poloniex/poloniex_websocket.go @@ -463,57 +463,58 @@ func (p *Poloniex) wsHandleTickerData(data []interface{}) error { // WsProcessOrderbookSnapshot processes a new orderbook snapshot into a local // of orderbooks func (p *Poloniex) WsProcessOrderbookSnapshot(ob []interface{}, symbol string) error { - askdata := ob[0].(map[string]interface{}) - var asks []orderbook.Item + if len(ob) != 2 { + return errors.New("incorrect orderbook data returned") + } + + askdata, ok := ob[0].(map[string]interface{}) + if !ok { + return errors.New("assertion failed for ask data") + } + + var book orderbook.Base for price, volume := range askdata { - assetPrice, err := strconv.ParseFloat(price, 64) + p, err := strconv.ParseFloat(price, 64) if err != nil { return err } - - assetVolume, err := strconv.ParseFloat(volume.(string), 64) + a, err := strconv.ParseFloat(volume.(string), 64) if err != nil { return err } - - asks = append(asks, orderbook.Item{ - Price: assetPrice, - Amount: assetVolume, - }) + book.Asks = append(book.Asks, orderbook.Item{Price: p, Amount: a}) + } + + bidData, ok := ob[1].(map[string]interface{}) + if !ok { + return errors.New("assertion failed for bid data") } - bidData := ob[1].(map[string]interface{}) - var bids []orderbook.Item for price, volume := range bidData { - assetPrice, err := strconv.ParseFloat(price, 64) + p, err := strconv.ParseFloat(price, 64) if err != nil { return err } - - assetVolume, err := strconv.ParseFloat(volume.(string), 64) + a, err := strconv.ParseFloat(volume.(string), 64) if err != nil { return err } - - bids = append(bids, orderbook.Item{ - Price: assetPrice, - Amount: assetVolume, - }) + book.Bids = append(book.Bids, orderbook.Item{Price: p, Amount: a}) } - var newOrderBook orderbook.Base - newOrderBook.Asks = asks - newOrderBook.Bids = bids - newOrderBook.AssetType = asset.Spot + // Both sides are completely out of order - sort needs to be used + book.Asks = orderbook.SortAsks(book.Asks) + book.Bids = orderbook.SortBids(book.Bids) + book.AssetType = asset.Spot var err error - newOrderBook.Pair, err = currency.NewPairFromString(symbol) + book.Pair, err = currency.NewPairFromString(symbol) if err != nil { return err } - newOrderBook.ExchangeName = p.Name + book.ExchangeName = p.Name - return p.Websocket.Orderbook.LoadSnapshot(&newOrderBook) + return p.Websocket.Orderbook.LoadSnapshot(&book) } // WsProcessOrderbookUpdate processes new orderbook updates diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index 6787dee6..b2200a4a 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -167,6 +167,7 @@ func (p *Poloniex) Setup(exch *config.ExchangeConfig) error { GenerateSubscriptions: p.GenerateDefaultSubscriptions, Features: &p.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, SortBuffer: true, SortBufferByUpdateIDs: true, }) @@ -318,50 +319,51 @@ func (p *Poloniex) FetchOrderbook(currencyPair currency.Pair, assetType asset.It } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (p *Poloniex) UpdateOrderbook(currencyPair currency.Pair, assetType asset.Item) (*orderbook.Base, error) { +func (p *Poloniex) UpdateOrderbook(c currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + callingBook := &orderbook.Base{ExchangeName: p.Name, Pair: c, AssetType: assetType} orderbookNew, err := p.GetOrderbook("", poloniexMaxOrderbookDepth) if err != nil { - return nil, err + return callingBook, err } enabledPairs, err := p.GetEnabledPairs(assetType) if err != nil { - return nil, err + return callingBook, err } for i := range enabledPairs { + book := &orderbook.Base{ + ExchangeName: p.Name, + Pair: enabledPairs[i], + AssetType: assetType} + fpair, err := p.FormatExchangeCurrency(enabledPairs[i], assetType) if err != nil { - return nil, err + return book, err } data, ok := orderbookNew.Data[fpair.String()] if !ok { continue } - orderBook := new(orderbook.Base) for y := range data.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Amount: data.Bids[y].Amount, Price: data.Bids[y].Price, }) } for y := range data.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + book.Asks = append(book.Asks, orderbook.Item{ Amount: data.Asks[y].Amount, Price: data.Asks[y].Price, }) } - orderBook.Pair = enabledPairs[i] - orderBook.ExchangeName = p.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } } - return orderbook.Get(p.Name, currencyPair, assetType) + return orderbook.Get(p.Name, c, assetType) } // UpdateAccountInfo retrieves balances for all enabled currencies for the diff --git a/exchanges/stream/buffer/buffer.go b/exchanges/stream/buffer/buffer.go index 7ef4d90e..14a1cae7 100644 --- a/exchanges/stream/buffer/buffer.go +++ b/exchanges/stream/buffer/buffer.go @@ -10,8 +10,31 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" ) +const packageError = "websocket orderbook buffer error: %w" + +var ( + errUnsetExchangeName = errors.New("exchange name unset") + errUnsetDataHandler = errors.New("datahandler unset") + errIssueBufferEnabledButNoLimit = errors.New("buffer enabled but no limit set") + errUpdateIsNil = errors.New("update is nil") + errUpdateNoTargets = errors.New("update bid/ask targets cannot be nil") +) + // Setup sets private variables -func (w *Orderbook) Setup(obBufferLimit int, bufferEnabled, sortBuffer, sortBufferByUpdateIDs, updateEntriesByID bool, exchangeName string, dataHandler chan interface{}) { +func (w *Orderbook) Setup(obBufferLimit int, + bufferEnabled, + sortBuffer, + sortBufferByUpdateIDs, + updateEntriesByID bool, exchangeName string, dataHandler chan interface{}) error { + if exchangeName == "" { + return fmt.Errorf(packageError, errUnsetExchangeName) + } + if dataHandler == nil { + return fmt.Errorf(packageError, errUnsetDataHandler) + } + if bufferEnabled && obBufferLimit < 1 { + return fmt.Errorf(packageError, errIssueBufferEnabledButNoLimit) + } w.obBufferLimit = obBufferLimit w.bufferEnabled = bufferEnabled w.sortBuffer = sortBuffer @@ -19,6 +42,19 @@ func (w *Orderbook) Setup(obBufferLimit int, bufferEnabled, sortBuffer, sortBuff w.updateEntriesByID = updateEntriesByID w.exchangeName = exchangeName w.dataHandler = dataHandler + w.ob = make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder) + return nil +} + +// validate validates update against setup values +func (w *Orderbook) validate(u *Update) error { + if u == nil { + return fmt.Errorf(packageError, errUpdateIsNil) + } + if len(u.Bids) == 0 && len(u.Asks) == 0 { + return fmt.Errorf(packageError, errUpdateNoTargets) + } + return nil } // Update updates a local buffer using bid targets and ask targets then updates @@ -27,13 +63,12 @@ func (w *Orderbook) Setup(obBufferLimit int, bufferEnabled, sortBuffer, sortBuff // Price target not found; append of price target // Price target found; amend volume of price target func (w *Orderbook) Update(u *Update) 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) + if err := w.validate(u); err != nil { + return err } w.m.Lock() defer w.m.Unlock() - obLookup, ok := w.ob[u.Pair][u.Asset] + obLookup, ok := w.ob[u.Pair.Base][u.Pair.Quote][u.Asset] if !ok { return fmt.Errorf("ob.Base could not be found for Exchange %s CurrencyPair: %s AssetType: %s", w.exchangeName, @@ -42,246 +77,345 @@ func (w *Orderbook) Update(u *Update) error { } if w.bufferEnabled { - overBufferLimit := w.processBufferUpdate(obLookup, u) - if !overBufferLimit { + processed, err := w.processBufferUpdate(obLookup, u) + if err != nil { + return err + } + + if !processed { return nil } } else { - w.processObUpdate(obLookup, u) + err := w.processObUpdate(obLookup, u) + if err != nil { + return err + } } - err := obLookup.Process() + + err := obLookup.ob.Process() if err != nil { return err } - if w.bufferEnabled { - // Reset the buffer - w.buffer[u.Pair][u.Asset] = nil - } - // Process in data handler - w.dataHandler <- obLookup + select { + case w.dataHandler <- obLookup.ob: + default: + } return nil } -func (w *Orderbook) processBufferUpdate(o *orderbook.Base, u *Update) bool { - if w.buffer == nil { - w.buffer = make(map[currency.Pair]map[asset.Item][]*Update) - } - if w.buffer[u.Pair] == nil { - w.buffer[u.Pair] = make(map[asset.Item][]*Update) - } - bufferLookup := w.buffer[u.Pair][u.Asset] - if len(bufferLookup) <= w.obBufferLimit { - bufferLookup = append(bufferLookup, u) - if len(bufferLookup) < w.obBufferLimit { - w.buffer[u.Pair][u.Asset] = bufferLookup - return false - } +// processBufferUpdate stores update into buffer, when buffer at capacity as +// defined by w.obBufferLimit it well then sort and apply updates. +func (w *Orderbook) processBufferUpdate(o *orderbookHolder, u *Update) (bool, error) { + *o.buffer = append(*o.buffer, *u) + if len(*o.buffer) < w.obBufferLimit { + return false, nil } + if w.sortBuffer { // sort by last updated to ensure each update is in order if w.sortBufferByUpdateIDs { - sort.Slice(bufferLookup, func(i, j int) bool { - return bufferLookup[i].UpdateID < bufferLookup[j].UpdateID + sort.Slice(*o.buffer, func(i, j int) bool { + return (*o.buffer)[i].UpdateID < (*o.buffer)[j].UpdateID }) } else { - sort.Slice(bufferLookup, func(i, j int) bool { - return bufferLookup[i].UpdateTime.Before(bufferLookup[j].UpdateTime) + sort.Slice(*o.buffer, func(i, j int) bool { + return (*o.buffer)[i].UpdateTime.Before((*o.buffer)[j].UpdateTime) }) } } - for i := range bufferLookup { - w.processObUpdate(o, bufferLookup[i]) + for i := range *o.buffer { + err := w.processObUpdate(o, &(*o.buffer)[i]) + if err != nil { + return false, err + } } - w.buffer[u.Pair][u.Asset] = bufferLookup - return true + // clear buffer of old updates + *o.buffer = nil + return true, nil } -func (w *Orderbook) processObUpdate(o *orderbook.Base, u *Update) { - o.LastUpdateID = u.UpdateID - +// processObUpdate processes updates either by its corresponding id or by +// price level +func (w *Orderbook) processObUpdate(o *orderbookHolder, u *Update) error { + o.ob.LastUpdateID = u.UpdateID if w.updateEntriesByID { - w.updateByIDAndAction(o, u) - } else { - w.updateAsksByPrice(o, u) - w.updateBidsByPrice(o, u) + return o.updateByIDAndAction(u) } + return o.updateByPrice(u) } -func (w *Orderbook) updateAsksByPrice(o *orderbook.Base, u *Update) { -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 +// updateByPrice ammends amount if match occurs by price, deletes if amount is +// zero or less and inserts if not found. +func (o *orderbookHolder) updateByPrice(updts *Update) error { +askUpdates: + for j := range updts.Asks { + for target := range o.ob.Asks { + if o.ob.Asks[target].Price == updts.Asks[j].Price { + if updts.Asks[j].Amount == 0 { + o.ob.Asks = append(o.ob.Asks[:target], o.ob.Asks[target+1:]...) + continue askUpdates } - o.Asks[k].Amount = u.Asks[j].Amount - continue updates + o.ob.Asks[target].Amount = updts.Asks[j].Amount + continue askUpdates } } - if u.Asks[j].Amount == 0 { + if updts.Asks[j].Amount <= 0 { continue } - o.Asks = append(o.Asks, u.Asks[j]) + insertAsk(updts.Asks[j], &o.ob.Asks) + if updts.MaxDepth != 0 && len(o.ob.Asks) > updts.MaxDepth { + o.ob.Asks = o.ob.Asks[:updts.MaxDepth] + } } - sort.Slice(o.Asks, func(i, j int) bool { - return o.Asks[i].Price < o.Asks[j].Price - }) -} - -func (w *Orderbook) updateBidsByPrice(o *orderbook.Base, u *Update) { -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 +bidUpdates: + for j := range updts.Bids { + for target := range o.ob.Bids { + if o.ob.Bids[target].Price == updts.Bids[j].Price { + if updts.Bids[j].Amount == 0 { + o.ob.Bids = append(o.ob.Bids[:target], o.ob.Bids[target+1:]...) + continue bidUpdates } - o.Bids[k].Amount = u.Bids[j].Amount - continue updates + o.ob.Bids[target].Amount = updts.Bids[j].Amount + continue bidUpdates } } - if u.Bids[j].Amount == 0 { + if updts.Bids[j].Amount <= 0 { continue } - o.Bids = append(o.Bids, u.Bids[j]) + insertBid(updts.Bids[j], &o.ob.Bids) + if updts.MaxDepth != 0 && len(o.ob.Bids) > updts.MaxDepth { + o.ob.Bids = o.ob.Bids[:updts.MaxDepth] + } } - sort.Slice(o.Bids, func(i, j int) bool { - return o.Bids[i].Price > o.Bids[j].Price - }) + return nil } // 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 *Orderbook) updateByIDAndAction(o *orderbook.Base, u *Update) { - switch u.Action { - case "update": - 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 - } - } +func (o *orderbookHolder) updateByIDAndAction(updts *Update) (err error) { + switch updts.Action { + case Amend: + err = applyUpdates(updts.Bids, o.ob.Bids) + if err != nil { + return err } - 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 - } - } + err = applyUpdates(updts.Asks, o.ob.Asks) + if err != nil { + return err } - case "delete": - 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 - } - } + case Delete: + // edge case for Bitfinex as their streaming endpoint duplicates deletes + bypassErr := o.ob.ExchangeName == "Bitfinex" && o.ob.IsFundingRate + err = deleteUpdates(updts.Bids, &o.ob.Bids, bypassErr) + if err != nil { + return fmt.Errorf("%s %s %v", o.ob.AssetType, o.ob.Pair, err) } - 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 - } - } + err = deleteUpdates(updts.Asks, &o.ob.Asks, bypassErr) + if err != nil { + return fmt.Errorf("%s %s %v", o.ob.AssetType, o.ob.Pair, err) } - case "insert": - 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 - }) - - case "update/insert": + case Insert: + insertUpdatesBid(updts.Bids, &o.ob.Bids) + insertUpdatesAsk(updts.Asks, &o.ob.Asks) + case UpdateInsert: updateBids: - 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 + for x := range updts.Bids { + for target := range o.ob.Bids { // First iteration finds ID matches + if o.ob.Bids[target].ID == updts.Bids[x].ID { + if o.ob.Bids[target].Price != updts.Bids[x].Price { + // Price change occurred so correct bid alignment is + // needed - delete instance and insert into correct + // price level + o.ob.Bids = append(o.ob.Bids[:target], o.ob.Bids[target+1:]...) + break + } + o.ob.Bids[target].Amount = updts.Bids[x].Amount continue updateBids } } - o.Bids = append(o.Bids, u.Bids[x]) + insertBid(updts.Bids[x], &o.ob.Bids) } - updateAsks: - 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 + for x := range updts.Asks { + for target := range o.ob.Asks { + if o.ob.Asks[target].ID == updts.Asks[x].ID { + if o.ob.Asks[target].Price != updts.Asks[x].Price { + // Price change occurred so correct ask alignment is + // needed - delete instance and insert into correct + // price level + o.ob.Asks = append(o.ob.Asks[:target], o.ob.Asks[target+1:]...) + break + } + o.ob.Asks[target].Amount = updts.Asks[x].Amount continue updateAsks } } - o.Asks = append(o.Asks, u.Asks[x]) + insertAsk(updts.Asks[x], &o.ob.Asks) } + default: + return fmt.Errorf("invalid action [%s]", updts.Action) + } + return nil +} + +// applyUpdates amends amount by ID and returns an error if not found +func applyUpdates(updts, book []orderbook.Item) error { +updates: + for x := range updts { + for y := range book { + if book[y].ID == updts[x].ID { + book[y].Amount = updts[x].Amount + continue updates + } + } + return fmt.Errorf("update cannot be applied id: %d not found", + updts[x].ID) + } + return nil +} + +// deleteUpdates removes updates from orderbook and returns an error if not +// found +func deleteUpdates(updt []orderbook.Item, book *[]orderbook.Item, bypassErr bool) error { +updates: + for x := range updt { + for y := range *book { + if (*book)[y].ID == updt[x].ID { + *book = append((*book)[:y], (*book)[y+1:]...) // nolint:gocritic + continue updates + } + } + // bypassErr is for expected duplication from endpoint. + if !bypassErr { + return fmt.Errorf("update cannot be deleted id: %d not found", + updt[x].ID) + } + } + return nil +} + +func insertAsk(updt orderbook.Item, book *[]orderbook.Item) { + for target := range *book { + if updt.Price < (*book)[target].Price { + insertItem(updt, book, target) + return + } + } + *book = append(*book, updt) +} + +func insertBid(updt orderbook.Item, book *[]orderbook.Item) { + for target := range *book { + if updt.Price > (*book)[target].Price { + insertItem(updt, book, target) + return + } + } + *book = append(*book, updt) +} + +// insertUpdatesBid inserts on **correctly aligned** book at price level +func insertUpdatesBid(updt []orderbook.Item, book *[]orderbook.Item) { +updates: + for x := range updt { + for target := range *book { + if updt[x].Price > (*book)[target].Price { + insertItem(updt[x], book, target) + continue updates + } + } + *book = append(*book, updt[x]) } } -// LoadSnapshot loads initial snapshot of ob data, overwrite allows full -// ob to be completely rewritten because the exchange is a doing a full -// update not an incremental one -func (w *Orderbook) LoadSnapshot(newOrderbook *orderbook.Base) error { - if len(newOrderbook.Asks) == 0 || len(newOrderbook.Bids) == 0 { - return fmt.Errorf("%v snapshot ask and bids are nil", w.exchangeName) +// insertUpdatesBid inserts on **correctly aligned** book at price level +func insertUpdatesAsk(updt []orderbook.Item, book *[]orderbook.Item) { +updates: + for x := range updt { + for target := range *book { + if updt[x].Price < (*book)[target].Price { + insertItem(updt[x], book, target) + continue updates + } + } + *book = append(*book, updt[x]) } +} - if newOrderbook.Pair.IsEmpty() { - return errors.New("websocket orderbook pair unset") - } - - if newOrderbook.AssetType.String() == "" { - return errors.New("websocket orderbook asset type unset") - } - - if newOrderbook.ExchangeName == "" { - return errors.New("websocket orderbook exchange name unset") - } +// insertItem inserts item in slice by target element this is an optimization +// to reduce the need for sorting algorithms +func insertItem(update orderbook.Item, book *[]orderbook.Item, target int) { + // TODO: extend slice by incoming update length before this gets hit + *book = append(*book, orderbook.Item{}) + copy((*book)[target+1:], (*book)[target:]) + (*book)[target] = update +} +// LoadSnapshot loads initial snapshot of ob data from websocket +func (w *Orderbook) LoadSnapshot(book *orderbook.Base) error { w.m.Lock() defer w.m.Unlock() - if w.ob == nil { - w.ob = make(map[currency.Pair]map[asset.Item]*orderbook.Base) - } - if w.ob[newOrderbook.Pair] == nil { - w.ob[newOrderbook.Pair] = make(map[asset.Item]*orderbook.Base) - } - w.ob[newOrderbook.Pair][newOrderbook.AssetType] = newOrderbook - err := newOrderbook.Process() + err := book.Process() if err != nil { return err } - w.dataHandler <- newOrderbook + m1, ok := w.ob[book.Pair.Base] + if !ok { + m1 = make(map[currency.Code]map[asset.Item]*orderbookHolder) + w.ob[book.Pair.Base] = m1 + } + m2, ok := m1[book.Pair.Quote] + if !ok { + m2 = make(map[asset.Item]*orderbookHolder) + m1[book.Pair.Quote] = m2 + } + m3, ok := m2[book.AssetType] + if !ok { + m3 = &orderbookHolder{ob: book, buffer: &[]Update{}} + m2[book.AssetType] = m3 + } else { + m3.ob.Bids = book.Bids + m3.ob.Asks = book.Asks + } + w.dataHandler <- book return nil } -// GetOrderbook use sparingly. Modifying anything here will ruin hash -// calculation and cause problems +// GetOrderbook returns orderbook stored in current buffer func (w *Orderbook) GetOrderbook(p currency.Pair, a asset.Item) *orderbook.Base { w.m.Lock() - ob := w.ob[p][a] - w.m.Unlock() - return ob + defer w.m.Unlock() + ptr, ok := w.ob[p.Base][p.Quote][a] + if !ok { + return nil + } + cpy := *ptr.ob + cpy.Asks = append(cpy.Asks[:0:0], cpy.Asks...) + cpy.Bids = append(cpy.Bids[:0:0], cpy.Bids...) + return &cpy } // FlushBuffer flushes w.ob data to be garbage collected and refreshed when a // connection is lost and reconnected func (w *Orderbook) FlushBuffer() { w.m.Lock() - w.ob = nil - w.buffer = nil + w.ob = make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder) w.m.Unlock() } + +// FlushOrderbook flushes independent orderbook +func (w *Orderbook) FlushOrderbook(p currency.Pair, a asset.Item) error { + w.m.Lock() + defer w.m.Unlock() + book, ok := w.ob[p.Base][p.Quote][a] + if !ok { + return fmt.Errorf("orderbook not associated with pair: [%s] and asset [%s]", p, a) + } + book.ob.Bids = nil + book.ob.Asks = nil + return nil +} diff --git a/exchanges/stream/buffer/buffer_test.go b/exchanges/stream/buffer/buffer_test.go index 5612834f..e7b10aeb 100644 --- a/exchanges/stream/buffer/buffer_test.go +++ b/exchanges/stream/buffer/buffer_test.go @@ -1,7 +1,7 @@ package buffer import ( - "fmt" + "errors" "math/rand" "testing" "time" @@ -12,12 +12,12 @@ import ( ) var itemArray = [][]orderbook.Item{ - {{Price: 1000, Amount: 1, ID: 1}}, - {{Price: 2000, Amount: 1, ID: 2}}, - {{Price: 3000, Amount: 1, ID: 3}}, - {{Price: 3000, Amount: 2, ID: 4}}, - {{Price: 4000, Amount: 0, ID: 6}}, - {{Price: 5000, Amount: 1, ID: 5}}, + {{Price: 1000, Amount: 1, ID: 1000}}, + {{Price: 2000, Amount: 1, ID: 2000}}, + {{Price: 3000, Amount: 1, ID: 3000}}, + {{Price: 3000, Amount: 2, ID: 4000}}, + {{Price: 4000, Amount: 0, ID: 6000}}, + {{Price: 5000, Amount: 1, ID: 5000}}, } var cp, _ = currency.NewPairFromString("BTCUSD") @@ -39,7 +39,12 @@ func createSnapshot() (obl *Orderbook, asks, bids []orderbook.Item, err error) { snapShot1.Bids = bids snapShot1.AssetType = asset.Spot snapShot1.Pair = cp - obl = &Orderbook{exchangeName: exchangeName, dataHandler: make(chan interface{}, 100)} + snapShot1.NotAggregated = true + obl = &Orderbook{ + exchangeName: exchangeName, + dataHandler: make(chan interface{}, 100), + ob: make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder), + } err = obl.LoadSnapshot(&snapShot1) return } @@ -76,7 +81,8 @@ func BenchmarkUpdateBidsByPrice(b *testing.B) { UpdateTime: time.Now(), Asset: asset.Spot, } - ob.updateBidsByPrice(ob.ob[cp][asset.Spot], update) + holder := ob.ob[cp.Base][cp.Quote][asset.Spot] + holder.updateByPrice(update) } } @@ -95,7 +101,8 @@ func BenchmarkUpdateAsksByPrice(b *testing.B) { UpdateTime: time.Now(), Asset: asset.Spot, } - ob.updateAsksByPrice(ob.ob[cp][asset.Spot], update) + holder := ob.ob[cp.Base][cp.Quote][asset.Spot] + holder.updateByPrice(update) } } @@ -114,7 +121,7 @@ func BenchmarkBufferPerformance(b *testing.B) { Price: 1337.1337, ID: 1337, } - obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem) + obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids = append(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids, dummyItem) update := &Update{ Bids: bids, Asks: asks, @@ -149,7 +156,7 @@ func BenchmarkBufferSortingPerformance(b *testing.B) { Price: 1337.1337, ID: 1337, } - obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem) + obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids = append(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids, dummyItem) update := &Update{ Bids: bids, Asks: asks, @@ -185,7 +192,7 @@ func BenchmarkBufferSortingByIDPerformance(b *testing.B) { Price: 1337.1337, ID: 1337, } - obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem) + obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids = append(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids, dummyItem) update := &Update{ Bids: bids, Asks: asks, @@ -219,7 +226,7 @@ func BenchmarkNoBufferPerformance(b *testing.B) { Price: 1337.1337, ID: 1337, } - obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem) + obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids = append(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids, dummyItem) update := &Update{ Bids: bids, Asks: asks, @@ -245,7 +252,8 @@ func TestUpdates(t *testing.T) { t.Error(err) } - obl.updateAsksByPrice(obl.ob[cp][asset.Spot], &Update{ + holder := obl.ob[cp.Base][cp.Quote][asset.Spot] + holder.updateByPrice(&Update{ Bids: itemArray[5], Asks: itemArray[5], Pair: cp, @@ -256,7 +264,7 @@ func TestUpdates(t *testing.T) { t.Error(err) } - obl.updateAsksByPrice(obl.ob[cp][asset.Spot], &Update{ + holder.updateByPrice(&Update{ Bids: itemArray[0], Asks: itemArray[0], Pair: cp, @@ -267,7 +275,7 @@ func TestUpdates(t *testing.T) { t.Error(err) } - if len(obl.ob[cp][asset.Spot].Asks) != 3 { + if len(holder.ob.Asks) != 3 { t.Error("Did not update") } } @@ -294,14 +302,13 @@ func TestHittingTheBuffer(t *testing.T) { t.Fatal(err) } } - if len(obl.ob[cp][asset.Spot].Asks) != 3 { - t.Log(obl.ob[cp][asset.Spot]) + if len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks) != 3 { t.Errorf("expected 3 entries, received: %v", - len(obl.ob[cp][asset.Spot].Asks)) + len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks)) } - if len(obl.ob[cp][asset.Spot].Bids) != 3 { + if len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids) != 3 { t.Errorf("expected 3 entries, received: %v", - len(obl.ob[cp][asset.Spot].Bids)) + len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids)) } } @@ -316,6 +323,9 @@ func TestInsertWithIDs(t *testing.T) { obl.obBufferLimit = 5 for i := range itemArray { asks := itemArray[i] + if asks[0].Amount <= 0 { + continue + } bids := itemArray[i] err = obl.Update(&Update{ Bids: bids, @@ -329,13 +339,13 @@ func TestInsertWithIDs(t *testing.T) { t.Fatal(err) } } - if len(obl.ob[cp][asset.Spot].Asks) != 6 { + if len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks) != 6 { t.Errorf("expected 6 entries, received: %v", - len(obl.ob[cp][asset.Spot].Asks)) + len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks)) } - if len(obl.ob[cp][asset.Spot].Bids) != 6 { + if len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids) != 6 { t.Errorf("expected 6 entries, received: %v", - len(obl.ob[cp][asset.Spot].Bids)) + len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids)) } } @@ -363,93 +373,13 @@ func TestSortIDs(t *testing.T) { t.Fatal(err) } } - if len(obl.ob[cp][asset.Spot].Asks) != 3 { + if len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks) != 3 { t.Errorf("expected 3 entries, received: %v", - len(obl.ob[cp][asset.Spot].Asks)) + len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks)) } - if len(obl.ob[cp][asset.Spot].Bids) != 3 { + if len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids) != 3 { t.Errorf("expected 3 entries, received: %v", - len(obl.ob[cp][asset.Spot].Bids)) - } -} - -// TestDeleteWithIDs logic test -func TestDeleteWithIDs(t *testing.T) { - obl, _, _, err := createSnapshot() - if err != nil { - t.Fatal(err) - } - - // 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{ - Amount: 1333337, - Price: 1337.1337, - 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 := range itemArray { - asks := itemArray[i] - bids := itemArray[i] - err = obl.Update(&Update{ - Bids: bids, - Asks: asks, - Pair: cp, - UpdateTime: time.Now(), - Asset: asset.Spot, - Action: "delete", - }) - if err != nil { - t.Fatal(err) - } - } - - if len(obl.ob[cp][asset.Spot].Asks) != 0 { - t.Errorf("expected 0 entries, received: %v", - len(obl.ob[cp][asset.Spot].Asks)) - } - if len(obl.ob[cp][asset.Spot].Bids) != 1 { - t.Errorf("expected 1 entries, received: %v", - len(obl.ob[cp][asset.Spot].Bids)) - } -} - -// TestUpdateWithIDs logic test -func TestUpdateWithIDs(t *testing.T) { - obl, _, _, err := createSnapshot() - if err != nil { - t.Fatal(err) - } - obl.updateEntriesByID = true - for i := range itemArray { - asks := itemArray[i] - bids := itemArray[i] - err = obl.Update(&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[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[cp][asset.Spot].Bids) != 1 { - t.Errorf("expected 1 entries, received: %v", - len(obl.ob[cp][asset.Spot].Bids)) + len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids)) } } @@ -480,9 +410,9 @@ func TestOutOfOrderIDs(t *testing.T) { } } // Index 1 since index 0 is price 7000 - if obl.ob[cp][asset.Spot].Asks[1].Price != 2000 { + if obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks[1].Price != 2000 { t.Errorf("expected sorted price to be 3000, received: %v", - obl.ob[cp][asset.Spot].Asks[1].Price) + obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks[1].Price) } } @@ -565,8 +495,7 @@ func TestRunUpdateWithoutAnyUpdates(t *testing.T) { if err == nil { t.Fatal("expected an error running update with no snapshot loaded") } - if err.Error() != fmt.Sprintf("%v cannot have bids and ask targets both nil", - exchangeName) { + if err.Error() != "websocket orderbook buffer error: update bid/ask targets cannot be nil" { t.Fatal("expected nil asks and bids error") } } @@ -574,18 +503,15 @@ func TestRunUpdateWithoutAnyUpdates(t *testing.T) { // TestRunSnapshotWithNoData logic test func TestRunSnapshotWithNoData(t *testing.T) { var obl Orderbook + obl.ob = make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder) + obl.dataHandler = make(chan interface{}, 1) var snapShot1 orderbook.Base - snapShot1.Asks = []orderbook.Item{} - snapShot1.Bids = []orderbook.Item{} snapShot1.AssetType = asset.Spot snapShot1.Pair = cp snapShot1.ExchangeName = "test" obl.exchangeName = "test" err := obl.LoadSnapshot(&snapShot1) - if err == nil { - t.Fatal("expected an error loading a snapshot") - } - if err.Error() != "test snapshot ask and bids are nil" { + if err != nil { t.Fatal(err) } } @@ -594,6 +520,7 @@ func TestRunSnapshotWithNoData(t *testing.T) { func TestLoadSnapshot(t *testing.T) { var obl Orderbook obl.dataHandler = make(chan interface{}, 100) + obl.ob = make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder) var snapShot1 orderbook.Base snapShot1.ExchangeName = "SnapshotWithOverride" asks := []orderbook.Item{ @@ -618,11 +545,11 @@ func TestFlushbuffer(t *testing.T) { if err != nil { t.Fatal(err) } - if obl.ob[cp][asset.Spot] == nil { + if obl.ob[cp.Base][cp.Quote][asset.Spot] == nil { t.Error("expected ob to have ask entries") } obl.FlushBuffer() - if obl.ob[cp][asset.Spot] != nil { + if obl.ob[cp.Base][cp.Quote][asset.Spot] != nil { t.Error("expected ob be flushed") } } @@ -631,6 +558,7 @@ func TestFlushbuffer(t *testing.T) { func TestInsertingSnapShots(t *testing.T) { var obl Orderbook obl.dataHandler = make(chan interface{}, 100) + obl.ob = make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder) var snapShot1 orderbook.Base snapShot1.ExchangeName = "WSORDERBOOKTEST1" asks := []orderbook.Item{ @@ -699,8 +627,8 @@ func TestInsertingSnapShots(t *testing.T) { {Price: 39, Amount: 7, ID: 22}, } - snapShot2.Asks = asks - snapShot2.Bids = bids + snapShot2.Asks = orderbook.SortAsks(asks) + snapShot2.Bids = orderbook.SortBids(bids) snapShot2.AssetType = asset.Spot snapShot2.Pair, err = currency.NewPairFromString("LTCUSD") if err != nil { @@ -740,8 +668,8 @@ func TestInsertingSnapShots(t *testing.T) { {Price: 39, Amount: 7, ID: 22}, } - snapShot3.Asks = asks - snapShot3.Bids = bids + snapShot3.Asks = orderbook.SortAsks(asks) + snapShot3.Bids = orderbook.SortBids(bids) snapShot3.AssetType = "FUTURES" snapShot3.Pair, err = currency.NewPairFromString("LTCUSD") if err != nil { @@ -751,20 +679,20 @@ func TestInsertingSnapShots(t *testing.T) { if err != nil { t.Fatal(err) } - if obl.ob[snapShot1.Pair][snapShot1.AssetType].Asks[0] != snapShot1.Asks[0] { + if obl.ob[snapShot1.Pair.Base][snapShot1.Pair.Quote][snapShot1.AssetType].ob.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]) + obl.ob[snapShot1.Pair.Base][snapShot1.Pair.Quote][snapShot1.AssetType].ob.Asks[0]) } - if obl.ob[snapShot2.Pair][snapShot2.AssetType].Asks[0] != snapShot2.Asks[0] { + if obl.ob[snapShot2.Pair.Base][snapShot1.Pair.Quote][snapShot2.AssetType].ob.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]) + obl.ob[snapShot2.Pair.Base][snapShot1.Pair.Quote][snapShot2.AssetType].ob.Asks[0]) } - if obl.ob[snapShot3.Pair][snapShot3.AssetType].Asks[0] != snapShot3.Asks[0] { + if obl.ob[snapShot3.Pair.Base][snapShot1.Pair.Quote][snapShot3.AssetType].ob.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]) + obl.ob[snapShot3.Pair.Base][snapShot1.Pair.Quote][snapShot3.AssetType].ob.Asks[0]) } } @@ -774,24 +702,66 @@ func TestGetOrderbook(t *testing.T) { t.Fatal(err) } ob := obl.GetOrderbook(cp, asset.Spot) - if obl.ob[cp][asset.Spot] != ob { - t.Error("Failed to get orderbook") + bufferOb := obl.ob[cp.Base][cp.Quote][asset.Spot] + if bufferOb.ob == ob { + t.Error("orderbooks should be separate in pointer value and not linked to orderbook package") + } + + if len(bufferOb.ob.Asks) != len(ob.Asks) || + len(bufferOb.ob.Bids) != len(ob.Bids) || + bufferOb.ob.AssetType != ob.AssetType || + bufferOb.ob.ExchangeName != ob.ExchangeName || + bufferOb.ob.LastUpdateID != ob.LastUpdateID || + bufferOb.ob.NotAggregated != ob.NotAggregated || + bufferOb.ob.Pair != ob.Pair { + t.Fatal("data on both books should be the same") } } func TestSetup(t *testing.T) { w := Orderbook{} - w.Setup(1, true, true, true, true, "hi", make(chan interface{})) - if w.obBufferLimit != 1 || + err := w.Setup(0, false, false, false, false, "", nil) + if err == nil || !errors.Is(err, errUnsetExchangeName) { + t.Fatalf("expected error %v but received %v", errUnsetExchangeName, err) + } + + err = w.Setup(0, false, false, false, false, "test", nil) + if err == nil || !errors.Is(err, errUnsetDataHandler) { + t.Fatalf("expected error %v but received %v", errUnsetDataHandler, err) + } + + err = w.Setup(0, true, false, false, false, "test", make(chan interface{})) + if err == nil || !errors.Is(err, errIssueBufferEnabledButNoLimit) { + t.Fatalf("expected error %v but received %v", errIssueBufferEnabledButNoLimit, err) + } + + err = w.Setup(1337, true, true, true, true, "test", make(chan interface{})) + if err != nil { + t.Fatal(err) + } + if w.obBufferLimit != 1337 || !w.bufferEnabled || !w.sortBuffer || !w.sortBufferByUpdateIDs || !w.updateEntriesByID || - w.exchangeName != "hi" { + w.exchangeName != "test" { t.Errorf("Setup incorrectly loaded %s", w.exchangeName) } } +func TestValidate(t *testing.T) { + w := Orderbook{} + err := w.validate(nil) + if err == nil || !errors.Is(err, errUpdateIsNil) { + t.Fatalf("expected error %v but received %v", errUpdateIsNil, err) + } + + err = w.validate(&Update{}) + if err == nil || !errors.Is(err, errUpdateNoTargets) { + t.Fatalf("expected error %v but received %v", errUpdateNoTargets, err) + } +} + func TestEnsureMultipleUpdatesViaPrice(t *testing.T) { obl, _, _, err := createSnapshot() if err != nil { @@ -799,7 +769,8 @@ func TestEnsureMultipleUpdatesViaPrice(t *testing.T) { } asks := bidAskGenerator() - obl.updateAsksByPrice(obl.ob[cp][asset.Spot], &Update{ + holder := obl.ob[cp.Base][cp.Quote][asset.Spot] + holder.updateByPrice(&Update{ Bids: asks, Asks: asks, Pair: cp, @@ -810,7 +781,327 @@ func TestEnsureMultipleUpdatesViaPrice(t *testing.T) { t.Error(err) } - if len(obl.ob[cp][asset.Spot].Asks) <= 3 { + if len(holder.ob.Asks) <= 3 { t.Errorf("Insufficient updates") } } + +func TestInsertItem(t *testing.T) { + update := []orderbook.Item{{Price: 4}} + + // Correctly aligned + asks := []orderbook.Item{ + { + Price: 1, + }, + { + Price: 2, + }, + { + Price: 3, + }, + { + Price: 5, + }, + { + Price: 6, + }, + { + Price: 7, + }, + } + + insertUpdatesAsk(update, &asks) + if asks[3].Price != 4 { + t.Fatal("incorrect insertion") + } + + bids := []orderbook.Item{ + { + Price: 7, + }, + { + Price: 6, + }, + { + Price: 5, + }, + { + Price: 3, + }, + { + Price: 2, + }, + { + Price: 1, + }, + } + + insertUpdatesBid(update, &bids) + if asks[3].Price != 4 { + t.Fatal("incorrect insertion") + } +} + +func deploySliceOrdered(size int) []orderbook.Item { + rand.Seed(time.Now().UnixNano()) + var items []orderbook.Item + for i := 0; i < size; i++ { + items = append(items, orderbook.Item{Amount: 1, Price: rand.Float64() + float64(i), ID: rand.Int63()}) // nolint:gosec // Not needed for tests + } + return items +} + +func TestUpdateByIDAndAction(t *testing.T) { + holder := orderbookHolder{} + + asks := deploySliceOrdered(100) + bids := append(asks[:0:0], asks...) + orderbook.Reverse(bids) + + book := &orderbook.Base{ + Bids: append(bids[:0:0], bids...), + Asks: append(asks[:0:0], asks...), + } + + err := book.Verify() + if err != nil { + t.Fatal(err) + } + + holder.ob = book + + err = holder.updateByIDAndAction(&Update{}) + if err == nil { + t.Fatal("error cannot be nil") + } + + err = holder.updateByIDAndAction(&Update{ + Action: Amend, + Bids: []orderbook.Item{ + { + Price: 100, + ID: 6969, + }, + }, + }) + if err == nil { + t.Fatal("error cannot be nil") + } + + // append to slice + err = holder.updateByIDAndAction(&Update{ + Action: UpdateInsert, + Bids: []orderbook.Item{ + { + Price: 0, + ID: 1337, + }, + }, + Asks: []orderbook.Item{ + { + Price: 100, + ID: 1337, + }, + }, + }) + if err != nil { + t.Fatal(err) + } + + if book.Bids[len(book.Bids)-1].Price != 0 { + t.Fatal("did not append bid item") + } + if book.Asks[len(book.Asks)-1].Price != 100 { + t.Fatal("did not append ask item") + } + + // Change amount + err = holder.updateByIDAndAction(&Update{ + Action: UpdateInsert, + Bids: []orderbook.Item{ + { + Price: 0, + ID: 1337, + Amount: 100, + }, + }, + Asks: []orderbook.Item{ + { + Price: 100, + ID: 1337, + Amount: 100, + }, + }, + }) + if err != nil { + t.Fatal(err) + } + + if book.Bids[len(book.Bids)-1].Amount != 100 { + t.Fatal("did not update bid amount") + } + + if book.Asks[len(book.Asks)-1].Amount != 100 { + t.Fatal("did not update ask amount") + } + + // Change price level + err = holder.updateByIDAndAction(&Update{ + Action: UpdateInsert, + Bids: []orderbook.Item{ + { + Price: 100, + ID: 1337, + Amount: 99, + }, + }, + Asks: []orderbook.Item{ + { + Price: 0, + ID: 1337, + Amount: 99, + }, + }, + }) + if err != nil { + t.Fatal(err) + } + + if book.Bids[0].Amount != 99 && book.Bids[0].Price != 100 { + t.Fatal("did not adjust bid item placement and details") + } + + if book.Asks[0].Amount != 99 && book.Asks[0].Amount != 0 { + t.Fatal("did not adjust ask item placement and details") + } + + book.Bids = append(bids[:0:0], bids...) // nolint:gocritic + book.Asks = append(asks[:0:0], asks...) // nolint:gocritic + + // Delete - not found + err = holder.updateByIDAndAction(&Update{ + Action: Delete, + Asks: []orderbook.Item{ + { + Price: 0, + ID: 1337, + Amount: 99, + }, + }, + }) + if err == nil { + t.Fatal("error cannot be nil") + } + err = holder.updateByIDAndAction(&Update{ + Action: Delete, + Bids: []orderbook.Item{ + { + Price: 0, + ID: 1337, + Amount: 99, + }, + }, + }) + if err == nil { + t.Fatal("error cannot be nil") + } + + // Delete - found + err = holder.updateByIDAndAction(&Update{ + Action: Delete, + Asks: []orderbook.Item{ + asks[0], + }, + }) + if err != nil { + t.Fatal(err) + } + + if len(book.Asks) != 99 { + t.Fatal("element not deleted") + } + + // Apply update + err = holder.updateByIDAndAction(&Update{ + Action: Amend, + Asks: []orderbook.Item{ + {ID: 123456}, + }, + }) + if err == nil { + t.Fatal("error cannot be nil") + } + + update := book.Asks[0] + update.Amount = 1337 + + err = holder.updateByIDAndAction(&Update{ + Action: Amend, + Asks: []orderbook.Item{ + update, + }, + }) + if err != nil { + t.Fatal(err) + } + + if book.Asks[0].Amount != 1337 { + t.Fatal("element not updated") + } +} + +func TestFlushOrderbook(t *testing.T) { + w := &Orderbook{} + err := w.Setup(5, false, false, false, false, "test", make(chan interface{}, 2)) + if err != nil { + t.Fatal(err) + } + + var snapShot1 orderbook.Base + snapShot1.ExchangeName = "Snapshooooot" + asks := []orderbook.Item{ + {Price: 4000, Amount: 1, ID: 8}, + } + bids := []orderbook.Item{ + {Price: 4000, Amount: 1, ID: 9}, + } + snapShot1.Asks = asks + snapShot1.Bids = bids + snapShot1.AssetType = asset.Spot + snapShot1.Pair = cp + + err = w.FlushOrderbook(cp, asset.Spot) + if err == nil { + t.Fatal("book not loaded error cannot be nil") + } + + o := w.GetOrderbook(cp, asset.Spot) + if o != nil { + t.Fatal("book not loaded, this should not happen") + } + + err = w.LoadSnapshot(&snapShot1) + if err != nil { + t.Fatal(err) + } + + err = w.LoadSnapshot(&snapShot1) + if err != nil { + t.Fatal(err) + } + + err = w.FlushOrderbook(cp, asset.Spot) + if err != nil { + t.Fatal(err) + } + + o = w.GetOrderbook(cp, asset.Spot) + if o == nil { + t.Fatal("cannot get book") + } + + if o.Bids != nil && o.Asks != nil { + t.Fatal("orderbook items not flushed") + } +} diff --git a/exchanges/stream/buffer/buffer_types.go b/exchanges/stream/buffer/buffer_types.go index 93020b85..ec19cedc 100644 --- a/exchanges/stream/buffer/buffer_types.go +++ b/exchanges/stream/buffer/buffer_types.go @@ -12,8 +12,7 @@ import ( // Orderbook defines a local cache of orderbooks for amending, appending // and deleting changes and updates the main store for a stream type Orderbook struct { - ob map[currency.Pair]map[asset.Item]*orderbook.Base - buffer map[currency.Pair]map[asset.Item][]*Update + ob map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder obBufferLimit int bufferEnabled bool sortBuffer bool @@ -24,13 +23,39 @@ type Orderbook struct { m sync.Mutex } +type orderbookHolder struct { + ob *orderbook.Base + buffer *[]Update +} + // Update stores orderbook updates and dictates what features to use when processing type Update struct { 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 + Action + Bids []orderbook.Item + Asks []orderbook.Item + Pair currency.Pair + + // Determines if there is a max depth of orderbooks and after an append we + // should remove any items that are outside of this scope. Kraken is the + // only exchange utilising this field. + MaxDepth int } + +// Action defines a set of differing states required to implement an incoming +// orderbook update used in conjunction with UpdateEntriesByID +type Action string + +const ( + // Amend applies amount adjustment by ID + Amend Action = "update" + // Delete removes price level from book by ID + Delete Action = "delete" + // Insert adds price level to book + Insert Action = "insert" + // UpdateInsert on conflict applies amount adjustment or appends new amount + // to book + UpdateInsert Action = "update/insert" +) diff --git a/exchanges/stream/websocket.go b/exchanges/stream/websocket.go index dd45eefc..41799f55 100644 --- a/exchanges/stream/websocket.go +++ b/exchanges/stream/websocket.go @@ -108,14 +108,13 @@ func (w *Websocket) Setup(s *WebsocketSetup) error { w.Wg = new(sync.WaitGroup) w.SetCanUseAuthenticatedEndpoints(s.AuthenticatedWebsocketAPISupport) - w.Orderbook.Setup(s.OrderbookBufferLimit, + return w.Orderbook.Setup(s.OrderbookBufferLimit, s.BufferEnabled, s.SortBuffer, s.SortBufferByUpdateIDs, s.UpdateEntriesByID, w.exchangeName, w.DataHandler) - return nil } // SetupNewConnection sets up an auth or unauth streaming connection diff --git a/exchanges/stream/websocket_test.go b/exchanges/stream/websocket_test.go index bc30b81a..15f190b6 100644 --- a/exchanges/stream/websocket_test.go +++ b/exchanges/stream/websocket_test.go @@ -92,7 +92,7 @@ func TestSetup(t *testing.T) { if err == nil { t.Fatal("error cannot be nil") } - w = &Websocket{} + w = &Websocket{DataHandler: make(chan interface{})} err = w.Setup(nil) if err == nil { t.Fatal("error cannot be nil") @@ -1207,6 +1207,7 @@ func TestSetupNewConnection(t *testing.T) { Init: true, TrafficAlert: make(chan struct{}), ReadMessageErrors: make(chan error), + DataHandler: make(chan interface{}), } err = web.Setup(defaultSetup) diff --git a/exchanges/ticker/ticker_types.go b/exchanges/ticker/ticker_types.go index ba37ed11..a645cd56 100644 --- a/exchanges/ticker/ticker_types.go +++ b/exchanges/ticker/ticker_types.go @@ -47,6 +47,14 @@ type Price struct { ExchangeName string `json:"exchangeName"` AssetType asset.Item `json:"assetType"` LastUpdated time.Time + + // Funding rate field variables + FlashReturnRate float64 + BidPeriod float64 + BidSize float64 + AskPeriod float64 + AskSize float64 + FlashReturnRateAmount float64 } // Ticker struct holds the ticker information for a currency pair and type diff --git a/exchanges/yobit/yobit_wrapper.go b/exchanges/yobit/yobit_wrapper.go index 28bc1d15..ea2113e8 100644 --- a/exchanges/yobit/yobit_wrapper.go +++ b/exchanges/yobit/yobit_wrapper.go @@ -236,18 +236,18 @@ func (y *Yobit) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderboo // UpdateOrderbook updates and returns the orderbook for a currency pair func (y *Yobit) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { - orderBook := new(orderbook.Base) + book := &orderbook.Base{ExchangeName: y.Name, Pair: p, AssetType: assetType} fpair, err := y.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } orderbookNew, err := y.GetDepth(fpair.String()) if err != nil { - return orderBook, err + return book, err } for i := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, + book.Bids = append(book.Bids, orderbook.Item{ Price: orderbookNew.Bids[i][0], Amount: orderbookNew.Bids[i][1], @@ -255,22 +255,16 @@ func (y *Yobit) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbo } for i := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, + book.Asks = append(book.Asks, orderbook.Item{ Price: orderbookNew.Asks[i][0], Amount: orderbookNew.Asks[i][1], }) } - - orderBook.Pair = p - orderBook.ExchangeName = y.Name - orderBook.AssetType = assetType - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(y.Name, p, assetType) } diff --git a/exchanges/zb/zb.go b/exchanges/zb/zb.go index 4ec22120..a45622d1 100644 --- a/exchanges/zb/zb.go +++ b/exchanges/zb/zb.go @@ -170,7 +170,7 @@ func (z *ZB) GetTicker(symbol string) (TickerResponse, error) { return res, err } -// GetTicker returns a ticker for a given symbol +// GetTrades returns trades for a given symbol func (z *ZB) GetTrades(symbol string) (TradeHistory, error) { urlPath := fmt.Sprintf("%s/%s/%s/%s?market=%s", z.API.Endpoints.URL, zbData, zbAPIVersion, zbTrades, symbol) var res TradeHistory diff --git a/exchanges/zb/zb_types.go b/exchanges/zb/zb_types.go index 7ae0a1c8..f8bf85cc 100644 --- a/exchanges/zb/zb_types.go +++ b/exchanges/zb/zb_types.go @@ -229,6 +229,7 @@ var orderSideMap = map[int64]order.Side{ 1: order.Sell, } +// TradeHistory defines a slice of historic trades type TradeHistory []struct { Amount float64 `json:"amount,string"` Date int64 `json:"date"` diff --git a/exchanges/zb/zb_websocket.go b/exchanges/zb/zb_websocket.go index 934f30af..a1d1e98c 100644 --- a/exchanges/zb/zb_websocket.go +++ b/exchanges/zb/zb_websocket.go @@ -122,17 +122,16 @@ func (z *ZB) wsHandleData(respRaw []byte) error { return err } - var asks []orderbook.Item + var book orderbook.Base for i := range depth.Asks { - asks = append(asks, orderbook.Item{ + book.Asks = append(book.Asks, orderbook.Item{ Amount: depth.Asks[i][1].(float64), Price: depth.Asks[i][0].(float64), }) } - var bids []orderbook.Item for i := range depth.Bids { - bids = append(bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Amount: depth.Bids[i][1].(float64), Price: depth.Bids[i][0].(float64), }) @@ -144,14 +143,12 @@ func (z *ZB) wsHandleData(respRaw []byte) error { return err } - var newOrderBook orderbook.Base - newOrderBook.Asks = asks - newOrderBook.Bids = bids - newOrderBook.AssetType = asset.Spot - newOrderBook.Pair = cPair - newOrderBook.ExchangeName = z.Name + orderbook.Reverse(book.Asks) // Reverse asks for correct alignment + book.AssetType = asset.Spot + book.Pair = cPair + book.ExchangeName = z.Name - err = z.Websocket.Orderbook.LoadSnapshot(&newOrderBook) + err = z.Websocket.Orderbook.LoadSnapshot(&book) if err != nil { return err } diff --git a/exchanges/zb/zb_wrapper.go b/exchanges/zb/zb_wrapper.go index 551957db..d6f186a9 100644 --- a/exchanges/zb/zb_wrapper.go +++ b/exchanges/zb/zb_wrapper.go @@ -167,6 +167,7 @@ func (z *ZB) Setup(exch *config.ExchangeConfig) error { Subscriber: z.Subscribe, Features: &z.Features.Supports.WebsocketCapabilities, OrderbookBufferLimit: exch.WebsocketOrderbookBufferLimit, + BufferEnabled: exch.WebsocketOrderbookBufferEnabled, }) if err != nil { return err @@ -291,40 +292,34 @@ func (z *ZB) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.B // UpdateOrderbook updates and returns the orderbook for a currency pair func (z *ZB) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { - orderBook := new(orderbook.Base) + book := &orderbook.Base{ExchangeName: z.Name, Pair: p, AssetType: assetType} currFormat, err := z.FormatExchangeCurrency(p, assetType) if err != nil { - return nil, err + return book, err } orderbookNew, err := z.GetOrderbook(currFormat.String()) if err != nil { - return orderBook, err + return book, err } for x := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + book.Bids = append(book.Bids, orderbook.Item{ Amount: orderbookNew.Bids[x][1], Price: orderbookNew.Bids[x][0], }) } for x := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + book.Asks = append(book.Asks, orderbook.Item{ Amount: orderbookNew.Asks[x][1], Price: orderbookNew.Asks[x][0], }) } - - orderBook.Pair = p - orderBook.AssetType = assetType - orderBook.ExchangeName = z.Name - - err = orderBook.Process() + err = book.Process() if err != nil { - return orderBook, err + return book, err } - return orderbook.Get(z.Name, p, assetType) } diff --git a/go.sum b/go.sum index 56a89552..0b5ae0a6 100644 --- a/go.sum +++ b/go.sum @@ -252,8 +252,7 @@ github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzR github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ= -github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= diff --git a/testdata/http_mock/binance/binance.json b/testdata/http_mock/binance/binance.json index 3c3cb482..d83ba1df 100644 --- a/testdata/http_mock/binance/binance.json +++ b/testdata/http_mock/binance/binance.json @@ -10995,7 +10995,7 @@ "q": "0.09509300" } ], - "queryString": "endTime=1577978345000&startTime=1577977445000&symbol=BTCUSDT", + "queryString": "endTime=1577978345000\u0026startTime=1577977445000\u0026symbol=BTCUSDT", "bodyParams": "", "headers": {} }, @@ -11022,21 +11022,21 @@ "q": "0.10000000" } ], - "queryString": "endTime=1577981045000&limit=1000&startTime=1577977445000&symbol=BTCUSDT", + "queryString": "endTime=1577981045000\u0026limit=1000\u0026startTime=1577977445000\u0026symbol=BTCUSDT", "bodyParams": "", "headers": {} }, { "data": [ { - "M": true, - "T": 1577977445500, - "a": 303004096, - "f": 329755557, - "l": 329755557, - "": false, - "p": "9195.09000000", - "q": "0.10000000" + "M": true, + "T": 1577977445500, + "a": 303004096, + "f": 329755557, + "l": 329755557, + "": false, + "p": "9195.09000000", + "q": "0.10000000" }, { "M": true, @@ -11059,21 +11059,21 @@ "q": "0.10000000" } ], - "queryString": "fromId=303004096&limit=1000&symbol=BTCUSDT", + "queryString": "fromId=303004096\u0026limit=1000\u0026symbol=BTCUSDT", "bodyParams": "", "headers": {} }, { "data": [ { - "M": true, - "T": 1577977445500, - "a": 303004096, - "f": 329755557, - "l": 329755557, - "": false, - "p": "9195.09000000", - "q": "0.10000000" + "M": true, + "T": 1577977445500, + "a": 303004096, + "f": 329755557, + "l": 329755557, + "": false, + "p": "9195.09000000", + "q": "0.10000000" }, { "M": true, @@ -11096,7 +11096,7 @@ "q": "0.10000000" } ], - "queryString": "limit=3&symbol=BTCUSDT", + "queryString": "limit=3\u0026symbol=BTCUSDT", "bodyParams": "", "headers": {} }, @@ -11113,7 +11113,7 @@ "q": "0.10000000" } ], - "queryString": "fromId=303004098&limit=1000&symbol=BTCUSDT", + "queryString": "fromId=303004098\u0026limit=1000\u0026symbol=BTCUSDT", "bodyParams": "", "headers": {} } @@ -11200,91 +11200,8011 @@ "data": { "asks": [ [ - "6621.80000000", - "0.00198100" + "19528.42000000", + "0.00600000" ], [ - "6622.14000000", - "4.00000000" + "19529.00000000", + "0.00600000" ], [ - "6622.46000000", - "2.30000000" + "19529.28000000", + "0.04403800" ], [ - "6622.47000000", - "1.18633300" + "19529.29000000", + "0.00227500" ], [ - "6622.64000000", - "4.00000000" + "19529.56000000", + "0.00600000" ], [ - "6622.73000000", - "0.02900000" + "19529.61000000", + "0.98075700" ], [ - "6622.76000000", - "0.12557700" + "19529.62000000", + "0.10182900" ], [ - "6622.81000000", - "2.08994200" + "19530.00000000", + "0.10881900" ], [ - "6622.82000000", + "19531.00000000", + "0.03750000" + ], + [ + "19531.38000000", + "0.14000000" + ], + [ + "19531.43000000", + "0.30725100" + ], + [ + "19531.50000000", + "0.68750000" + ], + [ + "19531.58000000", + "0.00600000" + ], + [ + "19531.87000000", + "0.30724500" + ], + [ + "19531.99000000", + "0.33797000" + ], + [ + "19532.00000000", + "0.03750000" + ], + [ + "19532.03000000", + "2.00000000" + ], + [ + "19532.06000000", + "0.07144200" + ], + [ + "19532.31000000", + "0.00600000" + ], + [ + "19532.50000000", + "0.03750000" + ], + [ + "19532.99000000", + "0.05120500" + ], + [ + "19533.00000000", + "0.03750000" + ], + [ + "19533.01000000", + "2.00000000" + ], + [ + "19533.09000000", + "0.27353500" + ], + [ + "19533.50000000", + "0.03750000" + ], + [ + "19533.69000000", + "0.05000000" + ], + [ + "19533.79000000", + "0.05156000" + ], + [ + "19533.96000000", "0.01500000" ], [ - "6623.17000000", - "0.16831300" + "19534.00000000", + "0.03750000" + ], + [ + "19534.14000000", + "0.00600000" + ], + [ + "19534.18000000", + "0.06928000" + ], + [ + "19534.19000000", + "0.00201300" + ], + [ + "19534.22000000", + "0.40067300" + ], + [ + "19534.50000000", + "0.03750000" + ], + [ + "19534.58000000", + "0.00600000" + ], + [ + "19534.61000000", + "0.35600000" + ], + [ + "19534.67000000", + "0.00069700" + ], + [ + "19534.68000000", + "0.88353900" + ], + [ + "19534.77000000", + "0.06130000" + ], + [ + "19534.91000000", + "1.00000000" + ], + [ + "19534.99000000", + "0.02560100" + ], + [ + "19535.00000000", + "0.03750000" + ], + [ + "19535.29000000", + "1.61700000" + ], + [ + "19535.32000000", + "0.09999900" + ], + [ + "19535.43000000", + "0.00128600" + ], + [ + "19535.50000000", + "0.03750000" + ], + [ + "19535.52000000", + "1.59300000" + ], + [ + "19535.59000000", + "0.00600000" + ], + [ + "19535.73000000", + "0.01700000" + ], + [ + "19535.78000000", + "1.58300000" + ], + [ + "19535.83000000", + "0.54660500" + ], + [ + "19536.26000000", + "1.74800000" + ], + [ + "19536.78000000", + "0.00180700" + ], + [ + "19536.84000000", + "0.10240300" + ], + [ + "19537.51000000", + "0.30300000" + ], + [ + "19537.68000000", + "0.14400000" + ], + [ + "19537.73000000", + "1.03000000" + ], + [ + "19538.25000000", + "0.80134300" + ], + [ + "19538.35000000", + "1.98901000" + ], + [ + "19538.49000000", + "0.15000000" + ], + [ + "19538.75000000", + "0.01400000" + ], + [ + "19538.91000000", + "0.14400000" + ], + [ + "19539.21000000", + "0.02000000" + ], + [ + "19539.24000000", + "0.32000000" + ], + [ + "19539.48000000", + "0.00051700" + ], + [ + "19539.66000000", + "0.32000000" + ], + [ + "19540.00000000", + "0.04283600" + ], + [ + "19540.43000000", + "0.30730100" + ], + [ + "19541.06000000", + "0.10224600" + ], + [ + "19541.27000000", + "0.15352100" + ], + [ + "19541.35000000", + "0.00600000" + ], + [ + "19541.43000000", + "0.30728600" + ], + [ + "19541.60000000", + "0.30600000" + ], + [ + "19541.69000000", + "0.14700000" + ], + [ + "19541.72000000", + "0.30000000" + ], + [ + "19541.89000000", + "0.00153300" + ], + [ + "19542.55000000", + "0.15150000" + ], + [ + "19542.60000000", + "0.00309000" + ], + [ + "19542.72000000", + "0.28099600" + ], + [ + "19542.77000000", + "0.06132600" + ], + [ + "19542.95000000", + "0.14550000" + ], + [ + "19543.32000000", + "0.00600000" + ], + [ + "19543.54000000", + "0.30300000" + ], + [ + "19543.94000000", + "1.57834700" + ], + [ + "19544.00000000", + "0.01735500" + ], + [ + "19544.14000000", + "0.16775700" + ], + [ + "19544.32000000", + "0.02671500" + ], + [ + "19544.47000000", + "0.05321700" + ], + [ + "19544.67000000", + "0.07668600" + ], + [ + "19545.00000000", + "0.07100600" + ], + [ + "19545.04000000", + "0.10643500" + ], + [ + "19545.12000000", + "0.50000000" + ], + [ + "19545.48000000", + "0.30725300" + ], + [ + "19546.16000000", + "0.05325400" + ], + [ + "19546.20000000", + "0.45000000" + ], + [ + "19546.33000000", + "0.40033200" + ], + [ + "19546.45000000", + "0.28800000" + ], + [ + "19546.57000000", + "0.14700000" + ], + [ + "19546.61000000", + "0.00600000" + ], + [ + "19547.00000000", + "0.00511500" + ], + [ + "19547.04000000", + "0.00297000" + ], + [ + "19547.07000000", + "0.12050900" + ], + [ + "19547.21000000", + "0.00600000" + ], + [ + "19547.74000000", + "0.10000000" + ], + [ + "19547.84000000", + "1.10400000" + ], + [ + "19547.88000000", + "0.23964400" + ], + [ + "19547.95000000", + "0.15600000" + ], + [ + "19548.24000000", + "0.28500000" + ], + [ + "19548.40000000", + "0.24576800" + ], + [ + "19548.64000000", + "0.00600000" + ], + [ + "19548.73000000", + "0.01020000" + ], + [ + "19548.87000000", + "0.10685800" + ], + [ + "19549.02000000", + "0.01534600" + ], + [ + "19549.21000000", + "1.00000000" + ], + [ + "19549.47000000", + "0.00202000" + ], + [ + "19549.53000000", + "0.00153500" + ], + [ + "19549.89000000", + "0.41925000" + ], + [ + "19549.98000000", + "0.00281200" + ], + [ + "19550.00000000", + "12.82517900" + ], + [ + "19550.01000000", + "0.55000000" + ], + [ + "19550.11000000", + "0.00393700" + ], + [ + "19550.12000000", + "0.00217400" + ], + [ + "19550.39000000", + "0.15150000" + ], + [ + "19550.62000000", + "0.00276200" + ], + [ + "19550.84000000", + "0.00708800" + ], + [ + "19550.94000000", + "0.15153900" + ], + [ + "19550.96000000", + "0.00201700" + ], + [ + "19550.97000000", + "0.00306000" + ], + [ + "19550.99000000", + "0.17486400" + ], + [ + "19551.00000000", + "0.00246100" + ], + [ + "19551.02000000", + "0.00075100" + ], + [ + "19551.47000000", + "0.94381900" + ], + [ + "19551.51000000", + "0.12786700" + ], + [ + "19551.53000000", + "0.00500000" + ], + [ + "19551.58000000", + "0.30000000" + ], + [ + "19551.99000000", + "0.01557400" + ], + [ + "19552.86000000", + "0.09520600" + ], + [ + "19552.87000000", + "0.00154200" + ], + [ + "19552.92000000", + "0.05000000" + ], + [ + "19553.11000000", + "0.01000000" + ], + [ + "19553.31000000", + "0.06000000" + ], + [ + "19553.48000000", + "0.05341600" + ], + [ + "19554.27000000", + "0.00202200" + ], + [ + "19554.29000000", + "0.15000000" + ], + [ + "19554.99000000", + "0.36359800" + ], + [ + "19555.00000000", + "7.91908400" + ], + [ + "19555.08000000", + "0.06909400" + ], + [ + "19555.10000000", + "0.14980000" + ], + [ + "19555.67000000", + "0.13643600" + ], + [ + "19555.68000000", + "0.06143400" + ], + [ + "19555.74000000", + "0.00078600" + ], + [ + "19555.82000000", + "0.84400000" + ], + [ + "19556.12000000", + "0.00201600" + ], + [ + "19556.39000000", + "0.18695700" + ], + [ + "19556.46000000", + "0.15000000" + ], + [ + "19556.98000000", + "0.00190000" + ], + [ + "19557.07000000", + "0.06131300" + ], + [ + "19557.09000000", + "0.00250000" + ], + [ + "19557.10000000", + "0.02321000" + ], + [ + "19557.32000000", + "0.01169500" + ], + [ + "19558.22000000", + "2.00000000" + ], + [ + "19559.00000000", + "0.75886700" + ], + [ + "19559.04000000", + "0.00326200" + ], + [ + "19559.05000000", + "0.10000000" + ], + [ + "19559.18000000", + "0.00521900" + ], + [ + "19559.21000000", + "0.31200000" + ], + [ + "19559.66000000", + "1.62000000" + ], + [ + "19559.72000000", + "0.06159400" + ], + [ + "19559.99000000", + "0.03187100" + ], + [ + "19560.00000000", + "10.92510500" + ], + [ + "19560.01000000", + "0.00184400" + ], + [ + "19560.08000000", + "0.00231000" + ], + [ + "19560.12000000", + "0.00148000" + ], + [ + "19560.15000000", + "0.00234900" + ], + [ + "19560.17000000", + "0.00250000" + ], + [ + "19560.18000000", + "0.02619900" + ], + [ + "19560.20000000", + "0.00125600" + ], + [ + "19560.22000000", + "0.00240000" + ], + [ + "19560.28000000", + "0.00103800" + ], + [ + "19560.29000000", + "0.01400000" + ], + [ + "19560.30000000", + "0.00217300" + ], + [ + "19560.31000000", + "0.02633800" + ], + [ + "19560.33000000", + "0.00102100" + ], + [ + "19560.44000000", + "0.00201800" + ], + [ + "19560.45000000", + "0.00693900" + ], + [ + "19560.52000000", + "0.01281600" + ], + [ + "19560.56000000", + "0.20350400" + ], + [ + "19560.60000000", + "0.00206800" + ], + [ + "19560.67000000", + "0.01784700" + ], + [ + "19560.68000000", + "0.10239900" + ], + [ + "19560.73000000", + "0.00077100" + ], + [ + "19560.82000000", + "0.00160800" + ], + [ + "19560.84000000", + "0.00129700" + ], + [ + "19560.86000000", + "0.06633900" + ], + [ + "19560.96000000", + "0.02145200" + ], + [ + "19560.98000000", + "0.00313700" + ], + [ + "19561.00000000", + "0.02851800" + ], + [ + "19561.05000000", + "0.01000000" + ], + [ + "19561.08000000", + "0.02448100" + ], + [ + "19561.09000000", + "0.00094900" + ], + [ + "19561.32000000", + "0.00202100" + ], + [ + "19561.37000000", + "0.00153400" + ], + [ + "19561.40000000", + "0.00203900" + ], + [ + "19561.71000000", + "0.00074100" + ], + [ + "19561.74000000", + "0.00201500" + ], + [ + "19561.80000000", + "0.00567600" + ], + [ + "19561.84000000", + "0.00204400" + ], + [ + "19561.88000000", + "0.00571200" + ], + [ + "19561.93000000", + "0.00056300" + ], + [ + "19562.00000000", + "0.00371600" + ], + [ + "19562.10000000", + "0.00201400" + ], + [ + "19562.14000000", + "0.00176800" + ], + [ + "19562.19000000", + "0.00155200" + ], + [ + "19562.31000000", + "0.00113100" + ], + [ + "19562.37000000", + "0.00901500" + ], + [ + "19562.40000000", + "0.01024500" + ], + [ + "19562.50000000", + "0.00055600" + ], + [ + "19562.59000000", + "0.01000000" + ], + [ + "19562.66000000", + "0.00900000" + ], + [ + "19562.73000000", + "0.00051600" + ], + [ + "19562.76000000", + "0.00148600" + ], + [ + "19562.77000000", + "0.00200000" + ], + [ + "19562.93000000", + "0.00056300" + ], + [ + "19562.96000000", + "0.00076900" + ], + [ + "19563.00000000", + "0.07890900" + ], + [ + "19563.04000000", + "0.00051600" + ], + [ + "19563.06000000", + "0.00326100" + ], + [ + "19563.12000000", + "2.06322700" + ], + [ + "19563.17000000", + "0.00206700" + ], + [ + "19563.22000000", + "0.01023100" + ], + [ + "19563.27000000", + "0.03561800" + ], + [ + "19563.32000000", + "0.00051600" + ], + [ + "19563.36000000", + "0.00127600" + ], + [ + "19563.40000000", + "0.00203000" + ], + [ + "19563.60000000", + "0.00104200" + ], + [ + "19563.67000000", + "0.00877100" + ], + [ + "19563.68000000", + "0.00051600" + ], + [ + "19563.71000000", + "0.00160800" + ], + [ + "19563.76000000", + "0.35000000" + ], + [ + "19563.80000000", + "0.00284600" + ], + [ + "19563.83000000", + "0.00078200" + ], + [ + "19563.92000000", + "0.01033500" + ], + [ + "19563.97000000", + "0.01904900" + ], + [ + "19564.00000000", + "1.00061500" + ], + [ + "19564.02000000", + "0.00058300" + ], + [ + "19564.06000000", + "1.00000000" + ], + [ + "19564.16000000", + "0.00160800" + ], + [ + "19564.28000000", + "0.09000000" + ], + [ + "19564.33000000", + "0.00052200" + ], + [ + "19564.39000000", + "0.02627400" + ], + [ + "19564.40000000", + "0.00103200" + ], + [ + "19564.47000000", + "0.00228800" + ], + [ + "19564.49000000", + "0.60370500" + ], + [ + "19564.53000000", + "0.00129000" + ], + [ + "19564.58000000", + "0.33977100" + ], + [ + "19564.64000000", + "0.00852100" + ], + [ + "19564.84000000", + "0.00251600" + ], + [ + "19564.88000000", + "0.01022700" + ], + [ + "19564.89000000", + "0.06142400" + ], + [ + "19564.94000000", + "0.00056800" + ], + [ + "19564.97000000", + "0.00060800" + ], + [ + "19565.00000000", + "0.01948900" + ], + [ + "19565.06000000", + "1.00000000" + ], + [ + "19565.09000000", + "0.00051700" + ], + [ + "19565.11000000", + "0.00200000" + ], + [ + "19565.24000000", + "0.00171600" + ], + [ + "19565.36000000", + "0.00321300" + ], + [ + "19565.39000000", + "0.00065600" + ], + [ + "19565.40000000", + "0.06800000" + ], + [ + "19565.43000000", + "0.00200000" + ], + [ + "19565.48000000", + "0.03174200" + ], + [ + "19565.51000000", + "0.02358800" + ], + [ + "19565.55000000", + "0.13504000" + ], + [ + "19565.62000000", + "0.02352100" + ], + [ + "19565.74000000", + "0.00157100" + ], + [ + "19565.77000000", + "0.00056600" + ], + [ + "19565.89000000", + "0.00200000" + ], + [ + "19565.98000000", + "0.00203900" + ], + [ + "19565.99000000", + "0.00107100" + ], + [ + "19566.00000000", + "1.10246700" + ], + [ + "19566.09000000", + "0.01000000" + ], + [ + "19566.26000000", + "0.00153300" + ], + [ + "19566.30000000", + "0.00502300" + ], + [ + "19566.36000000", + "0.00410900" + ], + [ + "19566.46000000", + "0.00051600" + ], + [ + "19566.61000000", + "0.00268400" + ], + [ + "19566.63000000", + "0.00168000" + ], + [ + "19566.64000000", + "0.00153400" + ], + [ + "19566.66000000", + "0.02096200" + ], + [ + "19566.81000000", + "0.00375000" + ], + [ + "19566.86000000", + "0.00051600" + ], + [ + "19566.91000000", + "0.00103600" + ], + [ + "19566.95000000", + "0.00192900" + ], + [ + "19567.00000000", + "1.82919200" + ], + [ + "19567.09000000", + "0.00400300" + ], + [ + "19567.12000000", + "0.00204800" + ], + [ + "19567.14000000", + "0.00093400" + ], + [ + "19567.34000000", + "0.00170000" + ], + [ + "19567.40000000", + "0.06487300" + ], + [ + "19567.45000000", + "0.00112900" + ], + [ + "19567.47000000", + "0.00056800" + ], + [ + "19567.50000000", + "0.00211200" + ], + [ + "19567.55000000", + "0.00154000" + ], + [ + "19567.57000000", + "0.10000000" + ], + [ + "19567.70000000", + "0.00215800" + ], + [ + "19567.74000000", + "0.00220000" + ], + [ + "19567.81000000", + "0.10233400" + ], + [ + "19567.89000000", + "0.16199400" + ], + [ + "19567.93000000", + "0.00203100" + ], + [ + "19567.96000000", + "0.03920000" + ], + [ + "19567.99000000", + "0.00070200" + ], + [ + "19568.00000000", + "0.28882400" + ], + [ + "19568.07000000", + "0.03651200" + ], + [ + "19568.09000000", + "0.01400000" + ], + [ + "19568.18000000", + "0.00566300" + ], + [ + "19568.22000000", + "2.00000000" + ], + [ + "19568.34000000", + "0.00699400" + ], + [ + "19568.42000000", + "0.00130000" + ], + [ + "19568.47000000", + "0.00281200" + ], + [ + "19568.51000000", + "0.00052500" + ], + [ + "19568.83000000", + "0.01000000" + ], + [ + "19568.90000000", + "0.01918400" + ], + [ + "19568.99000000", + "0.01780000" + ], + [ + "19569.00000000", + "0.62372200" + ], + [ + "19569.05000000", + "0.10000000" + ], + [ + "19569.10000000", + "0.00149300" + ], + [ + "19569.11000000", + "0.00147800" + ], + [ + "19569.23000000", + "0.00087000" + ], + [ + "19569.27000000", + "0.00129800" + ], + [ + "19569.29000000", + "0.00110700" + ], + [ + "19569.30000000", + "0.01429700" + ], + [ + "19569.47000000", + "0.00077000" + ], + [ + "19569.70000000", + "0.00436300" + ], + [ + "19569.72000000", + "0.00115500" + ], + [ + "19569.76000000", + "0.00065000" + ], + [ + "19569.80000000", + "0.00132900" + ], + [ + "19569.82000000", + "0.00412300" + ], + [ + "19569.83000000", + "0.00550000" + ], + [ + "19570.00000000", + "14.66347000" + ], + [ + "19570.05000000", + "0.00236000" + ], + [ + "19570.19000000", + "0.00161700" + ], + [ + "19570.28000000", + "0.02600000" + ], + [ + "19570.33000000", + "0.01028000" + ], + [ + "19570.39000000", + "0.00511000" + ], + [ + "19570.45000000", + "0.00148100" + ], + [ + "19570.51000000", + "0.00400800" + ], + [ + "19570.55000000", + "0.02000000" + ], + [ + "19570.65000000", + "0.02000000" + ], + [ + "19570.67000000", + "0.01029000" + ], + [ + "19570.73000000", + "0.00202900" + ], + [ + "19570.74000000", + "0.00250000" + ], + [ + "19570.79000000", + "0.00071600" + ], + [ + "19570.93000000", + "0.00102600" + ], + [ + "19571.00000000", + "0.65143400" + ], + [ + "19571.04000000", + "0.15814500" + ], + [ + "19571.15000000", + "0.00153300" + ], + [ + "19571.28000000", + "0.00963000" + ], + [ + "19571.30000000", + "0.00426400" + ], + [ + "19571.39000000", + "0.00060100" + ], + [ + "19571.40000000", + "0.00513900" + ], + [ + "19571.42000000", + "0.11598900" + ], + [ + "19571.43000000", + "0.02858200" + ], + [ + "19571.77000000", + "0.00056800" + ], + [ + "19571.78000000", + "0.00087600" + ], + [ + "19571.79000000", + "0.00174000" + ], + [ + "19571.83000000", + "0.00148300" + ], + [ + "19571.91000000", + "0.00061400" + ], + [ + "19571.93000000", + "0.00077100" + ], + [ + "19571.94000000", + "0.00056800" + ], + [ + "19571.97000000", + "0.00102400" + ], + [ + "19572.00000000", + "0.00327000" + ], + [ + "19572.13000000", + "0.00750000" + ], + [ + "19572.16000000", + "0.00724900" + ], + [ + "19572.20000000", + "0.00851000" + ], + [ + "19572.35000000", + "0.00516500" + ], + [ + "19572.38000000", + "0.00109000" + ], + [ + "19572.45000000", + "0.00051600" + ], + [ + "19572.57000000", + "0.00260000" + ], + [ + "19572.63000000", + "0.00087500" + ], + [ + "19572.72000000", + "0.00097300" + ], + [ + "19572.78000000", + "0.00250000" + ], + [ + "19572.87000000", + "0.00297100" + ], + [ + "19572.93000000", + "0.00153300" + ], + [ + "19572.95000000", + "0.00614700" + ], + [ + "19572.96000000", + "0.00217200" + ], + [ + "19573.00000000", + "0.28594600" + ], + [ + "19573.08000000", + "0.00105000" + ], + [ + "19573.11000000", + "0.00077100" + ], + [ + "19573.17000000", + "0.00077500" + ], + [ + "19573.21000000", + "0.00051600" + ], + [ + "19573.23000000", + "0.08644500" + ], + [ + "19573.24000000", + "0.00059100" + ], + [ + "19573.27000000", + "0.00210700" + ], + [ + "19573.43000000", + "1.36795900" + ], + [ + "19573.77000000", + "0.00223100" + ], + [ + "19573.86000000", + "0.01021800" + ], + [ + "19573.91000000", + "0.00051900" + ], + [ + "19573.95000000", + "0.00160800" + ], + [ + "19574.00000000", + "0.17709400" + ], + [ + "19574.14000000", + "0.02352100" + ], + [ + "19574.29000000", + "0.01298300" + ], + [ + "19574.37000000", + "0.00582100" + ], + [ + "19574.44000000", + "0.00202900" + ], + [ + "19574.45000000", + "0.10000000" + ], + [ + "19574.47000000", + "0.00052200" + ], + [ + "19574.53000000", + "0.00150500" + ], + [ + "19574.56000000", + "0.00077900" + ], + [ + "19574.61000000", + "0.03920000" + ], + [ + "19574.64000000", + "0.00153300" + ], + [ + "19574.67000000", + "0.00128400" + ], + [ + "19574.68000000", + "0.00051500" + ], + [ + "19574.74000000", + "0.00947500" + ], + [ + "19574.77000000", + "0.00203500" + ], + [ + "19574.96000000", + "0.00051800" + ], + [ + "19574.98000000", + "0.00802200" + ], + [ + "19575.00000000", + "2.02980800" + ], + [ + "19575.02000000", + "0.00350000" + ], + [ + "19575.04000000", + "0.01000000" + ], + [ + "19575.05000000", + "1.13163800" + ], + [ + "19575.10000000", + "0.00700900" + ], + [ + "19575.12000000", + "0.00127100" + ], + [ + "19575.15000000", + "0.00055000" + ], + [ + "19575.21000000", + "0.00400100" + ], + [ + "19575.25000000", + "0.00286800" + ], + [ + "19575.32000000", + "0.42930000" + ], + [ + "19575.36000000", + "0.02600000" + ], + [ + "19575.37000000", + "0.11080000" + ], + [ + "19575.41000000", + "0.00051600" + ], + [ + "19575.48000000", + "0.08800000" + ], + [ + "19575.52000000", + "0.20000000" + ], + [ + "19575.71000000", + "0.00052700" + ], + [ + "19575.81000000", + "0.00203900" + ], + [ + "19575.89000000", + "0.01400000" + ], + [ + "19575.92000000", + "0.00839800" + ], + [ + "19575.97000000", + "0.05136200" + ], + [ + "19576.00000000", + "0.41993900" + ], + [ + "19576.01000000", + "0.00198900" + ], + [ + "19576.04000000", + "0.00153200" + ], + [ + "19576.10000000", + "0.00502100" + ], + [ + "19576.15000000", + "0.00085600" + ], + [ + "19576.16000000", + "0.00317500" + ], + [ + "19576.22000000", + "0.00055500" + ], + [ + "19576.28000000", + "0.00330000" + ], + [ + "19576.34000000", + "0.00084200" + ], + [ + "19576.47000000", + "0.01038800" + ], + [ + "19576.51000000", + "0.00051600" + ], + [ + "19576.55000000", + "0.01423200" + ], + [ + "19576.57000000", + "0.00150100" + ], + [ + "19576.73000000", + "0.00160700" + ], + [ + "19576.77000000", + "0.10000000" + ], + [ + "19576.79000000", + "0.00102800" + ], + [ + "19576.81000000", + "0.00069000" + ], + [ + "19576.93000000", + "0.02033200" + ], + [ + "19576.96000000", + "0.00064700" + ], + [ + "19576.99000000", + "0.00493000" + ], + [ + "19577.00000000", + "0.17724300" + ], + [ + "19577.20000000", + "0.00163600" + ], + [ + "19577.25000000", + "0.01000000" + ], + [ + "19577.28000000", + "0.00051600" + ], + [ + "19577.47000000", + "0.00526200" + ], + [ + "19577.49000000", + "0.00104800" + ], + [ + "19577.53000000", + "0.10238800" + ], + [ + "19577.56000000", + "0.00256400" + ], + [ + "19577.77000000", + "0.00174200" + ], + [ + "19577.94000000", + "0.01549200" + ], + [ + "19577.99000000", + "0.00051600" + ], + [ + "19578.00000000", + "0.02589000" + ], + [ + "19578.01000000", + "0.00086800" + ], + [ + "19578.02000000", + "0.00300100" + ], + [ + "19578.14000000", + "0.01023500" + ], + [ + "19578.19000000", + "0.00129100" + ], + [ + "19578.23000000", + "0.00051500" + ], + [ + "19578.31000000", + "0.00051600" + ], + [ + "19578.36000000", + "0.14000000" + ], + [ + "19578.45000000", + "0.19691700" + ], + [ + "19578.50000000", + "0.00056600" + ], + [ + "19578.55000000", + "0.00276800" + ], + [ + "19578.61000000", + "0.00393500" + ], + [ + "19578.62000000", + "0.00085300" + ], + [ + "19578.72000000", + "0.00051600" + ], + [ + "19578.73000000", + "0.00077000" + ], + [ + "19578.86000000", + "0.00703500" + ], + [ + "19578.95000000", + "0.00185900" + ], + [ + "19578.99000000", + "0.00051600" + ], + [ + "19579.00000000", + "0.08352200" + ], + [ + "19579.05000000", + "0.10000000" + ], + [ + "19579.13000000", + "0.00516400" + ], + [ + "19579.20000000", + "0.00354500" + ], + [ + "19579.26000000", + "0.00150000" + ], + [ + "19579.32000000", + "0.02951100" + ], + [ + "19579.36000000", + "0.00070100" + ], + [ + "19579.45000000", + "0.00333200" + ], + [ + "19579.46000000", + "0.00115600" + ], + [ + "19579.68000000", + "0.00202500" + ], + [ + "19579.88000000", + "0.00051600" + ], + [ + "19579.89000000", + "0.00340000" + ], + [ + "19579.95000000", + "0.00568000" + ], + [ + "19579.97000000", + "0.37794700" + ], + [ + "19579.98000000", + "0.02893400" + ], + [ + "19579.99000000", + "0.00100000" + ], + [ + "19580.00000000", + "21.25989900" + ], + [ + "19580.01000000", + "0.09705400" + ], + [ + "19580.04000000", + "0.00056800" + ], + [ + "19580.09000000", + "0.00349400" + ], + [ + "19580.11000000", + "0.02500000" + ], + [ + "19580.18000000", + "0.10000000" + ], + [ + "19580.20000000", + "0.00200000" + ], + [ + "19580.27000000", + "0.00100000" + ], + [ + "19580.39000000", + "0.00359200" + ], + [ + "19580.46000000", + "0.00212200" + ], + [ + "19580.49000000", + "0.00279300" + ], + [ + "19580.55000000", + "0.00258200" + ], + [ + "19580.59000000", + "0.00273300" + ], + [ + "19580.60000000", + "0.00087600" + ], + [ + "19580.62000000", + "0.00845400" + ], + [ + "19580.68000000", + "0.00379100" + ], + [ + "19580.71000000", + "0.00064000" + ], + [ + "19580.75000000", + "0.00209800" + ], + [ + "19580.81000000", + "0.00513700" + ], + [ + "19580.84000000", + "0.00593700" + ], + [ + "19580.85000000", + "0.00149100" + ], + [ + "19580.88000000", + "0.00051900" + ], + [ + "19580.94000000", + "0.00153200" + ], + [ + "19581.00000000", + "0.03478300" + ], + [ + "19581.04000000", + "0.00100000" + ], + [ + "19581.17000000", + "0.00250000" + ], + [ + "19581.23000000", + "0.00051600" + ], + [ + "19581.45000000", + "0.00414500" + ], + [ + "19581.48000000", + "0.00383000" + ], + [ + "19581.54000000", + "0.00207100" + ], + [ + "19581.63000000", + "0.00075100" + ], + [ + "19581.66000000", + "0.00201900" + ], + [ + "19581.74000000", + "0.10973800" + ], + [ + "19581.79000000", + "0.02005100" + ], + [ + "19581.80000000", + "0.06510400" + ], + [ + "19581.81000000", + "0.00151800" + ], + [ + "19581.86000000", + "0.10000000" + ], + [ + "19581.94000000", + "0.00051500" + ], + [ + "19582.00000000", + "1.01171300" + ], + [ + "19582.07000000", + "0.00060000" + ], + [ + "19582.09000000", + "0.01000000" + ], + [ + "19582.16000000", + "0.01339400" + ], + [ + "19582.30000000", + "0.00116300" + ], + [ + "19582.43000000", + "0.00289100" + ], + [ + "19582.44000000", + "0.00783300" + ], + [ + "19582.46000000", + "0.03038200" + ], + [ + "19582.48000000", + "0.01404700" + ], + [ + "19582.50000000", + "0.01789600" + ], + [ + "19582.57000000", + "0.00103200" + ], + [ + "19582.58000000", + "0.00111600" + ], + [ + "19582.64000000", + "0.00080100" + ], + [ + "19582.65000000", + "0.00350000" + ], + [ + "19582.68000000", + "0.02352100" + ], + [ + "19582.70000000", + "0.00117800" + ], + [ + "19582.77000000", + "0.00077700" + ], + [ + "19582.81000000", + "0.00059600" + ], + [ + "19582.84000000", + "0.00399800" + ], + [ + "19582.87000000", + "0.00425500" + ], + [ + "19582.88000000", + "0.00572100" + ], + [ + "19582.97000000", + "0.01858300" + ], + [ + "19583.00000000", + "0.07620900" + ], + [ + "19583.13000000", + "0.00077100" + ], + [ + "19583.23000000", + "0.00061700" + ], + [ + "19583.26000000", + "0.01743100" + ], + [ + "19583.33000000", + "0.01685200" + ], + [ + "19583.34000000", + "0.00200000" + ], + [ + "19583.46000000", + "0.00080000" + ], + [ + "19583.47000000", + "0.00582300" + ], + [ + "19583.60000000", + "0.00311800" + ], + [ + "19583.69000000", + "0.01520000" + ], + [ + "19583.71000000", + "0.00205300" + ], + [ + "19583.86000000", + "0.00051600" + ], + [ + "19583.90000000", + "0.00516600" + ], + [ + "19583.99000000", + "0.00226100" + ], + [ + "19584.00000000", + "0.04702600" + ], + [ + "19584.14000000", + "0.05397900" + ], + [ + "19584.17000000", + "0.10000000" + ], + [ + "19584.33000000", + "0.01000000" + ], + [ + "19584.41000000", + "0.01280700" + ], + [ + "19584.46000000", + "0.07705500" + ], + [ + "19584.53000000", + "0.00127500" + ], + [ + "19584.57000000", + "0.00066700" + ], + [ + "19584.65000000", + "0.00677200" + ], + [ + "19584.76000000", + "0.00642100" + ], + [ + "19584.84000000", + "0.01000000" + ], + [ + "19585.00000000", + "1.24442400" + ], + [ + "19585.15000000", + "0.00367200" + ], + [ + "19585.27000000", + "0.01000000" + ], + [ + "19585.41000000", + "0.00115800" + ], + [ + "19585.55000000", + "0.11220600" + ], + [ + "19585.61000000", + "0.00098000" + ], + [ + "19585.66000000", + "0.00115900" + ], + [ + "19585.71000000", + "0.00370000" + ], + [ + "19585.83000000", + "0.00153200" + ], + [ + "19585.90000000", + "0.00501800" + ], + [ + "19585.96000000", + "0.00422600" + ], + [ + "19586.00000000", + "0.01876100" + ], + [ + "19586.04000000", + "0.00128500" + ], + [ + "19586.13000000", + "0.00052000" + ], + [ + "19586.16000000", + "0.00389400" + ], + [ + "19586.17000000", + "0.02827000" + ], + [ + "19586.20000000", + "0.00141800" + ], + [ + "19586.27000000", + "0.00530800" + ], + [ + "19586.29000000", + "0.03980600" + ], + [ + "19586.30000000", + "0.08000000" + ], + [ + "19586.31000000", + "0.00513500" + ], + [ + "19586.33000000", + "0.00308400" + ], + [ + "19586.34000000", + "0.00513900" + ], + [ + "19586.41000000", + "0.01998000" + ], + [ + "19586.45000000", + "0.00051500" + ], + [ + "19586.51000000", + "0.00062500" + ], + [ + "19586.55000000", + "0.03078600" + ], + [ + "19586.58000000", + "0.05238200" + ], + [ + "19586.61000000", + "0.01860400" + ], + [ + "19586.66000000", + "0.00397800" + ], + [ + "19586.74000000", + "0.00059200" + ], + [ + "19586.75000000", + "0.00514600" + ], + [ + "19586.89000000", + "0.00205000" + ], + [ + "19586.90000000", + "0.17148700" + ], + [ + "19586.94000000", + "0.00061600" + ], + [ + "19586.96000000", + "0.06248000" + ], + [ + "19586.98000000", + "0.00083600" + ], + [ + "19587.00000000", + "0.19891300" + ], + [ + "19587.01000000", + "0.01797100" + ], + [ + "19587.09000000", + "0.00308400" + ], + [ + "19587.12000000", + "0.00056100" + ], + [ + "19587.14000000", + "0.00104800" + ], + [ + "19587.15000000", + "0.00765800" + ], + [ + "19587.35000000", + "0.00313600" + ], + [ + "19587.56000000", + "0.00354900" + ], + [ + "19587.61000000", + "0.01041200" + ], + [ + "19587.64000000", + "0.00513500" + ], + [ + "19587.70000000", + "0.00154100" + ], + [ + "19587.77000000", + "0.00378400" + ], + [ + "19587.78000000", + "0.01000000" + ], + [ + "19587.89000000", + "0.00202600" + ], + [ + "19588.00000000", + "0.80146500" + ], + [ + "19588.03000000", + "0.00115300" + ], + [ + "19588.08000000", + "0.00520000" + ], + [ + "19588.23000000", + "0.00500000" + ], + [ + "19588.31000000", + "0.00154900" + ], + [ + "19588.39000000", + "0.00331000" + ], + [ + "19588.46000000", + "0.00661700" + ], + [ + "19588.47000000", + "0.00129000" + ], + [ + "19588.48000000", + "0.00255300" + ], + [ + "19588.59000000", + "0.00510000" + ], + [ + "19588.68000000", + "0.00256600" + ], + [ + "19588.78000000", + "0.00408700" + ], + [ + "19588.88000000", + "0.45386200" + ], + [ + "19588.90000000", + "0.31286000" + ], + [ + "19588.91000000", + "0.00600800" + ], + [ + "19588.93000000", + "0.00280500" + ], + [ + "19588.95000000", + "0.10000000" + ], + [ + "19589.00000000", + "0.08675500" + ], + [ + "19589.05000000", + "0.10000000" + ], + [ + "19589.07000000", + "0.00158800" + ], + [ + "19589.09000000", + "0.00253900" + ], + [ + "19589.15000000", + "0.01000000" + ], + [ + "19589.20000000", + "0.00077600" + ], + [ + "19589.22000000", + "0.00202300" + ], + [ + "19589.35000000", + "0.00051500" + ], + [ + "19589.37000000", + "0.02841200" + ], + [ + "19589.43000000", + "0.00172400" + ], + [ + "19589.44000000", + "0.00078000" + ], + [ + "19589.45000000", + "0.00513500" + ], + [ + "19589.57000000", + "0.07115000" + ], + [ + "19589.63000000", + "0.00051500" + ], + [ + "19589.65000000", + "0.00180000" + ], + [ + "19589.74000000", + "0.03000000" + ], + [ + "19589.79000000", + "0.00083000" + ], + [ + "19589.87000000", + "0.00620000" + ], + [ + "19589.88000000", + "0.00256000" + ], + [ + "19590.00000000", + "16.53691600" + ], + [ + "19590.01000000", + "0.00883300" + ], + [ + "19590.17000000", + "0.00909900" + ], + [ + "19590.22000000", + "0.00059100" + ], + [ + "19590.24000000", + "2.00000000" + ], + [ + "19590.26000000", + "0.00079400" + ], + [ + "19590.28000000", + "0.00255500" + ], + [ + "19590.33000000", + "0.01411500" + ], + [ + "19590.42000000", + "0.00456600" + ], + [ + "19590.52000000", + "0.00768900" + ], + [ + "19590.62000000", + "0.00100000" + ], + [ + "19590.68000000", + "0.00051500" + ], + [ + "19590.70000000", + "2.00000000" + ], + [ + "19590.71000000", + "0.07079500" + ], + [ + "19590.73000000", + "0.00153100" + ], + [ + "19590.78000000", + "0.00051500" + ], + [ + "19590.83000000", + "0.00062300" + ], + [ + "19590.90000000", + "0.00102900" + ], + [ + "19591.00000000", + "0.03279500" + ], + [ + "19591.06000000", + "0.02000000" + ], + [ + "19591.07000000", + "0.00170500" + ], + [ + "19591.21000000", + "0.02352100" + ], + [ + "19591.23000000", + "0.00204200" + ], + [ + "19591.47000000", + "0.01400000" + ], + [ + "19591.48000000", + "0.00102100" + ], + [ + "19591.54000000", + "0.00503500" + ], + [ + "19591.55000000", + "0.00125400" + ], + [ + "19591.64000000", + "0.00128400" + ], + [ + "19591.70000000", + "0.00104200" + ], + [ + "19591.72000000", + "0.03104100" + ], + [ + "19591.73000000", + "0.17676900" + ], + [ + "19591.74000000", + "0.00051500" + ], + [ + "19591.83000000", + "0.00136300" + ], + [ + "19591.88000000", + "0.00176800" + ], + [ + "19591.97000000", + "0.01069100" + ], + [ + "19592.00000000", + "0.04232700" + ], + [ + "19592.04000000", + "0.00236300" + ], + [ + "19592.08000000", + "0.00172000" + ], + [ + "19592.10000000", + "0.01016000" + ], + [ + "19592.11000000", + "0.03980600" + ], + [ + "19592.12000000", + "0.00154100" + ], + [ + "19592.13000000", + "0.01000000" + ], + [ + "19592.34000000", + "0.00158700" + ], + [ + "19592.48000000", + "0.00774800" + ], + [ + "19592.49000000", + "0.00051500" + ], + [ + "19592.62000000", + "0.00701700" + ], + [ + "19592.71000000", + "0.01176400" + ], + [ + "19592.83000000", + "0.00068500" + ], + [ + "19592.94000000", + "0.00278800" + ], + [ + "19592.97000000", + "0.00617400" + ], + [ + "19593.00000000", + "0.35718800" + ], + [ + "19593.07000000", + "0.00514500" + ], + [ + "19593.25000000", + "0.01000000" + ], + [ + "19593.31000000", + "0.00051600" + ], + [ + "19593.33000000", + "0.00398000" + ], + [ + "19593.36000000", + "0.00370500" + ], + [ + "19593.42000000", + "0.00113600" + ], + [ + "19593.45000000", + "0.00093100" + ], + [ + "19593.50000000", + "0.00725000" + ], + [ + "19593.60000000", + "0.00154800" + ], + [ + "19593.65000000", + "0.00052600" + ], + [ + "19593.71000000", + "0.00135200" + ], + [ + "19593.79000000", + "0.00060100" + ], + [ + "19593.84000000", + "0.00066400" + ], + [ + "19594.00000000", + "0.32760500" + ], + [ + "19594.02000000", + "0.00070000" + ], + [ + "19594.12000000", + "0.00512600" + ], + [ + "19594.16000000", + "0.00140200" + ], + [ + "19594.17000000", + "0.00909700" + ], + [ + "19594.19000000", + "0.00204300" + ], + [ + "19594.20000000", + "0.00269700" + ], + [ + "19594.21000000", + "0.01032800" + ], + [ + "19594.34000000", + "0.00077100" + ], + [ + "19594.35000000", + "0.00321200" + ], + [ + "19594.37000000", + "0.00568600" + ], + [ + "19594.47000000", + "0.00115400" + ], + [ + "19594.52000000", + "0.00052300" + ], + [ + "19594.55000000", + "0.01000000" + ], + [ + "19594.59000000", + "0.02196600" + ], + [ + "19594.69000000", + "0.01842500" + ], + [ + "19594.73000000", + "0.01800000" + ], + [ + "19594.78000000", + "0.00973900" + ], + [ + "19594.83000000", + "0.00153900" + ], + [ + "19594.86000000", + "0.43705000" + ], + [ + "19594.89000000", + "0.00102000" + ], + [ + "19594.91000000", + "0.38610000" + ], + [ + "19594.92000000", + "0.16711000" + ], + [ + "19595.00000000", + "0.57299700" + ], + [ + "19595.01000000", + "0.05000000" + ], + [ + "19595.09000000", + "0.04556900" + ], + [ + "19595.16000000", + "0.00056200" + ], + [ + "19595.25000000", + "0.00110400" + ], + [ + "19595.31000000", + "0.00161800" + ], + [ + "19595.44000000", + "0.02600000" + ], + [ + "19595.50000000", + "0.01382100" + ], + [ + "19595.52000000", + "0.02600000" + ], + [ + "19595.54000000", + "0.01394000" + ], + [ + "19595.55000000", + "0.01350000" + ], + [ + "19595.62000000", + "0.04225300" + ], + [ + "19595.63000000", + "0.00153100" + ], + [ + "19595.70000000", + "0.00501600" + ], + [ + "19595.72000000", + "0.00148900" + ], + [ + "19595.90000000", + "0.01401000" + ], + [ + "19595.95000000", + "0.06500000" + ], + [ + "19595.99000000", + "0.00076500" + ], + [ + "19596.00000000", + "0.05492000" + ], + [ + "19596.04000000", + "0.00468800" + ], + [ + "19596.05000000", + "0.00173100" + ], + [ + "19596.24000000", + "0.00153800" + ], + [ + "19596.31000000", + "0.03953100" + ], + [ + "19596.34000000", + "0.00306700" + ], + [ + "19596.43000000", + "0.01260000" + ], + [ + "19596.54000000", + "0.00169900" + ], + [ + "19596.61000000", + "0.00200000" + ], + [ + "19596.71000000", + "0.00178300" + ], + [ + "19596.72000000", + "1.47673000" + ], + [ + "19596.73000000", + "0.00217800" + ], + [ + "19596.84000000", + "0.00275900" + ], + [ + "19596.85000000", + "0.00402700" + ], + [ + "19596.88000000", + "0.01395500" + ], + [ + "19596.90000000", + "0.00411700" + ], + [ + "19596.92000000", + "0.00056200" + ], + [ + "19596.94000000", + "0.00075100" + ], + [ + "19596.96000000", + "0.04186300" + ], + [ + "19596.97000000", + "0.00056100" + ], + [ + "19596.98000000", + "0.13120600" + ], + [ + "19597.00000000", + "0.20354600" + ], + [ + "19597.31000000", + "0.00170400" + ], + [ + "19597.38000000", + "0.05109600" + ], + [ + "19597.40000000", + "0.00203400" + ], + [ + "19597.46000000", + "0.00057000" + ], + [ + "19597.48000000", + "0.00102600" + ], + [ + "19597.60000000", + "0.00080000" + ], + [ + "19597.65000000", + "0.00148100" + ], + [ + "19597.67000000", + "0.00510300" + ], + [ + "19597.97000000", + "0.00196700" + ], + [ + "19597.99000000", + "0.00299500" + ], + [ + "19598.00000000", + "2.81703700" + ], + [ + "19598.01000000", + "1.80702300" + ], + [ + "19598.12000000", + "0.00200000" + ], + [ + "19598.17000000", + "0.00065200" + ], + [ + "19598.23000000", + "0.00469200" + ], + [ + "19598.24000000", + "0.02092400" + ], + [ + "19598.33000000", + "0.00601300" + ], + [ + "19598.37000000", + "0.03910000" + ], + [ + "19598.42000000", + "0.00996600" + ], + [ + "19598.52000000", + "0.05102400" + ], + [ + "19598.54000000", + "0.07942500" + ], + [ + "19598.64000000", + "0.00103200" + ], + [ + "19598.73000000", + "0.00202700" + ], + [ + "19598.76000000", + "0.02600000" + ], + [ + "19598.88000000", + "0.00051500" + ], + [ + "19598.89000000", + "0.00051500" + ], + [ + "19598.94000000", + "0.00112200" + ], + [ + "19598.98000000", + "0.01000000" + ], + [ + "19598.99000000", + "0.71745700" + ], + [ + "19599.00000000", + "4.65022100" + ], + [ + "19599.05000000", + "0.10000000" + ], + [ + "19599.08000000", + "0.00051500" + ], + [ + "19599.09000000", + "0.00522800" + ], + [ + "19599.10000000", + "0.05000000" + ], + [ + "19599.11000000", + "0.00500000" + ], + [ + "19599.20000000", + "0.00714800" + ], + [ + "19599.21000000", + "0.34800000" + ], + [ + "19599.30000000", + "0.00062600" + ], + [ + "19599.42000000", + "0.00054200" + ], + [ + "19599.48000000", + "0.00581100" + ], + [ + "19599.49000000", + "0.00117300" + ], + [ + "19599.58000000", + "0.00769000" + ], + [ + "19599.65000000", + "0.00086700" + ], + [ + "19599.74000000", + "0.02352100" + ], + [ + "19599.75000000", + "0.00885000" + ], + [ + "19599.83000000", + "0.11245500" + ], + [ + "19599.90000000", + "0.02156800" + ], + [ + "19599.91000000", + "0.07000000" + ], + [ + "19599.92000000", + "0.00216400" + ], + [ + "19599.98000000", + "0.06551800" + ], + [ + "19599.99000000", + "0.42371900" + ], + [ + "19600.00000000", + "187.25561000" + ], + [ + "19600.03000000", + "0.15044800" + ], + [ + "19600.09000000", + "0.00051700" + ], + [ + "19600.13000000", + "0.00336100" + ], + [ + "19600.14000000", + "0.00308100" + ], + [ + "19600.15000000", + "0.13207700" + ], + [ + "19600.17000000", + "0.00146500" + ], + [ + "19600.23000000", + "0.00187900" + ], + [ + "19600.24000000", + "0.48275700" + ], + [ + "19600.28000000", + "0.03026300" + ], + [ + "19600.36000000", + "0.02314000" + ], + [ + "19600.39000000", + "0.00136300" + ], + [ + "19600.41000000", + "0.00130000" + ], + [ + "19600.46000000", + "0.00283800" + ], + [ + "19600.47000000", + "0.00076500" + ], + [ + "19600.48000000", + "0.00591600" + ], + [ + "19600.52000000", + "0.00275800" + ], + [ + "19600.55000000", + "0.10284900" + ], + [ + "19600.56000000", + "0.00051500" + ], + [ + "19600.60000000", + "0.00127500" + ], + [ + "19600.61000000", + "0.20655200" + ], + [ + "19600.65000000", + "0.00931100" + ], + [ + "19600.67000000", + "0.00654300" + ], + [ + "19600.68000000", + "0.00061700" + ], + [ + "19600.72000000", + "0.22746600" + ], + [ + "19600.73000000", + "0.00205300" + ], + [ + "19600.74000000", + "0.00416300" + ], + [ + "19600.79000000", + "0.00584800" + ], + [ + "19600.82000000", + "0.00115800" + ], + [ + "19600.83000000", + "0.00400900" + ], + [ + "19600.84000000", + "0.00129700" + ], + [ + "19600.88000000", + "0.78000000" + ], + [ + "19600.91000000", + "0.00066900" + ], + [ + "19600.95000000", + "0.00310100" + ], + [ + "19600.96000000", + "0.04183400" + ], + [ + "19600.98000000", + "0.00481300" + ], + [ + "19600.99000000", + "0.00224300" + ], + [ + "19601.00000000", + "0.03144100" + ], + [ + "19601.05000000", + "0.00104200" + ], + [ + "19601.12000000", + "0.00161700" + ], + [ + "19601.16000000", + "0.00305600" + ], + [ + "19601.18000000", + "0.00061500" + ], + [ + "19601.20000000", + "0.00332800" + ], + [ + "19601.39000000", + "0.00200000" + ], + [ + "19601.40000000", + "0.01231900" + ], + [ + "19601.60000000", + "0.00365800" + ], + [ + "19601.68000000", + "0.00431600" + ], + [ + "19601.80000000", + "2.00000000" + ], + [ + "19601.90000000", + "0.00256300" + ], + [ + "19602.02000000", + "0.00071000" + ], + [ + "19602.03000000", + "0.00063900" + ], + [ + "19602.04000000", + "0.00861700" + ], + [ + "19602.14000000", + "0.00051500" + ], + [ + "19602.16000000", + "0.00110400" + ], + [ + "19602.26000000", + "0.02617800" + ], + [ + "19602.63000000", + "0.00061900" + ], + [ + "19602.67000000", + "0.00245200" + ], + [ + "19602.71000000", + "0.00366100" + ], + [ + "19602.76000000", + "0.00200000" + ], + [ + "19602.89000000", + "0.00328900" + ], + [ + "19602.92000000", + "0.00051500" + ], + [ + "19602.96000000", + "0.00128900" + ], + [ + "19602.98000000", + "0.00170000" + ], + [ + "19603.00000000", + "0.02643700" + ], + [ + "19603.27000000", + "0.00200000" + ], + [ + "19603.30000000", + "0.00073200" + ], + [ + "19603.40000000", + "0.00161800" + ], + [ + "19603.62000000", + "0.03900000" + ], + [ + "19603.64000000", + "0.01020300" + ], + [ + "19603.75000000", + "0.00150100" + ], + [ + "19603.93000000", + "0.00159200" + ], + [ + "19603.99000000", + "0.00437000" + ], + [ + "19604.02000000", + "0.00051500" + ], + [ + "19604.16000000", + "0.00202800" + ], + [ + "19604.18000000", + "0.00204300" + ], + [ + "19604.29000000", + "0.00510500" + ], + [ + "19604.34000000", + "0.00204100" + ], + [ + "19604.54000000", + "0.00102800" + ], + [ + "19604.55000000", + "0.00106900" + ], + [ + "19604.56000000", + "0.00153800" + ], + [ + "19604.57000000", + "0.00158800" + ], + [ + "19604.69000000", + "0.00086700" + ], + [ + "19604.71000000", + "0.00090000" + ], + [ + "19604.74000000", + "0.03900000" + ], + [ + "19604.82000000", + "0.00200000" + ], + [ + "19604.86000000", + "0.00068900" + ], + [ + "19604.87000000", + "0.00513800" + ], + [ + "19604.90000000", + "0.00200000" + ], + [ + "19604.98000000", + "0.00051500" + ], + [ + "19605.00000000", + "0.01451700" + ], + [ + "19605.01000000", + "0.00060100" + ], + [ + "19605.03000000", + "0.00748900" + ], + [ + "19605.04000000", + "0.00355800" + ], + [ + "19605.08000000", + "0.00116100" + ], + [ + "19605.20000000", + "0.00241800" + ], + [ + "19605.24000000", + "0.00051100" + ], + [ + "19605.26000000", + "0.00185000" + ], + [ + "19605.33000000", + "0.00882900" + ], + [ + "19605.34000000", + "0.00410100" + ], + [ + "19605.42000000", + "0.00153000" + ], + [ + "19605.43000000", + "0.00056700" + ], + [ + "19605.49000000", + "0.01021600" + ], + [ + "19605.50000000", + "0.00501300" + ], + [ + "19605.51000000", + "0.00061600" + ], + [ + "19605.55000000", + "0.00205100" + ], + [ + "19605.57000000", + "0.00180900" + ], + [ + "19605.65000000", + "0.00056700" + ], + [ + "19605.72000000", + "0.00127300" + ], + [ + "19605.81000000", + "0.00076400" + ], + [ + "19605.83000000", + "0.00256600" + ], + [ + "19605.84000000", + "0.00061900" + ], + [ + "19605.87000000", + "0.00339000" + ], + [ + "19605.90000000", + "0.00057700" + ], + [ + "19606.00000000", + "0.01000000" + ], + [ + "19606.04000000", + "0.00510300" + ], + [ + "19606.08000000", + "0.00065000" + ], + [ + "19606.11000000", + "0.00071600" + ], + [ + "19606.15000000", + "0.00097400" + ], + [ + "19606.23000000", + "0.00376200" + ], + [ + "19606.34000000", + "0.00200000" + ], + [ + "19606.35000000", + "0.00215300" + ], + [ + "19606.37000000", + "0.00051500" + ], + [ + "19606.38000000", + "0.01134300" + ], + [ + "19606.40000000", + "0.00126900" + ], + [ + "19606.45000000", + "0.00100000" + ], + [ + "19606.57000000", + "0.00104300" + ], + [ + "19606.64000000", + "0.00072200" + ], + [ + "19606.77000000", + "0.00200000" + ], + [ + "19606.83000000", + "0.01275000" + ], + [ + "19606.84000000", + "0.00102500" + ], + [ + "19606.89000000", + "0.00210000" + ], + [ + "19606.90000000", + "0.00052200" + ], + [ + "19606.94000000", + "0.00764500" + ], + [ + "19606.98000000", + "0.22383600" + ], + [ + "19607.00000000", + "1.00003700" + ], + [ + "19607.01000000", + "0.00256600" + ], + [ + "19607.03000000", + "0.00144100" + ], + [ + "19607.14000000", + "0.04250000" + ], + [ + "19607.15000000", + "0.00106100" + ], + [ + "19607.19000000", + "0.00258800" + ], + [ + "19607.35000000", + "0.00051400" + ], + [ + "19607.41000000", + "0.00256600" + ], + [ + "19607.74000000", + "0.00063900" + ], + [ + "19607.82000000", + "0.00515900" + ], + [ + "19607.89000000", + "0.01284600" + ], + [ + "19607.97000000", + "0.00116000" + ], + [ + "19608.00000000", + "1.00000000" + ], + [ + "19608.01000000", + "0.00062400" + ], + [ + "19608.02000000", + "0.00093600" + ], + [ + "19608.04000000", + "0.00056200" ] ], "bids": [ [ - "6621.55000000", - "0.16356700" + "19528.41000000", + "5.17450500" ], [ - "6621.45000000", - "0.16352600" + "19527.25000000", + "0.00600000" ], [ - "6621.41000000", - "0.86091200" + "19527.22000000", + "0.03205800" ], [ - "6621.25000000", - "0.16914100" + "19527.03000000", + "0.02560000" ], [ - "6621.23000000", - "0.09193600" + "19526.73000000", + "0.05120700" ], [ - "6621.22000000", - "0.00755100" + "19526.71000000", + "0.04761200" ], [ - "6621.13000000", - "0.08432000" + "19526.52000000", + "0.43580700" ], [ - "6621.03000000", + "19526.51000000", + "2.00000000" + ], + [ + "19526.50000000", + "0.00600000" + ], + [ + "19526.33000000", + "0.14937800" + ], + [ + "19526.32000000", + "0.86400000" + ], + [ + "19525.97000000", + "1.80100000" + ], + [ + "19525.86000000", + "0.09844000" + ], + [ + "19525.66000000", + "0.05120700" + ], + [ + "19525.59000000", + "0.10000000" + ], + [ + "19525.48000000", + "0.03053200" + ], + [ + "19525.01000000", + "0.20000000" + ], + [ + "19525.00000000", + "0.01112300" + ], + [ + "19524.96000000", + "0.76824700" + ], + [ + "19524.95000000", + "0.00124700" + ], + [ + "19524.91000000", + "0.10240000" + ], + [ + "19524.78000000", + "0.00227600" + ], + [ + "19524.50000000", + "0.03750000" + ], + [ + "19524.49000000", + "0.05121200" + ], + [ + "19524.48000000", + "0.06800900" + ], + [ + "19524.12000000", + "0.10000000" + ], + [ + "19523.90000000", + "0.11427900" + ], + [ + "19523.57000000", + "0.22000000" + ], + [ + "19522.98000000", + "1.61700000" + ], + [ + "19522.27000000", + "0.01579800" + ], + [ + "19522.17000000", + "0.10000000" + ], + [ + "19522.16000000", + "0.35000000" + ], + [ + "19521.85000000", + "0.05000000" + ], + [ + "19521.55000000", + "0.15000000" + ], + [ + "19520.53000000", + "0.02036200" + ], + [ + "19520.52000000", + "0.15000000" + ], + [ + "19520.39000000", + "0.26110800" + ], + [ + "19520.06000000", + "0.00600000" + ], + [ + "19519.86000000", + "0.00285000" + ], + [ + "19519.77000000", + "0.00600000" + ], + [ + "19519.56000000", + "0.05112800" + ], + [ + "19519.32000000", + "0.04363600" + ], + [ + "19519.30000000", + "0.12990000" + ], + [ + "19518.85000000", + "0.30000000" + ], + [ + "19518.70000000", + "0.43109500" + ], + [ + "19518.41000000", + "0.30728600" + ], + [ + "19517.18000000", + "0.00600000" + ], + [ + "19516.33000000", + "0.15300000" + ], + [ + "19516.21000000", + "0.00600000" + ], + [ + "19515.99000000", + "0.00275300" + ], + [ + "19515.93000000", + "0.05299000" + ], + [ + "19515.90000000", + "0.15150000" + ], + [ + "19515.80000000", + "0.29700000" + ], + [ + "19515.76000000", + "0.45504000" + ], + [ + "19515.50000000", + "0.00600000" + ], + [ + "19515.24000000", + "0.01666700" + ], + [ + "19515.11000000", + "0.40033200" + ], + [ + "19514.76000000", + "0.30733400" + ], + [ + "19514.28000000", + "0.62659900" + ], + [ + "19514.16000000", + "0.10000000" + ], + [ + "19513.83000000", + "0.00297000" + ], + [ + "19513.81000000", + "0.53684600" + ], + [ + "19513.60000000", + "0.00600000" + ], + [ + "19513.46000000", + "0.01537400" + ], + [ + "19513.45000000", + "0.00353100" + ], + [ + "19512.58000000", + "0.09405500" + ], + [ + "19512.57000000", + "0.14850000" + ], + [ + "19512.46000000", + "1.00500000" + ], + [ + "19512.27000000", + "0.15374900" + ], + [ + "19512.23000000", + "0.00600000" + ], + [ + "19512.01000000", + "0.30000000" + ], + [ + "19511.88000000", + "0.28800000" + ], + [ + "19511.85000000", + "0.15450000" + ], + [ + "19511.72000000", + "0.30300000" + ], + [ + "19511.67000000", + "0.15150000" + ], + [ + "19511.51000000", + "1.27500000" + ], + [ + "19511.32000000", + "0.30738200" + ], + [ + "19511.22000000", + "0.00600000" + ], + [ + "19511.06000000", + "0.10598200" + ], + [ + "19510.62000000", + "0.42930000" + ], + [ + "19510.57000000", + "0.15148600" + ], + [ + "19510.31000000", + "0.55000000" + ], + [ + "19510.18000000", + "1.00000000" + ], + [ + "19510.00000000", + "0.00148600" + ], + [ + "19509.84000000", + "0.00291000" + ], + [ + "19509.46000000", + "1.03000000" + ], + [ + "19509.10000000", + "0.02671500" + ], + [ + "19508.57000000", + "0.10645100" + ], + [ + "19508.55000000", + "0.50000000" + ], + [ + "19508.52000000", + "0.15150000" + ], + [ + "19508.33000000", + "0.30743200" + ], + [ + "19507.77000000", + "0.30300000" + ], + [ + "19507.57000000", + "0.10000000" + ], + [ + "19507.40000000", + "0.00403300" + ], + [ + "19507.27000000", + "0.23957200" + ], + [ + "19507.00000000", + "1.01000000" + ], + [ + "19506.97000000", + "0.15100000" + ], + [ + "19506.78000000", + "0.01400000" + ], + [ + "19506.24000000", + "0.00102500" + ], + [ + "19506.16000000", + "0.98559200" + ], + [ + "19506.12000000", + "0.05326500" + ], + [ + "19505.99000000", + "0.80134700" + ], + [ + "19505.77000000", + "0.00093100" + ], + [ + "19505.68000000", + "0.00303000" + ], + [ + "19505.30000000", + "0.10685800" + ], + [ + "19505.24000000", + "0.06138300" + ], + [ + "19505.00000000", + "0.10000500" + ], + [ + "19504.59000000", + "0.30724500" + ], + [ + "19504.39000000", + "0.00066000" + ], + [ + "19503.70000000", + "1.26200000" + ], + [ + "19503.62000000", + "0.11080000" + ], + [ + "19503.59000000", + "0.30600000" + ], + [ + "19503.14000000", + "0.00201500" + ], + [ + "19502.71000000", + "0.00335900" + ], + [ + "19502.44000000", + "0.00102500" + ], + [ + "19502.36000000", + "0.00213900" + ], + [ + "19502.24000000", + "0.00063800" + ], + [ + "19501.77000000", + "0.00104300" + ], + [ + "19501.35000000", + "0.00059500" + ], + [ + "19501.17000000", + "0.32086400" + ], + [ + "19501.12000000", + "0.00062500" + ], + [ + "19501.10000000", + "0.02322000" + ], + [ + "19501.00000000", + "0.15341400" + ], + [ + "19500.77000000", + "0.00058800" + ], + [ + "19500.55000000", + "0.00199700" + ], + [ + "19500.33000000", + "0.05342300" + ], + [ + "19500.00000000", + "4.57768400" + ], + [ + "19499.99000000", + "0.21465900" + ], + [ + "19499.66000000", + "1.62000000" + ], + [ + "19499.47000000", + "0.00815600" + ], + [ + "19499.34000000", + "0.40640200" + ], + [ + "19499.32000000", + "0.00202300" + ], + [ + "19499.05000000", + "0.10000000" + ], + [ + "19499.00000000", + "0.00209900" + ], + [ + "19498.98000000", + "0.01400000" + ], + [ + "19498.23000000", + "0.35900700" + ], + [ + "19498.22000000", + "2.00000000" + ], + [ + "19497.93000000", + "0.10225600" + ], + [ + "19497.56000000", + "0.00092900" + ], + [ + "19497.25000000", + "0.00102500" + ], + [ + "19497.16000000", + "0.10000000" + ], + [ + "19497.09000000", + "0.00147400" + ], + [ + "19496.90000000", + "0.00102600" + ], + [ + "19496.66000000", + "0.00051300" + ], + [ + "19496.64000000", + "0.12822700" + ], + [ + "19496.63000000", + "0.28807200" + ], + [ + "19496.51000000", + "0.00059000" + ], + [ + "19495.55000000", + "0.04587400" + ], + [ + "19495.48000000", + "0.00067900" + ], + [ + "19495.43000000", + "0.00051300" + ], + [ + "19495.19000000", + "0.10184300" + ], + [ + "19495.09000000", + "1.05316000" + ], + [ + "19494.94000000", + "0.06500000" + ], + [ + "19494.85000000", + "0.00056400" + ], + [ + "19494.68000000", + "0.00202800" + ], + [ + "19494.62000000", + "0.00068000" + ], + [ + "19494.53000000", + "0.06142700" + ], + [ + "19494.49000000", + "0.00153900" + ], + [ + "19493.93000000", + "0.00707000" + ], + [ + "19493.92000000", + "0.00051300" + ], + [ + "19493.87000000", + "0.00280000" + ], + [ + "19493.84000000", + "0.00102500" + ], + [ + "19493.64000000", + "0.00201600" + ], + [ + "19493.50000000", + "0.00102400" + ], + [ + "19493.25000000", + "0.18701600" + ], + [ + "19493.19000000", + "0.00202700" + ], + [ + "19492.75000000", + "0.00090000" + ], + [ + "19492.68000000", + "0.00078100" + ], + [ + "19492.62000000", + "0.00061700" + ], + [ + "19492.61000000", + "0.02323000" + ], + [ + "19492.57000000", + "0.00051400" + ], + [ + "19491.83000000", + "0.00051400" + ], + [ + "19491.70000000", + "0.00174500" + ], + [ + "19491.19000000", + "0.01400000" + ], + [ + "19491.08000000", + "0.43630000" + ], + [ + "19490.70000000", + "0.10742000" + ], + [ + "19490.29000000", + "0.00103700" + ], + [ + "19490.24000000", + "0.06178700" + ], + [ + "19490.22000000", + "0.00074200" + ], + [ + "19490.14000000", + "0.00201800" + ], + [ + "19490.00000000", + "0.21283600" + ], + [ + "19489.93000000", + "0.13641500" + ], + [ + "19489.79000000", + "0.00075100" + ], + [ + "19489.75000000", + "0.00240000" + ], + [ + "19489.66000000", + "1.62000000" + ], + [ + "19489.48000000", + "0.00202400" + ], + [ + "19489.17000000", + "0.00100000" + ], + [ + "19488.92000000", + "0.01539300" + ], + [ + "19488.84000000", + "0.00060000" + ], + [ + "19488.69000000", + "0.10000000" + ], + [ + "19488.60000000", + "0.00620000" + ], + [ + "19488.37000000", + "0.00210000" + ], + [ + "19488.15000000", + "0.00057400" + ], + [ + "19487.58000000", + "0.00513200" + ], + [ + "19487.55000000", + "0.10232300" + ], + [ + "19487.54000000", + "0.00174900" + ], + [ + "19487.53000000", + "0.19844000" + ], + [ + "19487.34000000", + "0.00584700" + ], + [ + "19487.24000000", + "0.00089800" + ], + [ + "19487.17000000", + "0.00058000" + ], + [ + "19487.00000000", + "0.00615800" + ], + [ + "19486.80000000", + "0.00070100" + ], + [ + "19486.73000000", + "0.00513100" + ], + [ + "19486.72000000", + "0.10000000" + ], + [ + "19486.70000000", + "0.00372600" + ], + [ + "19486.54000000", + "0.00630900" + ], + [ + "19486.48000000", + "0.02000000" + ], + [ + "19486.39000000", + "0.10189800" + ], + [ + "19486.29000000", + "0.00202600" + ], + [ + "19486.00000000", + "0.01000000" + ], + [ + "19485.85000000", + "0.00100000" + ], + [ + "19485.80000000", + "0.01026400" + ], + [ + "19485.00000000", + "0.02052900" + ], + [ + "19484.86000000", + "0.00107800" + ], + [ + "19484.69000000", + "0.10189800" + ], + [ + "19484.65000000", + "0.00100000" + ], + [ + "19484.60000000", + "0.00201700" + ], + [ + "19484.50000000", + "0.00231000" + ], + [ + "19484.37000000", + "0.00203200" + ], + [ + "19484.21000000", + "1.81464200" + ], + [ + "19484.14000000", + "0.00059500" + ], + [ + "19484.12000000", + "0.02324000" + ], + [ + "19484.08000000", + "2.06000000" + ], + [ + "19484.04000000", + "0.00067800" + ], + [ + "19483.67000000", + "0.00051400" + ], + [ + "19483.47000000", + "0.00201300" + ], + [ + "19483.39000000", + "0.01400000" + ], + [ + "19483.23000000", + "0.00058800" + ], + [ + "19483.03000000", + "0.01531000" + ], + [ + "19482.72000000", + "0.10000000" + ], + [ + "19482.49000000", + "0.00052400" + ], + [ + "19482.16000000", + "0.00127700" + ], + [ + "19482.08000000", + "0.02000000" + ], + [ + "19482.05000000", + "0.00080900" + ], + [ + "19482.03000000", + "0.00052400" + ], + [ + "19482.02000000", + "0.00202500" + ], + [ + "19481.00000000", + "0.79835500" + ], + [ + "19480.50000000", + "0.01506300" + ], + [ + "19480.24000000", + "0.00203600" + ], + [ + "19480.00000000", + "1.22760900" + ], + [ + "19479.98000000", + "0.10000000" + ], + [ + "19479.86000000", + "0.00203300" + ], + [ + "19479.66000000", + "1.62000000" + ], + [ + "19479.64000000", + "0.15489300" + ], + [ + "19479.46000000", + "0.03080200" + ], + [ + "19479.45000000", + "0.01283500" + ], + [ + "19479.34000000", + "0.00202100" + ], + [ + "19479.19000000", + "0.00080000" + ], + [ + "19479.15000000", + "0.00154000" + ], + [ + "19479.05000000", + "0.00150000" + ], + [ + "19478.99000000", + "0.00072200" + ], + [ + "19478.68000000", + "0.00056400" + ], + [ + "19478.58000000", + "2.00000000" + ], + [ + "19478.57000000", + "0.00410800" + ], + [ + "19478.50000000", + "0.00051400" + ], + [ + "19478.27000000", + "0.00103400" + ], + [ + "19478.20000000", + "0.00082800" + ], + [ + "19477.42000000", + "0.00256500" + ], + [ + "19477.27000000", + "0.00057200" + ], + [ + "19476.99000000", + "0.00680600" + ], + [ + "19476.87000000", + "0.00615000" + ], + [ + "19476.09000000", + "0.00799900" + ], + [ + "19476.03000000", + "0.00080900" + ], + [ + "19476.00000000", + "0.00063800" + ], + [ + "19475.77000000", + "0.00074500" + ], + [ + "19475.63000000", + "0.02325000" + ], + [ + "19475.60000000", + "0.01400000" + ], + [ + "19475.42000000", + "0.00065800" + ], + [ + "19475.37000000", + "0.00072400" + ], + [ + "19475.14000000", + "0.00070000" + ], + [ + "19475.03000000", + "0.00079700" + ], + [ + "19475.00000000", + "0.08547500" + ], + [ + "19474.57000000", + "0.00066000" + ], + [ + "19474.43000000", + "0.00530800" + ], + [ + "19474.26000000", + "0.00319700" + ], + [ + "19474.10000000", + "0.00087600" + ], + [ + "19473.88000000", + "0.00059400" + ], + [ + "19473.72000000", + "0.00056500" + ], + [ + "19473.70000000", + "0.00149900" + ], + [ + "19473.68000000", + "0.00051400" + ], + [ + "19473.32000000", + "0.01033200" + ], + [ + "19473.09000000", + "0.00064100" + ], + [ + "19472.72000000", + "0.00287200" + ], + [ + "19472.29000000", + "0.00096400" + ], + [ + "19472.09000000", + "0.00058900" + ], + [ + "19471.49000000", + "0.38610000" + ], + [ + "19471.47000000", + "0.01000000" + ], + [ + "19471.43000000", + "0.00202000" + ], + [ + "19471.06000000", + "0.00059700" + ], + [ + "19470.83000000", + "0.00064800" + ], + [ + "19470.70000000", + "0.00060800" + ], + [ + "19470.65000000", + "0.02565400" + ], + [ + "19470.62000000", + "0.00680300" + ], + [ + "19470.11000000", + "0.00513600" + ], + [ + "19470.02000000", + "0.00080900" + ], + [ + "19470.00000000", + "0.23310000" + ], + [ + "19469.87000000", + "0.00066300" + ], + [ + "19469.66000000", + "1.62000000" + ], + [ + "19469.62000000", + "0.00100000" + ], + [ + "19469.31000000", + "0.00106100" + ], + [ + "19469.12000000", + "0.00118900" + ], + [ + "19468.43000000", + "0.00202200" + ], + [ + "19467.76000000", + "0.00060600" + ], + [ + "19467.21000000", + "0.00337200" + ], + [ + "19467.16000000", + "0.00058100" + ], + [ + "19467.15000000", + "0.02326100" + ], + [ + "19466.89000000", + "0.00204000" + ], + [ + "19466.66000000", + "0.00133700" + ], + [ + "19466.19000000", + "0.35000000" + ], + [ + "19465.86000000", + "0.03000000" + ], + [ + "19465.60000000", + "0.00100000" + ], + [ + "19465.12000000", + "0.02491700" + ], + [ + "19465.00000000", + "0.00513700" + ], + [ + "19464.90000000", + "0.00118900" + ], + [ + "19464.87000000", + "0.00116700" + ], + [ + "19464.58000000", + "0.00203400" + ], + [ + "19464.32000000", + "0.00203700" + ], + [ + "19464.01000000", + "0.00080900" + ], + [ + "19464.00000000", + "0.09339200" + ], + [ + "19463.93000000", + "0.00175900" + ], + [ + "19463.85000000", + "0.00056500" + ], + [ + "19463.74000000", + "0.00056500" + ], + [ + "19463.69000000", + "0.00443800" + ], + [ + "19463.67000000", + "0.00102800" + ], + [ + "19463.26000000", + "0.00280000" + ], + [ + "19462.10000000", + "0.00180200" + ], + [ + "19461.77000000", + "0.00064100" + ], + [ + "19461.50000000", + "0.00071200" + ], + [ + "19461.34000000", + "0.00179900" + ], + [ + "19461.22000000", + "0.00521900" + ], + [ + "19461.10000000", + "0.00127900" + ], + [ + "19460.95000000", + "0.00058900" + ], + [ + "19460.77000000", + "0.00423900" + ], + [ + "19460.65000000", + "0.00108000" + ], + [ + "19460.00000000", + "0.20212200" + ], + [ + "19459.96000000", + "0.00373200" + ], + [ + "19459.91000000", + "0.00057900" + ], + [ + "19459.68000000", + "0.00070000" + ], + [ + "19459.66000000", + "1.62000000" + ], + [ + "19459.45000000", + "0.35000000" + ], + [ + "19459.18000000", + "0.00075100" + ], + [ + "19458.80000000", + "0.00817300" + ], + [ + "19458.75000000", + "0.00063900" + ], + [ + "19458.70000000", + "0.00154200" + ], + [ + "19458.68000000", + "0.02327100" + ], + [ + "19458.66000000", + "0.00128400" + ], + [ + "19458.58000000", + "0.00063900" + ], + [ + "19458.42000000", + "0.00062500" + ], + [ + "19458.36000000", + "0.00060100" + ], + [ + "19458.33000000", + "0.00300000" + ], + [ + "19458.22000000", + "0.00296100" + ], + [ + "19458.00000000", + "0.00831000" + ], + [ + "19457.97000000", + "0.00514000" + ], + [ + "19457.92000000", + "0.06179200" + ], + [ + "19457.89000000", + "0.01800000" + ], + [ + "19457.71000000", + "0.00215800" + ], + [ + "19457.55000000", + "0.00202900" + ], + [ + "19456.88000000", + "0.00093400" + ], + [ + "19456.55000000", + "0.00128400" + ], + [ + "19456.52000000", + "0.00150000" + ], + [ + "19456.43000000", + "0.00062800" + ], + [ + "19456.40000000", + "0.00060900" + ], + [ + "19456.34000000", + "0.00124900" + ], + [ + "19456.25000000", + "0.00100000" + ], + [ + "19456.16000000", + "0.15419300" + ], + [ + "19456.10000000", + "0.00514300" + ], + [ + "19456.06000000", + "0.00205600" + ], + [ + "19455.69000000", + "0.00093000" + ], + [ + "19455.55000000", + "0.00059000" + ], + [ + "19455.45000000", + "0.00056500" + ], + [ + "19455.05000000", + "0.00061400" + ], + [ + "19455.00000000", + "0.00436900" + ], + [ + "19454.76000000", + "0.00051500" + ], + [ + "19454.18000000", + "0.00133600" + ], + [ + "19453.87000000", + "0.00061700" + ], + [ + "19453.84000000", + "0.13290000" + ], + [ + "19453.83000000", + "0.00154200" + ], + [ + "19453.48000000", + "0.00065000" + ], + [ + "19453.36000000", + "0.00063900" + ], + [ + "19453.25000000", + "0.00141200" + ], + [ + "19453.15000000", + "0.00514100" + ], + [ + "19452.95000000", + "0.00276300" + ], + [ + "19452.76000000", + "2.09399100" + ], + [ + "19452.32000000", + "0.00118900" + ], + [ + "19452.28000000", + "0.00060000" + ], + [ + "19452.12000000", + "0.00056300" + ], + [ + "19452.04000000", + "0.00064100" + ], + [ + "19451.99000000", + "0.00595100" + ], + [ + "19451.85000000", + "0.00110700" + ], + [ + "19451.72000000", + "0.00180000" + ], + [ + "19451.61000000", + "0.01028200" + ], + [ + "19451.58000000", + "0.00156800" + ], + [ + "19451.47000000", + "0.00056500" + ], + [ + "19451.25000000", + "0.02000000" + ], + [ + "19451.10000000", + "0.00059700" + ], + [ + "19451.04000000", + "0.00174600" + ], + [ + "19451.02000000", + "0.05000000" + ], + [ + "19451.01000000", + "0.01306300" + ], + [ + "19450.85000000", + "0.00120000" + ], + [ + "19450.57000000", + "0.00770100" + ], + [ + "19450.41000000", + "0.00141400" + ], + [ + "19450.34000000", + "0.00068800" + ], + [ + "19450.20000000", + "0.02328100" + ], + [ + "19450.07000000", + "0.00100000" + ], + [ + "19450.00000000", + "1.48181800" + ], + [ + "19449.96000000", + "0.00413400" + ], + [ + "19449.86000000", + "2.18448900" + ], + [ + "19449.82000000", + "0.00058900" + ], + [ + "19449.66000000", + "1.62000000" + ], + [ + "19449.61000000", + "0.00149600" + ], + [ + "19449.56000000", + "0.01544200" + ], + [ + "19449.34000000", + "0.01403200" + ], + [ + "19448.97000000", + "0.00417000" + ], + [ + "19448.94000000", + "0.10000000" + ], + [ + "19448.91000000", + "0.00514200" + ], + [ + "19448.85000000", + "0.00097600" + ], + [ + "19448.81000000", + "0.00256800" + ], + [ + "19448.56000000", + "0.00999800" + ], + [ + "19448.27000000", + "0.00060400" + ], + [ + "19447.81000000", + "0.00097600" + ], + [ + "19447.77000000", + "0.28879600" + ], + [ + "19447.34000000", + "0.19000000" + ], + [ + "19447.29000000", + "0.00205900" + ], + [ + "19446.99000000", + "0.00203100" + ], + [ + "19446.88000000", + "0.00056600" + ], + [ + "19446.69000000", + "0.04599000" + ], + [ + "19446.51000000", + "0.00056500" + ], + [ + "19446.38000000", + "0.00203000" + ], + [ + "19446.31000000", + "0.00061800" + ], + [ + "19446.29000000", + "0.02196600" + ], + [ + "19446.25000000", + "0.00136900" + ], + [ + "19446.23000000", + "0.00064100" + ], + [ + "19445.97000000", + "0.00081000" + ], + [ + "19445.80000000", + "0.00051500" + ], + [ + "19445.72000000", + "0.00267500" + ], + [ + "19445.70000000", + "0.00514200" + ], + [ + "19445.09000000", + "0.00063900" + ], + [ + "19444.79000000", + "0.00277400" + ], + [ + "19444.44000000", + "0.06700000" + ], + [ + "19444.24000000", + "0.00204100" + ], + [ + "19444.11000000", + "0.00154300" + ], + [ + "19444.02000000", + "0.00213200" + ], + [ + "19443.78000000", + "0.00082300" + ], + [ + "19443.00000000", + "0.06020400" + ], + [ + "19442.85000000", + "0.00200000" + ], + [ + "19442.83000000", + "0.00071000" + ], + [ + "19442.82000000", + "0.03019300" + ], + [ + "19442.75000000", + "0.00058000" + ], + [ + "19442.60000000", + "0.00073400" + ], + [ + "19442.31000000", + "0.00064200" + ], + [ + "19442.26000000", + "0.02572200" + ], + [ + "19442.21000000", + "1.28010100" + ], + [ + "19442.02000000", + "0.00090000" + ], + [ + "19442.00000000", + "0.00617300" + ], + [ + "19441.95000000", + "0.00051500" + ], + [ + "19441.92000000", + "0.00143000" + ], + [ + "19441.73000000", + "0.02329100" + ], + [ + "19441.45000000", + "0.01206800" + ], + [ + "19441.31000000", + "0.00064400" + ], + [ + "19440.66000000", + "0.00195500" + ], + [ + "19440.52000000", + "0.00056600" + ], + [ + "19440.42000000", + "0.00065400" + ], + [ + "19440.31000000", + "0.00074800" + ], + [ + "19440.27000000", + "4.28200000" + ], + [ + "19440.09000000", + "0.00146700" + ], + [ + "19440.06000000", + "0.00128000" + ], + [ + "19440.00000000", + "0.26793200" + ], + [ + "19439.96000000", + "0.00081000" + ], + [ + "19439.66000000", + "1.62000000" + ], + [ + "19439.48000000", + "0.00294700" + ], + [ + "19439.25000000", + "0.00154300" + ], + [ + "19439.20000000", + "0.00058200" + ], + [ + "19439.09000000", + "0.10000000" + ], + [ + "19438.94000000", + "0.00220900" + ], + [ + "19438.90000000", + "0.00211000" + ], + [ + "19438.81000000", + "0.00061700" + ], + [ + "19438.77000000", + "0.00100800" + ], + [ + "19438.75000000", + "0.00062800" + ], + [ + "19438.70000000", + "0.00059000" + ], + [ + "19438.47000000", + "0.00193000" + ], + [ + "19438.45000000", + "0.00175300" + ], + [ + "19438.23000000", + "0.00203500" + ], + [ + "19438.10000000", + "0.00069600" + ], + [ + "19437.97000000", + "0.00620000" + ], + [ + "19437.96000000", + "0.00052000" + ], + [ + "19437.57000000", + "0.00154300" + ], + [ + "19437.50000000", + "0.00055600" + ], + [ + "19437.32000000", + "0.00403600" + ], + [ + "19437.30000000", + "0.00125800" + ], + [ + "19437.13000000", + "0.00100000" + ], + [ + "19437.12000000", + "0.00066200" + ], + [ + "19436.96000000", + "0.00051500" + ], + [ + "19436.74000000", + "0.00184800" + ], + [ + "19436.66000000", + "0.00452600" + ], + [ + "19436.47000000", + "0.00108100" + ], + [ + "19436.30000000", + "0.00204300" + ], + [ + "19435.92000000", + "0.22371000" + ], + [ + "19435.71000000", + "0.00150000" + ], + [ + "19435.34000000", + "0.00093300" + ], + [ + "19435.33000000", + "0.00205800" + ], + [ + "19435.29000000", + "0.10000000" + ], + [ + "19434.96000000", + "0.03122800" + ], + [ + "19434.91000000", + "0.00140300" + ], + [ + "19434.78000000", + "0.02293100" + ], + [ + "19434.55000000", + "0.00965000" + ], + [ + "19434.49000000", + "0.00074700" + ], + [ + "19434.39000000", + "0.00154400" + ], + [ + "19434.38000000", + "0.00176200" + ], + [ + "19434.32000000", + "0.11411400" + ], + [ + "19433.94000000", + "0.00081100" + ], + [ + "19433.72000000", + "0.03236600" + ], + [ + "19433.40000000", + "0.00079000" + ], + [ + "19433.27000000", + "0.02330100" + ], + [ + "19433.26000000", + "0.00373700" + ], + [ + "19433.00000000", + "5.14725700" + ], + [ + "19432.65000000", + "0.00280000" + ], + [ + "19432.58000000", + "0.00064200" + ], + [ + "19432.43000000", + "0.02000000" + ], + [ + "19431.91000000", + "0.00051500" + ], + [ + "19431.80000000", + "0.00059900" + ], + [ + "19431.78000000", + "0.05182800" + ], + [ + "19431.75000000", + "0.00444600" + ], + [ + "19431.39000000", + "0.00062600" + ], + [ + "19431.32000000", + "0.00162200" + ], + [ + "19431.31000000", + "0.00064300" + ], + [ + "19431.29000000", + "0.00679000" + ], + [ + "19431.16000000", + "0.00059800" + ], + [ + "19431.03000000", + "0.00110000" + ], + [ + "19431.01000000", + "0.00205600" + ], + [ + "19430.98000000", + "0.00213700" + ], + [ + "19430.76000000", + "0.00514600" + ], + [ + "19430.72000000", + "0.00060000" + ], + [ + "19430.55000000", + "0.00136700" + ], + [ + "19430.52000000", + "0.00100000" + ], + [ + "19430.00000000", + "0.33384200" + ], + [ + "19429.66000000", + "1.62000000" + ], + [ + "19429.63000000", + "0.00460000" + ], + [ + "19429.53000000", + "0.00154400" + ], + [ + "19429.23000000", + "0.00128500" + ], + [ + "19429.00000000", + "0.03808800" + ], + [ + "19428.57000000", + "0.01116100" + ], + [ + "19428.56000000", + "0.02056200" + ], + [ + "19428.09000000", + "1.32379600" + ], + [ + "19428.00000000", + "0.04072800" + ], + [ + "19427.94000000", + "0.00203800" + ], + [ + "19427.92000000", + "0.00081100" + ], + [ + "19427.58000000", + "0.00059000" + ], + [ + "19427.35000000", + "0.00204200" + ], + [ + "19427.06000000", + "0.00060000" + ], + [ + "19427.00000000", + "0.00750000" + ], + [ + "19426.99000000", + "0.00098600" + ], + [ + "19426.61000000", + "0.00155700" + ], + [ + "19426.53000000", + "0.00314100" + ], + [ + "19426.44000000", + "0.00071400" + ], + [ + "19426.10000000", + "0.03016700" + ], + [ + "19425.78000000", + "0.00370000" + ], + [ + "19425.60000000", + "0.00058000" + ], + [ + "19425.43000000", + "0.00131700" + ], + [ + "19425.41000000", + "0.00110900" + ], + [ + "19425.35000000", + "0.18607000" + ], + [ + "19425.10000000", + "0.01106500" + ], + [ + "19425.00000000", + "20.87751100" + ], + [ + "19424.85000000", + "0.00108000" + ], + [ + "19424.81000000", + "0.02331100" + ], + [ + "19424.68000000", + "0.00154400" + ], + [ + "19424.62000000", + "0.00085500" + ], + [ + "19424.05000000", + "0.00584700" + ], + [ + "19424.00000000", + "0.59039800" + ], + [ + "19423.71000000", + "0.00203900" + ], + [ + "19423.65000000", + "0.00077100" + ], + [ + "19423.46000000", + "0.00092700" + ], + [ + "19423.21000000", + "0.00344900" + ], + [ + "19423.13000000", + "0.08519400" + ], + [ + "19423.09000000", + "0.00414000" + ], + [ + "19423.07000000", + "0.00237500" + ], + [ + "19422.85000000", + "0.00064200" + ], + [ + "19422.84000000", + "0.01287100" + ], + [ + "19422.83000000", + "0.00060700" + ], + [ + "19422.62000000", + "0.58530000" + ], + [ + "19422.00000000", + "0.06842800" + ], + [ + "19421.92000000", + "0.00257200" + ], + [ + "19421.91000000", + "0.00286900" + ], + [ + "19421.78000000", + "0.00511400" + ], + [ + "19421.77000000", + "0.05000000" + ], + [ + "19421.71000000", + "0.00061000" + ], + [ + "19421.59000000", + "0.00080000" + ], + [ + "19421.46000000", + "0.00157700" + ], + [ + "19421.13000000", + "0.00077500" + ], + [ + "19421.00000000", + "0.00711300" + ], + [ + "19420.91000000", + "0.05254300" + ], + [ + "19420.81000000", + "0.00066900" + ], + [ + "19420.65000000", + "0.00309000" + ], + [ + "19420.64000000", + "3.65770000" + ], + [ + "19420.47000000", + "0.00139000" + ], + [ + "19420.39000000", + "0.01020000" + ], + [ + "19420.33000000", + "0.00149800" + ], + [ + "19420.00000000", + "0.22892900" + ], + [ + "19419.99000000", + "0.00500000" + ], + [ + "19419.90000000", + "0.00260500" + ], + [ + "19419.82000000", + "0.00154500" + ], + [ + "19419.66000000", + "1.62000000" + ], + [ + "19419.62000000", + "0.06191900" + ], + [ + "19419.60000000", + "2.00000000" + ], + [ + "19419.59000000", + "0.00515000" + ], + [ + "19419.40000000", + "0.00064200" + ], + [ + "19419.05000000", + "0.00128100" + ], + [ + "19419.00000000", + "0.00515000" + ], + [ + "19418.96000000", + "0.00208400" + ], + [ + "19418.90000000", + "0.00066400" + ], + [ + "19418.71000000", + "0.00215300" + ], + [ + "19418.36000000", + "0.03500000" + ], + [ + "19418.34000000", + "0.00180600" + ], + [ + "19418.21000000", + "0.00819000" + ], + [ + "19417.96000000", + "0.00310100" + ], + [ + "19417.64000000", + "0.00433100" + ], + [ + "19417.05000000", + "0.00618100" + ], + [ + "19417.00000000", + "0.00492300" + ], + [ + "19416.84000000", + "0.00056600" + ], + [ + "19416.66000000", + "0.01530200" + ], + [ + "19416.47000000", + "0.00059000" + ], + [ + "19416.35000000", + "0.02332100" + ], + [ + "19416.29000000", + "0.00189300" + ], + [ + "19416.07000000", + "0.00525400" + ], + [ + "19416.05000000", + "0.00515100" + ], + [ + "19416.04000000", + "0.00085000" + ], + [ + "19416.00000000", + "0.19897500" + ], + [ + "19415.90000000", + "0.00081100" + ], + [ + "19415.85000000", + "0.04882700" + ], + [ + "19415.84000000", + "0.00132500" + ], + [ + "19415.73000000", + "0.00062500" + ], + [ + "19415.27000000", + "0.00358800" + ], + [ + "19414.97000000", + "0.00154500" + ], + [ + "19414.92000000", + "0.00102900" + ], + [ + "19414.85000000", + "0.00056600" + ], + [ + "19414.23000000", + "0.01779300" + ], + [ + "19414.16000000", + "0.00159300" + ], + [ + "19414.00000000", + "0.00197700" + ], + [ + "19413.84000000", + "0.00280000" + ], + [ + "19413.31000000", + "0.00618200" + ], + [ + "19413.30000000", + "0.00293700" + ], + [ + "19413.29000000", + "0.04734000" + ], + [ + "19413.26000000", + "0.04250000" + ], + [ + "19413.25000000", + "0.11000000" + ], + [ + "19413.12000000", + "0.00064300" + ], + [ + "19413.00000000", + "0.05255300" + ], + [ + "19412.99000000", + "0.00288100" + ], + [ + "19412.77000000", + "0.00088100" + ], + [ + "19412.75000000", + "0.00380000" + ], + [ + "19412.66000000", + "0.00695500" + ], + [ + "19412.53000000", + "0.00174500" + ], + [ + "19412.46000000", + "0.00257500" + ], + [ + "19412.43000000", + "0.00073500" + ], + [ + "19412.35000000", + "0.00314300" + ], + [ + "19412.32000000", + "0.00108200" + ], + [ + "19412.31000000", + "0.00304000" + ], + [ + "19412.30000000", + "0.00345200" + ], + [ + "19412.29000000", + "0.02091700" + ], + [ + "19412.28000000", + "0.01220900" + ], + [ + "19412.27000000", + "0.00618200" + ], + [ + "19412.26000000", + "0.00989200" + ], + [ + "19412.24000000", + "0.00521900" + ], + [ + "19412.19000000", + "0.00309100" + ], + [ + "19412.00000000", + "0.54105300" + ], + [ + "19411.82000000", + "0.00119300" + ], + [ + "19411.77000000", + "0.00100000" + ], + [ + "19411.38000000", + "0.00727100" + ], + [ + "19411.37000000", + "0.00341000" + ], + [ + "19411.31000000", + "0.24661400" + ], + [ + "19411.28000000", + "0.00058300" + ], + [ + "19411.26000000", + "1.02358500" + ], + [ + "19411.24000000", + "0.00059800" + ], + [ + "19411.11000000", + "0.01350000" + ], + [ + "19410.99000000", + "0.01109200" + ], + [ + "19410.98000000", + "0.00367900" + ], + [ + "19410.59000000", + "0.12337700" + ], + [ + "19410.52000000", + "1.81464200" + ], + [ + "19410.44000000", + "0.00059100" + ], + [ + "19410.43000000", + "0.00128700" + ], + [ + "19410.11000000", + "0.00154600" + ], + [ + "19410.03000000", + "0.00425000" + ], + [ + "19410.00000000", + "0.26987900" + ], + [ + "19409.89000000", + "0.00081200" + ], + [ + "19409.66000000", + "1.62000000" + ], + [ + "19409.50000000", + "0.00060500" + ], + [ + "19409.44000000", + "0.00128800" + ], + [ + "19409.18000000", + "0.00314400" + ], + [ + "19409.10000000", + "0.01343600" + ], + [ + "19409.09000000", + "0.00057200" + ], + [ + "19408.53000000", + "0.00350000" + ], + [ + "19408.47000000", + "0.00058100" + ], + [ + "19408.26000000", + "0.19925000" + ], + [ + "19408.16000000", + "0.19467800" + ], + [ + "19408.00000000", + "0.00093600" + ], + [ + "19407.89000000", + "0.02333200" + ], + [ + "19407.29000000", + "0.00966100" + ], + [ + "19407.25000000", + "0.00309300" + ], + [ + "19406.59000000", + "0.00374200" + ], + [ + "19406.16000000", + "0.15459000" + ], + [ + "19406.15000000", + "0.00980900" + ], + [ + "19405.37000000", + "0.00059100" + ], + [ + "19405.27000000", + "0.00154600" + ], + [ + "19405.18000000", + "0.00514800" + ], + [ + "19405.00000000", + "0.02576700" + ], + [ + "19404.91000000", + "0.00341700" + ], + [ + "19404.88000000", + "0.00176500" + ], + [ + "19404.82000000", + "0.00064100" + ], + [ + "19404.75000000", + "0.00515300" + ], + [ + "19404.54000000", + "0.00063800" + ], + [ + "19404.48000000", + "0.00064100" + ], + [ + "19404.16000000", + "0.01100000" + ], + [ + "19403.88000000", + "0.00081200" + ], + [ + "19403.57000000", + "0.00060200" + ], + [ + "19403.40000000", + "0.00051600" + ], + [ + "19403.39000000", + "0.00064300" + ], + [ + "19403.21000000", + "0.00102800" + ], + [ + "19403.00000000", + "0.00200000" + ], + [ + "19402.96000000", + "0.00113200" + ], + [ + "19402.92000000", + "0.00651500" + ], + [ + "19402.57000000", + "0.00167000" + ], + [ + "19402.50000000", + "0.00287100" + ], + [ + "19402.39000000", + "0.00061800" + ], + [ + "19402.25000000", + "0.00532500" + ], + [ + "19402.04000000", + "0.00280000" + ], + [ + "19402.00000000", + "0.06030800" + ], + [ + "19401.77000000", + "0.00154700" + ], + [ + "19401.45000000", + "0.00070000" + ], + [ + "19401.00000000", + "0.00958100" + ], + [ + "19400.90000000", + "0.00335100" + ], + [ + "19400.88000000", + "0.02160200" + ], + [ + "19400.41000000", + "0.00154600" + ], + [ + "19400.17000000", + "0.00257700" + ], + [ + "19400.02000000", + "0.00077200" + ], + [ + "19400.00000000", + "0.35108000" + ], + [ + "19399.99000000", + "0.00897400" + ], + [ + "19399.91000000", + "0.00515500" + ], + [ + "19399.87000000", + "0.00556300" + ], + [ + "19399.74000000", + "0.00062300" + ], + [ + "19399.66000000", + "1.62000000" + ], + [ + "19399.44000000", + "0.02334200" + ], + [ + "19399.14000000", + "0.00140500" + ], + [ + "19399.00000000", + "1.51135800" + ], + [ + "19398.91000000", + "0.00077500" + ], + [ + "19398.90000000", + "0.28952400" + ], + [ + "19398.30000000", + "0.00066000" + ], + [ + "19398.28000000", + "0.00206300" + ], + [ + "19398.06000000", + "0.00128300" + ], + [ + "19398.00000000", + "0.00682200" + ], + [ + "19397.95000000", + "0.00075100" + ], + [ + "19397.88000000", + "0.00081200" + ], + [ + "19397.83000000", + "0.04610500" + ], + [ + "19397.77000000", + "0.00297000" + ], + [ + "19397.31000000", + "0.00082000" + ], + [ + "19397.18000000", + "0.00994300" + ], + [ + "19396.86000000", + "0.00421000" + ], + [ + "19396.67000000", + "0.00133300" + ], + [ + "19396.62000000", + "0.00061400" + ], + [ + "19396.46000000", + "0.00232600" + ], + [ + "19396.42000000", + "0.00249000" + ], + [ + "19396.37000000", + "0.00057200" + ], + [ + "19396.26000000", + "0.00414500" + ], + [ + "19396.20000000", + "0.06719800" + ], + [ + "19396.00000000", + "0.00750000" + ], + [ + "19395.76000000", + "0.00680300" + ], + [ + "19395.56000000", + "0.00154700" + ], + [ + "19395.34000000", + "0.00275000" + ], + [ + "19395.00000000", + "0.02340900" + ], + [ + "19394.42000000", + "0.00515700" + ], + [ + "19394.34000000", "0.00172000" ], [ - "6620.94000000", - "0.30506700" + "19394.27000000", + "0.00059100" ], [ - "6620.93000000", + "19394.10000000", + "0.00125300" + ], + [ + "19393.93000000", + "0.06500000" + ], + [ + "19393.83000000", + "0.00216400" + ], + [ + "19393.69000000", + "0.02997400" + ], + [ + "19393.66000000", + "0.00064300" + ], + [ + "19393.29000000", + "0.00074800" + ], + [ + "19392.63000000", + "0.00515700" + ], + [ + "19392.60000000", + "0.00064300" + ], + [ + "19392.58000000", + "0.03683700" + ], + [ + "19392.52000000", + "0.00070000" + ], + [ + "19392.05000000", + "0.00515700" + ], + [ + "19391.87000000", + "0.00081200" + ], + [ + "19391.85000000", + "0.00309500" + ], + [ + "19391.72000000", + "0.00618800" + ], + [ + "19391.47000000", + "0.00257800" + ], + [ + "19391.45000000", + "0.00071500" + ], + [ + "19391.35000000", + "0.00058100" + ], + [ + "19391.34000000", + "0.00059900" + ], + [ + "19391.33000000", + "0.00062200" + ], + [ + "19391.32000000", + "0.00515700" + ], + [ + "19391.30000000", + "0.00090000" + ], + [ + "19391.23000000", + "0.00900500" + ], + [ + "19391.18000000", + "0.02578500" + ], + [ + "19391.10000000", + "0.00150000" + ], + [ + "19391.08000000", + "0.00063000" + ], + [ + "19390.99000000", + "0.02335200" + ], + [ + "19390.72000000", + "0.00154700" + ], + [ + "19390.29000000", + "0.06196700" + ], + [ + "19390.00000000", + "0.00725400" + ], + [ + "19389.57000000", + "0.00256400" + ], + [ + "19389.48000000", + "0.00175800" + ], + [ + "19389.47000000", + "0.01800000" + ], + [ + "19389.41000000", + "0.00515700" + ], + [ + "19389.25000000", + "0.25809800" + ], + [ + "19389.06000000", + "0.00100000" + ], + [ + "19388.98000000", + "0.00195300" + ], + [ + "19388.95000000", + "0.00155200" + ], + [ + "19388.54000000", + "0.00100000" + ], + [ + "19388.46000000", + "0.00061900" + ], + [ + "19388.23000000", + "0.00157300" + ], + [ + "19388.20000000", + "0.00108400" + ], + [ + "19388.00000000", + "0.01031600" + ], + [ + "19387.89000000", + "0.00356900" + ], + [ + "19387.75000000", + "0.00247300" + ], + [ + "19387.47000000", + "0.00061600" + ], + [ + "19387.34000000", + "0.00620000" + ], + [ + "19387.15000000", + "0.00088700" + ], + [ + "19387.12000000", + "0.00063900" + ], + [ + "19387.11000000", + "0.00206400" + ], + [ + "19387.08000000", + "0.00061100" + ], + [ + "19387.05000000", + "0.00067000" + ], + [ + "19386.46000000", + "0.00062200" + ], + [ + "19386.36000000", + "0.04268000" + ], + [ + "19386.27000000", + "0.00323900" + ], + [ + "19386.00000000", + "0.00288800" + ], + [ + "19385.87000000", + "0.00752000" + ], + [ + "19385.58000000", + "0.00524800" + ], + [ + "19385.15000000", + "0.00061700" + ], + [ + "19385.14000000", + "0.00108300" + ], + [ + "19385.07000000", + "0.00236800" + ], + [ + "19385.06000000", + "0.00075000" + ], + [ + "19385.04000000", + "0.00164200" + ], + [ + "19384.99000000", + "0.00562100" + ], + [ + "19384.89000000", + "0.06193600" + ], + [ + "19384.61000000", + "0.03000000" + ], + [ + "19383.99000000", + "0.00058500" + ], + [ + "19383.93000000", + "0.00064400" + ], + [ + "19383.80000000", + "0.00076500" + ], + [ + "19383.68000000", + "0.01289700" + ], + [ + "19383.59000000", + "0.00427700" + ], + [ + "19383.43000000", + "0.00064100" + ], + [ + "19383.40000000", + "0.00058300" + ], + [ + "19383.18000000", + "0.00059100" + ], + [ + "19382.78000000", + "0.00123800" + ], + [ + "19382.75000000", + "0.00180000" + ], + [ + "19382.60000000", + "0.01249900" + ], + [ + "19382.55000000", + "0.02336200" + ], + [ + "19381.82000000", + "0.00619100" + ], + [ + "19381.77000000", + "0.00099000" + ], + [ + "19381.55000000", + "0.00299500" + ], + [ + "19381.43000000", + "0.00088700" + ], + [ + "19381.36000000", + "0.00057300" + ], + [ + "19381.32000000", + "0.00065600" + ], + [ + "19381.03000000", + "0.00154800" + ], + [ + "19380.88000000", + "0.00224500" + ], + [ + "19380.86000000", + "0.00077400" + ], + [ + "19380.76000000", + "0.13290000" + ], + [ + "19380.25000000", + "0.02579900" + ], + [ + "19380.10000000", + "0.00073600" + ], + [ + "19380.00000000", + "0.10593700" + ], + [ + "19379.96000000", + "0.00374700" + ], + [ + "19379.94000000", + "0.05341700" + ], + [ + "19379.92000000", + "0.00126200" + ], + [ + "19379.87000000", + "0.00081300" + ], + [ + "19379.75000000", + "0.00595000" + ], + [ + "19379.61000000", + "0.02000000" + ], + [ + "19379.60000000", + "0.00103300" + ], + [ + "19379.41000000", + "0.10000000" + ], + [ + "19379.33000000", + "0.00075700" + ], + [ + "19379.31000000", + "0.00141800" + ], + [ + "19378.91000000", + "0.00162500" + ], + [ + "19378.64000000", + "0.00058200" + ], + [ + "19378.37000000", + "0.02196600" + ], + [ + "19378.01000000", + "0.00060800" + ], + [ + "19378.00000000", + "0.00516000" + ], + [ + "19377.81000000", + "0.00260000" + ], + [ + "19377.71000000", + "0.00820700" + ], + [ + "19377.66000000", + "0.00147200" + ], + [ + "19377.57000000", + "0.01450000" + ], + [ + "19377.46000000", + "0.00062400" + ], + [ + "19377.45000000", + "0.00480900" + ], + [ + "19377.27000000", + "0.00113400" + ], + [ + "19377.22000000", + "0.00516100" + ], + [ + "19377.20000000", + "0.00464400" + ], + [ + "19377.11000000", + "0.00567700" + ], + [ + "19377.09000000", + "0.00128400" + ], + [ + "19376.73000000", + "0.00077500" + ], + [ + "19376.55000000", + "0.00516100" + ], + [ + "19376.35000000", + "0.00065500" + ], + [ + "19376.18000000", + "0.00154800" + ], + [ + "19376.15000000", + "0.00111200" + ], + [ + "19375.71000000", + "0.00088700" + ], + [ + "19375.67000000", + "0.00080000" + ], + [ + "19375.49000000", + "0.01032400" + ], + [ + "19375.42000000", + "0.00176700" + ], + [ + "19375.03000000", + "0.00066400" + ], + [ + "19375.00000000", + "0.05877700" + ], + [ + "19374.86000000", + "0.00139300" + ], + [ + "19374.68000000", + "0.00697200" + ], + [ + "19374.62000000", + "0.00052600" + ], + [ + "19374.56000000", + "0.00060900" + ], + [ + "19374.25000000", + "0.00058200" + ], + [ + "19374.20000000", + "0.00064400" + ], + [ + "19374.14000000", + "0.00258000" + ], + [ + "19374.11000000", + "0.02337200" + ], + [ + "19373.88000000", + "0.00081300" + ], + [ + "19373.78000000", + "0.00266400" + ], + [ + "19373.33000000", + "0.00336600" + ], + [ + "19373.03000000", + "0.00062500" + ], + [ + "19372.81000000", + "0.10289900" + ], + [ + "19372.61000000", + "0.00060000" + ], + [ + "19372.60000000", + "0.00056700" + ], + [ + "19372.41000000", + "0.00210000" + ], + [ + "19372.36000000", + "0.00516200" + ], + [ + "19372.29000000", + "0.00619500" + ], + [ + "19372.10000000", + "0.00059200" + ], + [ + "19371.46000000", + "0.00060000" + ], + [ + "19371.42000000", + "0.00430000" + ], + [ + "19371.34000000", + "0.00154900" + ], + [ + "19371.31000000", + "0.00526600" + ], + [ + "19371.09000000", + "0.04893900" + ], + [ + "19370.85000000", + "1.60000000" + ], + [ + "19370.76000000", + "0.00516300" + ], + [ + "19370.55000000", + "0.00297100" + ], + [ + "19370.28000000", + "0.00150700" + ], + [ + "19370.00000000", + "0.10000000" + ], + [ + "19369.99000000", + "0.00088700" + ], + [ + "19369.75000000", + "0.06275700" + ], + [ + "19369.65000000", + "0.00069900" + ], + [ + "19369.61000000", + "0.02266400" + ], + [ + "19369.48000000", + "0.01719200" + ], + [ + "19369.46000000", + "0.00415100" + ], + [ + "19369.19000000", + "0.00523700" + ], + [ + "19369.18000000", + "0.00196300" + ], + [ + "19368.97000000", + "0.10000000" + ], + [ + "19368.72000000", + "0.02474600" + ], + [ + "19368.55000000", + "0.00913900" + ], + [ + "19368.54000000", + "0.04744900" + ], + [ + "19368.50000000", + "0.00516400" + ], + [ + "19368.42000000", + "0.00530000" + ], + [ + "19368.06000000", + "0.00066600" + ], + [ + "19368.04000000", + "0.00446000" + ], + [ + "19367.91000000", + "0.00697100" + ], + [ + "19367.88000000", + "0.00081300" + ], + [ + "19367.76000000", + "0.00056400" + ], + [ + "19367.69000000", + "0.00109900" + ], + [ + "19367.63000000", + "0.00219900" + ], + [ + "19367.60000000", + "0.00315000" + ], + [ + "19367.56000000", + "0.00304700" + ], + [ + "19367.55000000", + "0.00346000" + ], + [ + "19367.54000000", + "0.02096400" + ], + [ + "19367.52000000", + "0.01843300" + ], + [ + "19367.51000000", + "0.00991500" + ], + [ + "19367.49000000", + "0.00104800" + ], + [ + "19367.34000000", + "0.01345100" + ], + [ + "19367.29000000", + "0.27107500" + ], + [ + "19367.28000000", + "33.27800000" + ], + [ + "19367.01000000", + "0.00458200" + ], + [ + "19366.73000000", + "0.00199300" + ], + [ + "19366.50000000", + "0.00154900" + ], + [ + "19366.36000000", + "0.00057300" + ], + [ + "19366.26000000", + "0.00064500" + ], + [ + "19365.85000000", + "0.00064400" + ], + [ + "19365.67000000", + "0.02338200" + ], + [ + "19365.45000000", + "0.00065400" + ], + [ + "19365.43000000", + "0.00059200" + ], + [ + "19365.30000000", + "0.00450000" + ], + [ + "19364.70000000", "0.00200000" + ], + [ + "19364.66000000", + "0.01158500" + ], + [ + "19364.63000000", + "0.00100000" + ], + [ + "19364.47000000", + "0.00064400" + ], + [ + "19364.28000000", + "0.00345900" + ], + [ + "19364.27000000", + "0.00088800" + ], + [ + "19364.16000000", + "0.00467800" + ], + [ + "19364.12000000", + "0.00108500" + ], + [ + "19364.09000000", + "0.00129200" + ], + [ + "19363.89000000", + "0.00516300" + ], + [ + "19363.85000000", + "0.00116700" + ], + [ + "19363.84000000", + "0.01291000" + ], + [ + "19363.81000000", + "0.07130000" + ], + [ + "19363.36000000", + "0.00309600" + ], + [ + "19363.26000000", + "0.00521900" + ], + [ + "19363.24000000", + "0.00347300" + ], + [ + "19362.95000000", + "0.00152200" + ], + [ + "19362.94000000", + "0.00062800" + ], + [ + "19362.91000000", + "0.00109100" + ], + [ + "19362.67000000", + "0.10000000" + ], + [ + "19362.40000000", + "0.06204000" + ], + [ + "19362.30000000", + "0.00335700" + ], + [ + "19362.06000000", + "0.00110000" + ], + [ + "19362.00000000", + "1.00000000" + ], + [ + "19361.91000000", + "0.00150300" + ], + [ + "19361.90000000", + "0.05201500" + ], + [ + "19361.88000000", + "0.00081400" + ], + [ + "19361.66000000", + "0.00154900" + ], + [ + "19361.38000000", + "0.00170000" + ], + [ + "19361.21000000", + "0.00063000" + ], + [ + "19361.11000000", + "0.00136700" + ], + [ + "19361.02000000", + "0.00059200" + ], + [ + "19360.87000000", + "0.00132100" + ], + [ + "19360.85000000", + "1.60000000" ] ], - "lastUpdateId": 3327091030 + "lastUpdateId": 7145155359 }, - "queryString": "limit=10\u0026symbol=BTCUSDT", + "queryString": "limit=1000\u0026symbol=BTCUSDT", "bodyParams": "", "headers": {} } @@ -93849,50 +101769,49 @@ } ] }, - "/wapi/v3/withdrawHistory.html": { - "GET": [ + "/wapi/v3/withdrawHistory.html": { + "GET": [ + { + "data": { + "withdrawList": [ { - "data": { - "withdrawList": [ - { - "id":"7213fea8e94b4a5593d507237e5a555b", - "withdrawOrderId": "None", - "amount": 0.99, - "transactionFee": 0.01, - "address": "0x6915f16f8791d0a1cc2bf47c13a6b2a92000504b", - "asset": "ETH", - "txId": "0xdf33b22bdb2b28b1f75ccd201a4a4m6e7g83jy5fc5d5a9d1340961598cfcb0a1", - "applyTime": 1508198532000, - "status": 4 - }, - { - "id":"7213fea8e94b4a5534ggsd237e5a555b", - "withdrawOrderId": "withdrawtest", - "amount": 999.9999, - "transactionFee": 0.0001, - "address": "463tWEBn5XZJSxLU34r6g7h8jtxuNcDbjLSjkn3XAXHCbLrTTErJrBWYgHJQyrCwkNgYvyV3z8zctJLPCZy24jvb3NiTcTJ", - "addressTag": "342341222", - "txId": "b3c6219639c8ae3f9cf010cdc24fw7f7yt8j1e063f9b4bd1a05cb44c4b6e2509", - "asset": "XMR", - "applyTime": 1508198532000, - "status": 4 - } - ], - "success": true - } - , - "queryString": "asset=XBT&recvWindow=5000×tamp=1606747517000&signature=495922a57f23874994c9018ce17d9ba31d1d1cdaca24916d88bb7dd26a4c99f2", - "bodyParams": "", - "headers": { - "Key": [ - "" - ], - "X-Mbx-Apikey": [ - "" - ] - } + "id": "7213fea8e94b4a5593d507237e5a555b", + "withdrawOrderId": "None", + "amount": 0.99, + "transactionFee": 0.01, + "address": "0x6915f16f8791d0a1cc2bf47c13a6b2a92000504b", + "asset": "ETH", + "txId": "0xdf33b22bdb2b28b1f75ccd201a4a4m6e7g83jy5fc5d5a9d1340961598cfcb0a1", + "applyTime": 1508198532000, + "status": 4 + }, + { + "id": "7213fea8e94b4a5534ggsd237e5a555b", + "withdrawOrderId": "withdrawtest", + "amount": 999.9999, + "transactionFee": 0.0001, + "address": "463tWEBn5XZJSxLU34r6g7h8jtxuNcDbjLSjkn3XAXHCbLrTTErJrBWYgHJQyrCwkNgYvyV3z8zctJLPCZy24jvb3NiTcTJ", + "addressTag": "342341222", + "txId": "b3c6219639c8ae3f9cf010cdc24fw7f7yt8j1e063f9b4bd1a05cb44c4b6e2509", + "asset": "XMR", + "applyTime": 1508198532000, + "status": 4 } - ] - } + ], + "success": true + }, + "queryString": "asset=XBT\u0026recvWindow=5000\u0026timestamp=1606747517000\u0026signature=495922a57f23874994c9018ce17d9ba31d1d1cdaca24916d88bb7dd26a4c99f2", + "bodyParams": "", + "headers": { + "Key": [ + "" + ], + "X-Mbx-Apikey": [ + "" + ] + } + } + ] + } } } \ No newline at end of file