diff --git a/pom.xml b/pom.xml
index 405079c72f..ec7bc6a7de 100644
--- a/pom.xml
+++ b/pom.xml
@@ -50,6 +50,7 @@
xchange-bitcointoyou
xchange-bitfinex
xchange-bitflyer
+ xchange-bitget
xchange-bithumb
xchange-bitmex
xchange-bitso
diff --git a/xchange-bitget/.gitignore b/xchange-bitget/.gitignore
new file mode 100644
index 0000000000..e0acc9b7bf
--- /dev/null
+++ b/xchange-bitget/.gitignore
@@ -0,0 +1,5 @@
+# private properties for idea rest client
+http-client.private.env.json
+
+# private properties for integration tests
+integration-test.env.properties
\ No newline at end of file
diff --git a/xchange-bitget/README.md b/xchange-bitget/README.md
new file mode 100644
index 0000000000..f728ba8eda
--- /dev/null
+++ b/xchange-bitget/README.md
@@ -0,0 +1,21 @@
+## Using IntelliJ Idea HTTP client
+
+There are *.http files stored in `src/test/resources/rest` that can be used with IntelliJ Idea HTTP Client.
+
+Some requests need authorization, so the api credentials have to be stored in `http-client.private.env.json` in module's root. Sample content can be found in `example.http-client.private.env.json`
+
+> [!CAUTION]
+> Never commit your api credentials to the repository!
+
+
+[HTTP Client documentation](https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html)
+
+## Running integration tests that require API keys
+
+Integration tests that require API keys read them from environment variables. They can be defined in `integration-test.env.properties`. Sample content can be found in `example.integration-test.env.properties`.
+
+If no keys are provided the integration tests that need them are skipped.
+
+> [!CAUTION]
+> Never commit your api credentials to the repository!
+
diff --git a/xchange-bitget/example.http-client.private.env.json b/xchange-bitget/example.http-client.private.env.json
new file mode 100644
index 0000000000..6a0f22d976
--- /dev/null
+++ b/xchange-bitget/example.http-client.private.env.json
@@ -0,0 +1,7 @@
+{
+ "default": {
+ "api_key": "change_me",
+ "api_secret": "change_me",
+ "api_passphrase": "change_me"
+ }
+}
\ No newline at end of file
diff --git a/xchange-bitget/example.integration-test.env.properties b/xchange-bitget/example.integration-test.env.properties
new file mode 100644
index 0000000000..9bd1822a2d
--- /dev/null
+++ b/xchange-bitget/example.integration-test.env.properties
@@ -0,0 +1,3 @@
+apiKey=change_me
+secretKey=change_me
+passphrase=change_me
diff --git a/xchange-bitget/http-client.env.json b/xchange-bitget/http-client.env.json
new file mode 100644
index 0000000000..82e88360d1
--- /dev/null
+++ b/xchange-bitget/http-client.env.json
@@ -0,0 +1,5 @@
+{
+ "default": {
+ "api_host": "https://api.bitget.com"
+ }
+}
\ No newline at end of file
diff --git a/xchange-bitget/lombok.config b/xchange-bitget/lombok.config
new file mode 100644
index 0000000000..22b090cc4b
--- /dev/null
+++ b/xchange-bitget/lombok.config
@@ -0,0 +1,2 @@
+lombok.equalsAndHashCode.callSuper = call
+lombok.tostring.callsuper = call
\ No newline at end of file
diff --git a/xchange-bitget/pom.xml b/xchange-bitget/pom.xml
new file mode 100644
index 0000000000..ab88f4ced9
--- /dev/null
+++ b/xchange-bitget/pom.xml
@@ -0,0 +1,73 @@
+
+
+ 4.0.0
+
+ org.knowm.xchange
+ xchange-parent
+ 5.2.1-SNAPSHOT
+
+
+ xchange-bitget
+
+ XChange Bitget
+ XChange implementation for the Bitget Exchange
+
+ http://knowm.org/open-source/xchange/
+ 2012
+
+
+ Knowm Inc.
+ http://knowm.org/open-source/xchange/
+
+
+
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ ${version.fasterxml}
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+ org.knowm.xchange
+ xchange-core
+ ${project.version}
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+ test
+
+
+
+ org.wiremock
+ wiremock
+ test
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+ integration-test.env.properties
+
+
+
+
+
+
+
+
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/Bitget.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/Bitget.java
new file mode 100644
index 0000000000..2ccacc26cc
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/Bitget.java
@@ -0,0 +1,50 @@
+package org.knowm.xchange.bitget;
+
+import jakarta.ws.rs.GET;
+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.bitget.dto.BitgetException;
+import org.knowm.xchange.bitget.dto.BitgetResponse;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetCoinDto;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetMarketDepthDto;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetServerTime;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetSymbolDto;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetTickerDto;
+
+@Path("")
+@Produces(MediaType.APPLICATION_JSON)
+public interface Bitget {
+
+ @GET
+ @Path("api/v2/public/time")
+ BitgetResponse serverTime() throws IOException, BitgetException;
+
+
+ @GET
+ @Path("api/v2/spot/public/coins")
+ BitgetResponse> coins(@QueryParam("coin") String coin) throws IOException, BitgetException;
+
+
+ @GET
+ @Path("api/v2/spot/public/symbols")
+ BitgetResponse> symbols(@QueryParam("symbol") String symbol)
+ throws IOException, BitgetException;
+
+
+ @GET
+ @Path("api/v2/spot/market/tickers")
+ BitgetResponse> tickers(@QueryParam("symbol") String symbol)
+ throws IOException, BitgetException;
+
+
+ @GET
+ @Path("api/v2/spot/market/orderbook")
+ BitgetResponse orderbook(@QueryParam("symbol") String symbol)
+ throws IOException, BitgetException;
+
+
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/BitgetAdapters.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/BitgetAdapters.java
new file mode 100644
index 0000000000..61ab482b91
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/BitgetAdapters.java
@@ -0,0 +1,234 @@
+package org.knowm.xchange.bitget;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import lombok.experimental.UtilityClass;
+import org.knowm.xchange.bitget.dto.account.BitgetBalanceDto;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetMarketDepthDto;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetSymbolDto;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetSymbolDto.Status;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetTickerDto;
+import org.knowm.xchange.bitget.dto.trade.BitgetOrderInfoDto;
+import org.knowm.xchange.bitget.dto.trade.BitgetOrderInfoDto.BitgetOrderStatus;
+import org.knowm.xchange.bitget.dto.trade.BitgetPlaceOrderDto;
+import org.knowm.xchange.currency.Currency;
+import org.knowm.xchange.currency.CurrencyPair;
+import org.knowm.xchange.dto.Order;
+import org.knowm.xchange.dto.Order.OrderStatus;
+import org.knowm.xchange.dto.Order.OrderType;
+import org.knowm.xchange.dto.account.Balance;
+import org.knowm.xchange.dto.account.Wallet;
+import org.knowm.xchange.dto.marketdata.OrderBook;
+import org.knowm.xchange.dto.marketdata.Ticker;
+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
+public class BitgetAdapters {
+
+ private final Map SYMBOL_TO_CURRENCY_PAIR = new HashMap<>();
+
+
+ public CurrencyPair toCurrencyPair(String symbol) {
+ return SYMBOL_TO_CURRENCY_PAIR.get(symbol);
+ }
+
+
+ public String toString(Instrument instrument) {
+ return instrument == null ? null : instrument.getBase().toString() + instrument.getCounter().toString();
+ }
+
+
+ public String toString(Currency currency) {
+ return Optional.ofNullable(currency)
+ .map(Currency::getCurrencyCode)
+ .orElse(null);
+ }
+
+
+ public void putSymbolMapping(String symbol, CurrencyPair currencyPair) {
+ SYMBOL_TO_CURRENCY_PAIR.put(symbol, currencyPair);
+ }
+
+
+ public InstrumentMetaData toInstrumentMetaData(BitgetSymbolDto bitgetSymbolDto) {
+ InstrumentMetaData.Builder builder = new InstrumentMetaData.Builder()
+ .tradingFee(bitgetSymbolDto.getTakerFeeRate())
+ .minimumAmount(bitgetSymbolDto.getMinTradeAmount())
+ .maximumAmount(bitgetSymbolDto.getMaxTradeAmount())
+ .volumeScale(bitgetSymbolDto.getQuantityPrecision())
+ .priceScale(bitgetSymbolDto.getPricePrecision())
+ .marketOrderEnabled(bitgetSymbolDto.getStatus() == Status.ONLINE);
+
+ // set min quote amount for USDT
+ if (bitgetSymbolDto.getCurrencyPair().getCounter().equals(Currency.USDT)) {
+ builder.counterMinimumAmount(bitgetSymbolDto.getMinTradeUSDT());
+ }
+
+ return builder.build();
+ }
+
+
+ public Ticker toTicker(BitgetTickerDto bitgetTickerDto) {
+ CurrencyPair currencyPair = toCurrencyPair(bitgetTickerDto.getSymbol());
+ if (currencyPair == null) {
+ return null;
+ }
+ return new Ticker.Builder()
+ .instrument(currencyPair)
+ .open(bitgetTickerDto.getOpen24h())
+ .last(bitgetTickerDto.getLastPrice())
+ .bid(bitgetTickerDto.getBestBidPrice())
+ .ask(bitgetTickerDto.getBestAskPrice())
+ .high(bitgetTickerDto.getHigh24h())
+ .low(bitgetTickerDto.getLow24h())
+ .volume(bitgetTickerDto.getAssetVolume24h())
+ .quoteVolume(bitgetTickerDto.getQuoteVolume24h())
+ .timestamp(toDate(bitgetTickerDto.getTimestamp()))
+ .bidSize(bitgetTickerDto.getBestBidSize())
+ .askSize(bitgetTickerDto.getBestAskSize())
+ .percentageChange(bitgetTickerDto.getChange24h())
+ .build();
+ }
+
+
+ public Date toDate(Instant instant) {
+ return Optional.ofNullable(instant)
+ .map(Date::from)
+ .orElse(null);
+ }
+
+
+ public Balance toBalance(BitgetBalanceDto balance) {
+ return new Balance.Builder()
+ .currency(balance.getCurrency())
+ .available(balance.getAvailable())
+ .frozen(balance.getFrozen())
+ .timestamp(toDate(balance.getTimestamp()))
+ .build();
+ }
+
+
+ public Wallet toWallet(List bitgetBalanceDtos) {
+ List balances = bitgetBalanceDtos.stream()
+ .map(BitgetAdapters::toBalance)
+ .collect(Collectors.toList());
+
+ return Wallet.Builder.from(balances).id("spot").build();
+ }
+
+
+ public OrderBook toOrderBook(BitgetMarketDepthDto bitgetMarketDepthDto, Instrument instrument) {
+ List asks = bitgetMarketDepthDto.getAsks().stream()
+ .map(priceSizeEntry ->
+ new LimitOrder(
+ OrderType.ASK,
+ priceSizeEntry.getSize(),
+ instrument,
+ null,
+ null,
+ priceSizeEntry.getPrice())
+ )
+ .collect(Collectors.toList());
+
+ List bids = bitgetMarketDepthDto.getBids().stream()
+ .map(priceSizeEntry ->
+ new LimitOrder(
+ OrderType.BID,
+ priceSizeEntry.getSize(),
+ instrument,
+ null,
+ null,
+ priceSizeEntry.getPrice())
+ )
+ .collect(Collectors.toList());
+
+ return new OrderBook(toDate(bitgetMarketDepthDto.getTimestamp()), asks, bids);
+ }
+
+
+ public Order toOrder(BitgetOrderInfoDto order) {
+ if (order == null) {
+ return null;
+ }
+
+ Instrument instrument = toCurrencyPair(order.getSymbol());
+ Objects.requireNonNull(instrument);
+ OrderType orderType = order.getOrderSide();
+
+ Order.Builder builder;
+ switch (order.getOrderType()) {
+ case MARKET:
+ builder = new MarketOrder.Builder(orderType, instrument);
+ break;
+ case LIMIT:
+ builder = new LimitOrder.Builder(orderType, instrument)
+ .limitPrice(order.getPrice());
+ break;
+ default:
+ throw new IllegalArgumentException("Can't map " + order.getOrderType());
+ }
+
+ if (orderType == OrderType.BID) {
+ // buy orders fill quote
+ builder.cumulativeAmount(order.getQuoteVolume());
+ } else if (orderType == OrderType.ASK) {
+ // sell orders fill asset
+ builder.cumulativeAmount(order.getBaseVolume());
+ } else {
+ throw new IllegalArgumentException("Can't map " + orderType);
+ }
+
+ BigDecimal fee = order.getFee();
+ if (fee != null) {
+ builder.fee(fee);
+ }
+
+ return builder
+ .id(String.valueOf(order.getOrderId()))
+ .averagePrice(order.getPriceAvg())
+ .originalAmount(order.getSize())
+ .userReference(order.getClientOid())
+ .timestamp(toDate(order.getCreatedAt()))
+ .orderStatus(toOrderStatus(order.getOrderStatus()))
+ .build();
+ }
+
+
+ public OrderStatus toOrderStatus(BitgetOrderStatus bitgetOrderStatus) {
+ switch (bitgetOrderStatus) {
+ case PENDING:
+ return OrderStatus.NEW;
+ case PARTIALLY_FILLED:
+ return OrderStatus.PARTIALLY_FILLED;
+ case FILLED:
+ return OrderStatus.FILLED;
+ case CANCELLED:
+ return OrderStatus.CANCELED;
+ default:
+ throw new IllegalArgumentException("Can't map " + bitgetOrderStatus);
+ }
+ }
+
+
+ public BitgetPlaceOrderDto toBitgetPlaceOrderDto(MarketOrder marketOrder) {
+ return BitgetPlaceOrderDto.builder()
+ .symbol(toString(marketOrder.getInstrument()))
+ .orderSide(marketOrder.getType())
+ .orderType(BitgetOrderInfoDto.OrderType.MARKET)
+ .clientOid(marketOrder.getUserReference())
+ .size(marketOrder.getOriginalAmount())
+ .build();
+ }
+
+
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/BitgetAuthenticated.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/BitgetAuthenticated.java
new file mode 100644
index 0000000000..a22f8f75a1
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/BitgetAuthenticated.java
@@ -0,0 +1,58 @@
+package org.knowm.xchange.bitget;
+
+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;
+import jakarta.ws.rs.core.MediaType;
+import java.io.IOException;
+import java.util.List;
+import org.knowm.xchange.bitget.dto.BitgetException;
+import org.knowm.xchange.bitget.dto.BitgetResponse;
+import org.knowm.xchange.bitget.dto.account.BitgetBalanceDto;
+import org.knowm.xchange.bitget.dto.trade.BitgetOrderInfoDto;
+import org.knowm.xchange.bitget.dto.trade.BitgetPlaceOrderDto;
+import si.mazi.rescu.ParamsDigest;
+import si.mazi.rescu.SynchronizedValueFactory;
+
+@Path("")
+@Produces(MediaType.APPLICATION_JSON)
+public interface BitgetAuthenticated {
+
+ @GET
+ @Path("api/v2/spot/account/assets")
+ BitgetResponse> balances(
+ @HeaderParam("ACCESS-KEY") String apiKey,
+ @HeaderParam("ACCESS-SIGN") ParamsDigest signer,
+ @HeaderParam("ACCESS-PASSPHRASE") String passphrase,
+ @HeaderParam("ACCESS-TIMESTAMP") SynchronizedValueFactory timestamp)
+ throws IOException, BitgetException;
+
+
+ @GET
+ @Path("api/v2/spot/trade/orderInfo")
+ BitgetResponse> orderInfo(
+ @HeaderParam("ACCESS-KEY") String apiKey,
+ @HeaderParam("ACCESS-SIGN") ParamsDigest signer,
+ @HeaderParam("ACCESS-PASSPHRASE") String passphrase,
+ @HeaderParam("ACCESS-TIMESTAMP") SynchronizedValueFactory timestamp,
+ @QueryParam("orderId") String orderId)
+ throws IOException, BitgetException;
+
+
+ @POST
+ @Path("api/v2/spot/trade/place-order")
+ @Consumes(MediaType.APPLICATION_JSON)
+ BitgetResponse createOrder(
+ @HeaderParam("ACCESS-KEY") String apiKey,
+ @HeaderParam("ACCESS-SIGN") ParamsDigest signer,
+ @HeaderParam("ACCESS-PASSPHRASE") String passphrase,
+ @HeaderParam("ACCESS-TIMESTAMP") SynchronizedValueFactory timestamp,
+ BitgetPlaceOrderDto bitgetPlaceOrderDto)
+ throws IOException, BitgetException;
+
+
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/BitgetErrorAdapter.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/BitgetErrorAdapter.java
new file mode 100644
index 0000000000..3d50923b04
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/BitgetErrorAdapter.java
@@ -0,0 +1,37 @@
+package org.knowm.xchange.bitget;
+
+import lombok.experimental.UtilityClass;
+import org.knowm.xchange.bitget.dto.BitgetException;
+import org.knowm.xchange.exceptions.ExchangeException;
+import org.knowm.xchange.exceptions.FundsExceededException;
+import org.knowm.xchange.exceptions.InstrumentNotValidException;
+import org.knowm.xchange.exceptions.OrderAmountUnderMinimumException;
+
+@UtilityClass
+public class BitgetErrorAdapter {
+
+ public final int INVALID_PARAMETER = 40034;
+ public final int MIN_ORDER_SIZE = 13008;
+ public final int MIN_ORDER_AMOUNT = 45110;
+ public final int MIN_ORDER_QTY = 45111;
+ public final int INSUFFICIENT_BALANCE = 43012;
+
+ public ExchangeException adapt(BitgetException e) {
+
+ switch (e.getCode()) {
+ case INVALID_PARAMETER:
+ return new InstrumentNotValidException(e.getMessage(), e);
+
+ case MIN_ORDER_SIZE:
+ case MIN_ORDER_AMOUNT:
+ case MIN_ORDER_QTY:
+ return new OrderAmountUnderMinimumException(e.getMessage(), e);
+
+ case INSUFFICIENT_BALANCE:
+ return new FundsExceededException(e.getMessage(), e);
+
+ default:
+ return new ExchangeException(e.getMessage(), e);
+ }
+ }
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/BitgetExchange.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/BitgetExchange.java
new file mode 100644
index 0000000000..b7f3ce80ac
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/BitgetExchange.java
@@ -0,0 +1,57 @@
+package org.knowm.xchange.bitget;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.knowm.xchange.BaseExchange;
+import org.knowm.xchange.ExchangeSpecification;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetSymbolDto;
+import org.knowm.xchange.bitget.service.BitgetAccountService;
+import org.knowm.xchange.bitget.service.BitgetMarketDataService;
+import org.knowm.xchange.bitget.service.BitgetMarketDataServiceRaw;
+import org.knowm.xchange.bitget.service.BitgetTradeService;
+import org.knowm.xchange.dto.meta.ExchangeMetaData;
+import org.knowm.xchange.dto.meta.InstrumentMetaData;
+import org.knowm.xchange.instrument.Instrument;
+
+public class BitgetExchange extends BaseExchange {
+
+ @Override
+ protected void initServices() {
+ accountService = new BitgetAccountService(this);
+ marketDataService = new BitgetMarketDataService(this);
+ tradeService = new BitgetTradeService(this);
+ }
+
+ @Override
+ public ExchangeSpecification getDefaultExchangeSpecification() {
+ ExchangeSpecification specification = new ExchangeSpecification(getClass());
+ specification.setSslUri("https://api.bitget.com");
+ specification.setHost("www.bitget.com");
+ specification.setExchangeName("Bitget");
+ return specification;
+ }
+
+ @Override
+ public void remoteInit() throws IOException {
+ BitgetMarketDataServiceRaw bitgetMarketDataServiceRaw = (BitgetMarketDataServiceRaw) marketDataService;
+
+ // initialize symbol mappings
+ List bitgetSymbolDtos = bitgetMarketDataServiceRaw.getBitgetSymbolDtos(null);
+ bitgetSymbolDtos.forEach(bitgetSymbolDto -> {
+ BitgetAdapters.putSymbolMapping(bitgetSymbolDto.getSymbol(), bitgetSymbolDto.getCurrencyPair());
+ });
+
+
+ // initialize instrument metadata
+ Map instruments = bitgetSymbolDtos.stream()
+ .collect(Collectors.toMap(
+ BitgetSymbolDto::getCurrencyPair,
+ BitgetAdapters::toInstrumentMetaData)
+ );
+
+ exchangeMetaData = new ExchangeMetaData(instruments, null, null, null, null);
+
+ }
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/BitgetJacksonObjectMapperFactory.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/BitgetJacksonObjectMapperFactory.java
new file mode 100644
index 0000000000..350cf8e2b4
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/BitgetJacksonObjectMapperFactory.java
@@ -0,0 +1,27 @@
+package org.knowm.xchange.bitget.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;
+import si.mazi.rescu.serialization.jackson.DefaultJacksonObjectMapperFactory;
+
+public class BitgetJacksonObjectMapperFactory extends DefaultJacksonObjectMapperFactory {
+
+ @Override
+ public void configureObjectMapper(ObjectMapper objectMapper) {
+ super.configureObjectMapper(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);
+
+ // don't fail un unknown properties
+ objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+ // enable parsing to Instant
+ objectMapper.registerModule(new JavaTimeModule());
+ }
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/Config.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/Config.java
new file mode 100644
index 0000000000..f967008959
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/Config.java
@@ -0,0 +1,20 @@
+package org.knowm.xchange.bitget.config;
+
+import java.time.Clock;
+import lombok.Data;
+
+@Data
+public final class Config {
+
+ private Clock clock;
+
+ private static Config instance = new Config();
+
+ private Config() {
+ clock = Clock.systemDefaultZone();
+ }
+
+ public static Config getInstance() {
+ return instance;
+ }
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/converter/OrderTypeToStringConverter.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/converter/OrderTypeToStringConverter.java
new file mode 100644
index 0000000000..abe591c7f3
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/converter/OrderTypeToStringConverter.java
@@ -0,0 +1,20 @@
+package org.knowm.xchange.bitget.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-bitget/src/main/java/org/knowm/xchange/bitget/config/converter/StringToBooleanConverter.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/converter/StringToBooleanConverter.java
new file mode 100644
index 0000000000..abf89d8cc4
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/converter/StringToBooleanConverter.java
@@ -0,0 +1,13 @@
+package org.knowm.xchange.bitget.config.converter;
+
+import com.fasterxml.jackson.databind.util.StdConverter;
+import org.apache.commons.lang3.BooleanUtils;
+
+/** Converts string value to {@code Boolean} */
+public class StringToBooleanConverter extends StdConverter {
+
+ @Override
+ public Boolean convert(final String value) {
+ return BooleanUtils.toBoolean(value);
+ }
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/converter/StringToCurrencyConverter.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/converter/StringToCurrencyConverter.java
new file mode 100644
index 0000000000..00e2a5e870
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/converter/StringToCurrencyConverter.java
@@ -0,0 +1,13 @@
+package org.knowm.xchange.bitget.config.converter;
+
+import com.fasterxml.jackson.databind.util.StdConverter;
+import org.knowm.xchange.currency.Currency;
+
+/** Converts string value to {@code Currency} */
+public class StringToCurrencyConverter extends StdConverter {
+
+ @Override
+ public Currency convert(String value) {
+ return Currency.getInstance(value);
+ }
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/converter/StringToOrderTypeConverter.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/converter/StringToOrderTypeConverter.java
new file mode 100644
index 0000000000..0d747baa28
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/converter/StringToOrderTypeConverter.java
@@ -0,0 +1,21 @@
+package org.knowm.xchange.bitget.config.converter;
+
+import com.fasterxml.jackson.databind.util.StdConverter;
+import java.util.Locale;
+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.toUpperCase(Locale.ROOT)) {
+ case "BUY":
+ return OrderType.BID;
+ case "SELL":
+ return OrderType.ASK;
+ default:
+ throw new IllegalArgumentException("Can't map " + value);
+ }
+ }
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/deserializer/FeeDetailDeserializer.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/deserializer/FeeDetailDeserializer.java
new file mode 100644
index 0000000000..4628bd65e4
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/config/deserializer/FeeDetailDeserializer.java
@@ -0,0 +1,19 @@
+package org.knowm.xchange.bitget.config.deserializer;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import org.knowm.xchange.bitget.dto.trade.BitgetOrderInfoDto.FeeDetail;
+
+public class FeeDetailDeserializer extends JsonDeserializer {
+
+ @Override
+ public FeeDetail deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ String text = p.getText();
+ ObjectMapper objectMapper = (ObjectMapper) p.getCodec();
+
+ return objectMapper.readValue(text, FeeDetail.class);
+ }
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/BitgetException.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/BitgetException.java
new file mode 100644
index 0000000000..34d33813bd
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/BitgetException.java
@@ -0,0 +1,26 @@
+package org.knowm.xchange.bitget.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.time.Instant;
+import lombok.Builder;
+import lombok.Value;
+import lombok.extern.jackson.Jacksonized;
+
+@Value
+@Builder
+@Jacksonized
+public class BitgetException extends RuntimeException {
+
+ @JsonProperty("code")
+ Integer code;
+
+ @JsonProperty("msg")
+ String message;
+
+ @JsonProperty("requestTime")
+ Instant requestTime;
+
+ @JsonProperty("data")
+ Object data;
+
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/BitgetResponse.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/BitgetResponse.java
new file mode 100755
index 0000000000..82f44b7677
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/BitgetResponse.java
@@ -0,0 +1,34 @@
+package org.knowm.xchange.bitget.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.time.Instant;
+import lombok.Builder;
+import lombok.Data;
+import lombok.extern.jackson.Jacksonized;
+import si.mazi.rescu.ExceptionalReturnContentException;
+
+@Data
+@Builder
+@Jacksonized
+public class BitgetResponse {
+
+ @JsonProperty("code")
+ private Integer code;
+
+ @JsonProperty("msg")
+ private String message;
+
+ @JsonProperty("requestTime")
+ private Instant requestTime;
+
+ @JsonProperty("data")
+ private T data;
+
+ public void setCode(Integer code) {
+ if (code != 0) {
+ throw new ExceptionalReturnContentException(String.valueOf(code));
+ }
+ this.code = code;
+ }
+
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/account/BitgetBalanceDto.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/account/BitgetBalanceDto.java
new file mode 100755
index 0000000000..5e4813d3dd
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/account/BitgetBalanceDto.java
@@ -0,0 +1,37 @@
+package org.knowm.xchange.bitget.dto.account;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import java.math.BigDecimal;
+import java.time.Instant;
+import lombok.Builder;
+import lombok.Data;
+import lombok.extern.jackson.Jacksonized;
+import org.knowm.xchange.bitget.config.converter.StringToCurrencyConverter;
+import org.knowm.xchange.currency.Currency;
+
+@Data
+@Builder
+@Jacksonized
+public class BitgetBalanceDto {
+
+ @JsonProperty("coin")
+ @JsonDeserialize(converter = StringToCurrencyConverter.class)
+ private Currency currency;
+
+ @JsonProperty("available")
+ private BigDecimal available;
+
+ @JsonProperty("frozen")
+ private BigDecimal frozen;
+
+ @JsonProperty("locked")
+ private BigDecimal locked;
+
+ @JsonProperty("limitAvailable")
+ private BigDecimal limitAvailable;
+
+ @JsonProperty("uTime")
+ private Instant timestamp;
+
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetChainDto.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetChainDto.java
new file mode 100755
index 0000000000..62139145de
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetChainDto.java
@@ -0,0 +1,69 @@
+package org.knowm.xchange.bitget.dto.marketdata;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.math.BigDecimal;
+import lombok.Builder;
+import lombok.Data;
+import lombok.extern.jackson.Jacksonized;
+
+@Data
+@Builder
+@Jacksonized
+public class BitgetChainDto {
+
+ @JsonProperty("chain")
+ private String chain;
+
+ @JsonProperty("needTag")
+ private Boolean needTag;
+
+ @JsonProperty("withdrawable")
+ private Boolean isWithdrawEnabled;
+
+ @JsonProperty("rechargeable")
+ private Boolean isDepositEnabled;
+
+ @JsonProperty("withdrawFee")
+ private BigDecimal withdrawFee;
+
+ @JsonProperty("extraWithdrawFee")
+ private BigDecimal extraWithdrawFee;
+
+ @JsonProperty("depositConfirm")
+ private Integer depositConfirmBlockCount;
+
+ @JsonProperty("withdrawConfirm")
+ private Integer withdrawConfirmBlockCount;
+
+ @JsonProperty("minDepositAmount")
+ private BigDecimal minDepositAmount;
+
+ @JsonProperty("minWithdrawAmount")
+ private BigDecimal minWithdrawAmount;
+
+ @JsonProperty("browserUrl")
+ private String browserUrl;
+
+ @JsonProperty("contractAddress")
+ private String contractAddress;
+
+ @JsonProperty("withdrawStep")
+ private Integer withdrawStep;
+
+ @JsonProperty("withdrawMinScale")
+ private Integer withdrawMinScale;
+
+ @JsonProperty("congestion")
+ private Congestion congestion;
+
+
+ public static enum Congestion {
+ @JsonProperty("normal")
+ NORMAL,
+
+ @JsonProperty("congested")
+ CONGESTED;
+ }
+
+}
+
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetCoinDto.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetCoinDto.java
new file mode 100755
index 0000000000..60217f85ae
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetCoinDto.java
@@ -0,0 +1,35 @@
+package org.knowm.xchange.bitget.dto.marketdata;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import java.util.List;
+import lombok.Builder;
+import lombok.Data;
+import lombok.extern.jackson.Jacksonized;
+import org.knowm.xchange.bitget.config.converter.StringToBooleanConverter;
+import org.knowm.xchange.bitget.config.converter.StringToCurrencyConverter;
+import org.knowm.xchange.currency.Currency;
+
+@Data
+@Builder
+@Jacksonized
+public class BitgetCoinDto {
+
+ @JsonProperty("coinId")
+ private String coinId;
+
+ @JsonProperty("coin")
+ @JsonDeserialize(converter = StringToCurrencyConverter.class)
+ private Currency currency;
+
+ @JsonProperty("transfer")
+ private Boolean canTransfer;
+
+ @JsonProperty("areaCoin")
+ @JsonDeserialize(converter = StringToBooleanConverter.class)
+ private Boolean isAreaCoin;
+
+ @JsonProperty("chains")
+ private List chains;
+
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetMarketDepthDto.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetMarketDepthDto.java
new file mode 100644
index 0000000000..17746d1e79
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetMarketDepthDto.java
@@ -0,0 +1,36 @@
+package org.knowm.xchange.bitget.dto.marketdata;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.util.List;
+import lombok.Builder;
+import lombok.Data;
+import lombok.extern.jackson.Jacksonized;
+
+@Data
+@Builder
+@Jacksonized
+public class BitgetMarketDepthDto {
+
+ @JsonProperty("asks")
+ List asks;
+
+ @JsonProperty("bids")
+ List bids;
+
+ @JsonProperty("ts")
+ Instant timestamp;
+
+
+ @Data
+ @Builder
+ @Jacksonized
+ @JsonFormat(shape = JsonFormat.Shape.ARRAY)
+ public static class PriceSizeEntry {
+ BigDecimal price;
+
+ BigDecimal size;
+ }
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetServerTime.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetServerTime.java
new file mode 100644
index 0000000000..6af7180261
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetServerTime.java
@@ -0,0 +1,17 @@
+package org.knowm.xchange.bitget.dto.marketdata;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.time.Instant;
+import lombok.Builder;
+import lombok.Data;
+import lombok.extern.jackson.Jacksonized;
+
+@Data
+@Builder
+@Jacksonized
+public class BitgetServerTime {
+
+ @JsonProperty("serverTime")
+ private Instant serverTime;
+
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetSymbolDto.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetSymbolDto.java
new file mode 100755
index 0000000000..da353d48c2
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetSymbolDto.java
@@ -0,0 +1,86 @@
+package org.knowm.xchange.bitget.dto.marketdata;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import java.math.BigDecimal;
+import lombok.Builder;
+import lombok.Data;
+import lombok.extern.jackson.Jacksonized;
+import org.knowm.xchange.bitget.config.converter.StringToBooleanConverter;
+import org.knowm.xchange.bitget.config.converter.StringToCurrencyConverter;
+import org.knowm.xchange.currency.Currency;
+import org.knowm.xchange.currency.CurrencyPair;
+
+@Data
+@Builder
+@Jacksonized
+public class BitgetSymbolDto {
+
+ @JsonProperty("symbol")
+ private String symbol;
+
+ @JsonProperty("baseCoin")
+ @JsonDeserialize(converter = StringToCurrencyConverter.class)
+ private Currency base;
+
+ @JsonProperty("quoteCoin")
+ @JsonDeserialize(converter = StringToCurrencyConverter.class)
+ private Currency quote;
+
+ @JsonProperty("minTradeAmount")
+ private BigDecimal minTradeAmount;
+
+ @JsonProperty("maxTradeAmount")
+ private BigDecimal maxTradeAmount;
+
+ @JsonProperty("takerFeeRate")
+ private BigDecimal takerFeeRate;
+
+ @JsonProperty("makerFeeRate")
+ private BigDecimal makerFeeRate;
+
+ @JsonProperty("pricePrecision")
+ private Integer pricePrecision;
+
+ @JsonProperty("quantityPrecision")
+ private Integer quantityPrecision;
+
+ @JsonProperty("quotePrecision")
+ private Integer quotePrecision;
+
+ @JsonProperty("status")
+ private Status status;
+
+ @JsonProperty("minTradeUSDT")
+ private BigDecimal minTradeUSDT;
+
+ @JsonProperty("buyLimitPriceRatio")
+ private BigDecimal buyLimitPriceRatio;
+
+ @JsonProperty("sellLimitPriceRatio")
+ private BigDecimal sellLimitPriceRatio;
+
+ @JsonProperty("areaSymbol")
+ @JsonDeserialize(converter = StringToBooleanConverter.class)
+ private Boolean isAreaSymbol;
+
+
+ public CurrencyPair getCurrencyPair() {
+ return new CurrencyPair(base, quote);
+ }
+
+
+ public static enum Status {
+ @JsonProperty("offline")
+ OFFLINE,
+
+ @JsonProperty("gray")
+ GRAY,
+
+ @JsonProperty("online")
+ ONLINE,
+
+ @JsonProperty("halt")
+ HALT
+ }
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetTickerDto.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetTickerDto.java
new file mode 100755
index 0000000000..2fd05042b3
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/marketdata/BitgetTickerDto.java
@@ -0,0 +1,63 @@
+package org.knowm.xchange.bitget.dto.marketdata;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.math.BigDecimal;
+import java.time.Instant;
+import lombok.Builder;
+import lombok.Data;
+import lombok.extern.jackson.Jacksonized;
+
+@Data
+@Builder
+@Jacksonized
+public class BitgetTickerDto {
+
+ @JsonProperty("symbol")
+ private String symbol;
+
+ @JsonProperty("high24h")
+ private BigDecimal high24h;
+
+ @JsonProperty("open")
+ private BigDecimal open24h;
+
+ @JsonProperty("lastPr")
+ private BigDecimal lastPrice;
+
+ @JsonProperty("low24h")
+ private BigDecimal low24h;
+
+ @JsonProperty("quoteVolume")
+ private BigDecimal quoteVolume24h;
+
+ @JsonProperty("baseVolume")
+ private BigDecimal assetVolume24h;
+
+ @JsonProperty("usdtVolume")
+ private BigDecimal usdtVolume24h;
+
+ @JsonProperty("bidPr")
+ private BigDecimal bestBidPrice;
+
+ @JsonProperty("bidSz")
+ private BigDecimal bestBidSize;
+
+ @JsonProperty("askPr")
+ private BigDecimal bestAskPrice;
+
+ @JsonProperty("askSz")
+ private BigDecimal bestAskSize;
+
+ @JsonProperty("openUtc")
+ private BigDecimal openUtc;
+
+ @JsonProperty("ts")
+ private Instant timestamp;
+
+ @JsonProperty("changeUtc24h")
+ private BigDecimal changeUtc24h;
+
+ @JsonProperty("change24h")
+ private BigDecimal change24h;
+
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/trade/BitgetOrderInfoDto.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/trade/BitgetOrderInfoDto.java
new file mode 100755
index 0000000000..3a6c9e2dc9
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/trade/BitgetOrderInfoDto.java
@@ -0,0 +1,147 @@
+package org.knowm.xchange.bitget.dto.trade;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.util.Optional;
+import lombok.Builder;
+import lombok.Data;
+import lombok.extern.jackson.Jacksonized;
+import org.knowm.xchange.bitget.config.converter.StringToOrderTypeConverter;
+import org.knowm.xchange.bitget.config.deserializer.FeeDetailDeserializer;
+import org.knowm.xchange.dto.Order;
+
+@Data
+@Builder
+@Jacksonized
+public class BitgetOrderInfoDto {
+
+ @JsonProperty("userId")
+ private String acccountId;
+
+ @JsonProperty("symbol")
+ private String symbol;
+
+ @JsonProperty("orderId")
+ private String orderId;
+
+ @JsonProperty("clientOid")
+ private String clientOid;
+
+ @JsonProperty("price")
+ private BigDecimal price;
+
+ @JsonProperty("size")
+ private BigDecimal size;
+
+ @JsonProperty("orderType")
+ private OrderType orderType;
+
+ @JsonProperty("side")
+ @JsonDeserialize(converter = StringToOrderTypeConverter.class)
+ private Order.OrderType orderSide;
+
+ @JsonProperty("status")
+ private BitgetOrderStatus orderStatus;
+
+ @JsonProperty("priceAvg")
+ private BigDecimal priceAvg;
+
+ @JsonProperty("baseVolume")
+ private BigDecimal baseVolume;
+
+ @JsonProperty("quoteVolume")
+ private BigDecimal quoteVolume;
+
+ @JsonProperty("enterPointSource")
+ private String enterPointSource;
+
+ @JsonProperty("cTime")
+ private Instant createdAt;
+
+ @JsonProperty("uTime")
+ private Instant updatedAt;
+
+ @JsonProperty("orderSource")
+ private OrderSource orderSource;
+
+ @JsonProperty("feeDetail")
+ @JsonDeserialize(using = FeeDetailDeserializer.class)
+ private FeeDetail feeDetail;
+
+
+ public BigDecimal getFee() {
+ return Optional.ofNullable(feeDetail)
+ .map(FeeDetail::getNewFees)
+ .map(NewFees::getTotalFee)
+ .map(BigDecimal::abs)
+ .orElse(null);
+ }
+
+
+ public static enum OrderType {
+ @JsonProperty("limit")
+ LIMIT,
+
+ @JsonProperty("market")
+ MARKET
+ }
+
+
+ public static enum BitgetOrderStatus {
+ @JsonProperty("live")
+ PENDING,
+
+ @JsonProperty("partially_filled")
+ PARTIALLY_FILLED,
+
+ @JsonProperty("filled")
+ FILLED,
+
+ @JsonProperty("cancelled")
+ CANCELLED
+
+ }
+
+
+ public static enum OrderSource {
+ @JsonProperty("normal")
+ NORMAL,
+
+ @JsonProperty("market")
+ MARKET,
+
+ @JsonProperty("spot_trader_buy")
+ SPOT_TRADER_BUY,
+
+ @JsonProperty("spot_follower_buy")
+ SPOT_FOLLOWER_BUY,
+
+ @JsonProperty("spot_trader_sell")
+ SPOT_TRADER_SELL,
+
+ @JsonProperty("spot_follower_sell")
+ SPOT_FOLLOWER_SELL
+
+ }
+
+
+ @Data
+ @Builder
+ @Jacksonized
+ public static class NewFees {
+ @JsonProperty("t")
+ private BigDecimal totalFee;
+ }
+
+
+ @Data
+ @Builder
+ @Jacksonized
+ public static class FeeDetail {
+ @JsonProperty("newFees")
+ private NewFees newFees;
+ }
+
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/trade/BitgetPlaceOrderDto.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/trade/BitgetPlaceOrderDto.java
new file mode 100755
index 0000000000..b66048be66
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/dto/trade/BitgetPlaceOrderDto.java
@@ -0,0 +1,111 @@
+package org.knowm.xchange.bitget.dto.trade;
+
+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.bitget.config.converter.OrderTypeToStringConverter;
+import org.knowm.xchange.bitget.config.converter.StringToOrderTypeConverter;
+import org.knowm.xchange.bitget.dto.trade.BitgetOrderInfoDto.OrderType;
+import org.knowm.xchange.dto.Order;
+
+@Data
+@Builder
+@Jacksonized
+public class BitgetPlaceOrderDto {
+
+ @JsonProperty("symbol")
+ private String symbol;
+
+ @JsonProperty("side")
+ @JsonDeserialize(converter = StringToOrderTypeConverter.class)
+ @JsonSerialize(converter = OrderTypeToStringConverter.class)
+ private Order.OrderType orderSide;
+
+ @JsonProperty("orderType")
+ private OrderType orderType;
+
+ @JsonProperty("force")
+ private TimeInForce timeInForce;
+
+ @JsonProperty("price")
+ private BigDecimal price;
+
+ @JsonProperty("size")
+ private BigDecimal size;
+
+ @JsonProperty("clientOid")
+ private String clientOid;
+
+ @JsonProperty("triggerPrice")
+ private BigDecimal triggerPrice;
+
+ @JsonProperty("tpslType")
+ private TpSlType tpSlType;
+
+ @JsonProperty("requestTime")
+ private Instant requestTime;
+
+ @JsonProperty("receiveWindow")
+ private Instant receiveWindow;
+
+ @JsonProperty("stpMode")
+ private StpMode stpMode;
+
+ @JsonProperty("presetTakeProfitPrice")
+ private BigDecimal presetTakeProfitPrice;
+
+ @JsonProperty("executeTakeProfitPrice")
+ private BigDecimal executeTakeProfitPrice;
+
+ @JsonProperty("presetStopLossPrice")
+ private BigDecimal presetStopLossPrice;
+
+ @JsonProperty("executeStopLossPrice")
+ private BigDecimal executeStopLossPrice;
+
+
+ public enum TimeInForce {
+ @JsonProperty("gtc")
+ GOOD_TIL_CANCELLED,
+
+ @JsonProperty("post_only")
+ POST_ONLY,
+
+ @JsonProperty("fok")
+ FILL_OR_KILL,
+
+ @JsonProperty("ioc")
+ IMMEDIATE_OR_CANCEL
+ }
+
+
+ public enum TpSlType {
+ @JsonProperty("normal")
+ NORMAL,
+
+ @JsonProperty("tpsl")
+ SPOT_TP_SL
+ }
+
+
+ public enum StpMode {
+ @JsonProperty("none")
+ NONE,
+
+ @JsonProperty("cancel_taker")
+ CANCEL_TAKER,
+
+ @JsonProperty("cancel_maker")
+ CANCEL_MAKER,
+
+ @JsonProperty("cancel_both")
+ CANCEL_BOTH
+ }
+
+
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetAccountService.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetAccountService.java
new file mode 100644
index 0000000000..d8222013fd
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetAccountService.java
@@ -0,0 +1,33 @@
+package org.knowm.xchange.bitget.service;
+
+import java.io.IOException;
+import java.util.List;
+import org.knowm.xchange.bitget.BitgetAdapters;
+import org.knowm.xchange.bitget.BitgetErrorAdapter;
+import org.knowm.xchange.bitget.BitgetExchange;
+import org.knowm.xchange.bitget.dto.BitgetException;
+import org.knowm.xchange.bitget.dto.account.BitgetBalanceDto;
+import org.knowm.xchange.dto.account.AccountInfo;
+import org.knowm.xchange.dto.account.Wallet;
+import org.knowm.xchange.service.account.AccountService;
+
+public class BitgetAccountService extends BitgetAccountServiceRaw implements AccountService {
+
+ public BitgetAccountService(BitgetExchange exchange) {
+ super(exchange);
+ }
+
+
+ @Override
+ public AccountInfo getAccountInfo() throws IOException {
+ try {
+ List spotBalances = getBitgetBalances();
+ Wallet wallet = BitgetAdapters.toWallet(spotBalances);
+ return new AccountInfo(wallet);
+
+ } catch (BitgetException e) {
+ throw BitgetErrorAdapter.adapt(e);
+ }
+ }
+
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetAccountServiceRaw.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetAccountServiceRaw.java
new file mode 100644
index 0000000000..4ffc0195fe
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetAccountServiceRaw.java
@@ -0,0 +1,18 @@
+package org.knowm.xchange.bitget.service;
+
+import java.io.IOException;
+import java.util.List;
+import org.knowm.xchange.bitget.BitgetExchange;
+import org.knowm.xchange.bitget.dto.account.BitgetBalanceDto;
+
+public class BitgetAccountServiceRaw extends BitgetBaseService {
+
+ public BitgetAccountServiceRaw(BitgetExchange exchange) {
+ super(exchange);
+ }
+
+
+ public List getBitgetBalances() throws IOException {
+ return bitgetAuthenticated.balances(apiKey, bitgetDigest, passphrase, exchange.getNonceFactory()).getData();
+ }
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetBaseService.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetBaseService.java
new file mode 100644
index 0000000000..5d1f6bace4
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetBaseService.java
@@ -0,0 +1,35 @@
+package org.knowm.xchange.bitget.service;
+
+import org.knowm.xchange.bitget.Bitget;
+import org.knowm.xchange.bitget.BitgetAuthenticated;
+import org.knowm.xchange.bitget.BitgetExchange;
+import org.knowm.xchange.bitget.config.BitgetJacksonObjectMapperFactory;
+import org.knowm.xchange.client.ExchangeRestProxyBuilder;
+import org.knowm.xchange.service.BaseExchangeService;
+import org.knowm.xchange.service.BaseService;
+
+public class BitgetBaseService extends BaseExchangeService implements BaseService {
+
+ protected final String apiKey;
+ protected final String passphrase;
+ protected final Bitget bitget;
+ protected final BitgetAuthenticated bitgetAuthenticated;
+ protected final BitgetDigest bitgetDigest;
+
+ public BitgetBaseService(BitgetExchange exchange) {
+ super(exchange);
+ bitget = ExchangeRestProxyBuilder
+ .forInterface(Bitget.class, exchange.getExchangeSpecification())
+ .clientConfigCustomizer(clientConfig -> clientConfig.setJacksonObjectMapperFactory(new BitgetJacksonObjectMapperFactory()))
+ .build();
+ bitgetAuthenticated = ExchangeRestProxyBuilder
+ .forInterface(BitgetAuthenticated.class, exchange.getExchangeSpecification())
+ .clientConfigCustomizer(clientConfig -> clientConfig.setJacksonObjectMapperFactory(new BitgetJacksonObjectMapperFactory()))
+ .build();
+
+ apiKey = exchange.getExchangeSpecification().getApiKey();
+ passphrase = exchange.getExchangeSpecification().getPassword();
+ bitgetDigest = BitgetDigest.createInstance(exchange.getExchangeSpecification().getSecretKey());
+
+ }
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetDigest.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetDigest.java
new file mode 100644
index 0000000000..3d2a4ca24e
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetDigest.java
@@ -0,0 +1,45 @@
+package org.knowm.xchange.bitget.service;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Locale;
+import javax.crypto.Mac;
+import lombok.SneakyThrows;
+import org.apache.commons.lang3.StringUtils;
+import org.knowm.xchange.service.BaseParamsDigest;
+import si.mazi.rescu.RestInvocation;
+
+public final class BitgetDigest extends BaseParamsDigest {
+
+ private BitgetDigest(String secretKeyBase64) {
+ super(secretKeyBase64, HMAC_SHA_256);
+ }
+
+
+ public static BitgetDigest createInstance(String secretKeyBase64) {
+ return secretKeyBase64 == null ? null : new BitgetDigest(secretKeyBase64);
+ }
+
+
+ @SneakyThrows
+ @Override
+ public String digestParams(RestInvocation restInvocation) {
+ String method = restInvocation.getHttpMethod().toUpperCase(Locale.ROOT);
+ 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("ACCESS-TIMESTAMP");
+
+ String payloadToSign = String.format("%s%s/%s%s%s", timestamp, method, path, query, body);
+
+ Mac mac = getMac();
+ mac.update(payloadToSign.getBytes(StandardCharsets.UTF_8));
+
+ return Base64.getEncoder().encodeToString(mac.doFinal());
+ }
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetMarketDataService.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetMarketDataService.java
new file mode 100644
index 0000000000..f02332cdf9
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetMarketDataService.java
@@ -0,0 +1,96 @@
+package org.knowm.xchange.bitget.service;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import org.knowm.xchange.bitget.BitgetAdapters;
+import org.knowm.xchange.bitget.BitgetErrorAdapter;
+import org.knowm.xchange.bitget.BitgetExchange;
+import org.knowm.xchange.bitget.config.Config;
+import org.knowm.xchange.bitget.dto.BitgetException;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetTickerDto;
+import org.knowm.xchange.currency.CurrencyPair;
+import org.knowm.xchange.dto.marketdata.OrderBook;
+import org.knowm.xchange.dto.marketdata.Ticker;
+import org.knowm.xchange.dto.meta.ExchangeHealth;
+import org.knowm.xchange.instrument.Instrument;
+import org.knowm.xchange.service.marketdata.MarketDataService;
+import org.knowm.xchange.service.marketdata.params.Params;
+
+public class BitgetMarketDataService extends BitgetMarketDataServiceRaw implements
+ MarketDataService {
+
+ public BitgetMarketDataService(BitgetExchange exchange) {
+ super(exchange);
+ }
+
+ @Override
+ public ExchangeHealth getExchangeHealth() {
+ try {
+ Instant serverTime = getBitgetServerTime().getServerTime();
+ Instant localTime = Instant.now(Config.getInstance().getClock());
+
+ // timestamps shouldn't diverge by more than 10 minutes
+ if (Duration.between(serverTime, localTime).toMinutes() < 10) {
+ return ExchangeHealth.ONLINE;
+ }
+ } catch (BitgetException | IOException e) {
+ return ExchangeHealth.OFFLINE;
+ }
+
+ return ExchangeHealth.OFFLINE;
+ }
+
+
+ @Override
+ public Ticker getTicker(CurrencyPair currencyPair, Object... args) throws IOException {
+ return getTicker((Instrument) currencyPair, args);
+ }
+
+
+ @Override
+ public Ticker getTicker(Instrument instrument, Object... args) throws IOException {
+ try {
+ List tickers = getBitgetTickerDtos(instrument);
+ return BitgetAdapters.toTicker(tickers.get(0));
+
+ } catch (BitgetException e) {
+ throw BitgetErrorAdapter.adapt(e);
+ }
+ }
+
+
+ @Override
+ public List getTickers(Params params) throws IOException {
+ try {
+ return getBitgetTickerDtos(null).stream()
+ .map(BitgetAdapters::toTicker)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+
+ } catch (BitgetException e) {
+ throw BitgetErrorAdapter.adapt(e);
+ }
+ }
+
+
+ @Override
+ public OrderBook getOrderBook(CurrencyPair currencyPair, Object... args) throws IOException {
+ return getOrderBook((Instrument) currencyPair, args);
+ }
+
+
+ @Override
+ public OrderBook getOrderBook(Instrument instrument, Object... args) throws IOException {
+ Objects.requireNonNull(instrument);
+
+ try {
+ return BitgetAdapters.toOrderBook(getBitgetMarketDepthDtos(instrument), instrument);
+ } catch (BitgetException e) {
+ throw BitgetErrorAdapter.adapt(e);
+ }
+ }
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetMarketDataServiceRaw.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetMarketDataServiceRaw.java
new file mode 100644
index 0000000000..ee8882c941
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetMarketDataServiceRaw.java
@@ -0,0 +1,48 @@
+package org.knowm.xchange.bitget.service;
+
+import java.io.IOException;
+import java.util.List;
+import org.knowm.xchange.bitget.BitgetAdapters;
+import org.knowm.xchange.bitget.BitgetExchange;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetCoinDto;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetMarketDepthDto;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetServerTime;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetSymbolDto;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetTickerDto;
+import org.knowm.xchange.currency.Currency;
+import org.knowm.xchange.instrument.Instrument;
+
+public class BitgetMarketDataServiceRaw extends BitgetBaseService {
+
+
+ public BitgetMarketDataServiceRaw(BitgetExchange exchange) {
+ super(exchange);
+ }
+
+
+ public BitgetServerTime getBitgetServerTime() throws IOException {
+ return bitget.serverTime().getData();
+ }
+
+
+ public List getBitgetCoinDtoList(Currency currency) throws IOException {
+ return bitget.coins(BitgetAdapters.toString(currency)).getData();
+ }
+
+
+ public List getBitgetSymbolDtos(Instrument instrument) throws IOException {
+ return bitget.symbols(BitgetAdapters.toString(instrument)).getData();
+ }
+
+
+ public List getBitgetTickerDtos(Instrument instrument) throws IOException {
+ return bitget.tickers(BitgetAdapters.toString(instrument)).getData();
+ }
+
+
+ public BitgetMarketDepthDto getBitgetMarketDepthDtos(Instrument instrument) throws IOException {
+ return bitget.orderbook(BitgetAdapters.toString(instrument)).getData();
+ }
+
+
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetTradeService.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetTradeService.java
new file mode 100644
index 0000000000..73a52bf549
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetTradeService.java
@@ -0,0 +1,53 @@
+package org.knowm.xchange.bitget.service;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.commons.lang3.Validate;
+import org.knowm.xchange.bitget.BitgetAdapters;
+import org.knowm.xchange.bitget.BitgetErrorAdapter;
+import org.knowm.xchange.bitget.BitgetExchange;
+import org.knowm.xchange.bitget.dto.BitgetException;
+import org.knowm.xchange.bitget.dto.trade.BitgetOrderInfoDto;
+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.DefaultQueryOrderParam;
+import org.knowm.xchange.service.trade.params.orders.OrderQueryParams;
+
+public class BitgetTradeService extends BitgetTradeServiceRaw implements TradeService {
+
+ public BitgetTradeService(BitgetExchange exchange) {
+ super(exchange);
+ }
+
+
+ @Override
+ public Collection getOrder(OrderQueryParams... orderQueryParams) throws IOException {
+ Validate.validState(orderQueryParams.length == 1);
+ Validate.isInstanceOf(DefaultQueryOrderParam.class, orderQueryParams[0]);
+ DefaultQueryOrderParam params = (DefaultQueryOrderParam) orderQueryParams[0];
+
+ try {
+ BitgetOrderInfoDto orderStatus = bitgetOrderInfoDto(params.getOrderId());
+ return Collections.singletonList(BitgetAdapters.toOrder(orderStatus));
+ }
+ catch (BitgetException e) {
+ throw BitgetErrorAdapter.adapt(e);
+ }
+
+ }
+
+
+ @Override
+ public String placeMarketOrder(MarketOrder marketOrder) throws IOException {
+ try {
+ return createOrder(BitgetAdapters.toBitgetPlaceOrderDto(marketOrder)).getOrderId();
+ }
+ catch (BitgetException e) {
+ throw BitgetErrorAdapter.adapt(e);
+ }
+ }
+
+
+}
diff --git a/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetTradeServiceRaw.java b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetTradeServiceRaw.java
new file mode 100644
index 0000000000..870d26c649
--- /dev/null
+++ b/xchange-bitget/src/main/java/org/knowm/xchange/bitget/service/BitgetTradeServiceRaw.java
@@ -0,0 +1,30 @@
+package org.knowm.xchange.bitget.service;
+
+import java.io.IOException;
+import java.util.List;
+import org.knowm.xchange.bitget.BitgetExchange;
+import org.knowm.xchange.bitget.dto.trade.BitgetOrderInfoDto;
+import org.knowm.xchange.bitget.dto.trade.BitgetPlaceOrderDto;
+
+public class BitgetTradeServiceRaw extends BitgetBaseService {
+
+ public BitgetTradeServiceRaw(BitgetExchange exchange) {
+ super(exchange);
+ }
+
+
+ public BitgetOrderInfoDto bitgetOrderInfoDto(String orderId) throws IOException {
+ List results = bitgetAuthenticated.orderInfo(
+ apiKey, bitgetDigest, passphrase, exchange.getNonceFactory(), orderId).getData();
+ if (results.size() != 1) {
+ return null;
+ }
+ return results.get(0);
+ }
+
+
+ public BitgetOrderInfoDto createOrder(BitgetPlaceOrderDto bitgetPlaceOrderDto) throws IOException {
+ return bitgetAuthenticated.createOrder(apiKey, bitgetDigest, passphrase, exchange.getNonceFactory(), bitgetPlaceOrderDto).getData();
+ }
+
+}
diff --git a/xchange-bitget/src/main/resources/bitget.json b/xchange-bitget/src/main/resources/bitget.json
new file mode 100644
index 0000000000..2541d0ebf9
--- /dev/null
+++ b/xchange-bitget/src/main/resources/bitget.json
@@ -0,0 +1,5 @@
+{
+ "currency_pairs" : {},
+ "currencies" : {},
+ "share_rate_limits" : false
+}
diff --git a/xchange-bitget/src/test/java/org/knowm/xchange/bitget/BitgetExchangeIntegration.java b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/BitgetExchangeIntegration.java
new file mode 100644
index 0000000000..e22fc7207c
--- /dev/null
+++ b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/BitgetExchangeIntegration.java
@@ -0,0 +1,21 @@
+package org.knowm.xchange.bitget;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.knowm.xchange.currency.CurrencyPair;
+import org.knowm.xchange.dto.meta.InstrumentMetaData;
+import org.knowm.xchange.instrument.Instrument;
+
+class BitgetExchangeIntegration extends BitgetIntegrationTestParent {
+
+ @Test
+ void valid_metadata() {
+ assertThat(exchange.getExchangeMetaData()).isNotNull();
+ Map instruments = exchange.getExchangeMetaData().getInstruments();
+ assertThat(instruments).containsKey(CurrencyPair.BTC_USDT);
+ }
+
+
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/java/org/knowm/xchange/bitget/BitgetExchangeWiremock.java b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/BitgetExchangeWiremock.java
new file mode 100644
index 0000000000..446f4e6a93
--- /dev/null
+++ b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/BitgetExchangeWiremock.java
@@ -0,0 +1,53 @@
+package org.knowm.xchange.bitget;
+
+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 BitgetExchangeWiremock {
+
+ protected static BitgetExchange 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(BitgetExchange.class);
+ exSpec.setSslUri("http://localhost:" + wireMockServer.port());
+ exSpec.setApiKey("a");
+ exSpec.setSecretKey("b");
+ exSpec.setPassword("c");
+
+ if (IS_RECORDING) {
+ // use default url and record the requests
+ wireMockServer.startRecording(
+ new RecordSpecBuilder()
+ .forTarget("https://api.bitget.com")
+ .matchRequestBodyWithEqualToJson()
+ .extractTextBodiesOver(1L)
+ .chooseBodyMatchTypeAutomatically());
+ }
+
+ exchange = (BitgetExchange) ExchangeFactory.INSTANCE.createExchange(exSpec);
+ }
+
+ @AfterAll
+ public static void stop() {
+ if (IS_RECORDING) {
+ wireMockServer.stopRecording();
+ }
+ wireMockServer.stop();
+ }
+}
diff --git a/xchange-bitget/src/test/java/org/knowm/xchange/bitget/BitgetIntegrationTestParent.java b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/BitgetIntegrationTestParent.java
new file mode 100644
index 0000000000..52bee0a28a
--- /dev/null
+++ b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/BitgetIntegrationTestParent.java
@@ -0,0 +1,26 @@
+package org.knowm.xchange.bitget;
+
+import static org.assertj.core.api.Assumptions.assumeThat;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.knowm.xchange.ExchangeFactory;
+import org.knowm.xchange.dto.meta.ExchangeHealth;
+
+public class BitgetIntegrationTestParent {
+
+ protected static BitgetExchange exchange;
+
+ @BeforeAll
+ static void init() {
+ exchange = ExchangeFactory.INSTANCE.createExchange(BitgetExchange.class);
+ }
+
+ @BeforeEach
+ void exchange_online() {
+ // skip if offline
+ assumeThat(exchange.getMarketDataService().getExchangeHealth()).isEqualTo(ExchangeHealth.ONLINE);
+ }
+
+
+}
diff --git a/xchange-bitget/src/test/java/org/knowm/xchange/bitget/service/BitgetAccountServiceIntegration.java b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/service/BitgetAccountServiceIntegration.java
new file mode 100644
index 0000000000..637d669ef9
--- /dev/null
+++ b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/service/BitgetAccountServiceIntegration.java
@@ -0,0 +1,44 @@
+package org.knowm.xchange.bitget.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+
+import java.io.IOException;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.knowm.xchange.ExchangeFactory;
+import org.knowm.xchange.ExchangeSpecification;
+import org.knowm.xchange.bitget.BitgetExchange;
+import org.knowm.xchange.dto.account.AccountInfo;
+
+class BitgetAccountServiceIntegration {
+
+ static BitgetExchange exchange;
+
+
+ @BeforeAll
+ public static void credentialsPresent() {
+ // skip if there are no credentials
+ String apiKey = System.getProperty("apiKey");
+ String secretKey = System.getProperty("secretKey");
+ String passphrase = System.getProperty("passphrase");
+ assumeThat(apiKey).isNotEmpty();
+ assumeThat(secretKey).isNotEmpty();
+ assumeThat(passphrase).isNotEmpty();
+
+ ExchangeSpecification exSpec = new ExchangeSpecification(BitgetExchange.class);
+ exSpec.setApiKey(apiKey);
+ exSpec.setSecretKey(secretKey);
+ exSpec.setPassword(passphrase);
+ exchange = (BitgetExchange) ExchangeFactory.INSTANCE.createExchange(exSpec);
+ }
+
+
+ @Test
+ void valid_balances() throws IOException {
+ AccountInfo accountInfo = exchange.getAccountService().getAccountInfo();
+ assertThat(accountInfo.getWallet("spot").getBalances()).isNotEmpty();
+ }
+
+
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/java/org/knowm/xchange/bitget/service/BitgetDigestTest.java b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/service/BitgetDigestTest.java
new file mode 100644
index 0000000000..75fbd049f4
--- /dev/null
+++ b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/service/BitgetDigestTest.java
@@ -0,0 +1,40 @@
+package org.knowm.xchange.bitget.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import si.mazi.rescu.RestInvocation;
+
+@ExtendWith(MockitoExtension.class)
+class BitgetDigestTest {
+
+ @Mock
+ RestInvocation restInvocation;
+
+
+ @Test
+ void signature_no_query_params() {
+ BitgetDigest bitgetDigest = BitgetDigest.createInstance("secret");
+
+ when(restInvocation.getHttpMethod()).thenReturn("get");
+ when(restInvocation.getPath()).thenReturn("api/v2/spot/account/assets");
+ when(restInvocation.getQueryString()).thenReturn("");
+ when(restInvocation.getRequestBody()).thenReturn(null);
+ Map headers = new HashMap<>();
+ headers.put("ACCESS-TIMESTAMP", "1725040472073");
+ when(restInvocation.getHttpHeadersFromParams()).thenReturn(headers);
+
+ String actual = bitgetDigest.digestParams(restInvocation);
+ String expected = "p8vWylxL4JJCyy6B81n82ZEySiWChClCw99FTDocviA=";
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/java/org/knowm/xchange/bitget/service/BitgetMarketDataServiceIntegration.java b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/service/BitgetMarketDataServiceIntegration.java
new file mode 100644
index 0000000000..0a0d09f0bf
--- /dev/null
+++ b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/service/BitgetMarketDataServiceIntegration.java
@@ -0,0 +1,78 @@
+package org.knowm.xchange.bitget.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+import java.io.IOException;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.knowm.xchange.bitget.BitgetIntegrationTestParent;
+import org.knowm.xchange.currency.CurrencyPair;
+import org.knowm.xchange.dto.Order.OrderType;
+import org.knowm.xchange.dto.marketdata.OrderBook;
+import org.knowm.xchange.dto.marketdata.Ticker;
+import org.knowm.xchange.exceptions.InstrumentNotValidException;
+
+class BitgetMarketDataServiceIntegration extends BitgetIntegrationTestParent {
+
+ @Test
+ void valid_single_ticker() throws IOException {
+ Ticker ticker = exchange.getMarketDataService().getTicker(CurrencyPair.BTC_USDT);
+
+ assertThat(ticker.getInstrument()).isEqualTo(CurrencyPair.BTC_USDT);
+ assertThat(ticker.getLast()).isNotNull();
+
+ if (ticker.getBid().signum() > 0 && ticker.getAsk().signum() > 0) {
+ assertThat(ticker.getBid()).isLessThan(ticker.getAsk());
+ }
+
+ }
+
+
+ @Test
+ void check_exceptions() {
+ assertThatExceptionOfType(InstrumentNotValidException.class)
+ .isThrownBy(() ->
+ exchange.getMarketDataService().getTicker(new CurrencyPair("NONEXISTING/NONEXISTING")));
+
+ }
+
+
+ @Test
+ void valid_tickers() throws IOException {
+ List tickers = exchange.getMarketDataService().getTickers(null);
+ assertThat(tickers).isNotEmpty();
+
+ assertThat(tickers).allSatisfy(ticker -> {
+ assertThat(ticker.getInstrument()).isNotNull();
+ assertThat(ticker.getLast()).isNotNull();
+
+ if (ticker.getBid().signum() > 0 && ticker.getAsk().signum() > 0) {
+ assertThat(ticker.getBid()).isLessThan(ticker.getAsk());
+ }
+ });
+ }
+
+
+ @Test
+ void valid_orderbook() throws IOException {
+ OrderBook orderBook = exchange.getMarketDataService().getOrderBook(CurrencyPair.BTC_USDT);
+
+ assertThat(orderBook.getBids()).isNotEmpty();
+ assertThat(orderBook.getAsks()).isNotEmpty();
+
+ assertThat(orderBook.getAsks().get(0).getLimitPrice()).isGreaterThan(orderBook.getBids().get(0).getLimitPrice());
+
+ assertThat(orderBook.getBids()).allSatisfy(limitOrder -> {
+ assertThat(limitOrder.getInstrument()).isEqualTo(CurrencyPair.BTC_USDT);
+ assertThat(limitOrder.getType()).isEqualTo(OrderType.BID);
+ });
+
+ assertThat(orderBook.getAsks()).allSatisfy(limitOrder -> {
+ assertThat(limitOrder.getInstrument()).isEqualTo(CurrencyPair.BTC_USDT);
+ assertThat(limitOrder.getType()).isEqualTo(OrderType.ASK);
+ });
+ }
+
+
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/java/org/knowm/xchange/bitget/service/BitgetMarketDataServiceRawIntegration.java b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/service/BitgetMarketDataServiceRawIntegration.java
new file mode 100644
index 0000000000..ee24c10a9b
--- /dev/null
+++ b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/service/BitgetMarketDataServiceRawIntegration.java
@@ -0,0 +1,85 @@
+package org.knowm.xchange.bitget.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.knowm.xchange.currency.CurrencyPair.BTC_USDT;
+
+import java.io.IOException;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.knowm.xchange.bitget.BitgetIntegrationTestParent;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetCoinDto;
+import org.knowm.xchange.bitget.dto.marketdata.BitgetSymbolDto;
+import org.knowm.xchange.currency.Currency;
+
+class BitgetMarketDataServiceRawIntegration extends BitgetIntegrationTestParent {
+
+ BitgetMarketDataServiceRaw bitgetMarketDataServiceRaw = (BitgetMarketDataServiceRaw) exchange.getMarketDataService();
+
+ @Test
+ void valid_coins() throws IOException {
+ List coins = bitgetMarketDataServiceRaw.getBitgetCoinDtoList(null);
+
+ assertThat(coins).isNotEmpty();
+
+ // validate coins
+ assertThat(coins).allSatisfy(coin -> {
+ assertThat(coin.getCoinId()).isNotNull();
+ assertThat(coin.getCurrency()).isNotNull();
+
+ // validate each chain
+ assertThat(coin.getChains()).allSatisfy(chain -> {
+ assertThat(chain.getChain()).isNotNull();
+ });
+
+ });
+ }
+
+
+ @Test
+ void valid_coin() throws IOException {
+ List coins = bitgetMarketDataServiceRaw.getBitgetCoinDtoList(Currency.USDT);
+
+ assertThat(coins).hasSize(1);
+
+ assertThat(coins.get(0).getCurrency()).isEqualTo(Currency.USDT);
+ assertThat(coins.get(0).getCoinId()).isNotNull();
+ assertThat(coins.get(0).getChains()).allSatisfy(chain -> {
+ assertThat(chain.getChain()).isNotNull();
+ });
+
+ }
+
+
+ @Test
+ void valid_symbol() throws IOException {
+ List symbols = bitgetMarketDataServiceRaw.getBitgetSymbolDtos(BTC_USDT);
+
+ assertThat(symbols).hasSize(1);
+
+ BitgetSymbolDto symbol = symbols.get(0);
+ assertThat(symbol.getCurrencyPair()).isEqualTo(BTC_USDT);
+ assertThat(symbol.getPricePrecision()).isPositive();
+ assertThat(symbol.getQuantityPrecision()).isPositive();
+ assertThat(symbol.getQuotePrecision()).isPositive();
+
+ }
+
+
+ @Test
+ void valid_symbols() throws IOException {
+ List symbols = bitgetMarketDataServiceRaw.getBitgetSymbolDtos(null);
+
+ assertThat(symbols).isNotEmpty();
+
+ // validate symbols
+ assertThat(symbols).allSatisfy(symbol -> {
+ assertThat(symbol.getCurrencyPair()).isNotNull();
+ assertThat(symbol.getBase()).isNotNull();
+ assertThat(symbol.getQuote()).isNotNull();
+
+ });
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/java/org/knowm/xchange/bitget/service/BitgetTradeServiceTest.java b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/service/BitgetTradeServiceTest.java
new file mode 100644
index 0000000000..50356073bd
--- /dev/null
+++ b/xchange-bitget/src/test/java/org/knowm/xchange/bitget/service/BitgetTradeServiceTest.java
@@ -0,0 +1,95 @@
+package org.knowm.xchange.bitget.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Date;
+import org.junit.jupiter.api.Test;
+import org.knowm.xchange.bitget.BitgetExchangeWiremock;
+import org.knowm.xchange.currency.CurrencyPair;
+import org.knowm.xchange.dto.Order;
+import org.knowm.xchange.dto.Order.OrderStatus;
+import org.knowm.xchange.dto.Order.OrderType;
+import org.knowm.xchange.dto.trade.MarketOrder;
+import org.knowm.xchange.service.trade.TradeService;
+import org.knowm.xchange.service.trade.params.orders.DefaultQueryOrderParamInstrument;
+
+class BitgetTradeServiceTest extends BitgetExchangeWiremock {
+
+ TradeService tradeService = exchange.getTradeService();
+
+
+ @Test
+ void sell_order_details() throws IOException {
+ MarketOrder expected =
+ new MarketOrder.Builder(OrderType.ASK, new CurrencyPair("BGB/USDT"))
+ .id("1214193970718347264")
+ .userReference("t-valid-market-sell-order")
+ .timestamp(Date.from(Instant.parse("2024-09-01T17:38:41.929Z")))
+ .originalAmount(new BigDecimal("2"))
+ .orderStatus(OrderStatus.FILLED)
+ .cumulativeAmount(new BigDecimal("2"))
+ .averagePrice(new BigDecimal("0.9649"))
+ .fee(new BigDecimal("0.0019298"))
+ .build();
+
+ Collection orders = tradeService.getOrder("1214193970718347264");
+ assertThat(orders).hasSize(1);
+ assertThat(orders).first()
+ .usingComparatorForType(BigDecimal::compareTo, BigDecimal.class)
+ .usingRecursiveComparison().isEqualTo(expected);
+ }
+
+
+ @Test
+ void buy_order_details() throws IOException {
+ MarketOrder expected =
+ new MarketOrder.Builder(OrderType.BID, new CurrencyPair("BGB/USDT"))
+ .id("1214189703404097539")
+ .userReference("t-valid-market-buy-order")
+ .timestamp(Date.from(Instant.parse("2024-09-01T17:21:44.522Z")))
+ .originalAmount(new BigDecimal("2"))
+ .orderStatus(OrderStatus.FILLED)
+ .cumulativeAmount(new BigDecimal("1.9999925400000000"))
+ .averagePrice(new BigDecimal("0.9659000000000000"))
+ .fee(new BigDecimal("0.0020706"))
+ .build();
+
+ Collection orders = tradeService.getOrder(new DefaultQueryOrderParamInstrument(null, "1214189703404097539"));
+ assertThat(orders).hasSize(1);
+ assertThat(orders).first()
+ .usingComparatorForType(BigDecimal::compareTo, BigDecimal.class)
+ .usingRecursiveComparison().isEqualTo(expected);
+ }
+
+
+ @Test
+ void place_market_buy_order() throws IOException {
+ MarketOrder marketOrder =
+ new MarketOrder.Builder(OrderType.BID, new CurrencyPair("BGB/USDT"))
+ .userReference("t-valid-market-buy-order")
+ .originalAmount(BigDecimal.valueOf(2))
+ .build();
+
+ String actualResponse = exchange.getTradeService().placeMarketOrder(marketOrder);
+ assertThat(actualResponse).isEqualTo("1214189703404097539");
+ }
+
+
+ @Test
+ void place_market_sell_order() throws IOException {
+ MarketOrder marketOrder =
+ new MarketOrder.Builder(OrderType.ASK, new CurrencyPair("BGB/USDT"))
+ .userReference("t-valid-market-sell-order")
+ .originalAmount(BigDecimal.valueOf(2))
+ .build();
+
+ String actualResponse = exchange.getTradeService().placeMarketOrder(marketOrder);
+ assertThat(actualResponse).isEqualTo("1214193970718347264");
+ }
+
+
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/resources/__files/api_v2_spot_public_symbols.json b/xchange-bitget/src/test/resources/__files/api_v2_spot_public_symbols.json
new file mode 100644
index 0000000000..7c262633a4
--- /dev/null
+++ b/xchange-bitget/src/test/resources/__files/api_v2_spot_public_symbols.json
@@ -0,0 +1,58 @@
+{
+ "code": "00000",
+ "msg": "success",
+ "requestTime": 1725192127389,
+ "data": [
+ {
+ "symbol": "BTCUSDT",
+ "baseCoin": "BTC",
+ "quoteCoin": "USDT",
+ "minTradeAmount": "0",
+ "maxTradeAmount": "10000000000",
+ "takerFeeRate": "0.002",
+ "makerFeeRate": "0.002",
+ "pricePrecision": "2",
+ "quantityPrecision": "6",
+ "quotePrecision": "6",
+ "status": "online",
+ "minTradeUSDT": "1",
+ "buyLimitPriceRatio": "0.05",
+ "sellLimitPriceRatio": "0.05",
+ "areaSymbol": "no"
+ },
+ {
+ "symbol": "ETHUSDT",
+ "baseCoin": "ETH",
+ "quoteCoin": "USDT",
+ "minTradeAmount": "0",
+ "maxTradeAmount": "10000000000",
+ "takerFeeRate": "0.002",
+ "makerFeeRate": "0.002",
+ "pricePrecision": "2",
+ "quantityPrecision": "4",
+ "quotePrecision": "6",
+ "status": "online",
+ "minTradeUSDT": "1",
+ "buyLimitPriceRatio": "0.05",
+ "sellLimitPriceRatio": "0.05",
+ "areaSymbol": "no"
+ },
+ {
+ "symbol": "BGBUSDT",
+ "baseCoin": "BGB",
+ "quoteCoin": "USDT",
+ "minTradeAmount": "0",
+ "maxTradeAmount": "10000000000",
+ "takerFeeRate": "0.001",
+ "makerFeeRate": "0.001",
+ "pricePrecision": "4",
+ "quantityPrecision": "4",
+ "quotePrecision": "6",
+ "status": "online",
+ "minTradeUSDT": "1",
+ "buyLimitPriceRatio": "0.05",
+ "sellLimitPriceRatio": "0.05",
+ "areaSymbol": "no"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/resources/__files/api_v2_spot_trade_orderinfo-t-valid-market-buy-order.json b/xchange-bitget/src/test/resources/__files/api_v2_spot_trade_orderinfo-t-valid-market-buy-order.json
new file mode 100644
index 0000000000..6c03d91113
--- /dev/null
+++ b/xchange-bitget/src/test/resources/__files/api_v2_spot_trade_orderinfo-t-valid-market-buy-order.json
@@ -0,0 +1,30 @@
+{
+ "code": "00000",
+ "msg": "success",
+ "requestTime": 1725211448919,
+ "data": [
+ {
+ "userId": "1548914322",
+ "symbol": "BGBUSDT",
+ "orderId": "1214189703404097539",
+ "clientOid": "t-valid-market-buy-order",
+ "price": "0",
+ "size": "2.0000000000000000",
+ "orderType": "market",
+ "side": "buy",
+ "status": "filled",
+ "priceAvg": "0.9659000000000000",
+ "baseVolume": "2.0706000000000000",
+ "quoteVolume": "1.9999925400000000",
+ "enterPointSource": "API",
+ "feeDetail": "{\"BGB\":{\"deduction\":false,\"feeCoinCode\":\"BGB\",\"totalDeductionFee\":0,\"totalFee\":-0.0020706000000000},\"newFees\":{\"c\":0,\"d\":0,\"deduction\":false,\"r\":-0.0020706,\"t\":-0.0020706,\"totalDeductionFee\":0}}",
+ "orderSource": "market",
+ "tpslType": "normal",
+ "triggerPrice": null,
+ "quoteCoin": "USDT",
+ "baseCoin": "BGB",
+ "cTime": "1725211304522",
+ "uTime": "1725211304609"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/resources/__files/api_v2_spot_trade_orderinfo-t-valid-market-sell-order.json b/xchange-bitget/src/test/resources/__files/api_v2_spot_trade_orderinfo-t-valid-market-sell-order.json
new file mode 100644
index 0000000000..256eae5349
--- /dev/null
+++ b/xchange-bitget/src/test/resources/__files/api_v2_spot_trade_orderinfo-t-valid-market-sell-order.json
@@ -0,0 +1,30 @@
+{
+ "code": "00000",
+ "msg": "success",
+ "requestTime": 1725212468324,
+ "data": [
+ {
+ "userId": "1548914322",
+ "symbol": "BGBUSDT",
+ "orderId": "1214193970718347264",
+ "clientOid": "t-valid-market-sell-order",
+ "price": "0",
+ "size": "2.0000000000000000",
+ "orderType": "market",
+ "side": "sell",
+ "status": "filled",
+ "priceAvg": "0.9649000000000000",
+ "baseVolume": "2.0000000000000000",
+ "quoteVolume": "1.9298000000000000",
+ "enterPointSource": "API",
+ "feeDetail": "{\"newFees\":{\"c\":0,\"d\":0,\"deduction\":false,\"r\":-0.0019298,\"t\":-0.0019298,\"totalDeductionFee\":0},\"USDT\":{\"deduction\":false,\"feeCoinCode\":\"USDT\",\"totalDeductionFee\":0,\"totalFee\":-0.0019298000000000}}",
+ "orderSource": "market",
+ "tpslType": "normal",
+ "triggerPrice": null,
+ "quoteCoin": "USDT",
+ "baseCoin": "BGB",
+ "cTime": "1725212321929",
+ "uTime": "1725212322028"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/resources/__files/api_v2_spot_trade_place-buy-order.json b/xchange-bitget/src/test/resources/__files/api_v2_spot_trade_place-buy-order.json
new file mode 100644
index 0000000000..7a3a3b197b
--- /dev/null
+++ b/xchange-bitget/src/test/resources/__files/api_v2_spot_trade_place-buy-order.json
@@ -0,0 +1,9 @@
+{
+ "code": "00000",
+ "msg": "success",
+ "requestTime": 1725211304517,
+ "data": {
+ "orderId": "1214189703404097539",
+ "clientOid": "t-valid-market-buy-order"
+ }
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/resources/__files/api_v2_spot_trade_place-sell-order.json b/xchange-bitget/src/test/resources/__files/api_v2_spot_trade_place-sell-order.json
new file mode 100644
index 0000000000..0a7cf32d17
--- /dev/null
+++ b/xchange-bitget/src/test/resources/__files/api_v2_spot_trade_place-sell-order.json
@@ -0,0 +1,9 @@
+{
+ "code": "00000",
+ "msg": "success",
+ "requestTime": 1725212321924,
+ "data": {
+ "orderId": "1214193970718347264",
+ "clientOid": "t-valid-market-sell-order"
+ }
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/resources/logback.xml b/xchange-bitget/src/test/resources/logback.xml
new file mode 100644
index 0000000000..c823ac40a9
--- /dev/null
+++ b/xchange-bitget/src/test/resources/logback.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%contextName] [%thread] %-5level %logger{36} - %msg %xEx%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/xchange-bitget/src/test/resources/mappings/api_v2_spot_public_symbols.json b/xchange-bitget/src/test/resources/mappings/api_v2_spot_public_symbols.json
new file mode 100644
index 0000000000..428ffbcb55
--- /dev/null
+++ b/xchange-bitget/src/test/resources/mappings/api_v2_spot_public_symbols.json
@@ -0,0 +1,15 @@
+{
+ "id" : "1a8a3149-0eb1-429f-a06a-4a2242e18d42",
+ "name" : "api_v2_spot_public_symbols",
+ "request" : {
+ "url" : "/api/v2/spot/public/symbols",
+ "method" : "GET"
+ },
+ "response" : {
+ "status" : 200,
+ "bodyFileName" : "api_v2_spot_public_symbols.json"
+ },
+ "uuid" : "1a8a3149-0eb1-429f-a06a-4a2242e18d42",
+ "persistent" : true,
+ "insertionIndex" : 2
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/resources/mappings/api_v2_spot_trade_orderinfo-t-valid-market-buy-order.json b/xchange-bitget/src/test/resources/mappings/api_v2_spot_trade_orderinfo-t-valid-market-buy-order.json
new file mode 100644
index 0000000000..f7d02b466a
--- /dev/null
+++ b/xchange-bitget/src/test/resources/mappings/api_v2_spot_trade_orderinfo-t-valid-market-buy-order.json
@@ -0,0 +1,15 @@
+{
+ "id" : "aebaa168-bbc9-4819-98f5-c2336d01ca43",
+ "name" : "api_v2_spot_trade_orderinfo",
+ "request" : {
+ "url" : "/api/v2/spot/trade/orderInfo?orderId=1214189703404097539",
+ "method" : "GET"
+ },
+ "response" : {
+ "status" : 200,
+ "bodyFileName" : "api_v2_spot_trade_orderinfo-t-valid-market-buy-order.json"
+ },
+ "uuid" : "aebaa168-bbc9-4819-98f5-c2336d01ca43",
+ "persistent" : true,
+ "insertionIndex" : 1
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/resources/mappings/api_v2_spot_trade_orderinfo-t-valid-market-sell-order.json b/xchange-bitget/src/test/resources/mappings/api_v2_spot_trade_orderinfo-t-valid-market-sell-order.json
new file mode 100644
index 0000000000..e9789b0feb
--- /dev/null
+++ b/xchange-bitget/src/test/resources/mappings/api_v2_spot_trade_orderinfo-t-valid-market-sell-order.json
@@ -0,0 +1,15 @@
+{
+ "id" : "aebaa168-bbc9-4819-98f5-c2336d01ca43",
+ "name" : "api_v2_spot_trade_orderinfo",
+ "request" : {
+ "url" : "/api/v2/spot/trade/orderInfo?orderId=1214193970718347264",
+ "method" : "GET"
+ },
+ "response" : {
+ "status" : 200,
+ "bodyFileName" : "api_v2_spot_trade_orderinfo-t-valid-market-sell-order.json"
+ },
+ "uuid" : "aebaa168-bbc9-4819-98f5-c2336d01ca43",
+ "persistent" : true,
+ "insertionIndex" : 1
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/resources/mappings/api_v2_spot_trade_place-buy-order.json b/xchange-bitget/src/test/resources/mappings/api_v2_spot_trade_place-buy-order.json
new file mode 100644
index 0000000000..a6b55d3d18
--- /dev/null
+++ b/xchange-bitget/src/test/resources/mappings/api_v2_spot_trade_place-buy-order.json
@@ -0,0 +1,20 @@
+{
+ "id" : "bf637b08-636e-4c43-af21-90ccb86586f4",
+ "name" : "api_v2_spot_trade_place-order",
+ "request" : {
+ "url" : "/api/v2/spot/trade/place-order",
+ "method" : "POST",
+ "bodyPatterns" : [ {
+ "equalToJson" : "{\"symbol\":\"BGBUSDT\",\"side\":\"buy\",\"orderType\":\"market\",\"size\":2,\"clientOid\":\"t-valid-market-buy-order\"}",
+ "ignoreArrayOrder" : true,
+ "ignoreExtraElements" : true
+ } ]
+ },
+ "response" : {
+ "status" : 200,
+ "bodyFileName" : "api_v2_spot_trade_place-buy-order.json"
+ },
+ "uuid" : "bf637b08-636e-4c43-af21-90ccb86586f4",
+ "persistent" : true,
+ "insertionIndex" : 3
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/resources/mappings/api_v2_spot_trade_place-sell-order.json b/xchange-bitget/src/test/resources/mappings/api_v2_spot_trade_place-sell-order.json
new file mode 100644
index 0000000000..a951b592db
--- /dev/null
+++ b/xchange-bitget/src/test/resources/mappings/api_v2_spot_trade_place-sell-order.json
@@ -0,0 +1,20 @@
+{
+ "id" : "c04a9a18-7a57-4f15-86e4-b32cf57cea4e",
+ "name" : "api_v2_spot_trade_place-order",
+ "request" : {
+ "url" : "/api/v2/spot/trade/place-order",
+ "method" : "POST",
+ "bodyPatterns" : [ {
+ "equalToJson" : "{\"symbol\":\"BGBUSDT\",\"side\":\"sell\",\"orderType\":\"market\",\"size\":2,\"clientOid\":\"t-valid-market-sell-order\"}",
+ "ignoreArrayOrder" : true,
+ "ignoreExtraElements" : true
+ } ]
+ },
+ "response" : {
+ "status" : 200,
+ "bodyFileName" : "api_v2_spot_trade_place-sell-order.json"
+ },
+ "uuid" : "c04a9a18-7a57-4f15-86e4-b32cf57cea4e",
+ "persistent" : true,
+ "insertionIndex" : 5
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/resources/rest/common.http b/xchange-bitget/src/test/resources/rest/common.http
new file mode 100644
index 0000000000..41acdbe899
--- /dev/null
+++ b/xchange-bitget/src/test/resources/rest/common.http
@@ -0,0 +1,3 @@
+### Get Server Time
+GET {{api_host}}/api/v2/public/time
+
diff --git a/xchange-bitget/src/test/resources/rest/sign.js b/xchange-bitget/src/test/resources/rest/sign.js
new file mode 100644
index 0000000000..2daee39999
--- /dev/null
+++ b/xchange-bitget/src/test/resources/rest/sign.js
@@ -0,0 +1,22 @@
+export function gen_sign(method, request) {
+ const pattern = RegExp("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?");
+ const url = request.url.tryGetSubstituted();
+ const matches = url.match(pattern);
+
+ const path = matches[5];
+
+ let query = matches[7] || "";
+ if (query !== "") {
+ query = "?" + query;
+ }
+
+ const body = request.body.tryGetSubstituted() || "";
+ const timestamp = Math.floor(Date.now()).toFixed();
+
+ const payloadToSign = `${timestamp}${method}${path}${query}${body}`;
+ const apiSecret = request.environment.get("api_secret");
+ const sign = crypto.hmac.sha256().withTextSecret(apiSecret).updateWithText(payloadToSign).digest().toBase64();
+
+ request.variables.set("timestamp", timestamp);
+ request.variables.set("sign", sign);
+}
\ No newline at end of file
diff --git a/xchange-bitget/src/test/resources/rest/spot.http b/xchange-bitget/src/test/resources/rest/spot.http
new file mode 100644
index 0000000000..1fb8850c39
--- /dev/null
+++ b/xchange-bitget/src/test/resources/rest/spot.http
@@ -0,0 +1,94 @@
+### Get Coin Info
+GET {{api_host}}/api/v2/spot/public/coins?coin=usdt
+
+
+### Get OrderBook Depth
+GET {{api_host}}/api/v2/spot/market/orderbook?symbol=btcusdt
+
+
+### Get Symbol Info
+GET {{api_host}}/api/v2/spot/public/symbols
+
+
+### Get Ticker Information
+GET {{api_host}}/api/v2/spot/market/tickers
+
+
+### Get Account Information
+< {%
+ import {gen_sign} from 'sign.js'
+ gen_sign("GET", request);
+%}
+GET {{api_host}}/api/v2/spot/account/info
+ACCESS-KEY: {{api_key}}
+ACCESS-SIGN: {{sign}}
+ACCESS-TIMESTAMP: {{timestamp}}
+ACCESS-PASSPHRASE: {{api_passphrase}}
+
+
+### Get Account assets
+< {%
+ import {gen_sign} from 'sign.js'
+ gen_sign("GET", request);
+%}
+GET {{api_host}}/api/v2/spot/account/assets
+ACCESS-KEY: {{api_key}}
+ACCESS-SIGN: {{sign}}
+ACCESS-TIMESTAMP: {{timestamp}}
+ACCESS-PASSPHRASE: {{api_passphrase}}
+
+
+### Get Order Info
+< {%
+ import {gen_sign} from 'sign.js'
+ gen_sign("GET", request);
+%}
+GET {{api_host}}/api/v2/spot/trade/orderInfo?orderId=1213530920130613257
+ACCESS-KEY: {{api_key}}
+ACCESS-SIGN: {{sign}}
+ACCESS-TIMESTAMP: {{timestamp}}
+ACCESS-PASSPHRASE: {{api_passphrase}}
+
+
+### Place Market Buy Order
+< {%
+ import {gen_sign} from 'sign.js'
+ gen_sign("POST", request);
+%}
+POST {{api_host}}/api/v2/spot/trade/place-order
+ACCESS-KEY: {{api_key}}
+ACCESS-SIGN: {{sign}}
+ACCESS-TIMESTAMP: {{timestamp}}
+ACCESS-PASSPHRASE: {{api_passphrase}}
+Content-Type: application/json
+
+{
+ "symbol": "BGBUSDT",
+ "side": "buy",
+ "orderType": "market",
+ "size": "2",
+ "clientOid": "{{$random.uuid}}"
+}
+
+
+### Place Market Sell Order
+< {%
+ import {gen_sign} from 'sign.js'
+ gen_sign("POST", request);
+%}
+POST {{api_host}}/api/v2/spot/trade/place-order
+ACCESS-KEY: {{api_key}}
+ACCESS-SIGN: {{sign}}
+ACCESS-TIMESTAMP: {{timestamp}}
+ACCESS-PASSPHRASE: {{api_passphrase}}
+Content-Type: application/json
+
+{
+ "symbol": "BGBUSDT",
+ "side": "sell",
+ "orderType": "market",
+ "size": "3",
+ "clientOid": "{{$random.uuid}}"
+}
+
+