From 4e9dc7cf811d66907d41b3c61a8c57a536f17c55 Mon Sep 17 00:00:00 2001 From: rizer1980 <4340180@gmail.com> Date: Wed, 29 May 2024 19:57:38 +0300 Subject: [PATCH 01/10] [bybit] cancel order --- .../xchange/bybit/BybitAuthenticated.java | 17 ++++++++++++-- .../dto/trade/BybitCancelOrderPayload.java | 23 +++++++++++++++++++ .../bybit/service/BybitTradeService.java | 12 ++++++++++ .../bybit/service/BybitTradeServiceRaw.java | 16 +++++++++++++ 4 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 xchange-bybit/src/main/java/org/knowm/xchange/bybit/dto/trade/BybitCancelOrderPayload.java diff --git a/xchange-bybit/src/main/java/org/knowm/xchange/bybit/BybitAuthenticated.java b/xchange-bybit/src/main/java/org/knowm/xchange/bybit/BybitAuthenticated.java index 16fb0dea3d8..56155941838 100644 --- a/xchange-bybit/src/main/java/org/knowm/xchange/bybit/BybitAuthenticated.java +++ b/xchange-bybit/src/main/java/org/knowm/xchange/bybit/BybitAuthenticated.java @@ -13,6 +13,7 @@ import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import java.io.IOException; +import org.knowm.xchange.bybit.dto.trade.BybitCancelOrderPayload; import org.knowm.xchange.bybit.dto.trade.BybitPlaceOrderPayload; import org.knowm.xchange.bybit.dto.BybitResult; import org.knowm.xchange.bybit.dto.account.allcoins.BybitAllCoinsBalance; @@ -103,7 +104,19 @@ BybitResult placeLimitOrder( @HeaderParam(X_BAPI_SIGN) ParamsDigest signature, @HeaderParam(X_BAPI_TIMESTAMP) SynchronizedValueFactory timestamp, BybitPlaceOrderPayload payload) -// @FormParam("positionIdx") -// @FormParam("reduceOnly") throws IOException,BybitException; + + /** + * @apiSpec API + */ + @POST + @Path("/order/cancel") + @Consumes(MediaType.APPLICATION_JSON) + BybitResult cancelOrder( + @HeaderParam(X_BAPI_API_KEY) String apiKey, + @HeaderParam(X_BAPI_SIGN) ParamsDigest signature, + @HeaderParam(X_BAPI_TIMESTAMP) SynchronizedValueFactory timestamp, + BybitCancelOrderPayload payload) + throws IOException,BybitException; + } diff --git a/xchange-bybit/src/main/java/org/knowm/xchange/bybit/dto/trade/BybitCancelOrderPayload.java b/xchange-bybit/src/main/java/org/knowm/xchange/bybit/dto/trade/BybitCancelOrderPayload.java new file mode 100644 index 00000000000..d67c9e64899 --- /dev/null +++ b/xchange-bybit/src/main/java/org/knowm/xchange/bybit/dto/trade/BybitCancelOrderPayload.java @@ -0,0 +1,23 @@ +package org.knowm.xchange.bybit.dto.trade; + +import lombok.Getter; +import org.knowm.xchange.bybit.Bybit; +import org.knowm.xchange.bybit.dto.BybitCategory; + +@Getter +public class BybitCancelOrderPayload { + + private BybitCategory category; + private String symbol; + private String orderId; + private String orderLinkId; + private String orderFilter; + + public BybitCancelOrderPayload(BybitCategory category, String symbol, String orderId, String orderLinkId) { + this.category = category; + this.symbol = symbol; + this.orderId = orderId; + this.orderLinkId = orderLinkId; + } + +} diff --git a/xchange-bybit/src/main/java/org/knowm/xchange/bybit/service/BybitTradeService.java b/xchange-bybit/src/main/java/org/knowm/xchange/bybit/service/BybitTradeService.java index 27ef297dc47..3f9e154c0b2 100644 --- a/xchange-bybit/src/main/java/org/knowm/xchange/bybit/service/BybitTradeService.java +++ b/xchange-bybit/src/main/java/org/knowm/xchange/bybit/service/BybitTradeService.java @@ -1,6 +1,7 @@ package org.knowm.xchange.bybit.service; import static org.knowm.xchange.bybit.BybitAdapters.adaptBybitOrderDetails; +import static org.knowm.xchange.bybit.BybitAdapters.convertToBybitSymbol; import java.io.IOException; import java.util.ArrayList; @@ -72,4 +73,15 @@ public Collection getOrder(String... orderIds) throws IOException { return results; } + + public String cancelOrder(Order order) throws IOException { + BybitCategory category = BybitAdapters.getCategory(order.getInstrument()); + BybitResult response = cancelOrder(category, + convertToBybitSymbol(order.getInstrument()), order.getId(), order.getUserReference()); + if (response != null) { + return response.getResult().getOrderId(); + } else + return ""; + } + } diff --git a/xchange-bybit/src/main/java/org/knowm/xchange/bybit/service/BybitTradeServiceRaw.java b/xchange-bybit/src/main/java/org/knowm/xchange/bybit/service/BybitTradeServiceRaw.java index 37e2f4f430f..75c6e9611f2 100644 --- a/xchange-bybit/src/main/java/org/knowm/xchange/bybit/service/BybitTradeServiceRaw.java +++ b/xchange-bybit/src/main/java/org/knowm/xchange/bybit/service/BybitTradeServiceRaw.java @@ -6,6 +6,7 @@ import java.math.BigDecimal; import org.knowm.xchange.Exchange; import org.knowm.xchange.bybit.dto.BybitCategory; +import org.knowm.xchange.bybit.dto.trade.BybitCancelOrderPayload; import org.knowm.xchange.bybit.dto.trade.BybitPlaceOrderPayload; import org.knowm.xchange.bybit.dto.BybitResult; import org.knowm.xchange.bybit.dto.trade.BybitOrderResponse; @@ -65,4 +66,19 @@ public BybitResult placeLimitOrder( } return placeOrder; } + + public BybitResult cancelOrder(BybitCategory category,String symbol, + String orderId, String orderLinkId) throws IOException { + BybitCancelOrderPayload payload = new BybitCancelOrderPayload(category, symbol, orderId, orderLinkId); + BybitResult cancelOrder = + bybitAuthenticated.cancelOrder( + apiKey, + signatureCreator, + nonceFactory, + payload); + if (!cancelOrder.isSuccess()) { + throw createBybitExceptionFromResult(cancelOrder); + } + return cancelOrder; + } } From 1caa24f68f4f0811e2d680200d64bfe87c0260d1 Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Sat, 8 Jun 2024 01:20:49 +0200 Subject: [PATCH 02/10] [coinex] Add requests for orders --- .../src/test/resources/rest/sign.js | 2 +- .../src/test/resources/rest/spot.v2.http | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/xchange-coinex/src/test/resources/rest/sign.js b/xchange-coinex/src/test/resources/rest/sign.js index 0d42bda1c54..8acd291851a 100644 --- a/xchange-coinex/src/test/resources/rest/sign.js +++ b/xchange-coinex/src/test/resources/rest/sign.js @@ -4,7 +4,7 @@ export function gen_sign(method, request) { const matches = url.match(pattern); const path = matches[5]; - const query = matches[7] || ""; + const query = matches[6] || ""; const body = request.body.tryGetSubstituted() || ""; const timestamp = Math.floor(Date.now()).toFixed(); const payloadToSign = `${method}${path}${query}${body}${timestamp}`; diff --git a/xchange-coinex/src/test/resources/rest/spot.v2.http b/xchange-coinex/src/test/resources/rest/spot.v2.http index 6c90f75eee7..d51866af9e5 100644 --- a/xchange-coinex/src/test/resources/rest/spot.v2.http +++ b/xchange-coinex/src/test/resources/rest/spot.v2.http @@ -14,3 +14,52 @@ GET {{api_host}}/v2/spot/index GET {{api_host}}/v2/spot/depth?market=BTCUSDT&limit=50&interval=0 +### Order Status Query +< {% + import {gen_sign} from 'sign.js' + gen_sign("GET", request); +%} +GET {{api_host}}/v2/spot/order-status?market=BTCUSDT&order_id=120357286483 +X-COINEX-KEY: {{api_key}} +X-COINEX-TIMESTAMP: {{timestamp}} +X-COINEX-SIGN: {{sign}} + + +### Place Order +< {% + import {gen_sign} from 'sign.js' + gen_sign("POST", request); +%} +POST {{api_host}}/v2/spot/order +X-COINEX-KEY: {{api_key}} +X-COINEX-TIMESTAMP: {{timestamp}} +X-COINEX-SIGN: {{sign}} + +{ + "market": "BTCUSDT", + "market_type": "SPOT", + "side": "buy", + "type": "limit", + "amount": "10", + "price": "1", + "client_id": "{{$random.alphanumeric(32)}}", + "is_hide": false +} + + +### Cancel Order +< {% + import {gen_sign} from 'sign.js' + gen_sign("POST", request); +%} +POST {{api_host}}/v2/spot/cancel-order +X-COINEX-KEY: {{api_key}} +X-COINEX-TIMESTAMP: {{timestamp}} +X-COINEX-SIGN: {{sign}} + +{ + "market": "BTCUSDT", + "market_type": "SPOT", + "order_id": 120325813375 +} + From 9962fef8aaee0e024f6f7d32e0bad7506a495b31 Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Sat, 8 Jun 2024 01:22:00 +0200 Subject: [PATCH 03/10] [coinex] Fix digest with query params --- .../coinex/service/CoinexV2Digest.java | 3 +++ .../coinex/service/CoinexV2DigestTest.java | 21 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexV2Digest.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexV2Digest.java index 49f4c5b72fb..b548cd49ee2 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexV2Digest.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexV2Digest.java @@ -27,6 +27,9 @@ public String digestParams(RestInvocation restInvocation) { String path = restInvocation.getPath(); String query = StringUtils.defaultIfEmpty(restInvocation.getQueryString(), ""); + if (StringUtils.isNotEmpty(query)) { + query = "?" + query; + } String body = StringUtils.defaultIfEmpty(restInvocation.getRequestBody(), ""); String timestamp = restInvocation.getHttpHeadersFromParams().get("X-COINEX-TIMESTAMP"); diff --git a/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexV2DigestTest.java b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexV2DigestTest.java index fe7d6255f75..e472e59930d 100644 --- a/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexV2DigestTest.java +++ b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexV2DigestTest.java @@ -18,7 +18,7 @@ class CoinexV2DigestTest { RestInvocation restInvocation; @Test - void signature() { + void signature_no_query_params() { CoinexV2Digest coinexV2Digest = CoinexV2Digest.createInstance("a"); when(restInvocation.getHttpMethod()).thenReturn("GET"); @@ -36,4 +36,23 @@ void signature() { } + @Test + void signature_with_query_params() { + CoinexV2Digest coinexV2Digest = CoinexV2Digest.createInstance("a"); + + when(restInvocation.getHttpMethod()).thenReturn("GET"); + when(restInvocation.getPath()).thenReturn("v2/spot/order-status"); + when(restInvocation.getQueryString()).thenReturn("market=BTCUSDT&order_id=120355030142"); + when(restInvocation.getRequestBody()).thenReturn(null); + Map headers = new HashMap<>(); + headers.put("X-COINEX-TIMESTAMP", "1714992192553"); + when(restInvocation.getHttpHeadersFromParams()).thenReturn(headers); + + String actual = coinexV2Digest.digestParams(restInvocation); + String expected = "cd8fcab65fffa29e31bf29a6c73e783517601163ae123ad8da0b86e333bf76e3"; + + assertThat(actual).isEqualTo(expected); + } + + } \ No newline at end of file From f8e275fa054dba145e03eda6a61980337f6982c1 Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Sat, 8 Jun 2024 01:25:12 +0200 Subject: [PATCH 04/10] [coinex] Add getting of order --- .../knowm/xchange/coinex/CoinexAdapters.java | 48 +++++++++ .../xchange/coinex/CoinexAuthenticated.java | 13 +++ .../knowm/xchange/coinex/CoinexExchange.java | 2 + .../converter/OrderTypeToStringConverter.java | 22 +++++ .../StringToOrderStatusConverter.java | 28 ++++++ .../converter/StringToOrderTypeConverter.java | 22 +++++ .../coinex/dto/account/CoinexMarketType.java | 14 +++ .../coinex/dto/account/CoinexOrder.java | 97 +++++++++++++++++++ .../coinex/service/CoinexTradeService.java | 41 ++++++++ .../coinex/service/CoinexTradeServiceRaw.java | 22 +++++ 10 files changed, 309 insertions(+) create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/OrderTypeToStringConverter.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToOrderStatusConverter.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToOrderTypeConverter.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexMarketType.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexOrder.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeService.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeServiceRaw.java diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java index 937bcb1dcea..b939fa096ee 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java @@ -1,5 +1,6 @@ package org.knowm.xchange.coinex; +import java.math.MathContext; import java.time.Instant; import java.util.Collection; import java.util.Date; @@ -9,10 +10,12 @@ import java.util.stream.Collectors; import lombok.experimental.UtilityClass; import org.knowm.xchange.coinex.dto.account.CoinexBalanceInfo; +import org.knowm.xchange.coinex.dto.account.CoinexOrder; import org.knowm.xchange.coinex.dto.marketdata.CoinexCurrencyPairInfo; import org.knowm.xchange.coinex.dto.marketdata.CoinexMarketDepth; import org.knowm.xchange.coinex.dto.marketdata.CoinexTickerV1; import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.Order; import org.knowm.xchange.dto.Order.OrderType; import org.knowm.xchange.dto.account.Balance; import org.knowm.xchange.dto.account.Wallet; @@ -21,6 +24,7 @@ import org.knowm.xchange.dto.marketdata.Ticker.Builder; import org.knowm.xchange.dto.meta.InstrumentMetaData; import org.knowm.xchange.dto.trade.LimitOrder; +import org.knowm.xchange.dto.trade.MarketOrder; import org.knowm.xchange.instrument.Instrument; @UtilityClass @@ -67,6 +71,50 @@ public InstrumentMetaData toInstrumentMetaData(CoinexCurrencyPairInfo coinexCurr } + public Order toOrder(CoinexOrder coinexOrder) { + Order.Builder builder; + Instrument instrument = coinexOrder.getCurrencyPair(); + OrderType orderType = coinexOrder.getSide(); + + switch (coinexOrder.getType()) { + case "market": + builder = new MarketOrder.Builder(orderType, instrument); + break; + case "limit": + builder = new LimitOrder.Builder(orderType, instrument) + .limitPrice(coinexOrder.getPrice()); + break; + default: + throw new IllegalArgumentException("Can't map " + coinexOrder.getType()); + } + + if (orderType == OrderType.BID) { + // buy orders fill quote + builder.cumulativeAmount(coinexOrder.getFilledQuoteAmount()); + } + else if (orderType == OrderType.ASK) { + // sell orders fill asset + builder.cumulativeAmount(coinexOrder.getFilledAssetAmount()); + } + else { + throw new IllegalArgumentException("Can't map " + orderType); + } + + // average price + if (coinexOrder.getFilledAssetAmount() != null && coinexOrder.getFilledQuoteAmount() != null && coinexOrder.getFilledAssetAmount().signum() > 0) { + builder.averagePrice(coinexOrder.getFilledQuoteAmount().divide(coinexOrder.getFilledAssetAmount(), MathContext.DECIMAL32)); + } + + return builder + .id(String.valueOf(coinexOrder.getOrderId())) + .originalAmount(coinexOrder.getAmount()) + .userReference(coinexOrder.getClientId()) + .timestamp(Date.from(coinexOrder.getCreatedAt())) + .orderStatus(coinexOrder.getStatus()) + .build(); + } + + public OrderBook toOrderBook(CoinexMarketDepth coinexMarketDepth) { List asks = coinexMarketDepth.getDepth().getAsks().stream() .map(priceSizeEntry -> diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java index f69e6221c90..3a470be8ef4 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java @@ -4,12 +4,14 @@ import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import java.io.IOException; import java.util.List; import org.knowm.xchange.coinex.dto.CoinexException; import org.knowm.xchange.coinex.dto.CoinexResponse; import org.knowm.xchange.coinex.dto.account.CoinexBalanceInfo; +import org.knowm.xchange.coinex.dto.account.CoinexOrder; import si.mazi.rescu.ParamsDigest; import si.mazi.rescu.SynchronizedValueFactory; @@ -26,4 +28,15 @@ CoinexResponse> balances( throws IOException, CoinexException; + @GET + @Path("v2/spot/order-status") + CoinexResponse orderStatus( + @HeaderParam("X-COINEX-KEY") String apiKey, + @HeaderParam("X-COINEX-TIMESTAMP") SynchronizedValueFactory timestamp, + @HeaderParam("X-COINEX-SIGN") ParamsDigest signer, + @QueryParam("market") String market, + @QueryParam("order_id") String orderId) + throws IOException, CoinexException; + + } diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexExchange.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexExchange.java index 03c31363b18..0420629eb3a 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexExchange.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexExchange.java @@ -10,6 +10,7 @@ import org.knowm.xchange.coinex.service.CoinexAccountService; import org.knowm.xchange.coinex.service.CoinexMarketDataService; import org.knowm.xchange.coinex.service.CoinexMarketDataServiceRaw; +import org.knowm.xchange.coinex.service.CoinexTradeService; import org.knowm.xchange.currency.CurrencyPair; import org.knowm.xchange.dto.meta.ExchangeMetaData; import org.knowm.xchange.dto.meta.InstrumentMetaData; @@ -21,6 +22,7 @@ public class CoinexExchange extends BaseExchange { protected void initServices() { accountService = new CoinexAccountService(this); marketDataService = new CoinexMarketDataService(this); + tradeService = new CoinexTradeService(this); } @Override diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/OrderTypeToStringConverter.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/OrderTypeToStringConverter.java new file mode 100644 index 00000000000..24b0d92d646 --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/OrderTypeToStringConverter.java @@ -0,0 +1,22 @@ +package org.knowm.xchange.coinex.config.converter; + +import com.fasterxml.jackson.databind.util.StdConverter; +import org.knowm.xchange.dto.Order.OrderType; + +/** + * Converts {@code OrderType} to string + */ +public class OrderTypeToStringConverter extends StdConverter { + + @Override + public String convert(OrderType value) { + switch (value) { + case BID: + return "buy"; + case ASK: + return "sell"; + default: + throw new IllegalArgumentException("Can't map " + value); + } + } +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToOrderStatusConverter.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToOrderStatusConverter.java new file mode 100644 index 00000000000..60a9529b674 --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToOrderStatusConverter.java @@ -0,0 +1,28 @@ +package org.knowm.xchange.coinex.config.converter; + +import com.fasterxml.jackson.databind.util.StdConverter; +import org.knowm.xchange.dto.Order.OrderStatus; + +/** + * Converts string to {@code OrderStatus} + */ +public class StringToOrderStatusConverter extends StdConverter { + + @Override + public OrderStatus convert(String value) { + switch (value) { + case "open": + return OrderStatus.OPEN; + case "part_filled": + return OrderStatus.PARTIALLY_FILLED; + case "filled": + return OrderStatus.FILLED; + case "part_canceled": + return OrderStatus.PARTIALLY_CANCELED; + case "canceled": + return OrderStatus.CANCELED; + default: + throw new IllegalArgumentException("Can't map " + value); + } + } +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToOrderTypeConverter.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToOrderTypeConverter.java new file mode 100644 index 00000000000..5da07deb1fb --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToOrderTypeConverter.java @@ -0,0 +1,22 @@ +package org.knowm.xchange.coinex.config.converter; + +import com.fasterxml.jackson.databind.util.StdConverter; +import org.knowm.xchange.dto.Order.OrderType; + +/** + * Converts string to {@code OrderType} + */ +public class StringToOrderTypeConverter extends StdConverter { + + @Override + public OrderType convert(String value) { + switch (value) { + case "buy": + return OrderType.BID; + case "sell": + return OrderType.ASK; + default: + throw new IllegalArgumentException("Can't map " + value); + } + } +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexMarketType.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexMarketType.java new file mode 100644 index 00000000000..5a50945319e --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexMarketType.java @@ -0,0 +1,14 @@ +package org.knowm.xchange.coinex.dto.account; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum CoinexMarketType { + + SPOT, + MARGIN, + FUTURES; + +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexOrder.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexOrder.java new file mode 100644 index 00000000000..9a73768578f --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexOrder.java @@ -0,0 +1,97 @@ +package org.knowm.xchange.coinex.dto.account; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.math.BigDecimal; +import java.time.Instant; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; +import org.knowm.xchange.coinex.config.converter.OrderTypeToStringConverter; +import org.knowm.xchange.coinex.config.converter.StringToCurrencyConverter; +import org.knowm.xchange.coinex.config.converter.StringToCurrencyPairConverter; +import org.knowm.xchange.coinex.config.converter.StringToOrderStatusConverter; +import org.knowm.xchange.coinex.config.converter.StringToOrderTypeConverter; +import org.knowm.xchange.currency.Currency; +import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.Order.OrderStatus; +import org.knowm.xchange.dto.Order.OrderType; + +@Data +@Builder +@Jacksonized +public class CoinexOrder { + + @JsonProperty("amount") + private BigDecimal amount; + + @JsonProperty("base_fee") + private BigDecimal baseFee; + + @JsonProperty("ccy") + @JsonDeserialize(converter = StringToCurrencyConverter.class) + private Currency currency; + + @JsonProperty("client_id") + private String clientId; + + @JsonProperty("created_at") + private Instant createdAt; + + @JsonProperty("discount_fee") + private BigDecimal discountFee; + + @JsonProperty("filled_amount") + private BigDecimal filledAssetAmount; + + @JsonProperty("filled_value") + private BigDecimal filledQuoteAmount; + + @JsonProperty("last_fill_amount") + private BigDecimal lastFilledAssetAmount; + + @JsonProperty("last_fill_price") + private BigDecimal lastFillPrice; + + @JsonProperty("maker_fee_rate") + private BigDecimal makerFeeRate; + + @JsonProperty("market") + @JsonDeserialize(converter = StringToCurrencyPairConverter.class) + CurrencyPair currencyPair; + + @JsonProperty("market_type") + private CoinexMarketType marketType; + + @JsonProperty("order_id") + private Long orderId; + + @JsonProperty("price") + private BigDecimal price; + + @JsonProperty("quote_fee") + private BigDecimal quoteFee; + + @JsonProperty("side") + @JsonDeserialize(converter = StringToOrderTypeConverter.class) + @JsonSerialize(converter = OrderTypeToStringConverter.class) + OrderType side; + + @JsonProperty("status") + @JsonDeserialize(converter = StringToOrderStatusConverter.class) + OrderStatus status; + + @JsonProperty("taker_fee_rate") + private BigDecimal takerFeeRate; + + @JsonProperty("type") + private String type; + + @JsonProperty("unfilled_amount") + private BigDecimal unfilledAmount; + + @JsonProperty("updated_at") + private Instant updatedAt; + +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeService.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeService.java new file mode 100644 index 00000000000..39452334f7d --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeService.java @@ -0,0 +1,41 @@ +package org.knowm.xchange.coinex.service; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import org.apache.commons.lang3.Validate; +import org.knowm.xchange.coinex.CoinexAdapters; +import org.knowm.xchange.coinex.CoinexErrorAdapter; +import org.knowm.xchange.coinex.CoinexExchange; +import org.knowm.xchange.coinex.dto.CoinexException; +import org.knowm.xchange.coinex.dto.account.CoinexOrder; +import org.knowm.xchange.dto.Order; +import org.knowm.xchange.service.trade.TradeService; +import org.knowm.xchange.service.trade.params.orders.OrderQueryParamInstrument; +import org.knowm.xchange.service.trade.params.orders.OrderQueryParams; + +public class CoinexTradeService extends CoinexTradeServiceRaw implements TradeService { + + public CoinexTradeService(CoinexExchange exchange) { + super(exchange); + } + + + @Override + public Collection getOrder(OrderQueryParams... orderQueryParams) throws IOException { + Validate.validState(orderQueryParams.length == 1); + Validate.isInstanceOf(OrderQueryParamInstrument.class, orderQueryParams[0]); + + OrderQueryParamInstrument params = (OrderQueryParamInstrument) orderQueryParams[0]; + + try { + CoinexOrder gateioOrder = orderStatus(params.getInstrument(), params.getOrderId()); + return Collections.singletonList(CoinexAdapters.toOrder(gateioOrder)); + } + catch (CoinexException e) { + throw CoinexErrorAdapter.adapt(e); + } + } + + +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeServiceRaw.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeServiceRaw.java new file mode 100644 index 00000000000..499140c2dcc --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeServiceRaw.java @@ -0,0 +1,22 @@ +package org.knowm.xchange.coinex.service; + +import java.io.IOException; +import org.knowm.xchange.coinex.CoinexAdapters; +import org.knowm.xchange.coinex.CoinexExchange; +import org.knowm.xchange.coinex.dto.account.CoinexOrder; +import org.knowm.xchange.instrument.Instrument; + +public class CoinexTradeServiceRaw extends CoinexBaseService { + + public CoinexTradeServiceRaw(CoinexExchange exchange) { + super(exchange); + } + + + public CoinexOrder orderStatus(Instrument instrument, String orderId) throws IOException { + String market = CoinexAdapters.toString(instrument); + return coinexAuthenticated.orderStatus(apiKey, exchange.getNonceFactory(), coinexV2ParamsDigest, market, orderId).getData(); + } + + +} From 2346b2984b221f790d8d90120b95bab7a962020f Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Sat, 8 Jun 2024 01:25:28 +0200 Subject: [PATCH 05/10] [coinex] Map order-not-found error --- .../java/org/knowm/xchange/coinex/CoinexErrorAdapter.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexErrorAdapter.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexErrorAdapter.java index 0fcacfdfd20..81888ed34d4 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexErrorAdapter.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexErrorAdapter.java @@ -4,15 +4,20 @@ import org.knowm.xchange.coinex.dto.CoinexException; import org.knowm.xchange.exceptions.ExchangeException; import org.knowm.xchange.exceptions.InstrumentNotValidException; +import org.knowm.xchange.exceptions.OrderNotValidException; @UtilityClass public class CoinexErrorAdapter { + public final int ORDER_NOT_FOUND = 3600; public final int INVALID_MARKET_CODE = 3639; public ExchangeException adapt(CoinexException e) { switch (e.getCode()) { + case ORDER_NOT_FOUND: + return new OrderNotValidException(e.getMessage(), e); + case INVALID_MARKET_CODE: return new InstrumentNotValidException(e.getMessage(), e); From cc96b705d89144016730cd3f2cda1c43d72ad17a Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Sat, 8 Jun 2024 01:51:15 +0200 Subject: [PATCH 06/10] [coinex] Add placing of market orders --- .../org/knowm/xchange/coinex/CoinexAdapters.java | 13 +++++++++++++ .../xchange/coinex/CoinexAuthenticated.java | 11 +++++++++++ .../config/CoinexJacksonObjectMapperFactory.java | 4 ++++ .../converter/CurrencyPairToStringConverter.java | 16 ++++++++++++++++ .../xchange/coinex/dto/account/CoinexOrder.java | 2 ++ .../coinex/service/CoinexTradeService.java | 13 +++++++++++++ .../coinex/service/CoinexTradeServiceRaw.java | 5 +++++ 7 files changed, 64 insertions(+) create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/CurrencyPairToStringConverter.java diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java index b939fa096ee..951c0ea39d8 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java @@ -10,6 +10,7 @@ import java.util.stream.Collectors; import lombok.experimental.UtilityClass; import org.knowm.xchange.coinex.dto.account.CoinexBalanceInfo; +import org.knowm.xchange.coinex.dto.account.CoinexMarketType; import org.knowm.xchange.coinex.dto.account.CoinexOrder; import org.knowm.xchange.coinex.dto.marketdata.CoinexCurrencyPairInfo; import org.knowm.xchange.coinex.dto.marketdata.CoinexMarketDepth; @@ -56,6 +57,18 @@ public Balance toBalance(CoinexBalanceInfo balance) { } + public CoinexOrder toCoinexOrder(MarketOrder marketOrder) { + return CoinexOrder.builder() + .currencyPair((CurrencyPair) marketOrder.getInstrument()) + .marketType(CoinexMarketType.SPOT) + .side(marketOrder.getType()) + .type("market") + .clientId(marketOrder.getUserReference()) + .amount(marketOrder.getOriginalAmount()) + .build(); + } + + public CurrencyPair toCurrencyPair(String symbol) { return SYMBOL_TO_CURRENCY_PAIR.get(symbol); } diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java index 3a470be8ef4..1baa2e2e334 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java @@ -1,7 +1,9 @@ package org.knowm.xchange.coinex; +import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; @@ -27,6 +29,15 @@ CoinexResponse> balances( @HeaderParam("X-COINEX-SIGN") ParamsDigest signer) throws IOException, CoinexException; + @POST + @Path("v2/spot/order") + @Consumes(MediaType.APPLICATION_JSON) + CoinexResponse createOrder( + @HeaderParam("X-COINEX-KEY") String apiKey, + @HeaderParam("X-COINEX-TIMESTAMP") SynchronizedValueFactory timestamp, + @HeaderParam("X-COINEX-SIGN") ParamsDigest signer, + CoinexOrder coinexOrder) + throws IOException, CoinexException; @GET @Path("v2/spot/order-status") diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/CoinexJacksonObjectMapperFactory.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/CoinexJacksonObjectMapperFactory.java index a69e773bf92..451c51a5610 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/CoinexJacksonObjectMapperFactory.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/CoinexJacksonObjectMapperFactory.java @@ -1,5 +1,6 @@ package org.knowm.xchange.coinex.config; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; @@ -14,6 +15,9 @@ public void configureObjectMapper(ObjectMapper objectMapper) { // by default read timetamps as milliseconds objectMapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); + // don't write nulls + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + // enable parsing to Instant objectMapper.registerModule(new JavaTimeModule()); } diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/CurrencyPairToStringConverter.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/CurrencyPairToStringConverter.java new file mode 100644 index 00000000000..d635f82fef8 --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/CurrencyPairToStringConverter.java @@ -0,0 +1,16 @@ +package org.knowm.xchange.coinex.config.converter; + +import com.fasterxml.jackson.databind.util.StdConverter; +import org.knowm.xchange.coinex.CoinexAdapters; +import org.knowm.xchange.currency.CurrencyPair; + +/** + * Converts {@code CurrencyPair} to string + */ +public class CurrencyPairToStringConverter extends StdConverter { + + @Override + public String convert(CurrencyPair value) { + return CoinexAdapters.toString(value); + } +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexOrder.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexOrder.java index 9a73768578f..2f8eb172998 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexOrder.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexOrder.java @@ -8,6 +8,7 @@ import lombok.Builder; import lombok.Data; import lombok.extern.jackson.Jacksonized; +import org.knowm.xchange.coinex.config.converter.CurrencyPairToStringConverter; import org.knowm.xchange.coinex.config.converter.OrderTypeToStringConverter; import org.knowm.xchange.coinex.config.converter.StringToCurrencyConverter; import org.knowm.xchange.coinex.config.converter.StringToCurrencyPairConverter; @@ -59,6 +60,7 @@ public class CoinexOrder { @JsonProperty("market") @JsonDeserialize(converter = StringToCurrencyPairConverter.class) + @JsonSerialize(converter = CurrencyPairToStringConverter.class) CurrencyPair currencyPair; @JsonProperty("market_type") diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeService.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeService.java index 39452334f7d..3960a131664 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeService.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeService.java @@ -10,6 +10,7 @@ import org.knowm.xchange.coinex.dto.CoinexException; import org.knowm.xchange.coinex.dto.account.CoinexOrder; import org.knowm.xchange.dto.Order; +import org.knowm.xchange.dto.trade.MarketOrder; import org.knowm.xchange.service.trade.TradeService; import org.knowm.xchange.service.trade.params.orders.OrderQueryParamInstrument; import org.knowm.xchange.service.trade.params.orders.OrderQueryParams; @@ -21,6 +22,18 @@ public CoinexTradeService(CoinexExchange exchange) { } + @Override + public String placeMarketOrder(MarketOrder marketOrder) throws IOException { + try { + CoinexOrder coinexOrder = createOrder(CoinexAdapters.toCoinexOrder(marketOrder)); + return String.valueOf(coinexOrder.getOrderId()); + } + catch (CoinexException e) { + throw CoinexErrorAdapter.adapt(e); + } + } + + @Override public Collection getOrder(OrderQueryParams... orderQueryParams) throws IOException { Validate.validState(orderQueryParams.length == 1); diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeServiceRaw.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeServiceRaw.java index 499140c2dcc..d6466537635 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeServiceRaw.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexTradeServiceRaw.java @@ -13,6 +13,11 @@ public CoinexTradeServiceRaw(CoinexExchange exchange) { } + public CoinexOrder createOrder(CoinexOrder coinexOrder) throws IOException { + return coinexAuthenticated.createOrder(apiKey, exchange.getNonceFactory(), coinexV2ParamsDigest, coinexOrder).getData(); + } + + public CoinexOrder orderStatus(Instrument instrument, String orderId) throws IOException { String market = CoinexAdapters.toString(instrument); return coinexAuthenticated.orderStatus(apiKey, exchange.getNonceFactory(), coinexV2ParamsDigest, market, orderId).getData(); From 4c759079f5c2116882e51b0ee6dac9717eabd600 Mon Sep 17 00:00:00 2001 From: rizer1980 <4340180@gmail.com> Date: Mon, 3 Jun 2024 23:42:00 +0300 Subject: [PATCH 07/10] [bybit-stream] websocket complex order changes --- .../xchange/dto/account/OpenPosition.java | 25 +---- .../dto/trade/BybitComplexOrderChanges.java | 70 ++++++++++++++ .../trade/BybitComplexPositionChanges.java | 2 + .../dto/trade/BybitOrderChangesResponse.java | 92 +++++++++---------- .../bybit/BybitStreamAdapters.java | 63 ++++++++++--- .../bybit/BybitStreamingTradeService.java | 15 +++ 6 files changed, 183 insertions(+), 84 deletions(-) create mode 100644 xchange-stream-bybit/src/main/java/dto/trade/BybitComplexOrderChanges.java diff --git a/xchange-core/src/main/java/org/knowm/xchange/dto/account/OpenPosition.java b/xchange-core/src/main/java/org/knowm/xchange/dto/account/OpenPosition.java index 87a94bd9e38..847b44fb45c 100644 --- a/xchange-core/src/main/java/org/knowm/xchange/dto/account/OpenPosition.java +++ b/xchange-core/src/main/java/org/knowm/xchange/dto/account/OpenPosition.java @@ -6,8 +6,10 @@ import java.io.Serializable; import java.math.BigDecimal; import java.util.Objects; +import lombok.Getter; import org.knowm.xchange.instrument.Instrument; +@Getter @JsonInclude(JsonInclude.Include.NON_NULL) public class OpenPosition implements Serializable { /** The instrument */ @@ -43,29 +45,6 @@ public OpenPosition( this.unRealisedPnl = unRealisedPnl; } - public Instrument getInstrument() { - return instrument; - } - - public Type getType() { - return type; - } - - public BigDecimal getSize() { - return size; - } - - public BigDecimal getPrice() { - return price; - } - - public BigDecimal getLiquidationPrice() { - return liquidationPrice; - } - - public BigDecimal getUnRealisedPnl() { - return unRealisedPnl; - } @Override public boolean equals(final Object o) { diff --git a/xchange-stream-bybit/src/main/java/dto/trade/BybitComplexOrderChanges.java b/xchange-stream-bybit/src/main/java/dto/trade/BybitComplexOrderChanges.java new file mode 100644 index 00000000000..f25e891e695 --- /dev/null +++ b/xchange-stream-bybit/src/main/java/dto/trade/BybitComplexOrderChanges.java @@ -0,0 +1,70 @@ +package dto.trade; + +import com.fasterxml.jackson.annotation.JsonValue; +import java.math.BigDecimal; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.knowm.xchange.bybit.dto.BybitCategory; +import org.knowm.xchange.bybit.dto.trade.BybitOrderStatus; +import org.knowm.xchange.bybit.dto.trade.BybitOrderType; +import org.knowm.xchange.bybit.dto.trade.BybitSide; +import org.knowm.xchange.dto.Order; +import org.knowm.xchange.instrument.Instrument; + +@Getter +@Setter +public class BybitComplexOrderChanges extends Order { + private BybitCategory category; + private BigDecimal price; + private BybitSide side; + private BigDecimal leavesQty; + private BigDecimal leavesValue; + private BigDecimal cumExecValue; + private String feeCurrency; + private TimeInForce timeInForce; + private BybitOrderType orderType; + private boolean reduceOnly; + private Date updatedTime; + + public BybitComplexOrderChanges(OrderType type, BigDecimal originalAmount, + Instrument instrument, String id, Date timestamp, + BigDecimal averagePrice, BigDecimal cumulativeAmount, + BigDecimal fee, OrderStatus status, String userReference) { + super(type, originalAmount, instrument, id, timestamp, averagePrice, cumulativeAmount, fee, + status, userReference); + } + + public BybitComplexOrderChanges(OrderType type, BigDecimal originalAmount, Instrument instrument, + String id, Date timestamp, BigDecimal averagePrice, BigDecimal cumulativeAmount, + BigDecimal fee, OrderStatus status, String userReference, BybitCategory category, + BigDecimal price, BybitSide side, BigDecimal leavesQty, BigDecimal leavesValue, + BigDecimal cumExecValue, String feeCurrency, TimeInForce timeInForce, + BybitOrderType orderType, boolean reduceOnly, Date updatedTime) { + super(type, originalAmount, instrument, id, timestamp, averagePrice, cumulativeAmount, fee, + status, userReference); + this.category = category; + this.price = price; + this.side = side; + this.leavesQty = leavesQty; + this.leavesValue = leavesValue; + this.cumExecValue = cumExecValue; + this.feeCurrency = feeCurrency; + this.timeInForce = timeInForce; + this.orderType = orderType; + this.reduceOnly = reduceOnly; + this.updatedTime = updatedTime; + } + + @Getter + @AllArgsConstructor + public enum TimeInForce { + GTC("GTC"), + IOC("IOC"), + FOK("FOK"), + POSTONLY("PostOnly"); + @JsonValue + private final String value; + } +} diff --git a/xchange-stream-bybit/src/main/java/dto/trade/BybitComplexPositionChanges.java b/xchange-stream-bybit/src/main/java/dto/trade/BybitComplexPositionChanges.java index a618de62543..d89e5b1fd6d 100644 --- a/xchange-stream-bybit/src/main/java/dto/trade/BybitComplexPositionChanges.java +++ b/xchange-stream-bybit/src/main/java/dto/trade/BybitComplexPositionChanges.java @@ -2,11 +2,13 @@ import java.math.BigDecimal; import lombok.Getter; +import lombok.Setter; import org.knowm.xchange.bybit.dto.BybitCategory; import org.knowm.xchange.dto.account.OpenPosition; import org.knowm.xchange.instrument.Instrument; @Getter +@Setter public class BybitComplexPositionChanges extends OpenPosition { private BigDecimal positionValue; private BigDecimal leverage; diff --git a/xchange-stream-bybit/src/main/java/dto/trade/BybitOrderChangesResponse.java b/xchange-stream-bybit/src/main/java/dto/trade/BybitOrderChangesResponse.java index c7c63afe1d7..b5967ff78c4 100644 --- a/xchange-stream-bybit/src/main/java/dto/trade/BybitOrderChangesResponse.java +++ b/xchange-stream-bybit/src/main/java/dto/trade/BybitOrderChangesResponse.java @@ -19,52 +19,52 @@ public class BybitOrderChangesResponse { @Getter public static class BybitOrderChanges { - BybitCategory category; - String orderId; - String orderLinkId; - String isLeverage; - String blockTradeId; - String symbol; - String price; - String qty; - BybitSide side; - int positionIdx; - BybitOrderStatus orderStatus; - String createType; - String cancelType; - String rejectReason; - String avgPrice; - String leavesQty; - String leavesValue; - String cumExecQty; - String cumExecValue; - String cumExecFee; - String feeCurrency; - String timeInForce; - BybitOrderType orderType; - String stopOrderType; - String ocoTriggerBy; - String orderIv; - String marketUnit; - String triggerPrice; - String takeProfit; - String stopLoss; - String tpslMode; - String tpLimitPrice; - String slLimitPrice; - String tpTriggerBy; - String slTriggerBy; - int triggerDirection; - String triggerBy; - String lastPriceOnCreated; - boolean reduceOnly; - boolean closeOnTrigger; - String placeType; - String smpType; - int smpGroup; - String smpOrderId; - String createdTime; - String updatedTime; + private BybitCategory category; + private String orderId; + private String orderLinkId; + private String isLeverage; + private String blockTradeId; + private String symbol; + private String price; + private String qty; + private BybitSide side; + private int positionIdx; + private BybitOrderStatus orderStatus; + private String createType; + private String cancelType; + private String rejectReason; + private String avgPrice; + private String leavesQty; + private String leavesValue; + private String cumExecQty; + private String cumExecValue; + private String cumExecFee; + private String feeCurrency; + private String timeInForce; + private BybitOrderType orderType; + private String stopOrderType; + private String ocoTriggerBy; + private String orderIv; + private String marketUnit; + private String triggerPrice; + private String takeProfit; + private String stopLoss; + private String tpslMode; + private String tpLimitPrice; + private String slLimitPrice; + private String tpTriggerBy; + private String slTriggerBy; + private int triggerDirection; + private String triggerBy; + private String lastPriceOnCreated; + private boolean reduceOnly; + private boolean closeOnTrigger; + private String placeType; + private String smpType; + private int smpGroup; + private String smpOrderId; + private String createdTime; + private String updatedTime; } } diff --git a/xchange-stream-bybit/src/main/java/info/bitrich/xchangestream/bybit/BybitStreamAdapters.java b/xchange-stream-bybit/src/main/java/info/bitrich/xchangestream/bybit/BybitStreamAdapters.java index 0c56b3c1c73..7cfceb4749d 100644 --- a/xchange-stream-bybit/src/main/java/info/bitrich/xchangestream/bybit/BybitStreamAdapters.java +++ b/xchange-stream-bybit/src/main/java/info/bitrich/xchangestream/bybit/BybitStreamAdapters.java @@ -6,12 +6,15 @@ import static org.knowm.xchange.bybit.BybitAdapters.guessSymbol; import dto.marketdata.BybitPublicOrder; +import dto.trade.BybitComplexOrderChanges; +import dto.trade.BybitComplexOrderChanges.TimeInForce; import dto.trade.BybitComplexPositionChanges; import dto.trade.BybitOrderChangesResponse; import dto.trade.BybitOrderChangesResponse.BybitOrderChanges; import dto.trade.BybitPositionChangesResponse.BybitPositionChanges; import dto.trade.BybitTrade; import java.math.BigDecimal; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -94,9 +97,10 @@ public static List adaptOrdersChanges(List bybitOrderC } builder.fee(new BigDecimal(bybitOrderChange.getCumExecFee())). leverage(bybitOrderChange.getIsLeverage()).id(bybitOrderChange.getOrderId()) - .orderStatus(adaptBybitOrderStatus(bybitOrderChange.getOrderStatus())) - .timestamp(date).cumulativeAmount(new BigDecimal(bybitOrderChange.getCumExecQty())) - .originalAmount(new BigDecimal(bybitOrderChange.getQty())).id(bybitOrderChange.getOrderId()) + .orderStatus(adaptBybitOrderStatus(bybitOrderChange.getOrderStatus())) + .timestamp(date).cumulativeAmount(new BigDecimal(bybitOrderChange.getCumExecQty())) + .originalAmount(new BigDecimal(bybitOrderChange.getQty())) + .id(bybitOrderChange.getOrderId()) .userReference(bybitOrderChange.getOrderLinkId()); orders.add(builder.build()); } @@ -108,39 +112,68 @@ public static OpenPositions adaptPositionChanges( OpenPositions openPositions = new OpenPositions(new ArrayList<>()); for (BybitPositionChanges position : bybitPositionChanges) { OpenPosition.Type type = null; - if(!position.getSide().isEmpty()) { + if (!position.getSide().isEmpty()) { type = position.getSide().equals("Buy") ? Type.LONG : Type.SHORT; } BigDecimal liqPrice = null; - if(!position.getLiqPrice().isEmpty()) { + if (!position.getLiqPrice().isEmpty()) { liqPrice = new BigDecimal(position.getLiqPrice()); } OpenPosition openPosition = new OpenPosition(guessSymbol(position.getSymbol(), - position.getCategory()),type,new BigDecimal(position.getSize()), - new BigDecimal(position.getEntryPrice()), liqPrice, new BigDecimal(position.getUnrealisedPnl())); + position.getCategory()), type, new BigDecimal(position.getSize()), + new BigDecimal(position.getEntryPrice()), liqPrice, + new BigDecimal(position.getUnrealisedPnl())); openPositions.getOpenPositions().add(openPosition); } return openPositions; } - public static List adaptComplexPositionChanges(List data) { + public static List adaptComplexPositionChanges( + List data) { List result = new ArrayList<>(); for (BybitPositionChanges position : data) { OpenPosition.Type type = null; - if(!position.getSide().isEmpty()) { + if (!position.getSide().isEmpty()) { type = position.getSide().equals("Buy") ? Type.LONG : Type.SHORT; } BigDecimal liqPrice = null; - if(!position.getLiqPrice().isEmpty()) { + if (!position.getLiqPrice().isEmpty()) { liqPrice = new BigDecimal(position.getLiqPrice()); } - BybitComplexPositionChanges positionChanges = new BybitComplexPositionChanges(guessSymbol(position.getSymbol(), - position.getCategory()),type,new BigDecimal(position.getSize()), liqPrice,new BigDecimal(position.getUnrealisedPnl()), - new BigDecimal(position.getPositionValue()),new BigDecimal(position.getEntryPrice()), new BigDecimal(position.getLeverage()), - new BigDecimal(position.getTakeProfit()), new BigDecimal(position.getStopLoss()), new BigDecimal(position.getCurRealisedPnl()), - Long.parseLong(position.getCreatedTime()), Long.parseLong(position.getUpdatedTime()), position.getSeq()); + BybitComplexPositionChanges positionChanges = new BybitComplexPositionChanges( + guessSymbol(position.getSymbol(), + position.getCategory()), type, new BigDecimal(position.getSize()), liqPrice, + new BigDecimal(position.getUnrealisedPnl()), + new BigDecimal(position.getPositionValue()), new BigDecimal(position.getEntryPrice()), + new BigDecimal(position.getLeverage()), + new BigDecimal(position.getTakeProfit()), new BigDecimal(position.getStopLoss()), + new BigDecimal(position.getCurRealisedPnl()), + Long.parseLong(position.getCreatedTime()), Long.parseLong(position.getUpdatedTime()), + position.getSeq()); result.add(positionChanges); } return result; } + + public static List adaptComplexOrdersChanges( + List data) { + List result = new ArrayList<>(); + for (BybitOrderChanges change : data) { + Order.OrderType orderType = getOrderType(change.getSide()); + BigDecimal avgPrice = change.getAvgPrice().isEmpty() ? null : new BigDecimal(change.getAvgPrice()); + BybitComplexOrderChanges orderChanges = new BybitComplexOrderChanges(orderType, + new BigDecimal(change.getQty()), guessSymbol(change.getSymbol(),change.getCategory()), change.getOrderId(), + new Date(Long.parseLong(change.getCreatedTime())), avgPrice, + new BigDecimal(change.getCumExecQty()), new BigDecimal(change.getCumExecFee()), + adaptBybitOrderStatus(change.getOrderStatus()), change.getOrderLinkId(), + change.getCategory(), new BigDecimal(change.getPrice()), change.getSide(), + new BigDecimal(change.getLeavesQty()), new BigDecimal(change.getLeavesValue()), + new BigDecimal(change.getCumExecValue()), change.getFeeCurrency(), + TimeInForce.valueOf(change.getTimeInForce().toUpperCase()), change.getOrderType(), + change.isReduceOnly(), new Date(Long.parseLong(change.getUpdatedTime()))); + result.add(orderChanges); + } + return result; + } + } diff --git a/xchange-stream-bybit/src/main/java/info/bitrich/xchangestream/bybit/BybitStreamingTradeService.java b/xchange-stream-bybit/src/main/java/info/bitrich/xchangestream/bybit/BybitStreamingTradeService.java index af95ccb5974..0f60502f135 100644 --- a/xchange-stream-bybit/src/main/java/info/bitrich/xchangestream/bybit/BybitStreamingTradeService.java +++ b/xchange-stream-bybit/src/main/java/info/bitrich/xchangestream/bybit/BybitStreamingTradeService.java @@ -1,6 +1,7 @@ package info.bitrich.xchangestream.bybit; import com.fasterxml.jackson.databind.ObjectMapper; +import dto.trade.BybitComplexOrderChanges; import dto.trade.BybitComplexPositionChanges; import dto.trade.BybitOrderChangesResponse; import dto.trade.BybitPositionChangesResponse; @@ -34,6 +35,20 @@ public Observable getOrderChanges(BybitCategory category) { }); } + public Observable getComplexOrderChanges(BybitCategory category) { + String channelUniqueId = "order"; + if(category != null) { + channelUniqueId += "." + category.getValue(); + } + return streamingService + .subscribeChannel(channelUniqueId).flatMap( + node -> { + BybitOrderChangesResponse bybitOrderChangesResponse = mapper.treeToValue(node, BybitOrderChangesResponse.class); + return Observable.fromIterable( + BybitStreamAdapters.adaptComplexOrdersChanges(bybitOrderChangesResponse.getData())); + }); + } + public Observable getPositionChanges(BybitCategory category) { String channelUniqueId = "position"; if(category != null) { 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 08/10] [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 09/10] [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); } } From 6500c0f7fecee2a044f2bbe885b9b17d19611567 Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Mon, 10 Jun 2024 20:56:29 +0200 Subject: [PATCH 10/10] [coinex] Fix ticker mapping --- .../src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java index 951c0ea39d8..302bc9385e5 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java @@ -175,7 +175,7 @@ public Ticker toTicker(Instrument instrument, CoinexTickerV1 coinexTickerV1, Ins builder .open(coinexTickerV1.getOpen24h()) - .last(coinexTickerV1.getVolume24h()) + .last(coinexTickerV1.getLast()) .bid(coinexTickerV1.getBestBidPrice()) .ask(coinexTickerV1.getBestAskPrice()) .high(coinexTickerV1.getHigh24h())