From 51e2e42a1999b5eabb0076bc44dab6bc012756a9 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Fri, 13 Oct 2023 16:38:49 +1100 Subject: [PATCH] orderbook: Add optional orderbook.Item string fields for potential checksum calculations (#1354) * orderbook: Add optional orderbook.Item string fields for potential checksum calculations. * glorious: nits * glorious: nits * thrasher: nits * glorious: nits --------- Co-authored-by: Ryan O'Hara-Reid --- exchanges/kraken/kraken_test.go | 43 ++++---- exchanges/kraken/kraken_websocket.go | 130 ++++++++++++------------ exchanges/orderbook/depth.go | 46 +++++---- exchanges/orderbook/depth_test.go | 23 +++-- exchanges/orderbook/linked_list.go | 32 +++--- exchanges/orderbook/linked_list_test.go | 6 +- exchanges/orderbook/orderbook.go | 10 +- exchanges/orderbook/orderbook_test.go | 19 +++- exchanges/orderbook/orderbook_types.go | 64 +++++++----- 9 files changed, 205 insertions(+), 168 deletions(-) diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index a2e3ef76..03a95528 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -2040,28 +2040,29 @@ func TestGetHistoricTrades(t *testing.T) { 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}, + // NOTE: 0.00000500 float64 == 0.000005 + {Price: 0.05005, StrPrice: "0.05005", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05010, StrPrice: "0.05010", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05015, StrPrice: "0.05015", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05020, StrPrice: "0.05020", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05025, StrPrice: "0.05025", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05030, StrPrice: "0.05030", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05035, StrPrice: "0.05035", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05040, StrPrice: "0.05040", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05045, StrPrice: "0.05045", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05050, StrPrice: "0.05050", Amount: 0.00000500, StrAmount: "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}, + {Price: 0.05000, StrPrice: "0.05000", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04995, StrPrice: "0.04995", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04990, StrPrice: "0.04990", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04980, StrPrice: "0.04980", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04975, StrPrice: "0.04975", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04970, StrPrice: "0.04970", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04965, StrPrice: "0.04965", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04960, StrPrice: "0.04960", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04955, StrPrice: "0.04955", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04950, StrPrice: "0.04950", Amount: 0.00000500, StrAmount: "0.00000500"}, }, } @@ -2079,7 +2080,7 @@ func TestChecksumCalculation(t *testing.T) { t.Errorf("expected %s but received %s", expected, v) } - err := validateCRC32(&testOb, krakenAPIDocChecksum, 5, 8) + err := validateCRC32(&testOb, krakenAPIDocChecksum) if err != nil { t.Error(err) } diff --git a/exchanges/kraken/kraken_websocket.go b/exchanges/kraken/kraken_websocket.go index 59fb179a..4b422751 100644 --- a/exchanges/kraken/kraken_websocket.go +++ b/exchanges/kraken/kraken_websocket.go @@ -878,12 +878,13 @@ func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data map[ // wsProcessOrderBookPartial creates a new orderbook entry for a given currency pair func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, askData, bidData []interface{}) error { base := orderbook.Base{ - Pair: channelData.Pair, - Asset: asset.Spot, - VerifyOrderbook: k.CanVerifyOrderbook, - Bids: make(orderbook.Items, len(bidData)), - Asks: make(orderbook.Items, len(askData)), - MaxDepth: channelData.MaxDepth, + Pair: channelData.Pair, + Asset: asset.Spot, + VerifyOrderbook: k.CanVerifyOrderbook, + Bids: make(orderbook.Items, len(bidData)), + Asks: make(orderbook.Items, len(askData)), + MaxDepth: channelData.MaxDepth, + ChecksumStringRequired: true, } // Kraken ob data is timestamped per price, GCT orderbook data is // timestamped per entry using the highest last update time, we can attempt @@ -897,21 +898,35 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, as if len(asks) < 3 { return errors.New("unexpected asks length") } - price, err := strconv.ParseFloat(asks[0].(string), 64) + priceStr, ok := asks[0].(string) + if !ok { + return common.GetTypeAssertError("string", asks[0], "price") + } + 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 common.GetTypeAssertError("string", asks[1], "amount") + } + amount, err := strconv.ParseFloat(amountStr, 64) if err != nil { return err } - timeData, err := strconv.ParseFloat(asks[2].(string), 64) + tdStr, ok := asks[2].(string) + if !ok { + return common.GetTypeAssertError("string", asks[2], "time") + } + timeData, err := strconv.ParseFloat(tdStr, 64) if err != nil { return err } base.Asks[i] = orderbook.Item{ - Amount: amount, - Price: price, + Amount: amount, + StrAmount: amountStr, + Price: price, + StrPrice: priceStr, } askUpdatedTime := convert.TimeFromUnixTimestampDecimal(timeData) if highestLastUpdate.Before(askUpdatedTime) { @@ -927,22 +942,36 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, as if len(bids) < 3 { return errors.New("unexpected bids length") } - price, err := strconv.ParseFloat(bids[0].(string), 64) + priceStr, ok := bids[0].(string) + if !ok { + return common.GetTypeAssertError("string", bids[0], "price") + } + 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 common.GetTypeAssertError("string", bids[1], "amount") + } + amount, err := strconv.ParseFloat(amountStr, 64) if err != nil { return err } - timeData, err := strconv.ParseFloat(bids[2].(string), 64) + tdStr, ok := bids[2].(string) + if !ok { + return common.GetTypeAssertError("string", bids[2], "time") + } + timeData, err := strconv.ParseFloat(tdStr, 64) if err != nil { return err } base.Bids[i] = orderbook.Item{ - Amount: amount, - Price: price, + Amount: amount, + StrAmount: amountStr, + Price: price, + StrPrice: priceStr, } bidUpdateTime := convert.TimeFromUnixTimestampDecimal(timeData) @@ -968,7 +997,6 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, ask // 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 { @@ -1008,29 +1036,16 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, ask } update.Asks[i] = orderbook.Item{ - Amount: amount, - Price: price, + Amount: amount, + StrAmount: amountStr, + Price: price, + StrPrice: priceStr, } askUpdatedTime := convert.TimeFromUnixTimestampDecimal(timeData) 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 @@ -1071,29 +1086,16 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, ask } update.Bids[i] = orderbook.Item{ - Amount: amount, - Price: price, + Amount: amount, + StrAmount: amountStr, + Price: price, + StrPrice: priceStr, } bidUpdatedTime := convert.TimeFromUnixTimestampDecimal(timeData) 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 @@ -1115,29 +1117,23 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, ask return err } - return validateCRC32(book, uint32(token), priceDP, amtDP) + return validateCRC32(book, uint32(token)) } -func validateCRC32(b *orderbook.Base, token uint32, decPrice, decAmount int) error { - if decPrice == 0 || decAmount == 0 { - return fmt.Errorf("%s %s trailing decimal count not calculated", - b.Pair, - b.Asset) - } - +func validateCRC32(b *orderbook.Base, token uint32) error { var checkStr strings.Builder for i := 0; i < 10 && i < len(b.Asks); 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) + _, err := checkStr.WriteString(trim(b.Asks[i].StrPrice + trim(b.Asks[i].StrAmount))) + if err != nil { + return err + } } for i := 0; i < 10 && i < len(b.Bids); 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) + _, err := checkStr.WriteString(trim(b.Bids[i].StrPrice) + trim(b.Bids[i].StrAmount)) + if err != nil { + return err + } } if check := crc32.ChecksumIEEE([]byte(checkStr.String())); check != token { diff --git a/exchanges/orderbook/depth.go b/exchanges/orderbook/depth.go index 9827491d..cde8842c 100644 --- a/exchanges/orderbook/depth.go +++ b/exchanges/orderbook/depth.go @@ -78,17 +78,18 @@ func (d *Depth) Retrieve() (*Base, error) { return nil, d.validationError } return &Base{ - Bids: d.bids.retrieve(0), - Asks: d.asks.retrieve(0), - Exchange: d.exchange, - Asset: d.asset, - Pair: d.pair, - LastUpdated: d.lastUpdated, - LastUpdateID: d.lastUpdateID, - PriceDuplication: d.priceDuplication, - IsFundingRate: d.isFundingRate, - VerifyOrderbook: d.VerifyOrderbook, - MaxDepth: d.maxDepth, + Bids: d.bids.retrieve(0), + Asks: d.asks.retrieve(0), + Exchange: d.exchange, + Asset: d.asset, + Pair: d.pair, + LastUpdated: d.lastUpdated, + LastUpdateID: d.lastUpdateID, + PriceDuplication: d.priceDuplication, + IsFundingRate: d.isFundingRate, + VerifyOrderbook: d.VerifyOrderbook, + MaxDepth: d.maxDepth, + ChecksumStringRequired: d.checksumStringRequired, }, nil } @@ -282,17 +283,18 @@ func (d *Depth) UpdateInsertByID(update *Update) error { func (d *Depth) AssignOptions(b *Base) { d.m.Lock() d.options = options{ - exchange: b.Exchange, - pair: b.Pair, - asset: b.Asset, - lastUpdated: b.LastUpdated, - lastUpdateID: b.LastUpdateID, - priceDuplication: b.PriceDuplication, - isFundingRate: b.IsFundingRate, - VerifyOrderbook: b.VerifyOrderbook, - restSnapshot: b.RestSnapshot, - idAligned: b.IDAlignment, - maxDepth: b.MaxDepth, + exchange: b.Exchange, + pair: b.Pair, + asset: b.Asset, + lastUpdated: b.LastUpdated, + lastUpdateID: b.LastUpdateID, + priceDuplication: b.PriceDuplication, + isFundingRate: b.IsFundingRate, + VerifyOrderbook: b.VerifyOrderbook, + restSnapshot: b.RestSnapshot, + idAligned: b.IDAlignment, + maxDepth: b.MaxDepth, + checksumStringRequired: b.ChecksumStringRequired, } d.m.Unlock() } diff --git a/exchanges/orderbook/depth_test.go b/exchanges/orderbook/depth_test.go index d3ac0c32..ef2dc8a9 100644 --- a/exchanges/orderbook/depth_test.go +++ b/exchanges/orderbook/depth_test.go @@ -92,17 +92,18 @@ func TestRetrieve(t *testing.T) { d.asks.load([]Item{{Price: 1337}}, d.stack, time.Now()) d.bids.load([]Item{{Price: 1337}}, d.stack, time.Now()) d.options = options{ - exchange: "THE BIG ONE!!!!!!", - pair: currency.NewPair(currency.THETA, currency.USD), - asset: asset.DownsideProfitContract, - lastUpdated: time.Now(), - lastUpdateID: 1337, - priceDuplication: true, - isFundingRate: true, - VerifyOrderbook: true, - restSnapshot: true, - idAligned: true, - maxDepth: 10, + exchange: "THE BIG ONE!!!!!!", + pair: currency.NewPair(currency.THETA, currency.USD), + asset: asset.DownsideProfitContract, + lastUpdated: time.Now(), + lastUpdateID: 1337, + priceDuplication: true, + isFundingRate: true, + VerifyOrderbook: true, + restSnapshot: true, + idAligned: true, + maxDepth: 10, + checksumStringRequired: true, } // If we add anymore options to the options struct later this will complain diff --git a/exchanges/orderbook/linked_list.go b/exchanges/orderbook/linked_list.go index d5e7a0ea..0b64f309 100644 --- a/exchanges/orderbook/linked_list.go +++ b/exchanges/orderbook/linked_list.go @@ -100,8 +100,10 @@ updates: // Only apply changes when zero values are not present, Bitmex // for example sends 0 price values. tip.Value.Price = updts[x].Price + tip.Value.StrPrice = updts[x].StrPrice } tip.Value.Amount = updts[x].Amount + tip.Value.StrAmount = updts[x].StrAmount continue updates } return fmt.Errorf("update error: %w ID: %d not found", @@ -190,7 +192,7 @@ func (ll *linkedList) updateInsertByPrice(updts Items, stack *stack, maxChainLen for x := range updts { for tip := &ll.head; ; tip = &(*tip).Next { if *tip == nil { - insertHeadSpecific(ll, updts[x], stack) + insertHeadSpecific(ll, &updts[x], stack) break } if (*tip).Value.Price == updts[x].Price { // Match check @@ -198,6 +200,7 @@ func (ll *linkedList) updateInsertByPrice(updts Items, stack *stack, maxChainLen stack.Push(deleteAtTip(ll, tip), tn) } else { // Amend current amount value (*tip).Value.Amount = updts[x].Amount + (*tip).Value.StrAmount = updts[x].StrAmount } break // Continue updates } @@ -208,7 +211,7 @@ func (ll *linkedList) updateInsertByPrice(updts Items, stack *stack, maxChainLen // to a non-existent price level (OTC/Hidden order) so we can // break instantly and reduce the traversal of the entire chain. if updts[x].Amount > 0 { - insertAtTip(ll, tip, updts[x], stack) + insertAtTip(ll, tip, &updts[x], stack) } break // Continue updates } @@ -217,7 +220,7 @@ func (ll *linkedList) updateInsertByPrice(updts Items, stack *stack, maxChainLen // This check below is just a catch all in the event the above // zero value check fails if updts[x].Amount > 0 { - insertAtTail(ll, tip, updts[x], stack) + insertAtTail(ll, tip, &updts[x], stack) } break } @@ -255,7 +258,9 @@ updates: if tip.Next == nil { // no movement needed just a re-adjustment tip.Value.Price = updts[x].Price + tip.Value.StrPrice = updts[x].StrPrice tip.Value.Amount = updts[x].Amount + tip.Value.StrAmount = updts[x].StrAmount continue updates } // bookmark tip to move this node to correct price level @@ -264,6 +269,7 @@ updates: } // no price change, amend amount and continue update tip.Value.Amount = updts[x].Amount + tip.Value.StrAmount = updts[x].StrAmount continue updates // continue to next update } @@ -305,7 +311,7 @@ updates: } if tip.Next == nil { - if shiftBookmark(tip, &bookmark, &ll.head, updts[x]) { + if shiftBookmark(tip, &bookmark, &ll.head, &updts[x]) { continue updates } } @@ -352,7 +358,7 @@ func (ll *linkedList) insertUpdates(updts Items, stack *stack, comp comparison) } if (*tip).Next == nil { // Tail - insertAtTail(ll, tip, updts[x], stack) + insertAtTail(ll, tip, &updts[x], stack) break // Continue updates } prev = *tip @@ -778,9 +784,9 @@ func deleteAtTip(ll *linkedList, tip **Node) *Node { } // insertAtTip inserts at a tip target (can inline) -func insertAtTip(ll *linkedList, tip **Node, updt Item, stack *stack) { +func insertAtTip(ll *linkedList, tip **Node, updt *Item, stack *stack) { n := stack.Pop() - n.Value = updt + n.Value = *updt n.Next = *tip n.Prev = (*tip).Prev if (*tip).Prev == nil { // Tip is at head @@ -797,9 +803,9 @@ func insertAtTip(ll *linkedList, tip **Node, updt Item, stack *stack) { } // insertAtTail inserts at tail end of node chain (can inline) -func insertAtTail(ll *linkedList, tip **Node, updt Item, stack *stack) { +func insertAtTail(ll *linkedList, tip **Node, updt *Item, stack *stack) { n := stack.Pop() - n.Value = updt + n.Value = *updt // Reference tip to new node (*tip).Next = n // Reference new node with current tip @@ -810,9 +816,9 @@ func insertAtTail(ll *linkedList, tip **Node, updt Item, stack *stack) { // insertHeadSpecific inserts at head specifically there might be an instance // where the liquidity on an exchange does fall to zero through a streaming // endpoint then it comes back online. (can inline) -func insertHeadSpecific(ll *linkedList, updt Item, stack *stack) { +func insertHeadSpecific(ll *linkedList, updt *Item, stack *stack) { n := stack.Pop() - n.Value = updt + n.Value = *updt ll.head = n ll.length++ } @@ -842,12 +848,12 @@ func insertNodeAtBookmark(ll *linkedList, bookmark, n *Node) { // shiftBookmark moves a bookmarked node to the tip's next position or if nil, // sets tip as bookmark (can inline) -func shiftBookmark(tip *Node, bookmark, head **Node, updt Item) bool { +func shiftBookmark(tip *Node, bookmark, head **Node, updt *Item) bool { if *bookmark == nil { // End of the chain and no bookmark set *bookmark = tip // Set tip to bookmark so we can set a new node there return false } - (*bookmark).Value = updt + (*bookmark).Value = *updt (*bookmark).Next.Prev = (*bookmark).Prev if (*bookmark).Prev == nil { // Bookmark is at head *head = (*bookmark).Next diff --git a/exchanges/orderbook/linked_list_test.go b/exchanges/orderbook/linked_list_test.go index c5d66312..93768937 100644 --- a/exchanges/orderbook/linked_list_test.go +++ b/exchanges/orderbook/linked_list_test.go @@ -1421,7 +1421,7 @@ func TestShiftBookmark(t *testing.T) { // associate tips prev field with the correct prev node tip.Prev = tipprev - if !shiftBookmark(tip, &bookmarkedNode, nil, Item{Amount: 1336, ID: 1337, Price: 9999}) { + if !shiftBookmark(tip, &bookmarkedNode, nil, &Item{Amount: 1336, ID: 1337, Price: 9999}) { t.Fatal("There should be liquidity so we don't need to set tip to bookmark") } @@ -1453,7 +1453,7 @@ func TestShiftBookmark(t *testing.T) { var nilBookmark *Node - if shiftBookmark(tip, &nilBookmark, nil, Item{Amount: 1336, ID: 1337, Price: 9999}) { + if shiftBookmark(tip, &nilBookmark, nil, &Item{Amount: 1336, ID: 1337, Price: 9999}) { t.Fatal("there should not be a bookmarked node") } @@ -1466,7 +1466,7 @@ func TestShiftBookmark(t *testing.T) { bookmarkedNode.Next = originalBookmarkNext tip.Next = nil - if !shiftBookmark(tip, &bookmarkedNode, &head, Item{Amount: 1336, ID: 1337, Price: 9999}) { + if !shiftBookmark(tip, &bookmarkedNode, &head, &Item{Amount: 1336, ID: 1337, Price: 9999}) { t.Fatal("There should be liquidity so we don't need to set tip to bookmark") } diff --git a/exchanges/orderbook/orderbook.go b/exchanges/orderbook/orderbook.go index 93a93666..d5a8ca5e 100644 --- a/exchanges/orderbook/orderbook.go +++ b/exchanges/orderbook/orderbook.go @@ -225,11 +225,11 @@ func (b *Base) Verify() error { len(b.Bids), len(b.Asks)) } - err := checkAlignment(b.Bids, b.IsFundingRate, b.PriceDuplication, b.IDAlignment, dsc, b.Exchange) + err := checkAlignment(b.Bids, b.IsFundingRate, b.PriceDuplication, b.IDAlignment, b.ChecksumStringRequired, dsc, b.Exchange) if err != nil { return fmt.Errorf(bidLoadBookFailure, b.Exchange, b.Pair, b.Asset, err) } - err = checkAlignment(b.Asks, b.IsFundingRate, b.PriceDuplication, b.IDAlignment, asc, b.Exchange) + err = checkAlignment(b.Asks, b.IsFundingRate, b.PriceDuplication, b.IDAlignment, b.ChecksumStringRequired, asc, b.Exchange) if err != nil { return fmt.Errorf(askLoadBookFailure, b.Exchange, b.Pair, b.Asset, err) } @@ -257,7 +257,7 @@ var dsc = func(current Item, previous Item) error { } // checkAlignment validates full orderbook -func checkAlignment(depth Items, fundingRate, priceDuplication, isIDAligned bool, c checker, exch string) error { +func checkAlignment(depth Items, fundingRate, priceDuplication, isIDAligned, requiresChecksumString bool, c checker, exch string) error { for i := range depth { if depth[i].Price == 0 { switch { @@ -273,6 +273,10 @@ func checkAlignment(depth Items, fundingRate, priceDuplication, isIDAligned bool if fundingRate && depth[i].Period == 0 { return errPeriodUnset } + if requiresChecksumString && (depth[i].StrAmount == "" || depth[i].StrPrice == "") { + return errChecksumStringNotSet + } + if i != 0 { prev := i - 1 if err := c(depth[i], depth[prev]); err != nil { diff --git a/exchanges/orderbook/orderbook_test.go b/exchanges/orderbook/orderbook_test.go index b78e8551..5c8b0deb 100644 --- a/exchanges/orderbook/orderbook_test.go +++ b/exchanges/orderbook/orderbook_test.go @@ -759,16 +759,29 @@ func TestCheckAlignment(t *testing.T) { Period: 1337, }, } - err := checkAlignment(itemWithFunding, true, true, false, dsc, "Bitfinex") + err := checkAlignment(itemWithFunding, true, true, false, false, dsc, "Bitfinex") if err != nil { t.Error(err) } - err = checkAlignment(itemWithFunding, false, true, false, dsc, "Bitfinex") + err = checkAlignment(itemWithFunding, false, true, false, false, dsc, "Bitfinex") if !errors.Is(err, errPriceNotSet) { t.Fatalf("received: %v but expected: %v", err, errPriceNotSet) } - err = checkAlignment(itemWithFunding, true, true, false, dsc, "Binance") + err = checkAlignment(itemWithFunding, true, true, false, false, dsc, "Binance") if !errors.Is(err, errPriceNotSet) { t.Fatalf("received: %v but expected: %v", err, errPriceNotSet) } + + itemWithFunding[0].Price = 1337 + err = checkAlignment(itemWithFunding, true, true, false, true, dsc, "Binance") + if !errors.Is(err, errChecksumStringNotSet) { + t.Fatalf("received: %v but expected: %v", err, errChecksumStringNotSet) + } + + itemWithFunding[0].StrAmount = "1337.0000000" + itemWithFunding[0].StrPrice = "1337.0000000" + err = checkAlignment(itemWithFunding, true, true, false, true, dsc, "Binance") + if !errors.Is(err, nil) { + t.Fatalf("received: %v but expected: %v", err, nil) + } } diff --git a/exchanges/orderbook/orderbook_types.go b/exchanges/orderbook/orderbook_types.go index 3235f9a1..229cdcd8 100644 --- a/exchanges/orderbook/orderbook_types.go +++ b/exchanges/orderbook/orderbook_types.go @@ -21,18 +21,19 @@ const ( // Vars for the orderbook package var ( - errExchangeNameUnset = errors.New("orderbook exchange name not set") - errPairNotSet = errors.New("orderbook currency pair not set") - errAssetTypeNotSet = errors.New("orderbook asset type not set") - errCannotFindOrderbook = errors.New("cannot find orderbook(s)") - errPriceNotSet = errors.New("price cannot be zero") - errAmountInvalid = errors.New("amount cannot be less or equal to zero") - errPriceOutOfOrder = errors.New("pricing out of order") - errIDOutOfOrder = errors.New("ID out of order") - errDuplication = errors.New("price duplication") - errIDDuplication = errors.New("id duplication") - errPeriodUnset = errors.New("funding rate period is unset") - errNotEnoughLiquidity = errors.New("not enough liquidity") + errExchangeNameUnset = errors.New("orderbook exchange name not set") + errPairNotSet = errors.New("orderbook currency pair not set") + errAssetTypeNotSet = errors.New("orderbook asset type not set") + errCannotFindOrderbook = errors.New("cannot find orderbook(s)") + errPriceNotSet = errors.New("price cannot be zero") + errAmountInvalid = errors.New("amount cannot be less or equal to zero") + errPriceOutOfOrder = errors.New("pricing out of order") + errIDOutOfOrder = errors.New("ID out of order") + errDuplication = errors.New("price duplication") + errIDDuplication = errors.New("id duplication") + errPeriodUnset = errors.New("funding rate period is unset") + errNotEnoughLiquidity = errors.New("not enough liquidity") + errChecksumStringNotSet = errors.New("checksum string not set") ) var service = Service{ @@ -57,8 +58,16 @@ type Exchange struct { // Item stores the amount and price values type Item struct { Amount float64 - Price float64 - ID int64 + // StrAmount is a string representation of the amount. e.g. 0.00000100 this + // parsed as a float will constrict comparison to 1e-6 not 1e-8 or + // potentially will round value which is not ideal. + StrAmount string + Price float64 + // StrPrice is a string representation of the price. e.g. 0.00000100 this + // parsed as a float will constrict comparison to 1e-6 not 1e-8 or + // potentially will round value which is not ideal. + StrPrice string + ID int64 // Funding rate field Period int64 @@ -100,6 +109,10 @@ type Base struct { // should remove any items that are outside of this scope. Bittrex and // Kraken utilise this field. MaxDepth int + // ChecksumStringRequired defines if the checksum is built from the raw + // string representations of the price and amount. This helps alleviate any + // potential rounding issues. + ChecksumStringRequired bool } type byOBPrice []Item @@ -109,17 +122,18 @@ func (a byOBPrice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byOBPrice) Less(i, j int) bool { return a[i].Price < a[j].Price } type options struct { - exchange string - pair currency.Pair - asset asset.Item - lastUpdated time.Time - lastUpdateID int64 - priceDuplication bool - isFundingRate bool - VerifyOrderbook bool - restSnapshot bool - idAligned bool - maxDepth int + exchange string + pair currency.Pair + asset asset.Item + lastUpdated time.Time + lastUpdateID int64 + priceDuplication bool + isFundingRate bool + VerifyOrderbook bool + restSnapshot bool + idAligned bool + checksumStringRequired bool + maxDepth int } // Action defines a set of differing states required to implement an incoming