From 9059486064f7a0786d6a9d7bd85f0d631db09fa2 Mon Sep 17 00:00:00 2001 From: rizer1980 <4340180@gmail.com> Date: Fri, 24 May 2024 19:39:49 +0300 Subject: [PATCH 1/2] [core] orderbook sync --- .../xchange/dto/marketdata/OrderBook.java | 113 ++++--- .../dto/marketdata/ConcurrencyTest.java | 214 +++++++++++++ .../xchange/dto/marketdata/OrderBookOld.java | 280 ++++++++++++++++++ .../service/GateioMarketDataServiceTest.java | 2 +- 4 files changed, 569 insertions(+), 40 deletions(-) create mode 100644 xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/ConcurrencyTest.java create mode 100644 xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/OrderBookOld.java diff --git a/xchange-core/src/main/java/org/knowm/xchange/dto/marketdata/OrderBook.java b/xchange-core/src/main/java/org/knowm/xchange/dto/marketdata/OrderBook.java index e22013554c6..9d74d0295d2 100644 --- a/xchange-core/src/main/java/org/knowm/xchange/dto/marketdata/OrderBook.java +++ b/xchange-core/src/main/java/org/knowm/xchange/dto/marketdata/OrderBook.java @@ -1,6 +1,7 @@ package org.knowm.xchange.dto.marketdata; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import java.io.Serializable; import java.math.BigDecimal; @@ -8,8 +9,10 @@ import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.concurrent.locks.StampedLock; import java.util.stream.Collectors; import java.util.stream.Stream; +import lombok.Getter; import org.knowm.xchange.dto.Order.OrderType; import org.knowm.xchange.dto.trade.LimitOrder; import org.knowm.xchange.instrument.Instrument; @@ -18,14 +21,17 @@ public final class OrderBook implements Serializable { private static final long serialVersionUID = -7788306758114464314L; - + @JsonIgnore public final StampedLock lock = new StampedLock(); /** the asks */ + @Getter private final List asks; /** the bids */ + @Getter private final List bids; /** the timestamp of the orderbook according to the exchange's server, null if not provided */ + @Getter private Date timeStamp; /** @@ -113,21 +119,6 @@ private static LimitOrder withAmount(LimitOrder limitOrder, BigDecimal tradeable return new LimitOrder(type, tradeableAmount, instrument, id, date, limit); } - public Date getTimeStamp() { - - return timeStamp; - } - - public List getAsks() { - - return asks; - } - - public List getBids() { - - return bids; - } - public List getOrders(OrderType type) { return type == OrderType.ASK ? asks : bids; @@ -141,23 +132,35 @@ public List getOrders(OrderType type) { * @param limitOrder the new LimitOrder */ public void update(LimitOrder limitOrder) { - update(getOrders(limitOrder.getType()), limitOrder); - updateDate(limitOrder.getTimestamp()); } // Replace the amount for limitOrder's price in the provided list. - private void update(List asks, LimitOrder limitOrder) { - - int idx = Collections.binarySearch(asks, limitOrder); - if (idx >= 0) { - asks.remove(idx); - } else { - idx = -idx - 1; - } - - if (limitOrder.getRemainingAmount().compareTo(BigDecimal.ZERO) != 0) { - asks.add(idx, limitOrder); + private void update(List limitOrders, LimitOrder limitOrder) { + long stamp = lock.readLock(); + int idx = Collections.binarySearch(limitOrders, limitOrder); + try { + while (true) { + long writeStamp = lock.tryConvertToWriteLock(stamp); + if (writeStamp != 0L) { + stamp = writeStamp; + if (idx >= 0) limitOrders.remove(idx); + else idx = -idx - 1; + if (limitOrder.getRemainingAmount().compareTo(BigDecimal.ZERO) != 0) + limitOrders.add(idx, limitOrder); + updateDate(limitOrder.getTimestamp()); + break; + } else { + lock.unlockRead(stamp); + stamp = lock.writeLock(); + // here wee need to recheck idx, because it is possible that orderBook changed between + // unlockRead and lockWrite + if (recheckIdx(limitOrders, limitOrder, idx)) + idx = Collections.binarySearch(limitOrders, limitOrder); + } + } + } finally { + lock.unlock(stamp); } } @@ -169,22 +172,54 @@ private void update(List asks, LimitOrder limitOrder) { * @param orderBookUpdate the new OrderBookUpdate */ public void update(OrderBookUpdate orderBookUpdate) { - + long stamp = lock.readLock(); LimitOrder limitOrder = orderBookUpdate.getLimitOrder(); List limitOrders = getOrders(limitOrder.getType()); int idx = Collections.binarySearch(limitOrders, limitOrder); - if (idx >= 0) { - limitOrders.remove(idx); - } else { - idx = -idx - 1; + try { + while (true) { + long writeStamp = lock.tryConvertToWriteLock(stamp); + if (writeStamp != 0L) { + stamp = writeStamp; + if (idx >= 0) limitOrders.remove(idx); + else idx = -idx - 1; + if (orderBookUpdate.getTotalVolume().compareTo(BigDecimal.ZERO) != 0) { + LimitOrder updatedOrder = withAmount(limitOrder, orderBookUpdate.getTotalVolume()); + limitOrders.add(idx, updatedOrder); + } + updateDate(limitOrder.getTimestamp()); + break; + } else { + lock.unlockRead(stamp); + stamp = lock.writeLock(); + // here wee need to recheck idx, because it is possible that orderBook changed between + // unlockRead and lockWrite + if (recheckIdx(limitOrders, limitOrder, idx)) + idx = Collections.binarySearch(limitOrders, limitOrder); + } + } + } finally { + lock.unlock(stamp); } + } - if (orderBookUpdate.getTotalVolume().compareTo(BigDecimal.ZERO) != 0) { - LimitOrder updatedOrder = withAmount(limitOrder, orderBookUpdate.getTotalVolume()); - limitOrders.add(idx, updatedOrder); + private boolean recheckIdx(List limitOrders, LimitOrder limitOrder, int idx) { + if (idx >= 0) { + // if positive, null check or compare + return limitOrders.get(idx) == null || limitOrders.get(idx).compareTo(limitOrder) != 0; + } else { + // on end of array, null check or one check + if (limitOrders.size() == -idx - 1) { + return limitOrders.get(-idx - 2) == null + || limitOrders.get(-idx - 2).compareTo(limitOrder) >= 0; + } else + // if negative, check that of limitOrders.get(reversed idx) limitOrders.get(reversed idx-1) + // and is lower and bigger than limitOrder + return (limitOrders.get(-idx - 1) == null + || limitOrders.get(-idx - 1).compareTo(limitOrder) <= 0) + && (limitOrders.get(-idx - 2) == null + || limitOrders.get(-idx - 2).compareTo(limitOrder) >= 0); } - - updateDate(limitOrder.getTimestamp()); } // Replace timeStamp if the provided date is non-null and in the future diff --git a/xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/ConcurrencyTest.java b/xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/ConcurrencyTest.java new file mode 100644 index 00000000000..9cbc4364d6d --- /dev/null +++ b/xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/ConcurrencyTest.java @@ -0,0 +1,214 @@ +package org.knowm.xchange.dto.marketdata; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import lombok.var; +import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.Order.OrderType; +import org.knowm.xchange.dto.trade.LimitOrder; +import org.knowm.xchange.instrument.Instrument; + +public class ConcurrencyTest { + + static Instrument inst = new CurrencyPair("BTC/USDT"); + + public static void main(String[] args) throws InterruptedException, ExecutionException { + OrderBook orderBook1 = new OrderBook(new Date(), initOrderBookAsks(), initOrderBookBids(), + true); + OrderBook orderBook2 = new OrderBook(new Date(), initOrderBookAsks(), initOrderBookBids(), + true); + OrderBookOld orderBookOld = new OrderBookOld(new Date(), initOrderBookAsks(), + initOrderBookBids(), true); + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(50); + newWay(orderBook1, executor); + executor.awaitTermination(100L, TimeUnit.SECONDS); + executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(50); + oldWay(orderBook2, executor); + executor.awaitTermination(100L, TimeUnit.SECONDS); + executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(50); + oldOB(orderBookOld, executor); + executor.awaitTermination(100L, TimeUnit.SECONDS); + if (orderBook1.getAsks().get(0).getOriginalAmount() + .equals(orderBook2.getAsks().get(0).getOriginalAmount()) + && orderBook1.getAsks().get(0).getOriginalAmount() + .equals(orderBookOld.getAsks().get(0).getOriginalAmount())) { + System.out.println("OrderBooks equals"); + } + } + + + private static List initOrderBookAsks() { + List asks = new ArrayList<>(); + asks.add(new LimitOrder(OrderType.ASK, new BigDecimal(1), inst, "", new Date(), + new BigDecimal(103))); + asks.add(new LimitOrder(OrderType.ASK, new BigDecimal(1), inst, "", new Date(), + new BigDecimal(102))); + asks.add(new LimitOrder(OrderType.ASK, new BigDecimal(1), inst, "", new Date(), + new BigDecimal(101))); + return asks; + } + + private static List initOrderBookBids() { + List bids = new ArrayList<>(); + bids.add( + new LimitOrder(OrderType.BID, new BigDecimal(1), inst, "", new Date(), new BigDecimal(99))); + bids.add( + new LimitOrder(OrderType.BID, new BigDecimal(1), inst, "", new Date(), new BigDecimal(98))); + bids.add( + new LimitOrder(OrderType.BID, new BigDecimal(1), inst, "", new Date(), new BigDecimal(97))); + return bids; + } + + private static void newWay(OrderBook orderBook, ThreadPoolExecutor executor) + throws InterruptedException { + System.out.printf("OrderBook before %s%n", orderBook); + for (int i = 0; i < 50; i++) { + executor.execute(() -> updateOrderBook1(orderBook, false)); + executor.execute(() -> readOrderBook(orderBook, false)); + executor.execute(() -> updateOrderBook2(orderBook, false)); + } + executor.shutdown(); + executor.awaitTermination(100L, TimeUnit.SECONDS); + System.out.printf("OrderBook after %s%n", orderBook); + } + + private static void oldWay(OrderBook orderBook, ThreadPoolExecutor executor) + throws InterruptedException { + System.out.printf("OrderBook before %s%n", orderBook); + for (int i = 0; i < 50; i++) { + executor.execute(() -> updateOrderBook1(orderBook, true)); + executor.execute(() -> readOrderBook(orderBook, true)); + executor.execute(() -> updateOrderBook2(orderBook, true)); + } + executor.shutdown(); + executor.awaitTermination(100L, TimeUnit.SECONDS); + System.out.printf("OrderBook after %s%n", orderBook); + } + + private static void oldOB(OrderBookOld orderBookOld, ThreadPoolExecutor executor) + throws InterruptedException { + System.out.printf("OrderBookOld before %s%n", orderBookOld); + for (int i = 0; i < 50; i++) { + executor.execute(() -> updateOrderBookOld1(orderBookOld)); + executor.execute(() -> readOrderBookOld(orderBookOld)); + executor.execute(() -> updateOrderBookOld2(orderBookOld)); + } + executor.shutdown(); + executor.awaitTermination(100L, TimeUnit.SECONDS); + System.out.printf("OrderBookOld after %s%n", orderBookOld); + } + + public static void updateOrderBook1(OrderBook orderBook, boolean oldWay) { + Random rand = new Random(123); + for (int i = 0; i < 100000; i++) { + OrderBookUpdate orderBookUpdateAsk = new OrderBookUpdate(OrderType.ASK, new BigDecimal(0), + inst, new BigDecimal(101), new Date(), new BigDecimal(rand.nextInt())); + OrderBookUpdate orderBookUpdateBid = new OrderBookUpdate(OrderType.BID, new BigDecimal(0), + inst, new BigDecimal(99), new Date(), new BigDecimal(rand.nextInt())); + if (oldWay) { + synchronized (orderBook) { + orderBook.update(orderBookUpdateAsk); + orderBook.update(orderBookUpdateBid); + } + } else { + orderBook.update(orderBookUpdateAsk); + orderBook.update(orderBookUpdateBid); + } + } + } + + public static void updateOrderBookOld1(OrderBookOld orderBookOld) { + Random rand = new Random(123); + for (int i = 0; i < 100000; i++) { + OrderBookUpdate orderBookUpdateAsk = new OrderBookUpdate(OrderType.ASK, new BigDecimal(0), + inst, new BigDecimal(101), new Date(), new BigDecimal(rand.nextInt())); + OrderBookUpdate orderBookUpdateBid = new OrderBookUpdate(OrderType.BID, new BigDecimal(0), + inst, new BigDecimal(99), new Date(), new BigDecimal(rand.nextInt())); + synchronized (orderBookOld) { + orderBookOld.update(orderBookUpdateAsk); + orderBookOld.update(orderBookUpdateBid); + } + } + } + + private static void updateOrderBook2(OrderBook orderBook, boolean oldWay) { + Random rand = new Random(123); + for (int i = 0; i < 100000; i++) { + LimitOrder bookUpdateAsk = new LimitOrder(OrderType.ASK, new BigDecimal(rand.nextInt()), + inst, "", new Date(), new BigDecimal(101)); + LimitOrder bookUpdateBid = new LimitOrder(OrderType.BID, new BigDecimal(rand.nextInt()), + inst, "", new Date(), new BigDecimal(99)); + if (oldWay) { + synchronized (orderBook) { + orderBook.update(bookUpdateAsk); + orderBook.update(bookUpdateBid); + } + } else { + orderBook.update(bookUpdateAsk); + orderBook.update(bookUpdateBid); + } + } + } + + private static void updateOrderBookOld2(OrderBookOld orderBookOld) { + Random rand = new Random(123); + for (int i = 0; i < 100000; i++) { + LimitOrder bookUpdateAsk = new LimitOrder(OrderType.ASK, new BigDecimal(rand.nextInt()), + inst, "", new Date(), new BigDecimal(101)); + LimitOrder bookUpdateBid = new LimitOrder(OrderType.BID, new BigDecimal(rand.nextInt()), + inst, "", new Date(), new BigDecimal(99)); + synchronized (orderBookOld) { + orderBookOld.update(bookUpdateAsk); + orderBookOld.update(bookUpdateBid); + } + } + } + + private static void readOrderBook(OrderBook orderBook, boolean oldWay) { + for (int i = 0; i < 1200000; i++) { + int temp = 0; + if (oldWay) { + synchronized (orderBook) { + for (LimitOrder ask : orderBook.getAsks()) { + temp += ask.hashCode(); + } + for (LimitOrder bid : orderBook.getBids()) { + temp += bid.hashCode(); + } + } + } else { + var stamp = orderBook.lock.readLock(); + for (LimitOrder ask : orderBook.getAsks()) { + temp += ask.hashCode(); + } + for (LimitOrder bid : orderBook.getBids()) { + temp += bid.hashCode(); + } + orderBook.lock.unlockRead(stamp); + } + } + } + + private static void readOrderBookOld(OrderBookOld orderBookOld) { + for (int i = 0; i < 1200000; i++) { + int temp = 0; + synchronized (orderBookOld) { + for (LimitOrder ask : orderBookOld.getAsks()) { + temp += ask.hashCode(); + } + for (LimitOrder bid : orderBookOld.getBids()) { + temp += bid.hashCode(); + } + } + } + } + +} + diff --git a/xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/OrderBookOld.java b/xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/OrderBookOld.java new file mode 100644 index 00000000000..576ef60626a --- /dev/null +++ b/xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/OrderBookOld.java @@ -0,0 +1,280 @@ +package org.knowm.xchange.dto.marketdata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.knowm.xchange.dto.Order.OrderType; +import org.knowm.xchange.dto.trade.LimitOrder; +import org.knowm.xchange.instrument.Instrument; + + /** DTO representing the exchange order book */ + public final class OrderBookOld implements Serializable { + + private static final long serialVersionUID = -7788306758114464314L; + + /** the asks */ + private final List asks; + + /** the bids */ + private final List bids; + + /** the timestamp of the orderbook according to the exchange's server, null if not provided */ + private Date timeStamp; + + /** + * Constructor + * + * @param timeStamp - the timestamp of the orderbook according to the exchange's server, null if + * not provided + * @param asks The ASK orders + * @param bids The BID orders + */ + @JsonCreator + public OrderBookOld( + @JsonProperty("timeStamp") Date timeStamp, + @JsonProperty("asks") List asks, + @JsonProperty("bids") List bids) { + + this(timeStamp, asks, bids, false); + } + + /** + * Constructor + * + * @param timeStamp - the timestamp of the orderbook according to the exchange's server, null if + * not provided + * @param asks The ASK orders + * @param bids The BID orders + * @param sort True if the asks and bids need to be sorted + */ + public OrderBookOld(Date timeStamp, List asks, List bids, boolean sort) { + + this.timeStamp = timeStamp; + if (sort) { + this.asks = new ArrayList<>(asks); + this.bids = new ArrayList<>(bids); + Collections.sort(this.asks); + Collections.sort(this.bids); + } else { + this.asks = asks; + this.bids = bids; + } + } + + /** + * Constructor + * + * @param timeStamp - the timestamp of the orderbook according to the exchange's server, null if + * not provided + * @param asks The ASK orders + * @param bids The BID orders + */ + public OrderBookOld(Date timeStamp, Stream asks, Stream bids) { + + this(timeStamp, asks, bids, false); + } + + /** + * Constructor + * + * @param timeStamp - the timestamp of the orderbook according to the exchange's server, null if + * not provided + * @param asks The ASK orders + * @param bids The BID orders + * @param sort True if the asks and bids need to be sorted + */ + public OrderBookOld(Date timeStamp, Stream asks, Stream bids, boolean sort) { + + this.timeStamp = timeStamp; + if (sort) { + this.asks = asks.sorted().collect(Collectors.toList()); + this.bids = bids.sorted().collect(Collectors.toList()); + } else { + this.asks = asks.collect(Collectors.toList()); + this.bids = bids.collect(Collectors.toList()); + } + } + + // Returns a copy of limitOrder with tradeableAmount replaced. + private static LimitOrder withAmount(LimitOrder limitOrder, BigDecimal tradeableAmount) { + + OrderType type = limitOrder.getType(); + Instrument instrument = limitOrder.getInstrument(); + String id = limitOrder.getId(); + Date date = limitOrder.getTimestamp(); + BigDecimal limit = limitOrder.getLimitPrice(); + return new LimitOrder(type, tradeableAmount, instrument, id, date, limit); + } + + public Date getTimeStamp() { + + return timeStamp; + } + + public List getAsks() { + + return asks; + } + + public List getBids() { + + return bids; + } + + public List getOrders(OrderType type) { + + return type == OrderType.ASK ? asks : bids; + } + + /** + * Given a new LimitOrder, it will replace a matching limit order in the orderbook if one is + * found, or add the new LimitOrder if one is not. timeStamp will be updated if the new timestamp + * is non-null and in the future. + * + * @param limitOrder the new LimitOrder + */ + public void update(LimitOrder limitOrder) { + + update(getOrders(limitOrder.getType()), limitOrder); + updateDate(limitOrder.getTimestamp()); + } + + // Replace the amount for limitOrder's price in the provided list. + private void update(List asks, LimitOrder limitOrder) { + + int idx = Collections.binarySearch(asks, limitOrder); + if (idx >= 0) { + asks.remove(idx); + } else { + idx = -idx - 1; + } + + if (limitOrder.getRemainingAmount().compareTo(BigDecimal.ZERO) != 0) { + asks.add(idx, limitOrder); + } + } + + /** + * Given an OrderBookUpdate, it will replace a matching limit order in the orderbook if one is + * found, or add a new if one is not. timeStamp will be updated if the new timestamp is non-null + * and in the future. + * + * @param orderBookUpdate the new OrderBookUpdate + */ + public void update(OrderBookUpdate orderBookUpdate) { + + LimitOrder limitOrder = orderBookUpdate.getLimitOrder(); + List limitOrders = getOrders(limitOrder.getType()); + int idx = Collections.binarySearch(limitOrders, limitOrder); + if (idx >= 0) { + limitOrders.remove(idx); + } else { + idx = -idx - 1; + } + + if (orderBookUpdate.getTotalVolume().compareTo(BigDecimal.ZERO) != 0) { + LimitOrder updatedOrder = withAmount(limitOrder, orderBookUpdate.getTotalVolume()); + limitOrders.add(idx, updatedOrder); + } + + updateDate(limitOrder.getTimestamp()); + } + + // Replace timeStamp if the provided date is non-null and in the future + // TODO should this raise an exception if the order timestamp is in the past? + private void updateDate(Date updateDate) { + + if (updateDate != null && (timeStamp == null || updateDate.after(timeStamp))) { + this.timeStamp = updateDate; + } + } + + @Override + public int hashCode() { + + int hash = 17; + hash = 31 * hash + (this.timeStamp != null ? this.timeStamp.hashCode() : 0); + for (LimitOrder order : this.bids) { + hash = 31 * hash + order.hashCode(); + } + for (LimitOrder order : this.asks) { + hash = 31 * hash + order.hashCode(); + } + return hash; + } + + @Override + public boolean equals(Object obj) { + + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final org.knowm.xchange.dto.marketdata.OrderBookOld other = (org.knowm.xchange.dto.marketdata.OrderBookOld) obj; + if (this.timeStamp == null + ? other.timeStamp != null + : !this.timeStamp.equals(other.timeStamp)) { + return false; + } + if (this.bids.size() != other.bids.size()) { + return false; + } + for (int index = 0; index < this.bids.size(); index++) { + if (!this.bids.get(index).equals(other.bids.get(index))) { + return false; + } + } + if (this.asks.size() != other.asks.size()) { + return false; + } + for (int index = 0; index < this.asks.size(); index++) { + if (!this.asks.get(index).equals(other.asks.get(index))) { + return false; + } + } + return true; + } + + /** + * Identical to {@link #equals(Object) equals} method except that this ignores different + * timestamps. In other words, this version of equals returns true if the order internal to the + * OrderBooks are equal but their timestamps are unequal. It returns false if any order between + * the two are different. + * + * @param ob + * @return + */ + public boolean ordersEqual(org.knowm.xchange.dto.marketdata.OrderBook ob) { + + if (ob == null) { + return false; + } + + Date timestamp = new Date(); + org.knowm.xchange.dto.marketdata.OrderBook thisOb = new org.knowm.xchange.dto.marketdata.OrderBook(timestamp, this.getAsks(), this.getBids()); + org.knowm.xchange.dto.marketdata.OrderBook thatOb = new org.knowm.xchange.dto.marketdata.OrderBook(timestamp, ob.getAsks(), ob.getBids()); + return thisOb.equals(thatOb); + } + + @Override + public String toString() { + + return "OrderBook [timestamp: " + + timeStamp + + ", asks=" + + asks.toString() + + ", bids=" + + bids.toString() + + "]"; + } + } + diff --git a/xchange-gateio-v4/src/test/java/org/knowm/xchange/gateio/service/GateioMarketDataServiceTest.java b/xchange-gateio-v4/src/test/java/org/knowm/xchange/gateio/service/GateioMarketDataServiceTest.java index 5a5f5fbd8f4..bd653c8dadc 100644 --- a/xchange-gateio-v4/src/test/java/org/knowm/xchange/gateio/service/GateioMarketDataServiceTest.java +++ b/xchange-gateio-v4/src/test/java/org/knowm/xchange/gateio/service/GateioMarketDataServiceTest.java @@ -51,7 +51,7 @@ void getOrderBook_valid() throws IOException { assertThat(actual) .usingRecursiveComparison() - .ignoringFieldsMatchingRegexes(".*userReference") + .ignoringFieldsMatchingRegexes(".*userReference",".*lock") .isEqualTo(expected); } From a224830d0646f1c38f0d2a4b52ff9a521796fb28 Mon Sep 17 00:00:00 2001 From: rizer1980 <4340180@gmail.com> Date: Fri, 24 May 2024 19:39:49 +0300 Subject: [PATCH 2/2] [core] orderbook sync --- .../xchange/dto/marketdata/OrderBook.java | 113 ++++--- .../dto/marketdata/ConcurrencyTest.java | 214 +++++++++++++ .../xchange/dto/marketdata/OrderBookOld.java | 280 ++++++++++++++++++ .../service/GateioMarketDataServiceTest.java | 2 +- .../service/GateioMarketDataServiceTest.java | 2 +- 5 files changed, 570 insertions(+), 41 deletions(-) create mode 100644 xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/ConcurrencyTest.java create mode 100644 xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/OrderBookOld.java diff --git a/xchange-core/src/main/java/org/knowm/xchange/dto/marketdata/OrderBook.java b/xchange-core/src/main/java/org/knowm/xchange/dto/marketdata/OrderBook.java index e22013554c6..9d74d0295d2 100644 --- a/xchange-core/src/main/java/org/knowm/xchange/dto/marketdata/OrderBook.java +++ b/xchange-core/src/main/java/org/knowm/xchange/dto/marketdata/OrderBook.java @@ -1,6 +1,7 @@ package org.knowm.xchange.dto.marketdata; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import java.io.Serializable; import java.math.BigDecimal; @@ -8,8 +9,10 @@ import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.concurrent.locks.StampedLock; import java.util.stream.Collectors; import java.util.stream.Stream; +import lombok.Getter; import org.knowm.xchange.dto.Order.OrderType; import org.knowm.xchange.dto.trade.LimitOrder; import org.knowm.xchange.instrument.Instrument; @@ -18,14 +21,17 @@ public final class OrderBook implements Serializable { private static final long serialVersionUID = -7788306758114464314L; - + @JsonIgnore public final StampedLock lock = new StampedLock(); /** the asks */ + @Getter private final List asks; /** the bids */ + @Getter private final List bids; /** the timestamp of the orderbook according to the exchange's server, null if not provided */ + @Getter private Date timeStamp; /** @@ -113,21 +119,6 @@ private static LimitOrder withAmount(LimitOrder limitOrder, BigDecimal tradeable return new LimitOrder(type, tradeableAmount, instrument, id, date, limit); } - public Date getTimeStamp() { - - return timeStamp; - } - - public List getAsks() { - - return asks; - } - - public List getBids() { - - return bids; - } - public List getOrders(OrderType type) { return type == OrderType.ASK ? asks : bids; @@ -141,23 +132,35 @@ public List getOrders(OrderType type) { * @param limitOrder the new LimitOrder */ public void update(LimitOrder limitOrder) { - update(getOrders(limitOrder.getType()), limitOrder); - updateDate(limitOrder.getTimestamp()); } // Replace the amount for limitOrder's price in the provided list. - private void update(List asks, LimitOrder limitOrder) { - - int idx = Collections.binarySearch(asks, limitOrder); - if (idx >= 0) { - asks.remove(idx); - } else { - idx = -idx - 1; - } - - if (limitOrder.getRemainingAmount().compareTo(BigDecimal.ZERO) != 0) { - asks.add(idx, limitOrder); + private void update(List limitOrders, LimitOrder limitOrder) { + long stamp = lock.readLock(); + int idx = Collections.binarySearch(limitOrders, limitOrder); + try { + while (true) { + long writeStamp = lock.tryConvertToWriteLock(stamp); + if (writeStamp != 0L) { + stamp = writeStamp; + if (idx >= 0) limitOrders.remove(idx); + else idx = -idx - 1; + if (limitOrder.getRemainingAmount().compareTo(BigDecimal.ZERO) != 0) + limitOrders.add(idx, limitOrder); + updateDate(limitOrder.getTimestamp()); + break; + } else { + lock.unlockRead(stamp); + stamp = lock.writeLock(); + // here wee need to recheck idx, because it is possible that orderBook changed between + // unlockRead and lockWrite + if (recheckIdx(limitOrders, limitOrder, idx)) + idx = Collections.binarySearch(limitOrders, limitOrder); + } + } + } finally { + lock.unlock(stamp); } } @@ -169,22 +172,54 @@ private void update(List asks, LimitOrder limitOrder) { * @param orderBookUpdate the new OrderBookUpdate */ public void update(OrderBookUpdate orderBookUpdate) { - + long stamp = lock.readLock(); LimitOrder limitOrder = orderBookUpdate.getLimitOrder(); List limitOrders = getOrders(limitOrder.getType()); int idx = Collections.binarySearch(limitOrders, limitOrder); - if (idx >= 0) { - limitOrders.remove(idx); - } else { - idx = -idx - 1; + try { + while (true) { + long writeStamp = lock.tryConvertToWriteLock(stamp); + if (writeStamp != 0L) { + stamp = writeStamp; + if (idx >= 0) limitOrders.remove(idx); + else idx = -idx - 1; + if (orderBookUpdate.getTotalVolume().compareTo(BigDecimal.ZERO) != 0) { + LimitOrder updatedOrder = withAmount(limitOrder, orderBookUpdate.getTotalVolume()); + limitOrders.add(idx, updatedOrder); + } + updateDate(limitOrder.getTimestamp()); + break; + } else { + lock.unlockRead(stamp); + stamp = lock.writeLock(); + // here wee need to recheck idx, because it is possible that orderBook changed between + // unlockRead and lockWrite + if (recheckIdx(limitOrders, limitOrder, idx)) + idx = Collections.binarySearch(limitOrders, limitOrder); + } + } + } finally { + lock.unlock(stamp); } + } - if (orderBookUpdate.getTotalVolume().compareTo(BigDecimal.ZERO) != 0) { - LimitOrder updatedOrder = withAmount(limitOrder, orderBookUpdate.getTotalVolume()); - limitOrders.add(idx, updatedOrder); + private boolean recheckIdx(List limitOrders, LimitOrder limitOrder, int idx) { + if (idx >= 0) { + // if positive, null check or compare + return limitOrders.get(idx) == null || limitOrders.get(idx).compareTo(limitOrder) != 0; + } else { + // on end of array, null check or one check + if (limitOrders.size() == -idx - 1) { + return limitOrders.get(-idx - 2) == null + || limitOrders.get(-idx - 2).compareTo(limitOrder) >= 0; + } else + // if negative, check that of limitOrders.get(reversed idx) limitOrders.get(reversed idx-1) + // and is lower and bigger than limitOrder + return (limitOrders.get(-idx - 1) == null + || limitOrders.get(-idx - 1).compareTo(limitOrder) <= 0) + && (limitOrders.get(-idx - 2) == null + || limitOrders.get(-idx - 2).compareTo(limitOrder) >= 0); } - - updateDate(limitOrder.getTimestamp()); } // Replace timeStamp if the provided date is non-null and in the future diff --git a/xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/ConcurrencyTest.java b/xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/ConcurrencyTest.java new file mode 100644 index 00000000000..9cbc4364d6d --- /dev/null +++ b/xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/ConcurrencyTest.java @@ -0,0 +1,214 @@ +package org.knowm.xchange.dto.marketdata; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import lombok.var; +import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.Order.OrderType; +import org.knowm.xchange.dto.trade.LimitOrder; +import org.knowm.xchange.instrument.Instrument; + +public class ConcurrencyTest { + + static Instrument inst = new CurrencyPair("BTC/USDT"); + + public static void main(String[] args) throws InterruptedException, ExecutionException { + OrderBook orderBook1 = new OrderBook(new Date(), initOrderBookAsks(), initOrderBookBids(), + true); + OrderBook orderBook2 = new OrderBook(new Date(), initOrderBookAsks(), initOrderBookBids(), + true); + OrderBookOld orderBookOld = new OrderBookOld(new Date(), initOrderBookAsks(), + initOrderBookBids(), true); + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(50); + newWay(orderBook1, executor); + executor.awaitTermination(100L, TimeUnit.SECONDS); + executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(50); + oldWay(orderBook2, executor); + executor.awaitTermination(100L, TimeUnit.SECONDS); + executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(50); + oldOB(orderBookOld, executor); + executor.awaitTermination(100L, TimeUnit.SECONDS); + if (orderBook1.getAsks().get(0).getOriginalAmount() + .equals(orderBook2.getAsks().get(0).getOriginalAmount()) + && orderBook1.getAsks().get(0).getOriginalAmount() + .equals(orderBookOld.getAsks().get(0).getOriginalAmount())) { + System.out.println("OrderBooks equals"); + } + } + + + private static List initOrderBookAsks() { + List asks = new ArrayList<>(); + asks.add(new LimitOrder(OrderType.ASK, new BigDecimal(1), inst, "", new Date(), + new BigDecimal(103))); + asks.add(new LimitOrder(OrderType.ASK, new BigDecimal(1), inst, "", new Date(), + new BigDecimal(102))); + asks.add(new LimitOrder(OrderType.ASK, new BigDecimal(1), inst, "", new Date(), + new BigDecimal(101))); + return asks; + } + + private static List initOrderBookBids() { + List bids = new ArrayList<>(); + bids.add( + new LimitOrder(OrderType.BID, new BigDecimal(1), inst, "", new Date(), new BigDecimal(99))); + bids.add( + new LimitOrder(OrderType.BID, new BigDecimal(1), inst, "", new Date(), new BigDecimal(98))); + bids.add( + new LimitOrder(OrderType.BID, new BigDecimal(1), inst, "", new Date(), new BigDecimal(97))); + return bids; + } + + private static void newWay(OrderBook orderBook, ThreadPoolExecutor executor) + throws InterruptedException { + System.out.printf("OrderBook before %s%n", orderBook); + for (int i = 0; i < 50; i++) { + executor.execute(() -> updateOrderBook1(orderBook, false)); + executor.execute(() -> readOrderBook(orderBook, false)); + executor.execute(() -> updateOrderBook2(orderBook, false)); + } + executor.shutdown(); + executor.awaitTermination(100L, TimeUnit.SECONDS); + System.out.printf("OrderBook after %s%n", orderBook); + } + + private static void oldWay(OrderBook orderBook, ThreadPoolExecutor executor) + throws InterruptedException { + System.out.printf("OrderBook before %s%n", orderBook); + for (int i = 0; i < 50; i++) { + executor.execute(() -> updateOrderBook1(orderBook, true)); + executor.execute(() -> readOrderBook(orderBook, true)); + executor.execute(() -> updateOrderBook2(orderBook, true)); + } + executor.shutdown(); + executor.awaitTermination(100L, TimeUnit.SECONDS); + System.out.printf("OrderBook after %s%n", orderBook); + } + + private static void oldOB(OrderBookOld orderBookOld, ThreadPoolExecutor executor) + throws InterruptedException { + System.out.printf("OrderBookOld before %s%n", orderBookOld); + for (int i = 0; i < 50; i++) { + executor.execute(() -> updateOrderBookOld1(orderBookOld)); + executor.execute(() -> readOrderBookOld(orderBookOld)); + executor.execute(() -> updateOrderBookOld2(orderBookOld)); + } + executor.shutdown(); + executor.awaitTermination(100L, TimeUnit.SECONDS); + System.out.printf("OrderBookOld after %s%n", orderBookOld); + } + + public static void updateOrderBook1(OrderBook orderBook, boolean oldWay) { + Random rand = new Random(123); + for (int i = 0; i < 100000; i++) { + OrderBookUpdate orderBookUpdateAsk = new OrderBookUpdate(OrderType.ASK, new BigDecimal(0), + inst, new BigDecimal(101), new Date(), new BigDecimal(rand.nextInt())); + OrderBookUpdate orderBookUpdateBid = new OrderBookUpdate(OrderType.BID, new BigDecimal(0), + inst, new BigDecimal(99), new Date(), new BigDecimal(rand.nextInt())); + if (oldWay) { + synchronized (orderBook) { + orderBook.update(orderBookUpdateAsk); + orderBook.update(orderBookUpdateBid); + } + } else { + orderBook.update(orderBookUpdateAsk); + orderBook.update(orderBookUpdateBid); + } + } + } + + public static void updateOrderBookOld1(OrderBookOld orderBookOld) { + Random rand = new Random(123); + for (int i = 0; i < 100000; i++) { + OrderBookUpdate orderBookUpdateAsk = new OrderBookUpdate(OrderType.ASK, new BigDecimal(0), + inst, new BigDecimal(101), new Date(), new BigDecimal(rand.nextInt())); + OrderBookUpdate orderBookUpdateBid = new OrderBookUpdate(OrderType.BID, new BigDecimal(0), + inst, new BigDecimal(99), new Date(), new BigDecimal(rand.nextInt())); + synchronized (orderBookOld) { + orderBookOld.update(orderBookUpdateAsk); + orderBookOld.update(orderBookUpdateBid); + } + } + } + + private static void updateOrderBook2(OrderBook orderBook, boolean oldWay) { + Random rand = new Random(123); + for (int i = 0; i < 100000; i++) { + LimitOrder bookUpdateAsk = new LimitOrder(OrderType.ASK, new BigDecimal(rand.nextInt()), + inst, "", new Date(), new BigDecimal(101)); + LimitOrder bookUpdateBid = new LimitOrder(OrderType.BID, new BigDecimal(rand.nextInt()), + inst, "", new Date(), new BigDecimal(99)); + if (oldWay) { + synchronized (orderBook) { + orderBook.update(bookUpdateAsk); + orderBook.update(bookUpdateBid); + } + } else { + orderBook.update(bookUpdateAsk); + orderBook.update(bookUpdateBid); + } + } + } + + private static void updateOrderBookOld2(OrderBookOld orderBookOld) { + Random rand = new Random(123); + for (int i = 0; i < 100000; i++) { + LimitOrder bookUpdateAsk = new LimitOrder(OrderType.ASK, new BigDecimal(rand.nextInt()), + inst, "", new Date(), new BigDecimal(101)); + LimitOrder bookUpdateBid = new LimitOrder(OrderType.BID, new BigDecimal(rand.nextInt()), + inst, "", new Date(), new BigDecimal(99)); + synchronized (orderBookOld) { + orderBookOld.update(bookUpdateAsk); + orderBookOld.update(bookUpdateBid); + } + } + } + + private static void readOrderBook(OrderBook orderBook, boolean oldWay) { + for (int i = 0; i < 1200000; i++) { + int temp = 0; + if (oldWay) { + synchronized (orderBook) { + for (LimitOrder ask : orderBook.getAsks()) { + temp += ask.hashCode(); + } + for (LimitOrder bid : orderBook.getBids()) { + temp += bid.hashCode(); + } + } + } else { + var stamp = orderBook.lock.readLock(); + for (LimitOrder ask : orderBook.getAsks()) { + temp += ask.hashCode(); + } + for (LimitOrder bid : orderBook.getBids()) { + temp += bid.hashCode(); + } + orderBook.lock.unlockRead(stamp); + } + } + } + + private static void readOrderBookOld(OrderBookOld orderBookOld) { + for (int i = 0; i < 1200000; i++) { + int temp = 0; + synchronized (orderBookOld) { + for (LimitOrder ask : orderBookOld.getAsks()) { + temp += ask.hashCode(); + } + for (LimitOrder bid : orderBookOld.getBids()) { + temp += bid.hashCode(); + } + } + } + } + +} + diff --git a/xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/OrderBookOld.java b/xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/OrderBookOld.java new file mode 100644 index 00000000000..576ef60626a --- /dev/null +++ b/xchange-core/src/test/java/org/knowm/xchange/dto/marketdata/OrderBookOld.java @@ -0,0 +1,280 @@ +package org.knowm.xchange.dto.marketdata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.knowm.xchange.dto.Order.OrderType; +import org.knowm.xchange.dto.trade.LimitOrder; +import org.knowm.xchange.instrument.Instrument; + + /** DTO representing the exchange order book */ + public final class OrderBookOld implements Serializable { + + private static final long serialVersionUID = -7788306758114464314L; + + /** the asks */ + private final List asks; + + /** the bids */ + private final List bids; + + /** the timestamp of the orderbook according to the exchange's server, null if not provided */ + private Date timeStamp; + + /** + * Constructor + * + * @param timeStamp - the timestamp of the orderbook according to the exchange's server, null if + * not provided + * @param asks The ASK orders + * @param bids The BID orders + */ + @JsonCreator + public OrderBookOld( + @JsonProperty("timeStamp") Date timeStamp, + @JsonProperty("asks") List asks, + @JsonProperty("bids") List bids) { + + this(timeStamp, asks, bids, false); + } + + /** + * Constructor + * + * @param timeStamp - the timestamp of the orderbook according to the exchange's server, null if + * not provided + * @param asks The ASK orders + * @param bids The BID orders + * @param sort True if the asks and bids need to be sorted + */ + public OrderBookOld(Date timeStamp, List asks, List bids, boolean sort) { + + this.timeStamp = timeStamp; + if (sort) { + this.asks = new ArrayList<>(asks); + this.bids = new ArrayList<>(bids); + Collections.sort(this.asks); + Collections.sort(this.bids); + } else { + this.asks = asks; + this.bids = bids; + } + } + + /** + * Constructor + * + * @param timeStamp - the timestamp of the orderbook according to the exchange's server, null if + * not provided + * @param asks The ASK orders + * @param bids The BID orders + */ + public OrderBookOld(Date timeStamp, Stream asks, Stream bids) { + + this(timeStamp, asks, bids, false); + } + + /** + * Constructor + * + * @param timeStamp - the timestamp of the orderbook according to the exchange's server, null if + * not provided + * @param asks The ASK orders + * @param bids The BID orders + * @param sort True if the asks and bids need to be sorted + */ + public OrderBookOld(Date timeStamp, Stream asks, Stream bids, boolean sort) { + + this.timeStamp = timeStamp; + if (sort) { + this.asks = asks.sorted().collect(Collectors.toList()); + this.bids = bids.sorted().collect(Collectors.toList()); + } else { + this.asks = asks.collect(Collectors.toList()); + this.bids = bids.collect(Collectors.toList()); + } + } + + // Returns a copy of limitOrder with tradeableAmount replaced. + private static LimitOrder withAmount(LimitOrder limitOrder, BigDecimal tradeableAmount) { + + OrderType type = limitOrder.getType(); + Instrument instrument = limitOrder.getInstrument(); + String id = limitOrder.getId(); + Date date = limitOrder.getTimestamp(); + BigDecimal limit = limitOrder.getLimitPrice(); + return new LimitOrder(type, tradeableAmount, instrument, id, date, limit); + } + + public Date getTimeStamp() { + + return timeStamp; + } + + public List getAsks() { + + return asks; + } + + public List getBids() { + + return bids; + } + + public List getOrders(OrderType type) { + + return type == OrderType.ASK ? asks : bids; + } + + /** + * Given a new LimitOrder, it will replace a matching limit order in the orderbook if one is + * found, or add the new LimitOrder if one is not. timeStamp will be updated if the new timestamp + * is non-null and in the future. + * + * @param limitOrder the new LimitOrder + */ + public void update(LimitOrder limitOrder) { + + update(getOrders(limitOrder.getType()), limitOrder); + updateDate(limitOrder.getTimestamp()); + } + + // Replace the amount for limitOrder's price in the provided list. + private void update(List asks, LimitOrder limitOrder) { + + int idx = Collections.binarySearch(asks, limitOrder); + if (idx >= 0) { + asks.remove(idx); + } else { + idx = -idx - 1; + } + + if (limitOrder.getRemainingAmount().compareTo(BigDecimal.ZERO) != 0) { + asks.add(idx, limitOrder); + } + } + + /** + * Given an OrderBookUpdate, it will replace a matching limit order in the orderbook if one is + * found, or add a new if one is not. timeStamp will be updated if the new timestamp is non-null + * and in the future. + * + * @param orderBookUpdate the new OrderBookUpdate + */ + public void update(OrderBookUpdate orderBookUpdate) { + + LimitOrder limitOrder = orderBookUpdate.getLimitOrder(); + List limitOrders = getOrders(limitOrder.getType()); + int idx = Collections.binarySearch(limitOrders, limitOrder); + if (idx >= 0) { + limitOrders.remove(idx); + } else { + idx = -idx - 1; + } + + if (orderBookUpdate.getTotalVolume().compareTo(BigDecimal.ZERO) != 0) { + LimitOrder updatedOrder = withAmount(limitOrder, orderBookUpdate.getTotalVolume()); + limitOrders.add(idx, updatedOrder); + } + + updateDate(limitOrder.getTimestamp()); + } + + // Replace timeStamp if the provided date is non-null and in the future + // TODO should this raise an exception if the order timestamp is in the past? + private void updateDate(Date updateDate) { + + if (updateDate != null && (timeStamp == null || updateDate.after(timeStamp))) { + this.timeStamp = updateDate; + } + } + + @Override + public int hashCode() { + + int hash = 17; + hash = 31 * hash + (this.timeStamp != null ? this.timeStamp.hashCode() : 0); + for (LimitOrder order : this.bids) { + hash = 31 * hash + order.hashCode(); + } + for (LimitOrder order : this.asks) { + hash = 31 * hash + order.hashCode(); + } + return hash; + } + + @Override + public boolean equals(Object obj) { + + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final org.knowm.xchange.dto.marketdata.OrderBookOld other = (org.knowm.xchange.dto.marketdata.OrderBookOld) obj; + if (this.timeStamp == null + ? other.timeStamp != null + : !this.timeStamp.equals(other.timeStamp)) { + return false; + } + if (this.bids.size() != other.bids.size()) { + return false; + } + for (int index = 0; index < this.bids.size(); index++) { + if (!this.bids.get(index).equals(other.bids.get(index))) { + return false; + } + } + if (this.asks.size() != other.asks.size()) { + return false; + } + for (int index = 0; index < this.asks.size(); index++) { + if (!this.asks.get(index).equals(other.asks.get(index))) { + return false; + } + } + return true; + } + + /** + * Identical to {@link #equals(Object) equals} method except that this ignores different + * timestamps. In other words, this version of equals returns true if the order internal to the + * OrderBooks are equal but their timestamps are unequal. It returns false if any order between + * the two are different. + * + * @param ob + * @return + */ + public boolean ordersEqual(org.knowm.xchange.dto.marketdata.OrderBook ob) { + + if (ob == null) { + return false; + } + + Date timestamp = new Date(); + org.knowm.xchange.dto.marketdata.OrderBook thisOb = new org.knowm.xchange.dto.marketdata.OrderBook(timestamp, this.getAsks(), this.getBids()); + org.knowm.xchange.dto.marketdata.OrderBook thatOb = new org.knowm.xchange.dto.marketdata.OrderBook(timestamp, ob.getAsks(), ob.getBids()); + return thisOb.equals(thatOb); + } + + @Override + public String toString() { + + return "OrderBook [timestamp: " + + timeStamp + + ", asks=" + + asks.toString() + + ", bids=" + + bids.toString() + + "]"; + } + } + diff --git a/xchange-gateio-v4/src/test/java/org/knowm/xchange/gateio/service/GateioMarketDataServiceTest.java b/xchange-gateio-v4/src/test/java/org/knowm/xchange/gateio/service/GateioMarketDataServiceTest.java index 5a5f5fbd8f4..bd653c8dadc 100644 --- a/xchange-gateio-v4/src/test/java/org/knowm/xchange/gateio/service/GateioMarketDataServiceTest.java +++ b/xchange-gateio-v4/src/test/java/org/knowm/xchange/gateio/service/GateioMarketDataServiceTest.java @@ -51,7 +51,7 @@ void getOrderBook_valid() throws IOException { assertThat(actual) .usingRecursiveComparison() - .ignoringFieldsMatchingRegexes(".*userReference") + .ignoringFieldsMatchingRegexes(".*userReference",".*lock") .isEqualTo(expected); } diff --git a/xchange-gateio/src/test/java/org/knowm/xchange/gateio/service/GateioMarketDataServiceTest.java b/xchange-gateio/src/test/java/org/knowm/xchange/gateio/service/GateioMarketDataServiceTest.java index 23026bcfcc3..e23c9d051db 100644 --- a/xchange-gateio/src/test/java/org/knowm/xchange/gateio/service/GateioMarketDataServiceTest.java +++ b/xchange-gateio/src/test/java/org/knowm/xchange/gateio/service/GateioMarketDataServiceTest.java @@ -55,7 +55,7 @@ public void valid_orderbook() throws IOException { assertThat(actual) .usingRecursiveComparison() - .ignoringFieldsMatchingRegexes(".*userReference") + .ignoringFieldsMatchingRegexes(".*userReference",".*lock") .isEqualTo(expected); } }