diff --git a/xchange-coinex/pom.xml b/xchange-coinex/pom.xml index 86435f01cd3..6d02c422545 100644 --- a/xchange-coinex/pom.xml +++ b/xchange-coinex/pom.xml @@ -45,6 +45,12 @@ test + + org.wiremock + wiremock + test + + 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 26b8cea69f0..0220160819f 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 @@ -64,6 +64,18 @@ public CoinexOrder toCoinexOrder(MarketOrder marketOrder) { .build(); } + public CoinexOrder toCoinexOrder(LimitOrder limitOrder) { + return CoinexOrder.builder() + .currencyPair((CurrencyPair) limitOrder.getInstrument()) + .marketType(CoinexMarketType.SPOT) + .side(limitOrder.getType()) + .type("limit") + .price(limitOrder.getLimitPrice()) + .clientId(limitOrder.getUserReference()) + .amount(limitOrder.getOriginalAmount()) + .build(); + } + public CurrencyPair toCurrencyPair(String symbol) { return SYMBOL_TO_CURRENCY_PAIR.get(symbol); } @@ -193,4 +205,26 @@ public Wallet toWallet(List coinexBalanceInfos) { return Wallet.Builder.from(balances).id("spot").build(); } + + public String toString(OrderType orderType) { + if (orderType == null) { + return null; + } + switch (orderType) { + case BID: + return "buy"; + case ASK: + return "sell"; + default: + throw new IllegalArgumentException("Can't map " + orderType); + } + } + + public String toString(CoinexMarketType coinexMarketType) { + if (coinexMarketType == null) { + return null; + } + return coinexMarketType.toString(); + } + } 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 f3063c4273f..4089c7df4dd 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 @@ -14,6 +14,7 @@ import org.knowm.xchange.coinex.dto.CoinexResponse; import org.knowm.xchange.coinex.dto.account.CoinexBalanceInfo; import org.knowm.xchange.coinex.dto.account.CoinexOrder; +import org.knowm.xchange.coinex.dto.trade.CoinexCancelOrderRequest; import si.mazi.rescu.ParamsDigest; import si.mazi.rescu.SynchronizedValueFactory; @@ -29,6 +30,21 @@ CoinexResponse> balances( @HeaderParam("X-COINEX-SIGN") ParamsDigest signer) throws IOException, CoinexException; + @GET + @Path("v2/spot/pending-order") + CoinexResponse> pendingOrders( + @HeaderParam("X-COINEX-KEY") String apiKey, + @HeaderParam("X-COINEX-TIMESTAMP") SynchronizedValueFactory timestamp, + @HeaderParam("X-COINEX-SIGN") ParamsDigest signer, + @QueryParam("market") String market, + @QueryParam("market_type") String marketType, + @QueryParam("side") String side, + @QueryParam("client_id") String clientOid, + @QueryParam("page") Integer page, + @QueryParam("limit") Integer limit + ) + throws IOException, CoinexException; + @POST @Path("v2/spot/order") @Consumes(MediaType.APPLICATION_JSON) 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 index b66d12d359f..03dbf26fec2 100644 --- 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 @@ -1,6 +1,7 @@ package org.knowm.xchange.coinex.config.converter; import com.fasterxml.jackson.databind.util.StdConverter; +import org.knowm.xchange.coinex.CoinexAdapters; import org.knowm.xchange.dto.Order.OrderType; /** Converts {@code OrderType} to string */ @@ -8,13 +9,6 @@ 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); - } + return CoinexAdapters.toString(value); } } 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 48533755e5b..f70266c2bd6 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 @@ -3,15 +3,27 @@ import java.io.IOException; import java.util.Collection; import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; 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.coinex.service.params.CoinexOpenOrdersParams; import org.knowm.xchange.dto.Order; +import org.knowm.xchange.dto.trade.LimitOrder; import org.knowm.xchange.dto.trade.MarketOrder; +import org.knowm.xchange.dto.trade.OpenOrders; import org.knowm.xchange.service.trade.TradeService; +import org.knowm.xchange.service.trade.params.CancelOrderParams; +import org.knowm.xchange.service.trade.params.DefaultCancelOrderByInstrumentAndIdParams; +import org.knowm.xchange.service.trade.params.InstrumentParam; +import org.knowm.xchange.service.trade.params.orders.OpenOrdersParamLimit; +import org.knowm.xchange.service.trade.params.orders.OpenOrdersParamOffset; +import org.knowm.xchange.service.trade.params.orders.OpenOrdersParams; import org.knowm.xchange.service.trade.params.orders.OrderQueryParamInstrument; import org.knowm.xchange.service.trade.params.orders.OrderQueryParams; @@ -31,6 +43,45 @@ public String placeMarketOrder(MarketOrder marketOrder) throws IOException { } } + @Override + public OpenOrders getOpenOrders() throws IOException { + return getOpenOrders(null); + } + + @Override + public OpenOrders getOpenOrders(OpenOrdersParams params) throws IOException { + CoinexOpenOrdersParams.CoinexOpenOrdersParamsBuilder builder = CoinexOpenOrdersParams.builder(); + + if (params instanceof InstrumentParam) { + builder.instrument(((InstrumentParam) params).getInstrument()); + } + + if (params instanceof OpenOrdersParamLimit) { + builder.limit(((OpenOrdersParamLimit) params).getLimit()); + } + + if (params instanceof OpenOrdersParamOffset) { + builder.offset(((OpenOrdersParamOffset) params).getOffset()); + } + + List limitOrders = pendingOrders(builder.build()).stream() + .map(CoinexAdapters::toOrder) + .map(LimitOrder.class::cast) + .collect(Collectors.toList()); + + return new OpenOrders(limitOrders); + } + + @Override + public String placeLimitOrder(LimitOrder limitOrder) throws IOException { + try { + CoinexOrder coinexOrder = createOrder(CoinexAdapters.toCoinexOrder(limitOrder)); + 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 33f5dd6baa6..a2c28992c8f 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 @@ -1,9 +1,14 @@ package org.knowm.xchange.coinex.service; import java.io.IOException; +import java.util.List; +import java.util.Optional; import org.knowm.xchange.coinex.CoinexAdapters; import org.knowm.xchange.coinex.CoinexExchange; +import org.knowm.xchange.coinex.dto.account.CoinexMarketType; import org.knowm.xchange.coinex.dto.account.CoinexOrder; +import org.knowm.xchange.coinex.dto.trade.CoinexCancelOrderRequest; +import org.knowm.xchange.coinex.service.params.CoinexOpenOrdersParams; import org.knowm.xchange.instrument.Instrument; public class CoinexTradeServiceRaw extends CoinexBaseService { @@ -24,4 +29,14 @@ public CoinexOrder orderStatus(Instrument instrument, String orderId) throws IOE .orderStatus(apiKey, exchange.getNonceFactory(), coinexV2ParamsDigest, market, orderId) .getData(); } + + public List pendingOrders(CoinexOpenOrdersParams coinexOpenOrdersParams) throws IOException { + String market = CoinexAdapters.toString(coinexOpenOrdersParams.getInstrument()); + Integer page = coinexOpenOrdersParams.getOffset(); + Integer limit = Optional.ofNullable(coinexOpenOrdersParams.getLimit()).orElse(CoinexOpenOrdersParams.DEFAULT_LIMIT); + return coinexAuthenticated + .pendingOrders(apiKey, exchange.getNonceFactory(), coinexV2ParamsDigest, market, CoinexAdapters.toString(CoinexMarketType.SPOT), + null, null, page, limit) + .getData(); + } } diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/params/CoinexOpenOrdersParams.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/params/CoinexOpenOrdersParams.java new file mode 100644 index 00000000000..2e5755a033c --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/params/CoinexOpenOrdersParams.java @@ -0,0 +1,23 @@ +package org.knowm.xchange.coinex.service.params; + +import lombok.Builder; +import lombok.Data; +import org.knowm.xchange.instrument.Instrument; +import org.knowm.xchange.service.trade.params.orders.DefaultOpenOrdersParam; +import org.knowm.xchange.service.trade.params.orders.OpenOrdersParamInstrument; +import org.knowm.xchange.service.trade.params.orders.OpenOrdersParamLimit; +import org.knowm.xchange.service.trade.params.orders.OpenOrdersParamOffset; + +@Data +@Builder +public class CoinexOpenOrdersParams extends DefaultOpenOrdersParam implements OpenOrdersParamLimit, + OpenOrdersParamOffset, OpenOrdersParamInstrument { + + public static final Integer DEFAULT_LIMIT = 1000; + + private Instrument instrument; + + private Integer limit; + + private Integer offset; +} diff --git a/xchange-coinex/src/test/java/org/knowm/xchange/coinex/CoinexExchangeWiremock.java b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/CoinexExchangeWiremock.java new file mode 100644 index 00000000000..e3fadd9569e --- /dev/null +++ b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/CoinexExchangeWiremock.java @@ -0,0 +1,52 @@ +package org.knowm.xchange.coinex; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.recording.RecordSpecBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.knowm.xchange.ExchangeFactory; +import org.knowm.xchange.ExchangeSpecification; + +/** Sets up the wiremock for exchange */ +public abstract class CoinexExchangeWiremock { + + protected static CoinexExchange exchange; + +// private static final boolean IS_RECORDING = true; + private static final boolean IS_RECORDING = false; + + private static WireMockServer wireMockServer; + + @BeforeAll + public static void initExchange() { + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + + ExchangeSpecification exSpec = new ExchangeSpecification(CoinexExchange.class); + exSpec.setSslUri("http://localhost:" + wireMockServer.port()); + exSpec.setApiKey(System.getProperty("apiKey", "abc")); + exSpec.setSecretKey(System.getProperty("secretKey", "bcd")); + + if (IS_RECORDING) { + // use default url and record the requests + wireMockServer.startRecording( + new RecordSpecBuilder() + .forTarget("https://api.coinex.com") + .matchRequestBodyWithEqualToJson() + .extractTextBodiesOver(1L) + .chooseBodyMatchTypeAutomatically()); + } + + exchange = (CoinexExchange) ExchangeFactory.INSTANCE.createExchange(exSpec); + } + + @AfterAll + public static void stop() { + if (IS_RECORDING) { + wireMockServer.stopRecording(); + } + wireMockServer.stop(); + } +} diff --git a/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexTradeServiceTest.java b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexTradeServiceTest.java new file mode 100644 index 00000000000..e8d2f872b28 --- /dev/null +++ b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexTradeServiceTest.java @@ -0,0 +1,41 @@ +package org.knowm.xchange.coinex.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import org.junit.jupiter.api.Test; +import org.knowm.xchange.coinex.CoinexExchangeWiremock; +import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.trade.OpenOrders; +import org.knowm.xchange.service.trade.TradeService; +import org.knowm.xchange.service.trade.params.orders.DefaultOpenOrdersParamInstrument; + +class CoinexTradeServiceTest extends CoinexExchangeWiremock { + + TradeService tradeService = exchange.getTradeService(); + + @Test + void all_open_orders() throws IOException { + OpenOrders actual = tradeService.getOpenOrders(); + + assertThat(actual.getOpenOrders()).hasSize(2); + assertThat(actual.getHiddenOrders()).isEmpty(); + + assertThat(actual.getAllOpenOrders().get(0).getInstrument()) + .isEqualTo(CurrencyPair.ETH_USDT); + assertThat(actual.getAllOpenOrders().get(1).getInstrument()) + .isEqualTo(CurrencyPair.BTC_USDT); + } + + @Test + void filtered_open_orders() throws IOException { + OpenOrders actual = tradeService.getOpenOrders(new DefaultOpenOrdersParamInstrument(CurrencyPair.BTC_USDT)); + + assertThat(actual.getOpenOrders()).hasSize(1); + assertThat(actual.getHiddenOrders()).isEmpty(); + + assertThat(actual.getAllOpenOrders().get(0).getInstrument()) + .isEqualTo(CurrencyPair.BTC_USDT); + } + +} \ No newline at end of file diff --git a/xchange-coinex/src/test/resources/__files/v2_spot_market.json b/xchange-coinex/src/test/resources/__files/v2_spot_market.json new file mode 100644 index 00000000000..79ee8c5ae53 --- /dev/null +++ b/xchange-coinex/src/test/resources/__files/v2_spot_market.json @@ -0,0 +1,32 @@ +{ + "code": 0, + "data": [ + { + "base_ccy": "ETH", + "base_ccy_precision": 8, + "is_amm_available": false, + "is_margin_available": true, + "is_pre_trading_available": false, + "maker_fee_rate": "0.002", + "market": "ETHUSDT", + "min_amount": "0.0005", + "quote_ccy": "USDT", + "quote_ccy_precision": 2, + "taker_fee_rate": "0.002" + }, + { + "base_ccy": "BTC", + "base_ccy_precision": 8, + "is_amm_available": false, + "is_margin_available": true, + "is_pre_trading_available": false, + "maker_fee_rate": "0.002", + "market": "BTCUSDT", + "min_amount": "0.00005", + "quote_ccy": "USDT", + "quote_ccy_precision": 2, + "taker_fee_rate": "0.002" + } + ], + "message": "OK" +} \ No newline at end of file diff --git a/xchange-coinex/src/test/resources/__files/v2_spot_pending-order_all-open-orders.json b/xchange-coinex/src/test/resources/__files/v2_spot_pending-order_all-open-orders.json new file mode 100644 index 00000000000..8953db3dd61 --- /dev/null +++ b/xchange-coinex/src/test/resources/__files/v2_spot_pending-order_all-open-orders.json @@ -0,0 +1,56 @@ +{ + "code": 0, + "data": [ + { + "order_id": 136215261928, + "market": "ETHUSDT", + "market_type": "SPOT", + "side": "buy", + "type": "limit", + "ccy": "ETH", + "amount": "0.00142842", + "price": "3500.37", + "client_id": "", + "created_at": 1734126286694, + "updated_at": 1734126286694, + "base_fee": "0", + "quote_fee": "0", + "discount_fee": "0", + "maker_fee_rate": "0.0018", + "taker_fee_rate": "0.0018", + "last_fill_amount": "0", + "last_fill_price": "0", + "unfilled_amount": "0.00142842", + "filled_amount": "0", + "filled_value": "0" + }, + { + "order_id": 136215219959, + "market": "BTCUSDT", + "market_type": "SPOT", + "side": "buy", + "type": "limit", + "ccy": "BTC", + "amount": "0.00005263", + "price": "95000.99", + "client_id": "", + "created_at": 1734126246894, + "updated_at": 1734126246894, + "base_fee": "0", + "quote_fee": "0", + "discount_fee": "0", + "maker_fee_rate": "0.0018", + "taker_fee_rate": "0.0018", + "last_fill_amount": "0", + "last_fill_price": "0", + "unfilled_amount": "0.00005263", + "filled_amount": "0", + "filled_value": "0" + } + ], + "message": "OK", + "pagination": { + "total": 8, + "has_next": false + } +} \ No newline at end of file diff --git a/xchange-coinex/src/test/resources/__files/v2_spot_pending-order_filtered-open-orders.json b/xchange-coinex/src/test/resources/__files/v2_spot_pending-order_filtered-open-orders.json new file mode 100644 index 00000000000..b441194af9b --- /dev/null +++ b/xchange-coinex/src/test/resources/__files/v2_spot_pending-order_filtered-open-orders.json @@ -0,0 +1,33 @@ +{ + "code": 0, + "data": [ + { + "order_id": 136215219959, + "market": "BTCUSDT", + "market_type": "SPOT", + "side": "buy", + "type": "limit", + "ccy": "BTC", + "amount": "0.00005263", + "price": "95000.99", + "client_id": "", + "created_at": 1734126246894, + "updated_at": 1734126246894, + "base_fee": "0", + "quote_fee": "0", + "discount_fee": "0", + "maker_fee_rate": "0.0018", + "taker_fee_rate": "0.0018", + "last_fill_amount": "0", + "last_fill_price": "0", + "unfilled_amount": "0.00005263", + "filled_amount": "0", + "filled_value": "0" + } + ], + "message": "OK", + "pagination": { + "total": 1, + "has_next": false + } +} \ No newline at end of file diff --git a/xchange-coinex/src/test/resources/mappings/v2_spot_market.json b/xchange-coinex/src/test/resources/mappings/v2_spot_market.json new file mode 100644 index 00000000000..ffe573829b3 --- /dev/null +++ b/xchange-coinex/src/test/resources/mappings/v2_spot_market.json @@ -0,0 +1,15 @@ +{ + "id" : "ed00de1b-ef6b-474a-8a0c-7e8071df933d", + "name" : "v2_spot_market", + "request" : { + "url" : "/v2/spot/market", + "method" : "GET" + }, + "response" : { + "status" : 200, + "bodyFileName" : "v2_spot_market.json" + }, + "uuid" : "ed00de1b-ef6b-474a-8a0c-7e8071df933d", + "persistent" : true, + "insertionIndex" : 2 +} \ No newline at end of file diff --git a/xchange-coinex/src/test/resources/mappings/v2_spot_pending-order_all-open-orders.json b/xchange-coinex/src/test/resources/mappings/v2_spot_pending-order_all-open-orders.json new file mode 100644 index 00000000000..2aeeacb1192 --- /dev/null +++ b/xchange-coinex/src/test/resources/mappings/v2_spot_pending-order_all-open-orders.json @@ -0,0 +1,15 @@ +{ + "id" : "6c09969d-9280-4749-be8e-37294b537a7e", + "name" : "v2_spot_pending-order", + "request" : { + "url" : "/v2/spot/pending-order?market_type=SPOT&limit=1000", + "method" : "GET" + }, + "response" : { + "status" : 200, + "bodyFileName" : "v2_spot_pending-order_all-open-orders.json" + }, + "uuid" : "6c09969d-9280-4749-be8e-37294b537a7e", + "persistent" : true, + "insertionIndex" : 2 +} \ No newline at end of file diff --git a/xchange-coinex/src/test/resources/mappings/v2_spot_pending-order_filtered-open-orders.json b/xchange-coinex/src/test/resources/mappings/v2_spot_pending-order_filtered-open-orders.json new file mode 100644 index 00000000000..d8fee4e627b --- /dev/null +++ b/xchange-coinex/src/test/resources/mappings/v2_spot_pending-order_filtered-open-orders.json @@ -0,0 +1,15 @@ +{ + "id" : "32a5c085-969b-4539-98e1-a1eafb02fb0c", + "name" : "v2_spot_pending-order", + "request" : { + "url" : "/v2/spot/pending-order?market=BTCUSDT&market_type=SPOT&limit=1000", + "method" : "GET" + }, + "response" : { + "status" : 200, + "bodyFileName" : "v2_spot_pending-order_filtered-open-orders.json" + }, + "uuid" : "32a5c085-969b-4539-98e1-a1eafb02fb0c", + "persistent" : true, + "insertionIndex" : 3 +} \ No newline at end of file diff --git a/xchange-coinex/src/test/resources/rest/spot.v2.http b/xchange-coinex/src/test/resources/rest/spot.v2.http index d51866af9e5..cf3cc9f9847 100644 --- a/xchange-coinex/src/test/resources/rest/spot.v2.http +++ b/xchange-coinex/src/test/resources/rest/spot.v2.http @@ -25,6 +25,17 @@ X-COINEX-TIMESTAMP: {{timestamp}} X-COINEX-SIGN: {{sign}} +### Get Unfilled Order +< {% + import {gen_sign} from 'sign.js' + gen_sign("GET", request); +%} +GET {{api_host}}/v2/spot/pending-order?market_type=SPOT&limit=1000 +X-COINEX-KEY: {{api_key}} +X-COINEX-TIMESTAMP: {{timestamp}} +X-COINEX-SIGN: {{sign}} + + ### Place Order < {% import {gen_sign} from 'sign.js'