diff --git a/README.md b/README.md index a70a9969f..544012538 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ except for the trading strategies - you'll need to write those yourself! A simpl is included to get you started with the Trading API - see [Ta4j](https://github.com/ta4j/ta4j) for more ideas. Exchange Adapters for using [Bitstamp](https://www.bitstamp.net), [Bitfinex](https://www.bitfinex.com), -[Kraken](https://www.kraken.com), [Gemini](https://gemini.com/), and [Coinbase Pro](https://pro.coinbase.com/) are included. +[Kraken](https://www.kraken.com), and [Gemini](https://gemini.com/) are included. Feel free to improve these or contribute new adapters to the project; that would be [shiny!](https://en.wikipedia.org/wiki/Firefly_(TV_series)) diff --git a/bxbot-exchanges/src/integration-test/java/com/gazbert/bxbot/exchanges/CoinbaseProIT.java b/bxbot-exchanges/src/integration-test/java/com/gazbert/bxbot/exchanges/CoinbaseProIT.java deleted file mode 100644 index deca79c71..000000000 --- a/bxbot-exchanges/src/integration-test/java/com/gazbert/bxbot/exchanges/CoinbaseProIT.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2021 Gareth Jon Lynch - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.gazbert.bxbot.exchanges; - -import static org.easymock.EasyMock.createMock; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.replay; -import static org.easymock.EasyMock.verify; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - -import com.gazbert.bxbot.exchange.api.AuthenticationConfig; -import com.gazbert.bxbot.exchange.api.ExchangeAdapter; -import com.gazbert.bxbot.exchange.api.ExchangeConfig; -import com.gazbert.bxbot.exchange.api.NetworkConfig; -import com.gazbert.bxbot.exchange.api.OtherConfig; -import com.gazbert.bxbot.trading.api.BalanceInfo; -import com.gazbert.bxbot.trading.api.MarketOrderBook; -import com.gazbert.bxbot.trading.api.Ticker; -import java.math.BigDecimal; -import java.util.Arrays; -import java.util.List; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; - -/** - * Basic integration testing with Coinbase Pro exchange. - * - * @author gazbert - */ -public class CoinbaseProIT { - - private static final String MARKET_ID = "BTC-GBP"; - private static final BigDecimal BUY_ORDER_PRICE = new BigDecimal("450.176"); - private static final BigDecimal BUY_ORDER_QUANTITY = new BigDecimal("0.01"); - - private static final String PASSPHRASE = "lePassPhrase"; - private static final String KEY = "key123"; - private static final String SECRET = "notGonnaTellYa"; - private static final List nonFatalNetworkErrorCodes = Arrays.asList(502, 503, 504); - private static final List nonFatalNetworkErrorMessages = - Arrays.asList( - "Connection refused", - "Connection reset", - "Remote host closed connection during handshake"); - - private ExchangeConfig exchangeConfig; - private AuthenticationConfig authenticationConfig; - private NetworkConfig networkConfig; - private OtherConfig otherConfig; - - /** Create some exchange config - the TradingEngine would normally do this. */ - @Before - public void setupForEachTest() { - authenticationConfig = createMock(AuthenticationConfig.class); - expect(authenticationConfig.getItem("passphrase")).andReturn(PASSPHRASE); - expect(authenticationConfig.getItem("key")).andReturn(KEY); - expect(authenticationConfig.getItem("secret")).andReturn(SECRET); - - networkConfig = createMock(NetworkConfig.class); - expect(networkConfig.getConnectionTimeout()).andReturn(30); - expect(networkConfig.getNonFatalErrorCodes()).andReturn(nonFatalNetworkErrorCodes); - expect(networkConfig.getNonFatalErrorMessages()).andReturn(nonFatalNetworkErrorMessages); - - otherConfig = createMock(OtherConfig.class); - expect(otherConfig.getItem("buy-fee")).andReturn("0.25"); - expect(otherConfig.getItem("sell-fee")).andReturn("0.25"); - expect(otherConfig.getItem("time-server-bias")).andReturn("1"); - - exchangeConfig = createMock(ExchangeConfig.class); - expect(exchangeConfig.getAuthenticationConfig()).andReturn(authenticationConfig); - expect(exchangeConfig.getNetworkConfig()).andReturn(networkConfig); - expect(exchangeConfig.getOtherConfig()).andReturn(otherConfig); - } - - @Test - public void testPublicApiCalls() throws Exception { - replay(authenticationConfig, networkConfig, otherConfig, exchangeConfig); - - final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter(); - exchangeAdapter.init(exchangeConfig); - - assertNotNull(exchangeAdapter.getLatestMarketPrice(MARKET_ID)); - - final MarketOrderBook orderBook = exchangeAdapter.getMarketOrders(MARKET_ID); - assertFalse(orderBook.getBuyOrders().isEmpty()); - assertFalse(orderBook.getSellOrders().isEmpty()); - - final Ticker ticker = exchangeAdapter.getTicker(MARKET_ID); - assertNotNull(ticker.getLast()); - assertNotNull(ticker.getAsk()); - assertNotNull(ticker.getBid()); - assertNotNull(ticker.getHigh()); - assertNotNull(ticker.getLow()); - assertNotNull(ticker.getOpen()); - assertNotNull(ticker.getVolume()); - assertNull(ticker.getVwap()); // not provided by Coinbase Pro - assertNotNull(ticker.getTimestamp()); - - verify(authenticationConfig, networkConfig, otherConfig, exchangeConfig); - } - - /* - * You'll need to change the PASSPHRASE, KEY, SECRET, constants to real-world values. - */ - @Ignore("Disabled. Integration testing authenticated API calls requires your secret credentials!") - @Test - public void testAuthenticatedApiCalls() throws Exception { - replay(authenticationConfig, networkConfig, otherConfig, exchangeConfig); - - final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter(); - exchangeAdapter.init(exchangeConfig); - - final BalanceInfo balanceInfo = exchangeAdapter.getBalanceInfo(); - assertNotNull(balanceInfo.getBalancesAvailable().get("BTC")); - - // Careful here: make sure the SELL_ORDER_PRICE is sensible! - // final String orderId = exchangeAdapter.createOrder(MARKET_ID, OrderType.BUY, - // BUY_ORDER_QUANTITY, BUY_ORDER_PRICE); - // final List openOrders = exchangeAdapter.getYourOpenOrders(MARKET_ID); - // assertTrue(openOrders.stream().anyMatch(o -> o.getId().equals(orderId))); - // assertTrue(exchangeAdapter.cancelOrder(orderId, MARKET_ID)); - - verify(authenticationConfig, networkConfig, otherConfig, exchangeConfig); - } -} diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/CoinbaseProExchangeAdapter.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/CoinbaseProExchangeAdapter.java deleted file mode 100644 index 89fd69a2d..000000000 --- a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/CoinbaseProExchangeAdapter.java +++ /dev/null @@ -1,965 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 Gareth Jon Lynch - * Copyright (c) 2019 David Huertas - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.gazbert.bxbot.exchanges; - -import com.gazbert.bxbot.exchange.api.AuthenticationConfig; -import com.gazbert.bxbot.exchange.api.ExchangeAdapter; -import com.gazbert.bxbot.exchange.api.ExchangeConfig; -import com.gazbert.bxbot.exchange.api.OtherConfig; -import com.gazbert.bxbot.exchanges.trading.api.impl.BalanceInfoImpl; -import com.gazbert.bxbot.exchanges.trading.api.impl.MarketOrderBookImpl; -import com.gazbert.bxbot.exchanges.trading.api.impl.MarketOrderImpl; -import com.gazbert.bxbot.exchanges.trading.api.impl.OpenOrderImpl; -import com.gazbert.bxbot.exchanges.trading.api.impl.TickerImpl; -import com.gazbert.bxbot.trading.api.BalanceInfo; -import com.gazbert.bxbot.trading.api.ExchangeNetworkException; -import com.gazbert.bxbot.trading.api.MarketOrder; -import com.gazbert.bxbot.trading.api.MarketOrderBook; -import com.gazbert.bxbot.trading.api.OpenOrder; -import com.gazbert.bxbot.trading.api.OrderType; -import com.gazbert.bxbot.trading.api.Ticker; -import com.gazbert.bxbot.trading.api.TradingApi; -import com.gazbert.bxbot.trading.api.TradingApiException; -import com.google.common.base.MoreObjects; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.annotations.SerializedName; -import jakarta.xml.bind.DatatypeConverter; -import java.io.Serial; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.text.DecimalFormat; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import lombok.extern.log4j.Log4j2; - -/** - * Exchange Adapter for integrating with the CoinbasePro exchange. The CoinbasePro API is documented - * here. - * - *

DISCLAIMER: This Exchange Adapter is provided as-is; it might have bugs in it and you - * could lose money. Despite running live on COINBASE PRO, it has only been unit tested up until the - * point of calling the {@link #sendPublicRequestToExchange(String, Map)} and {@link - * #sendAuthenticatedRequestToExchange(String, String, Map)} methods. Use it at our own risk! - * - * - *

This adapter only supports the CoinbasePro REST - * API. The design of the API and documentation is excellent. - * - *

The adapter currently only supports Limit Orders. It was originally - * developed and tested for BTC-GBP market, but it should work for BTC-USD or BTC-EUR. - * - *

Exchange fees are loaded from the exchange.yaml file on startup; they are not fetched from the - * exchange at runtime as the CoinbasePro REST API does not support this. The fees are used across - * all markets. Make sure you keep an eye on the exchange fees and update the config accordingly. - * - *

NOTE: CoinbasePro requires all price values to be limited to 2 decimal places when creating - * orders. This adapter truncates any prices with more than 2 decimal places and rounds using {@link - * java.math.RoundingMode#HALF_EVEN}, E.g. 250.176 would be sent to the exchange as 250.18. - * - *

The Exchange Adapter is not thread safe. It expects to be called using a single - * thread in order to preserve trade execution order. The {@link URLConnection} achieves this by - * blocking/waiting on the input stream (response) for each API call. - * - *

The {@link TradingApi} calls will throw a {@link ExchangeNetworkException} if a network error - * occurs trying to connect to the exchange. A {@link TradingApiException} is thrown for - * all other failures. - * - * @author davidhuertas - * @since 1.0 - */ -@Log4j2 -public final class CoinbaseProExchangeAdapter extends AbstractExchangeAdapter - implements ExchangeAdapter { - private static final String PUBLIC_API_BASE_URL = "https://api.pro.coinbase.com/"; - private static final String AUTHENTICATED_API_URL = PUBLIC_API_BASE_URL; - - private static final String UNEXPECTED_ERROR_MSG = - "Unexpected error has occurred in COINBASE PRO Exchange Adapter. "; - private static final String UNEXPECTED_IO_ERROR_MSG = - "Failed to connect to Exchange due to unexpected IO error."; - - private static final String PRODUCTS = "products/"; - private static final String PRICE = "price"; - - private static final String PASSPHRASE_PROPERTY_NAME = "passphrase"; - private static final String KEY_PROPERTY_NAME = "key"; - private static final String SECRET_PROPERTY_NAME = "secret"; - - private static final String BUY_FEE_PROPERTY_NAME = "buy-fee"; - private static final String SELL_FEE_PROPERTY_NAME = "sell-fee"; - private static final String SERVER_TIME_BIAS_PROPERTY_NAME = "time-server-bias"; - - private BigDecimal buyFeePercentage; - private BigDecimal sellFeePercentage; - private Long timeServerBias; - - private String passphrase = ""; - private String key = ""; - private String secret = ""; - - private Mac mac; - private boolean initializedMacAuthentication = false; - - private Gson gson; - - /** Constructs the Exchange Adapter. */ - public CoinbaseProExchangeAdapter() { - // No extra init. - } - - @Override - public void init(ExchangeConfig config) { - log.info("About to initialise COINBASE PRO ExchangeConfig: " + config); - setAuthenticationConfig(config); - setNetworkConfig(config); - setOtherConfig(config); - - initSecureMessageLayer(); - initGson(); - } - - // -------------------------------------------------------------------------- - // COINBASE PRO API Calls adapted to the Trading API. - // See https://docs.pro.coinbase.com/#api - // -------------------------------------------------------------------------- - - @Override - public String createOrder( - String marketId, OrderType orderType, BigDecimal quantity, BigDecimal price) - throws TradingApiException, ExchangeNetworkException { - try { - /* - * Build Limit Order: https://docs.pro.coinbase.com/#place-a-new-order - * - * stp param optional - (Self-trade prevention flag) defaults to 'dc' Decrease & - * Cancel - * post_only param optional - defaults to 'false' - * time_in_force param optional - defaults to 'GTC' Good til Cancel - * client_oid param is optional - thia adapter does not use it. - */ - final Map params = createRequestParamMap(); - - if (orderType == OrderType.BUY) { - params.put("side", "buy"); - } else if (orderType == OrderType.SELL) { - params.put("side", "sell"); - } else { - final String errorMsg = - "Invalid order type: " - + orderType - + " - Can only be " - + OrderType.BUY.getStringValue() - + " or " - + OrderType.SELL.getStringValue(); - log.error(errorMsg); - throw new IllegalArgumentException(errorMsg); - } - - params.put("product_id", marketId); - - // note we need to limit price to 2 decimal places else exchange will barf - params.put(PRICE, new DecimalFormat("#.##", getDecimalFormatSymbols()).format(price)); - - // note we need to limit size to 8 decimal places else exchange will barf - params.put( - "size", new DecimalFormat("#.########", getDecimalFormatSymbols()).format(quantity)); - - final ExchangeHttpResponse response = - sendAuthenticatedRequestToExchange("POST", "orders", params); - log.debug("Create Order response: " + response); - - if (response.getStatusCode() == HttpURLConnection.HTTP_OK) { - final CoinbaseProOrder createOrderResponse = - gson.fromJson(response.getPayload(), CoinbaseProOrder.class); - if (createOrderResponse != null - && (createOrderResponse.id != null && !createOrderResponse.id.isEmpty())) { - return createOrderResponse.id; - } else { - final String errorMsg = "Failed to place order on exchange. Error response: " + response; - log.error(errorMsg); - throw new TradingApiException(errorMsg); - } - } else { - final String errorMsg = "Failed to create order on exchange. Details: " + response; - log.error(errorMsg); - throw new TradingApiException(errorMsg); - } - - } catch (ExchangeNetworkException | TradingApiException e) { - throw e; - - } catch (Exception e) { - log.error(UNEXPECTED_ERROR_MSG, e); - throw new TradingApiException(UNEXPECTED_ERROR_MSG, e); - } - } - - /* - * marketId is not needed for cancelling orders on this exchange. - */ - @Override - public boolean cancelOrder(String orderId, String marketIdNotNeeded) - throws TradingApiException, ExchangeNetworkException { - try { - final ExchangeHttpResponse response = - sendAuthenticatedRequestToExchange("DELETE", "orders/" + orderId, null); - - log.debug("Cancel Order response: " + response); - - if (response.getStatusCode() == HttpURLConnection.HTTP_OK) { - // 1 Nov 2017 - COINBASE PRO API no longer returns cancelled orderId in array payload; - // it returns [null]... - return true; - } else { - final String errorMsg = "Failed to cancel order on exchange. Details: " + response; - log.error(errorMsg); - return false; - } - - } catch (ExchangeNetworkException | TradingApiException e) { - throw e; - - } catch (Exception e) { - log.error(UNEXPECTED_ERROR_MSG, e); - throw new TradingApiException(UNEXPECTED_ERROR_MSG, e); - } - } - - @Override - public List getYourOpenOrders(String marketId) - throws TradingApiException, ExchangeNetworkException { - try { - // we use default request no-param call - only open or un-settled orders are returned. - // As soon as an order is no longer open and settled, it will no longer appear in the default - // request. - final ExchangeHttpResponse response = - sendAuthenticatedRequestToExchange("GET", "orders", null); - - log.debug("Open Orders response: " + response); - - if (response.getStatusCode() == HttpURLConnection.HTTP_OK) { - final CoinbaseProOrder[] coinbaseProOpenOrders = - gson.fromJson(response.getPayload(), CoinbaseProOrder[].class); - final List ordersToReturn = new ArrayList<>(); - for (final CoinbaseProOrder openOrder : coinbaseProOpenOrders) { - - if (!marketId.equalsIgnoreCase(openOrder.productId)) { - continue; - } - - OrderType orderType; - switch (openOrder.side) { - case "buy": - orderType = OrderType.BUY; - break; - case "sell": - orderType = OrderType.SELL; - break; - default: - throw new TradingApiException( - "Unrecognised order type received in getYourOpenOrders(). Value: " - + openOrder.side); - } - - final OpenOrder order = - new OpenOrderImpl( - openOrder.id, - Date.from(Instant.parse(openOrder.createdAt)), - marketId, - orderType, - openOrder.price, - openOrder.size.subtract( - openOrder.filledSize), // quantity remaining - not provided by COINBASE PRO - openOrder.size, // orig quantity - openOrder.price.multiply(openOrder.size) // total - not provided by COINBASE PRO - ); - - ordersToReturn.add(order); - } - return ordersToReturn; - } else { - final String errorMsg = - "Failed to get your open orders from exchange. Details: " + response; - log.error(errorMsg); - throw new TradingApiException(errorMsg); - } - - } catch (ExchangeNetworkException | TradingApiException e) { - throw e; - - } catch (Exception e) { - log.error(UNEXPECTED_ERROR_MSG, e); - throw new TradingApiException(UNEXPECTED_ERROR_MSG, e); - } - } - - @Override - public MarketOrderBook getMarketOrders(String marketId) - throws TradingApiException, ExchangeNetworkException { - try { - final Map params = createRequestParamMap(); - params.put("level", "2"); // "2" = Top 50 bids and asks (aggregated) - - final ExchangeHttpResponse response = - sendPublicRequestToExchange(PRODUCTS + marketId + "/book", params); - - log.debug("Market Orders response: " + response); - - if (response.getStatusCode() == HttpURLConnection.HTTP_OK) { - final CoinbaseProBookWrapper orderBook = - gson.fromJson(response.getPayload(), CoinbaseProBookWrapper.class); - - final List buyOrders = new ArrayList<>(); - for (CoinbaseProMarketOrder coinbaseProBuyOrder : orderBook.bids) { - final MarketOrder buyOrder = - new MarketOrderImpl( - OrderType.BUY, - coinbaseProBuyOrder.get(0), - coinbaseProBuyOrder.get(1), - coinbaseProBuyOrder.get(0).multiply(coinbaseProBuyOrder.get(1))); - buyOrders.add(buyOrder); - } - - final List sellOrders = new ArrayList<>(); - for (CoinbaseProMarketOrder coinbaseProSellOrder : orderBook.asks) { - final MarketOrder sellOrder = - new MarketOrderImpl( - OrderType.SELL, - coinbaseProSellOrder.get(0), - coinbaseProSellOrder.get(1), - coinbaseProSellOrder.get(0).multiply(coinbaseProSellOrder.get(1))); - sellOrders.add(sellOrder); - } - return new MarketOrderBookImpl(marketId, sellOrders, buyOrders); - - } else { - final String errorMsg = - "Failed to get market order book from exchange. Details: " + response; - log.error(errorMsg); - throw new TradingApiException(errorMsg); - } - - } catch (ExchangeNetworkException | TradingApiException e) { - throw e; - - } catch (Exception e) { - log.error(UNEXPECTED_ERROR_MSG, e); - throw new TradingApiException(UNEXPECTED_ERROR_MSG, e); - } - } - - @Override - public BalanceInfo getBalanceInfo() throws TradingApiException, ExchangeNetworkException { - try { - final ExchangeHttpResponse response = - sendAuthenticatedRequestToExchange("GET", "accounts", null); - - log.debug("Balance Info response: " + response); - - if (response.getStatusCode() == HttpURLConnection.HTTP_OK) { - final CoinbaseProAccount[] coinbaseProAccounts = - gson.fromJson(response.getPayload(), CoinbaseProAccount[].class); - - final HashMap balancesAvailable = new HashMap<>(); - final HashMap balancesOnHold = new HashMap<>(); - - for (final CoinbaseProAccount coinbaseProAccount : coinbaseProAccounts) { - balancesAvailable.put(coinbaseProAccount.currency, coinbaseProAccount.available); - balancesOnHold.put(coinbaseProAccount.currency, coinbaseProAccount.hold); - } - return new BalanceInfoImpl(balancesAvailable, balancesOnHold); - } else { - final String errorMsg = - "Failed to get your wallet balance info from exchange. Details: " + response; - log.error(errorMsg); - throw new TradingApiException(errorMsg); - } - - } catch (ExchangeNetworkException | TradingApiException e) { - throw e; - } catch (Exception e) { - log.error(UNEXPECTED_ERROR_MSG, e); - throw new TradingApiException(UNEXPECTED_ERROR_MSG, e); - } - } - - @Override - public BigDecimal getLatestMarketPrice(String marketId) - throws ExchangeNetworkException, TradingApiException { - try { - final ExchangeHttpResponse response = - sendPublicRequestToExchange(PRODUCTS + marketId + "/ticker", null); - - log.debug("Latest Market Price response: " + response); - - if (response.getStatusCode() == HttpURLConnection.HTTP_OK) { - final CoinbaseProTicker coinbaseProTicker = - gson.fromJson(response.getPayload(), CoinbaseProTicker.class); - return coinbaseProTicker.price; - } else { - final String errorMsg = "Failed to get market ticker from exchange. Details: " + response; - log.error(errorMsg); - throw new TradingApiException(errorMsg); - } - - } catch (ExchangeNetworkException | TradingApiException e) { - throw e; - - } catch (Exception e) { - log.error(UNEXPECTED_ERROR_MSG, e); - throw new TradingApiException(UNEXPECTED_ERROR_MSG, e); - } - } - - /* - * COINBASE PRO does not provide API call for fetching % buy fee; it only provides the fee - * monetary value for a given order via e.g. /orders/ API call. We load the % fee - * statically from exchange.yaml file. - */ - @Override - public BigDecimal getPercentageOfBuyOrderTakenForExchangeFee(String marketId) { - return buyFeePercentage; - } - - /* - * COINBASE PRO does not provide API call for fetching % sell fee; it only provides the fee - * monetary value for a given order via e.g. /orders/ API call. We load the % fee - * statically from exchange.yaml file. - */ - @Override - public BigDecimal getPercentageOfSellOrderTakenForExchangeFee(String marketId) { - return sellFeePercentage; - } - - @Override - public String getImplName() { - return "COINBASE PRO REST API v1"; - } - - @Override - public Ticker getTicker(String marketId) throws ExchangeNetworkException, TradingApiException { - try { - final ExchangeHttpResponse tickerResponse = - sendPublicRequestToExchange(PRODUCTS + marketId + "/ticker", null); - - log.debug("Ticker response: " + tickerResponse); - - if (tickerResponse.getStatusCode() == HttpURLConnection.HTTP_OK) { - final CoinbaseProTicker coinbaseProTicker = - gson.fromJson(tickerResponse.getPayload(), CoinbaseProTicker.class); - - final TickerImpl ticker = - new TickerImpl( - coinbaseProTicker.price, - coinbaseProTicker.bid, - coinbaseProTicker.ask, - null, // low, - null, // high, - null, // open, - coinbaseProTicker.volume, - null, // vwap - not supplied by COINBASE PRO - Date.from(Instant.parse(coinbaseProTicker.time)).getTime()); - - // Now we need to call the stats operation to get the 24hr indicators - final ExchangeHttpResponse statsResponse = - sendPublicRequestToExchange(PRODUCTS + marketId + "/stats", null); - - log.debug("Stats response: " + statsResponse); - - if (statsResponse.getStatusCode() == HttpURLConnection.HTTP_OK) { - final CoinbaseProStats coinbaseProStats = - gson.fromJson(statsResponse.getPayload(), CoinbaseProStats.class); - ticker.setLow(coinbaseProStats.low); - ticker.setHigh(coinbaseProStats.high); - ticker.setOpen(coinbaseProStats.open); - } else { - final String errorMsg = "Failed to get stats from exchange. Details: " + statsResponse; - log.error(errorMsg); - throw new TradingApiException(errorMsg); - } - - return ticker; - - } else { - final String errorMsg = - "Failed to get market ticker from exchange. Details: " + tickerResponse; - log.error(errorMsg); - throw new TradingApiException(errorMsg); - } - - } catch (ExchangeNetworkException | TradingApiException e) { - throw e; - - } catch (Exception e) { - log.error(UNEXPECTED_ERROR_MSG, e); - throw new TradingApiException(UNEXPECTED_ERROR_MSG, e); - } - } - - // -------------------------------------------------------------------------- - // GSON classes for JSON responses. - // See https://docs.pro.coinbase.com/#api - // -------------------------------------------------------------------------- - - /** - * GSON class for COINBASE PRO '/orders' API call response. - * - *

There are other critters in here different to what is spec'd. - */ - private static class CoinbaseProOrder { - - String id; - BigDecimal price; - BigDecimal size; - - @SerializedName("product_id") - String productId; // e.g. "BTC-GBP", "BTC-USD" - - String side; // "buy" or "sell" - String stp; // Self-Trade Prevention flag, e.g. "dc" - String type; // order type, e.g. "limit" - - @SerializedName("time_in_force") - String timeInForce; // e.g. "GTC" (Good Til Cancelled) - - @SerializedName("post_only") - boolean postOnly; // shows in book + provides exchange liquidity, but will not execute - - @SerializedName("created_at") - String createdAt; // e.g. "2014-11-14 06:39:55.189376+00" - - @SerializedName("fill_fees") - BigDecimal fillFees; - - @SerializedName("filled_size") - BigDecimal filledSize; - - String status; // e.g. "open" - boolean settled; - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("id", id) - .add(PRICE, price) - .add("size", size) - .add("productId", productId) - .add("side", side) - .add("stp", stp) - .add("type", type) - .add("timeInForce", timeInForce) - .add("postOnly", postOnly) - .add("createdAt", createdAt) - .add("fillFees", fillFees) - .add("filledSize", filledSize) - .add("status", status) - .add("settled", settled) - .toString(); - } - } - - /** GSON class for COINBASE PRO '/products/{marketId}/book' API call response. */ - private static class CoinbaseProBookWrapper { - - long sequence; - List bids; - List asks; - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("sequence", sequence) - .add("bids", bids) - .add("asks", asks) - .toString(); - } - } - - /** - * GSON class for holding Market Orders. First element in array is price, second element is - * amount, third is number of orders. - */ - private static class CoinbaseProMarketOrder extends ArrayList { - - @Serial private static final long serialVersionUID = -4919711220797077759L; - } - - /** GSON class for COINBASE PRO '/products/{marketId}/ticker' API call response. */ - private static class CoinbaseProTicker { - - @SerializedName("trade_id") - long tradeId; - - BigDecimal price; - BigDecimal size; - BigDecimal bid; - BigDecimal ask; - BigDecimal volume; - String time; // e.g. "2015-10-14T19:19:36.604735Z" - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("tradeId", tradeId) - .add(PRICE, price) - .add("size", size) - .add("bid", bid) - .add("ask", ask) - .add("volume", volume) - .add("time", time) - .toString(); - } - } - - /** GSON class for COINBASE PRO '/products/<product-id>/stats' API call response. */ - private static class CoinbaseProStats { - - BigDecimal open; - BigDecimal high; - BigDecimal low; - BigDecimal volume; - BigDecimal last; - - @SerializedName("volume_30day") - String volume30Day; - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("open", open) - .add("high", high) - .add("low", low) - .add("volume", volume) - .add("last", last) - .add("volume30Day", volume30Day) - .toString(); - } - } - - /** GSON class for COINBASE PRO '/accounts' API call response. */ - private static class CoinbaseProAccount { - - String id; - String currency; - BigDecimal balance; // e.g. "0.0000000000000000" - BigDecimal hold; - BigDecimal available; - - @SerializedName("profile_id") // no idea what this is?! - String profileId; - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("id", id) - .add("currency", currency) - .add("balance", balance) - .add("hold", hold) - .add("available", available) - .add("profileId", profileId) - .toString(); - } - } - - // -------------------------------------------------------------------------- - // Transport layer methods - // -------------------------------------------------------------------------- - - private ExchangeHttpResponse sendPublicRequestToExchange( - String apiMethod, Map params) - throws ExchangeNetworkException, TradingApiException { - if (params == null) { - params = createRequestParamMap(); // no params, so empty query string - } - - // Request headers required by Exchange - final Map requestHeaders = new HashMap<>(); - - try { - - final StringBuilder queryString = new StringBuilder(); - if (params.size() > 0) { - - queryString.append("?"); - - for (final Map.Entry param : params.entrySet()) { - if (queryString.length() > 1) { - queryString.append("&"); - } - queryString.append(param.getKey()); - queryString.append("="); - queryString.append(URLEncoder.encode(param.getValue(), StandardCharsets.UTF_8)); - } - - requestHeaders.put("Content-Type", "application/x-www-form-urlencoded"); - } - - final URL url = new URL(PUBLIC_API_BASE_URL + apiMethod + queryString); - return makeNetworkRequest(url, "GET", null, requestHeaders); - - } catch (MalformedURLException e) { - final String errorMsg = UNEXPECTED_IO_ERROR_MSG; - log.error(errorMsg, e); - throw new TradingApiException(errorMsg, e); - } - } - - /* - * Makes an authenticated API call to the COINBASE PRO exchange. - * - * The COINBASE PRO authentication process is complex, but well documented: - * https://docs.pro.coinbase.com/#creating-a-request - * - * All REST requests must contain the following headers: - * - * CB-ACCESS-KEY The api key as a string. - * CB-ACCESS-SIGN The base64-encoded signature (see Signing a Message). - * CB-ACCESS-TIMESTAMP A timestamp for your request. - * CB-ACCESS-PASSPHRASE The passphrase you specified when creating the API key. - * - * The CB-ACCESS-TIMESTAMP header MUST be number of seconds since Unix Epoch in UTC. - * Decimal values are allowed. - * - * Your timestamp must be within 30 seconds of the api service time or your request will be - * considered expired and rejected. We recommend using the time endpoint to query for the API - * server time if you believe there many be time skew between your server and the API servers. - * - * All request bodies should have content type application/json and be valid JSON. - * - * The CB-ACCESS-SIGN header is generated by creating a sha256 HMAC using the base64-decoded - * secret key on the prehash string: - * - * timestamp + method + requestPath + body (where + represents string concatenation) - * - * and base64-encode the output. - * The timestamp value is the same as the CB-ACCESS-TIMESTAMP header. - * - * The body is the request body string or omitted if there is no request body - * (typically for GET requests). - * - * The method should be UPPER CASE. - * - * Remember to first base64-decode the alphanumeric secret string (resulting in 64 bytes) before - * using it as the key for HMAC. Also, base64-encode the digest output before sending in the - * header. - */ - private ExchangeHttpResponse sendAuthenticatedRequestToExchange( - String httpMethod, String apiMethod, Map params) - throws ExchangeNetworkException, TradingApiException { - - if (!initializedMacAuthentication) { - final String errorMsg = "MAC Message security layer has not been initialized."; - log.error(errorMsg); - throw new IllegalStateException(errorMsg); - } - - try { - if (params == null) { - // create empty map for non-param API calls - params = createRequestParamMap(); - } - - // Build the request - final String invocationUrl; - String requestBody = ""; - - switch (httpMethod) { - case "GET": - log.debug("Building secure GET request..."); - // Build (optional) query param string - final StringBuilder queryParamBuilder = new StringBuilder(); - for (final Map.Entry param : params.entrySet()) { - if (queryParamBuilder.length() > 0) { - queryParamBuilder.append("&"); - } - queryParamBuilder.append(param.getKey()); - queryParamBuilder.append("="); - queryParamBuilder.append(param.getValue()); - } - - final String queryParams = queryParamBuilder.toString(); - log.debug("Query param string: " + queryParams); - - if (params.isEmpty()) { - invocationUrl = AUTHENTICATED_API_URL + apiMethod; - } else { - invocationUrl = AUTHENTICATED_API_URL + apiMethod + "?" + queryParams; - } - break; - - case "POST": - log.debug("Building secure POST request..."); - invocationUrl = AUTHENTICATED_API_URL + apiMethod; - requestBody = gson.toJson(params); - break; - - case "DELETE": - log.debug("Building secure DELETE request..."); - invocationUrl = AUTHENTICATED_API_URL + apiMethod; - break; - - default: - throw new IllegalArgumentException( - "Don't know how to build secure [" + httpMethod + "] request!"); - } - - // Get UNIX EPOCH in secs and add the time-server bias - final long timeServer = Instant.now().getEpochSecond() + timeServerBias; - final String timestamp = Long.toString(timeServer); - log.debug("Server UNIX EPOCH in seconds: " + timestamp); - - // Build the signature string: timestamp + method + requestPath + body - final String signatureBuilder = - timestamp + httpMethod.toUpperCase() + "/" + apiMethod + requestBody; - - // Sign the signature string and Base64 encode it - mac.reset(); - mac.update(signatureBuilder.getBytes(StandardCharsets.UTF_8)); - final String signature = DatatypeConverter.printBase64Binary(mac.doFinal()); - - // Request headers required by Exchange - final Map requestHeaders = createHeaderParamMap(); - requestHeaders.put("Content-Type", "application/json"); - requestHeaders.put("CB-ACCESS-KEY", key); - requestHeaders.put("CB-ACCESS-SIGN", signature); - requestHeaders.put("CB-ACCESS-TIMESTAMP", timestamp); - requestHeaders.put("CB-ACCESS-PASSPHRASE", passphrase); - - final URL url = new URL(invocationUrl); - return makeNetworkRequest(url, httpMethod, requestBody, requestHeaders); - - } catch (MalformedURLException e) { - final String errorMsg = UNEXPECTED_IO_ERROR_MSG; - log.error(errorMsg, e); - throw new TradingApiException(errorMsg, e); - } - } - - /* - * Initialises the secure messaging layer. - * Sets up the MAC to safeguard the data we send to the exchange. - * Used to encrypt the hash of the entire message with the private key to ensure message - * integrity. We fail hard n fast if any of this stuff blows. - */ - private void initSecureMessageLayer() { - try { - // COINBASE PRO secret is in Base64, so we must decode it first. - final byte[] decodedBase64Secret = DatatypeConverter.parseBase64Binary(secret); - - final SecretKeySpec keyspec = new SecretKeySpec(decodedBase64Secret, "HmacSHA256"); - mac = Mac.getInstance("HmacSHA256"); - mac.init(keyspec); - initializedMacAuthentication = true; - } catch (NoSuchAlgorithmException e) { - final String errorMsg = "Failed to setup MAC security. HINT: Is HMAC-SHA256 installed?"; - log.error(errorMsg, e); - throw new IllegalStateException(errorMsg, e); - } catch (InvalidKeyException e) { - final String errorMsg = "Failed to setup MAC security. Secret key seems invalid!"; - log.error(errorMsg, e); - throw new IllegalArgumentException(errorMsg, e); - } - } - - // -------------------------------------------------------------------------- - // Config methods - // -------------------------------------------------------------------------- - - private void setAuthenticationConfig(ExchangeConfig exchangeConfig) { - final AuthenticationConfig authenticationConfig = getAuthenticationConfig(exchangeConfig); - passphrase = getAuthenticationConfigItem(authenticationConfig, PASSPHRASE_PROPERTY_NAME); - key = getAuthenticationConfigItem(authenticationConfig, KEY_PROPERTY_NAME); - secret = getAuthenticationConfigItem(authenticationConfig, SECRET_PROPERTY_NAME); - } - - private void setOtherConfig(ExchangeConfig exchangeConfig) { - final OtherConfig otherConfig = getOtherConfig(exchangeConfig); - - final String buyFeeInConfig = getOtherConfigItem(otherConfig, BUY_FEE_PROPERTY_NAME); - buyFeePercentage = - new BigDecimal(buyFeeInConfig).divide(new BigDecimal("100"), 8, RoundingMode.HALF_UP); - log.info("Buy fee % in BigDecimal format: " + buyFeePercentage); - - final String sellFeeInConfig = getOtherConfigItem(otherConfig, SELL_FEE_PROPERTY_NAME); - sellFeePercentage = - new BigDecimal(sellFeeInConfig).divide(new BigDecimal("100"), 8, RoundingMode.HALF_UP); - log.info("Sell fee % in BigDecimal format: " + sellFeePercentage); - - final String serverTimeBiasInConfig = - getOtherConfigItem(otherConfig, SERVER_TIME_BIAS_PROPERTY_NAME); - timeServerBias = Long.parseLong(serverTimeBiasInConfig); - log.info("Time server bias in long format: " + timeServerBias); - } - - // -------------------------------------------------------------------------- - // Util methods - // -------------------------------------------------------------------------- - - private void initGson() { - final GsonBuilder gsonBuilder = new GsonBuilder(); - gson = gsonBuilder.create(); - } - - /* - * Hack for unit-testing request params passed to transport layer. - */ - private Map createRequestParamMap() { - return new HashMap<>(); - } - - /* - * Hack for unit-testing header params passed to transport layer. - */ - private Map createHeaderParamMap() { - return new HashMap<>(); - } - - /* - * Hack for unit-testing transport layer. - */ - private ExchangeHttpResponse makeNetworkRequest( - URL url, String httpMethod, String postData, Map requestHeaders) - throws TradingApiException, ExchangeNetworkException { - return super.sendNetworkRequest(url, httpMethod, postData, requestHeaders); - } -} diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/accounts.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/accounts.json deleted file mode 100644 index ed4942c77..000000000 --- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/accounts.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "id": "7262ae65-fbc3-4d11-b959-25f1befc7a21", - "currency": "BTC", - "balance": "200.000000000000009", - "hold": "100.0000000000000005", - "available": "100.0000000000000004", - "profile_id": "5624aa4a-85f6-462f-a4d9-bac80ea184c7" - }, - { - "id": "de2b5848-533e-4374-9004-cd8ea985f8cc", - "currency": "GBP", - "balance": "1000.0000000000000003", - "hold": "499.9900000000000002", - "available": "501.0100000000000001", - "profile_id": "5624aa4a-85f6-462f-a4d9-bac80ea184c2" - }, - { - "id": "864881f5-bd18-4970-b254-e7cd2fef6b4d", - "currency": "EUR", - "balance": "0.0000000000000000", - "hold": "0.0000000000000000", - "available": "0.0000000000000000", - "profile_id": "5622aa4a-87f6-462f-c4d9-bac80ef184d0" - } -] \ No newline at end of file diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/book.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/book.json deleted file mode 100644 index 72ff024c1..000000000 --- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/book.json +++ /dev/null @@ -1,507 +0,0 @@ -{ - "sequence": 95643108, - "bids": [ - [ - "165.87", - "16.2373", - 10 - ], - [ - "165.86", - "21.645", - 3 - ], - [ - "165.85", - "0.357", - 3 - ], - [ - "165.84", - "15.4652", - 1 - ], - [ - "165.83", - "0.08", - 2 - ], - [ - "165.82", - "0.207", - 4 - ], - [ - "165.8", - "3.46334", - 1 - ], - [ - "165.78", - "43.355346", - 6 - ], - [ - "165.77", - "3.157", - 2 - ], - [ - "165.76", - "32.6", - 4 - ], - [ - "165.75", - "0.02042", - 1 - ], - [ - "165.74", - "0.3264", - 3 - ], - [ - "165.73", - "0.902", - 4 - ], - [ - "165.72", - "0.07", - 1 - ], - [ - "165.71", - "0.38466", - 6 - ], - [ - "165.7", - "23.32458", - 12 - ], - [ - "165.69", - "13.6215", - 1 - ], - [ - "165.68", - "0.08", - 2 - ], - [ - "165.66", - "0.03", - 1 - ], - [ - "165.65", - "0.02", - 2 - ], - [ - "165.63", - "0.051", - 1 - ], - [ - "165.52", - "18.39", - 1 - ], - [ - "165.31", - "0.016", - 1 - ], - [ - "165.3", - "0.0183", - 1 - ], - [ - "165.28", - "0.088", - 1 - ], - [ - "165.26", - "0.053", - 2 - ], - [ - "165.24", - "0.02", - 1 - ], - [ - "165.23", - "3.65", - 3 - ], - [ - "165.22", - "0.01", - 1 - ], - [ - "165.18", - "3.3559", - 2 - ], - [ - "165.16", - "0.031", - 1 - ], - [ - "165.12", - "2.01", - 1 - ], - [ - "165.07", - "3.15", - 1 - ], - [ - "165", - "2.5", - 3 - ], - [ - "164.91", - "3.5", - 1 - ], - [ - "164.62", - "3.64", - 1 - ], - [ - "164.56", - "1.676", - 1 - ], - [ - "164.5", - "1.66", - 1 - ], - [ - "164.49", - "7.82071", - 1 - ], - [ - "164.44", - "3", - 1 - ], - [ - "164.38", - "2.06", - 1 - ], - [ - "164.2", - "1.21", - 1 - ], - [ - "164.13", - "0.02", - 1 - ], - [ - "164.05", - "1.7", - 1 - ], - [ - "164.03", - "3.906", - 1 - ], - [ - "163.97", - "0.6281", - 1 - ], - [ - "163.91", - "2.6", - 1 - ], - [ - "163.84", - "4.7602", - 1 - ], - [ - "163.75", - "2.314", - 1 - ], - [ - "163.73", - "0.3", - 1 - ] - ], - "asks": [ - [ - "165.96", - "24.31", - 1 - ], - [ - "166.05", - "0.01", - 1 - ], - [ - "166.08", - "0.027", - 1 - ], - [ - "166.1", - "3.98718", - 1 - ], - [ - "166.12", - "0.0815", - 2 - ], - [ - "166.13", - "0.1404", - 2 - ], - [ - "166.14", - "0.108", - 3 - ], - [ - "166.16", - "0.784", - 3 - ], - [ - "166.17", - "1.41472", - 3 - ], - [ - "166.18", - "1.3774", - 5 - ], - [ - "166.19", - "0.3284", - 6 - ], - [ - "166.2", - "0.02", - 1 - ], - [ - "166.21", - "0.1", - 1 - ], - [ - "166.22", - "0.203", - 6 - ], - [ - "166.23", - "0.152", - 2 - ], - [ - "166.24", - "3.0575", - 11 - ], - [ - "166.26", - "0.0656", - 2 - ], - [ - "166.27", - "0.01", - 1 - ], - [ - "166.29", - "17.43", - 2 - ], - [ - "166.3", - "0.86", - 1 - ], - [ - "166.33", - "0.6327395", - 10 - ], - [ - "166.46", - "132.536", - 5 - ], - [ - "166.91", - "0.19", - 1 - ], - [ - "167.37", - "0.13", - 1 - ], - [ - "167.6", - "2.214", - 1 - ], - [ - "167.63", - "0.2142", - 1 - ], - [ - "167.66", - "2.64", - 1 - ], - [ - "167.72", - "3.12", - 1 - ], - [ - "167.76", - "0.01", - 1 - ], - [ - "167.78", - "4.2", - 2 - ], - [ - "167.88", - "4.387", - 1 - ], - [ - "167.98", - "4.1482", - 1 - ], - [ - "168.08", - "3.234", - 1 - ], - [ - "168.1", - "1.8", - 1 - ], - [ - "168.54", - "0.013", - 1 - ], - [ - "170.3", - "0.026", - 1 - ], - [ - "171.16", - "0.8", - 1 - ], - [ - "171.25", - "0.11", - 1 - ], - [ - "171.38", - "0.051", - 1 - ], - [ - "171.55", - "0.0138", - 1 - ], - [ - "171.85", - "0.1", - 1 - ], - [ - "174.24", - "3", - 1 - ], - [ - "175.11", - "0.017", - 1 - ], - [ - "179.29", - "0.57", - 1 - ], - [ - "182.58", - "0.09", - 1 - ], - [ - "183.73", - "0.2", - 1 - ], - [ - "186.28", - "0.3", - 1 - ], - [ - "186.39", - "0.39", - 1 - ], - [ - "186.65", - "8", - 1 - ], - [ - "186.83", - "1.25", - 1 - ] - ] -} \ No newline at end of file diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/cancel.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/cancel.json deleted file mode 100644 index d390bd800..000000000 --- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/cancel.json +++ /dev/null @@ -1 +0,0 @@ -["3ecf7a12-fc89-4d3d-baef-f158f80b3bd3"] \ No newline at end of file diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/new_buy_order.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/new_buy_order.json deleted file mode 100644 index 1c47f17cb..000000000 --- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/new_buy_order.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "193d2ad9-e671-4d66-9211-7f75f6380231", - "price": "280.18000000", - "size": "0.01000000", - "product_id": "BTC-GBP", - "side": "buy", - "stp": "dc", - "type": "limit", - "time_in_force": "GTC", - "post_only": false, - "created_at": "2015-10-17T14:48:18.873Z", - "fill_fees": "0.0000000000000000", - "filled_size": "0.00000000", - "status": "pending", - "settled": false -} \ No newline at end of file diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/new_sell_order.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/new_sell_order.json deleted file mode 100644 index 2f244ec18..000000000 --- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/new_sell_order.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "693d7ad9-e671-4d66-9911-7f75f6380134", - "price": "290.18000000", - "size": "0.01000000", - "product_id": "BTC-GBP", - "side": "sell", - "stp": "dc", - "type": "limit", - "time_in_force": "GTC", - "post_only": false, - "created_at": "2015-10-17T14:43:18.873Z", - "fill_fees": "0.0000000000000000", - "filled_size": "0.00000000", - "status": "pending", - "settled": false -} \ No newline at end of file diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/orders.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/orders.json deleted file mode 100644 index 14e56d03f..000000000 --- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/orders.json +++ /dev/null @@ -1,66 +0,0 @@ -[ - { - "id": "cdad7602-f290-41e5-a64d-42a1a20fd02", - "price": "275.00000000", - "size": "0.01000000", - "product_id": "BTC-GBP", - "side": "sell", - "stp": "dc", - "type": "limit", - "time_in_force": "GTC", - "post_only": false, - "created_at": "2015-10-15T21:10:38.193Z", - "fill_fees": "0.0000000000000000", - "filled_size": "0.00500000", - "status": "open", - "settled": false - }, - { - "id": "09cac657-df6c-40ef-97b9-4e64b181dec1", - "price": "270.00000000", - "size": "0.01000000", - "product_id": "BTC-GBP", - "side": "sell", - "stp": "dc", - "type": "limit", - "time_in_force": "GTC", - "post_only": false, - "created_at": "2015-10-15T21:10:10.569Z", - "fill_fees": "0.0000000000000000", - "filled_size": "0.00000000", - "status": "open", - "settled": false - }, - { - "id": "09cac657-df6c-10ef-97b9-4e64b181dec1", - "price": "2001.02", - "size": "0.01000000", - "product_id": "BTC-USD", - "side": "sell", - "stp": "dc", - "type": "limit", - "time_in_force": "GTC", - "post_only": false, - "created_at": "2015-10-15T21:10:10.569Z", - "fill_fees": "0.0000000000000000", - "filled_size": "0.00000000", - "status": "open", - "settled": false - }, - { - "id": "09ca1657-df6c-10ef-97b9-4e64b181dec1", - "price": "341.42000000", - "size": "2.01000000", - "product_id": "ETH-USD", - "side": "sell", - "stp": "dc", - "type": "limit", - "time_in_force": "GTC", - "post_only": false, - "created_at": "2015-10-15T21:10:10.569Z", - "fill_fees": "0.0000000000000000", - "filled_size": "0.00000000", - "status": "open", - "settled": false - } -] \ No newline at end of file diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/stats.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/stats.json deleted file mode 100644 index 755d7b916..000000000 --- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/stats.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "open": "13609.53000000", - "high": "14899.00000000", - "low": "13409.97000000", - "volume": "607.54445656", - "last": "14744.81000000", - "volume_30day": "22412.37849136" -} \ No newline at end of file diff --git a/bxbot-exchanges/src/test/exchange-data/coinbasepro/ticker.json b/bxbot-exchanges/src/test/exchange-data/coinbasepro/ticker.json deleted file mode 100644 index 265129f79..000000000 --- a/bxbot-exchanges/src/test/exchange-data/coinbasepro/ticker.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "trade_id": 29582, - "price": "14744.9", - "size": "2.6108", - "bid":"14744.8", - "ask":"14744.81", - "volume": "607.54445656", - "time": "2017-10-14T19:19:36.604735Z" -} diff --git a/bxbot-exchanges/src/test/java/com/gazbert/bxbot/exchanges/TestCoinbaseProExchangeAdapter.java b/bxbot-exchanges/src/test/java/com/gazbert/bxbot/exchanges/TestCoinbaseProExchangeAdapter.java deleted file mode 100644 index 3f6c1875c..000000000 --- a/bxbot-exchanges/src/test/java/com/gazbert/bxbot/exchanges/TestCoinbaseProExchangeAdapter.java +++ /dev/null @@ -1,1224 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 Gareth Jon Lynch - * Copyright (c) 2019 David Huertas - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.gazbert.bxbot.exchanges; - -import static org.easymock.EasyMock.anyObject; -import static org.easymock.EasyMock.anyString; -import static org.easymock.EasyMock.eq; -import static org.easymock.EasyMock.expect; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - -import com.gazbert.bxbot.exchange.api.AuthenticationConfig; -import com.gazbert.bxbot.exchange.api.ExchangeAdapter; -import com.gazbert.bxbot.exchange.api.ExchangeConfig; -import com.gazbert.bxbot.exchange.api.NetworkConfig; -import com.gazbert.bxbot.exchange.api.OtherConfig; -import com.gazbert.bxbot.trading.api.BalanceInfo; -import com.gazbert.bxbot.trading.api.ExchangeNetworkException; -import com.gazbert.bxbot.trading.api.MarketOrderBook; -import com.gazbert.bxbot.trading.api.OpenOrder; -import com.gazbert.bxbot.trading.api.OrderType; -import com.gazbert.bxbot.trading.api.Ticker; -import com.gazbert.bxbot.trading.api.TradingApiException; -import com.google.gson.GsonBuilder; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.text.DecimalFormat; -import java.time.Instant; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.api.easymock.PowerMock; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; - -/** - * Tests the behaviour of the COINBASE PRO Exchange Adapter. - * - * @author davidhuertas - */ -@RunWith(PowerMockRunner.class) -@PowerMockIgnore({ - "javax.crypto.*", - "javax.management.*", - "com.sun.org.apache.xerces.*", - "javax.xml.parsers.*", - "org.xml.sax.*", - "org.w3c.dom.*", - "javax.xml.datatype.*" -}) -@PrepareForTest(CoinbaseProExchangeAdapter.class) -public class TestCoinbaseProExchangeAdapter extends AbstractExchangeAdapterTest { - - private static final String BOOK_JSON_RESPONSE = "./src/test/exchange-data/coinbasepro/book.json"; - private static final String ORDERS_JSON_RESPONSE = - "./src/test/exchange-data/coinbasepro/orders.json"; - private static final String ACCOUNTS_JSON_RESPONSE = - "./src/test/exchange-data/coinbasepro/accounts.json"; - private static final String TICKER_JSON_RESPONSE = - "./src/test/exchange-data/coinbasepro/ticker.json"; - private static final String NEW_BUY_ORDER_JSON_RESPONSE = - "./src/test/exchange-data/coinbasepro/new_buy_order.json"; - private static final String NEW_SELL_ORDER_JSON_RESPONSE = - "./src/test/exchange-data/coinbasepro/new_sell_order.json"; - private static final String CANCEL_ORDER_JSON_RESPONSE = - "./src/test/exchange-data/coinbasepro/cancel.json"; - private static final String STATS_JSON_RESPONSE = - "./src/test/exchange-data/coinbasepro/stats.json"; - - private static final String MARKET_ID = "BTC-GBP"; - private static final String ORDER_BOOK_DEPTH_LEVEL = - "2"; // "2" = Top 50 bids and asks (aggregated) - private static final BigDecimal BUY_ORDER_PRICE = new BigDecimal("200.18"); - private static final BigDecimal BUY_ORDER_QUANTITY = new BigDecimal("0.01"); - private static final BigDecimal SELL_ORDER_PRICE = new BigDecimal("300.176"); - private static final BigDecimal SELL_ORDER_QUANTITY = new BigDecimal("0.01"); - private static final String ORDER_ID_TO_CANCEL = "3ecf7a12-fc89-4d3d-baef-f158f80b3bd3"; - - private static final String BOOK = "products/" + MARKET_ID + "/book"; - private static final String ORDERS = "orders"; - private static final String ACCOUNTS = "accounts"; - private static final String TICKER = "products/" + MARKET_ID + "/ticker"; - private static final String NEW_ORDER = "orders"; - private static final String CANCEL_ORDER = "orders/" + ORDER_ID_TO_CANCEL; - private static final String STATS = "products/" + MARKET_ID + "/stats"; - - private static final String MOCKED_CREATE_REQUEST_PARAM_MAP_METHOD = "createRequestParamMap"; - private static final String MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD = - "sendAuthenticatedRequestToExchange"; - private static final String MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD = - "sendPublicRequestToExchange"; - private static final String MOCKED_CREATE_REQUEST_HEADER_MAP_METHOD = "createHeaderParamMap"; - private static final String MOCKED_MAKE_NETWORK_REQUEST_METHOD = "makeNetworkRequest"; - - private static final String PASSPHRASE = "lePassPhrase"; - private static final String KEY = "key123"; - private static final String SECRET = "notGonnaTellYa"; - private static final List nonFatalNetworkErrorCodes = Arrays.asList(502, 503, 504); - private static final List nonFatalNetworkErrorMessages = - Arrays.asList( - "Connection refused", - "Connection reset", - "Remote host closed connection during handshake"); - - private static final String PUBLIC_API_BASE_URL = "https://api.pro.coinbase.com/"; - private static final String AUTHENTICATED_API_URL = PUBLIC_API_BASE_URL; - - private ExchangeConfig exchangeConfig; - private AuthenticationConfig authenticationConfig; - private NetworkConfig networkConfig; - private OtherConfig otherConfig; - - /** Create some exchange config - the TradingEngine would normally do this. */ - @Before - public void setupForEachTest() { - authenticationConfig = PowerMock.createMock(AuthenticationConfig.class); - expect(authenticationConfig.getItem("passphrase")).andReturn(PASSPHRASE); - expect(authenticationConfig.getItem("key")).andReturn(KEY); - expect(authenticationConfig.getItem("secret")).andReturn(SECRET); - - networkConfig = PowerMock.createMock(NetworkConfig.class); - expect(networkConfig.getConnectionTimeout()).andReturn(30); - expect(networkConfig.getNonFatalErrorCodes()).andReturn(nonFatalNetworkErrorCodes); - expect(networkConfig.getNonFatalErrorMessages()).andReturn(nonFatalNetworkErrorMessages); - - otherConfig = PowerMock.createMock(OtherConfig.class); - expect(otherConfig.getItem("buy-fee")).andReturn("0.25"); - expect(otherConfig.getItem("sell-fee")).andReturn("0.25"); - expect(otherConfig.getItem("time-server-bias")).andReturn("82"); - - exchangeConfig = PowerMock.createMock(ExchangeConfig.class); - expect(exchangeConfig.getAuthenticationConfig()).andReturn(authenticationConfig); - expect(exchangeConfig.getNetworkConfig()).andReturn(networkConfig); - expect(exchangeConfig.getOtherConfig()).andReturn(otherConfig); - } - - // -------------------------------------------------------------------------- - // Create Orders tests - // -------------------------------------------------------------------------- - - @Test - @SuppressWarnings("unchecked") - public void testCreateOrderToBuyIsSuccessful() throws Exception { - // Load the canned response from the exchange - final byte[] encoded = Files.readAllBytes(Paths.get(NEW_BUY_ORDER_JSON_RESPONSE)); - final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse = - new AbstractExchangeAdapter.ExchangeHttpResponse( - 200, "OK", new String(encoded, StandardCharsets.UTF_8)); - - // Mock out param map, so we can assert the contents passed to the transport - // layer are what we expect. - final Map requestParamMap = PowerMock.createMock(Map.class); - expect( - requestParamMap.put( - "size", - new DecimalFormat("#.########", getDecimalFormatSymbols()) - .format(BUY_ORDER_QUANTITY))) - .andStubReturn(null); - expect( - requestParamMap.put( - "price", - new DecimalFormat("#.##", getDecimalFormatSymbols()).format(BUY_ORDER_PRICE))) - .andStubReturn(null); - expect(requestParamMap.put("side", "buy")).andStubReturn(null); - expect(requestParamMap.put("product_id", MARKET_ID)).andStubReturn(null); - - // Partial mock so we do not send stuff down the wire - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, - MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD, - MOCKED_CREATE_REQUEST_PARAM_MAP_METHOD); - - PowerMock.expectPrivate(exchangeAdapter, MOCKED_CREATE_REQUEST_PARAM_MAP_METHOD) - .andReturn(requestParamMap); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD, - eq("POST"), - eq(NEW_ORDER), - eq(requestParamMap)) - .andReturn(exchangeResponse); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - final String orderId = - exchangeAdapter.createOrder(MARKET_ID, OrderType.BUY, BUY_ORDER_QUANTITY, BUY_ORDER_PRICE); - assertEquals("193d2ad9-e671-4d66-9211-7f75f6380231", orderId); - - PowerMock.verifyAll(); - } - - @Test - @SuppressWarnings("unchecked") - public void testCreateOrderToSellIsSuccessful() throws Exception { - final byte[] encoded = Files.readAllBytes(Paths.get(NEW_SELL_ORDER_JSON_RESPONSE)); - final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse = - new AbstractExchangeAdapter.ExchangeHttpResponse( - 200, "OK", new String(encoded, StandardCharsets.UTF_8)); - - final Map requestParamMap = PowerMock.createMock(Map.class); - expect( - requestParamMap.put( - "size", - new DecimalFormat("#.########", getDecimalFormatSymbols()) - .format(SELL_ORDER_QUANTITY))) - .andStubReturn(null); - expect( - requestParamMap.put( - "price", - new DecimalFormat("#.##", getDecimalFormatSymbols()).format(SELL_ORDER_PRICE))) - .andStubReturn(null); - expect(requestParamMap.put("side", "sell")).andStubReturn(null); - expect(requestParamMap.put("product_id", MARKET_ID)).andStubReturn(null); - - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, - MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD, - MOCKED_CREATE_REQUEST_PARAM_MAP_METHOD); - - PowerMock.expectPrivate(exchangeAdapter, MOCKED_CREATE_REQUEST_PARAM_MAP_METHOD) - .andReturn(requestParamMap); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD, - eq("POST"), - eq(NEW_ORDER), - eq(requestParamMap)) - .andReturn(exchangeResponse); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - final String orderId = - exchangeAdapter.createOrder( - MARKET_ID, OrderType.SELL, SELL_ORDER_QUANTITY, SELL_ORDER_PRICE); - assertEquals("693d7ad9-e671-4d66-9911-7f75f6380134", orderId); - - PowerMock.verifyAll(); - } - - @Test(expected = ExchangeNetworkException.class) - public void testCreateOrderHandlesExchangeNetworkException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD, - eq("POST"), - eq(NEW_ORDER), - anyObject(Map.class)) - .andThrow( - new ExchangeNetworkException( - " When it comes to the safety of these people, there's me and " - + "then there's God, understand?")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.createOrder(MARKET_ID, OrderType.SELL, SELL_ORDER_QUANTITY, SELL_ORDER_PRICE); - PowerMock.verifyAll(); - } - - @Test(expected = TradingApiException.class) - public void testCreateOrderHandlesUnexpectedException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD, - eq("POST"), - eq(NEW_ORDER), - anyObject(Map.class)) - .andThrow( - new IllegalArgumentException( - " We all see what we want to see. Coffey looks and he sees Russians. He sees hate " - + "and fear. You have to look with better eyes than that")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.createOrder(MARKET_ID, OrderType.BUY, BUY_ORDER_QUANTITY, BUY_ORDER_PRICE); - PowerMock.verifyAll(); - } - - // -------------------------------------------------------------------------- - // Cancel Order tests - // -------------------------------------------------------------------------- - - @Test - public void testCancelOrderIsSuccessful() throws Exception { - final byte[] encoded = Files.readAllBytes(Paths.get(CANCEL_ORDER_JSON_RESPONSE)); - final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse = - new AbstractExchangeAdapter.ExchangeHttpResponse( - 200, "OK", new String(encoded, StandardCharsets.UTF_8)); - - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD); - - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD, - eq("DELETE"), - eq(CANCEL_ORDER), - eq(null)) - .andReturn(exchangeResponse); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - // marketId arg not needed for cancelling orders on this exchange. - final boolean success = exchangeAdapter.cancelOrder(ORDER_ID_TO_CANCEL, null); - assertTrue(success); - PowerMock.verifyAll(); - } - - @Test(expected = ExchangeNetworkException.class) - public void testCancelOrderHandlesExchangeNetworkException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD); - - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD, - eq("DELETE"), - eq(CANCEL_ORDER), - eq(null)) - .andThrow( - new ExchangeNetworkException( - "We don't need them. We can't trust them. We may have to take steps." - + " We're gonna have to take steps.")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - // marketId arg not needed for cancelling orders on this exchange. - exchangeAdapter.cancelOrder(ORDER_ID_TO_CANCEL, null); - PowerMock.verifyAll(); - } - - @Test(expected = TradingApiException.class) - public void testCancelOrderHandlesUnexpectedException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD); - - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD, - eq("DELETE"), - eq(CANCEL_ORDER), - eq(null)) - .andThrow( - new IllegalStateException( - "Fluid breathing system, we just got it. You use it when you go really deep.")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - // marketId arg not needed for cancelling orders on this exchange. - exchangeAdapter.cancelOrder(ORDER_ID_TO_CANCEL, null); - PowerMock.verifyAll(); - } - - // -------------------------------------------------------------------------- - // Get Your Open Orders tests - // -------------------------------------------------------------------------- - - @Test - public void testGettingYourOpenOrdersSuccessfully() throws Exception { - final byte[] encoded = Files.readAllBytes(Paths.get(ORDERS_JSON_RESPONSE)); - final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse = - new AbstractExchangeAdapter.ExchangeHttpResponse( - 200, "OK", new String(encoded, StandardCharsets.UTF_8)); - - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD); - - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD, - eq("GET"), - eq(ORDERS), - eq(null)) - .andReturn(exchangeResponse); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - final List openOrders = exchangeAdapter.getYourOpenOrders(MARKET_ID); - - // assert some key stuff; we're not testing GSON here. - assertEquals(2, openOrders.size()); - assertEquals(MARKET_ID, openOrders.get(0).getMarketId()); - assertEquals("cdad7602-f290-41e5-a64d-42a1a20fd02", openOrders.get(0).getId()); - assertSame(OrderType.SELL, openOrders.get(0).getType()); - assertEquals( - openOrders.get(0).getCreationDate(), Date.from(Instant.parse("2015-10-15T21:10:38.193Z"))); - assertEquals(0, openOrders.get(0).getPrice().compareTo(new BigDecimal("275.00000000"))); - assertEquals( - 0, openOrders.get(0).getOriginalQuantity().compareTo(new BigDecimal("0.01000000"))); - assertEquals(0, openOrders.get(0).getQuantity().compareTo(new BigDecimal("0.00500000"))); - assertEquals( - 0, - openOrders - .get(0) - .getTotal() - .compareTo( - openOrders.get(0).getPrice().multiply(openOrders.get(0).getOriginalQuantity()))); - - PowerMock.verifyAll(); - } - - @Test(expected = ExchangeNetworkException.class) - public void testGettingYourOpenOrdersHandlesExchangeNetworkException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD, - eq("GET"), - eq(ORDERS), - eq(null)) - .andThrow(new ExchangeNetworkException("Bond. James Bond.")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.getYourOpenOrders(MARKET_ID); - PowerMock.verifyAll(); - } - - @Test(expected = TradingApiException.class) - public void testGettingYourOpenOrdersHandlesUnexpectedException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD, - eq("GET"), - eq(ORDERS), - eq(null)) - .andThrow( - new IllegalStateException( - "All those moments will be lost in time... like tears in rain.")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.getYourOpenOrders(MARKET_ID); - PowerMock.verifyAll(); - } - - // -------------------------------------------------------------------------- - // Get Market Orders tests - // -------------------------------------------------------------------------- - - @Test - @SuppressWarnings("unchecked") - public void testGettingMarketOrders() throws Exception { - final byte[] encoded = Files.readAllBytes(Paths.get(BOOK_JSON_RESPONSE)); - final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse = - new AbstractExchangeAdapter.ExchangeHttpResponse( - 200, "OK", new String(encoded, StandardCharsets.UTF_8)); - - final Map requestParamMap = PowerMock.createMock(Map.class); - expect(requestParamMap.put("level", ORDER_BOOK_DEPTH_LEVEL)).andStubReturn(null); - - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, - MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, - MOCKED_CREATE_REQUEST_PARAM_MAP_METHOD); - - PowerMock.expectPrivate(exchangeAdapter, MOCKED_CREATE_REQUEST_PARAM_MAP_METHOD) - .andReturn(requestParamMap); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, - eq(BOOK), - eq(requestParamMap)) - .andReturn(exchangeResponse); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - final MarketOrderBook marketOrderBook = exchangeAdapter.getMarketOrders(MARKET_ID); - - // assert some key stuff; we're not testing GSON here. - assertEquals(MARKET_ID, marketOrderBook.getMarketId()); - - final BigDecimal buyPrice = new BigDecimal("165.87"); - final BigDecimal buyQuantity = new BigDecimal("16.2373"); - final BigDecimal buyTotal = buyPrice.multiply(buyQuantity); - - assertEquals(50, marketOrderBook.getBuyOrders().size()); - assertSame(OrderType.BUY, marketOrderBook.getBuyOrders().get(0).getType()); - assertEquals(0, marketOrderBook.getBuyOrders().get(0).getPrice().compareTo(buyPrice)); - assertEquals(0, marketOrderBook.getBuyOrders().get(0).getQuantity().compareTo(buyQuantity)); - assertEquals(0, marketOrderBook.getBuyOrders().get(0).getTotal().compareTo(buyTotal)); - - final BigDecimal sellPrice = new BigDecimal("165.96"); - final BigDecimal sellQuantity = new BigDecimal("24.31"); - final BigDecimal sellTotal = sellPrice.multiply(sellQuantity); - - assertEquals(50, marketOrderBook.getSellOrders().size()); - assertSame(OrderType.SELL, marketOrderBook.getSellOrders().get(0).getType()); - assertEquals(0, marketOrderBook.getSellOrders().get(0).getPrice().compareTo(sellPrice)); - assertEquals(0, marketOrderBook.getSellOrders().get(0).getQuantity().compareTo(sellQuantity)); - assertEquals(0, marketOrderBook.getSellOrders().get(0).getTotal().compareTo(sellTotal)); - - PowerMock.verifyAll(); - } - - @Test(expected = ExchangeNetworkException.class) - public void testGettingMarketOrdersHandlesExchangeNetworkException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD); - - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, - eq(BOOK), - anyObject(Map.class)) - .andThrow(new ExchangeNetworkException("Re-verify our range to target... one ping only.")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.getMarketOrders(MARKET_ID); - PowerMock.verifyAll(); - } - - @Test(expected = TradingApiException.class) - public void testGettingMarketOrdersHandlesUnexpectedException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD); - - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, - eq(BOOK), - anyObject(Map.class)) - .andThrow( - new IllegalArgumentException( - "Mr. Ambassador, you have nearly a hundred naval vessels operating in the " - + "North Atlantic right now. Your aircraft has dropped enough sonar buoys " - + "so that a man could walk from Greenland to Iceland to Scotland without " - + "getting his feet wet. Now, shall we dispense with the bull?")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.getMarketOrders(MARKET_ID); - PowerMock.verifyAll(); - } - - // -------------------------------------------------------------------------- - // Get Latest Market Price tests - // -------------------------------------------------------------------------- - - @Test - public void testGettingLatestMarketPriceSuccessfully() throws Exception { - final byte[] encoded = Files.readAllBytes(Paths.get(TICKER_JSON_RESPONSE)); - final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse = - new AbstractExchangeAdapter.ExchangeHttpResponse( - 200, "OK", new String(encoded, StandardCharsets.UTF_8)); - - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD); - - PowerMock.expectPrivate( - exchangeAdapter, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, eq(TICKER), eq(null)) - .andReturn(exchangeResponse); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - final BigDecimal latestMarketPrice = - exchangeAdapter.getLatestMarketPrice(MARKET_ID).setScale(8, RoundingMode.HALF_UP); - assertEquals(0, latestMarketPrice.compareTo(new BigDecimal("14744.9"))); - - PowerMock.verifyAll(); - } - - @Test(expected = ExchangeNetworkException.class) - public void testGettingLatestMarketPriceHandlesExchangeNetworkException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD); - PowerMock.expectPrivate( - exchangeAdapter, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, eq(TICKER), eq(null)) - .andThrow( - new ExchangeNetworkException("I need your clothes, your boots and your motorcycle.")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.getLatestMarketPrice(MARKET_ID); - PowerMock.verifyAll(); - } - - @Test(expected = TradingApiException.class) - public void testGettingLatestMarketPriceHandlesUnexpectedException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD); - PowerMock.expectPrivate( - exchangeAdapter, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, eq(TICKER), eq(null)) - .andThrow(new IllegalArgumentException("Come with me if you want to live.")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.getLatestMarketPrice(MARKET_ID); - PowerMock.verifyAll(); - } - - // -------------------------------------------------------------------------- - // Get Balance Info tests - // -------------------------------------------------------------------------- - - @Test - public void testGettingBalanceInfoSuccessfully() throws Exception { - final byte[] encoded = Files.readAllBytes(Paths.get(ACCOUNTS_JSON_RESPONSE)); - final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse = - new AbstractExchangeAdapter.ExchangeHttpResponse( - 200, "OK", new String(encoded, StandardCharsets.UTF_8)); - - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD, - eq("GET"), - eq(ACCOUNTS), - eq(null)) - .andReturn(exchangeResponse); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - final BalanceInfo balanceInfo = exchangeAdapter.getBalanceInfo(); - - // assert some key stuff; we're not testing GSON here. - assertEquals( - 0, - balanceInfo - .getBalancesAvailable() - .get("BTC") - .compareTo(new BigDecimal("100.0000000000000004"))); - assertEquals( - 0, - balanceInfo - .getBalancesAvailable() - .get("GBP") - .compareTo(new BigDecimal("501.0100000000000001"))); - assertEquals(0, balanceInfo.getBalancesAvailable().get("EUR").compareTo(new BigDecimal("0"))); - - assertEquals( - 0, - balanceInfo - .getBalancesOnHold() - .get("BTC") - .compareTo(new BigDecimal("100.0000000000000005"))); - assertEquals( - 0, - balanceInfo - .getBalancesOnHold() - .get("GBP") - .compareTo(new BigDecimal("499.9900000000000002"))); - assertEquals(0, balanceInfo.getBalancesOnHold().get("EUR").compareTo(new BigDecimal("0"))); - - PowerMock.verifyAll(); - } - - @Test(expected = ExchangeNetworkException.class) - public void testGettingBalanceInfoHandlesExchangeNetworkException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD, - eq("GET"), - eq(ACCOUNTS), - eq(null)) - .andThrow( - new ExchangeNetworkException( - "Three o'clock is always too late or too early for anything you want to do.")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.getBalanceInfo(); - PowerMock.verifyAll(); - } - - @Test(expected = TradingApiException.class) - public void testGettingBalanceInfoHandlesUnexpectedException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_SEND_AUTHENTICATED_REQUEST_TO_EXCHANGE_METHOD, - eq("GET"), - eq(ACCOUNTS), - eq(null)) - .andThrow( - new IllegalStateException( - "There is a time for many words, and there is also a time for sleep.")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.getBalanceInfo(); - PowerMock.verifyAll(); - } - - // -------------------------------------------------------------------------- - // Get Ticker tests - // -------------------------------------------------------------------------- - - @Test - public void testGettingTickerSuccessfully() throws Exception { - final byte[] encodedTicker = Files.readAllBytes(Paths.get(TICKER_JSON_RESPONSE)); - final AbstractExchangeAdapter.ExchangeHttpResponse tickerExchangeResponse = - new AbstractExchangeAdapter.ExchangeHttpResponse( - 200, "OK", new String(encodedTicker, StandardCharsets.UTF_8)); - - final byte[] encodedStats = Files.readAllBytes(Paths.get(STATS_JSON_RESPONSE)); - final AbstractExchangeAdapter.ExchangeHttpResponse statsExchangeResponse = - new AbstractExchangeAdapter.ExchangeHttpResponse( - 200, "OK", new String(encodedStats, StandardCharsets.UTF_8)); - - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD); - - PowerMock.expectPrivate( - exchangeAdapter, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, eq(TICKER), eq(null)) - .andReturn(tickerExchangeResponse); - PowerMock.expectPrivate( - exchangeAdapter, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, eq(STATS), eq(null)) - .andReturn(statsExchangeResponse); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - final Ticker ticker = exchangeAdapter.getTicker(MARKET_ID); - - assertEquals(0, ticker.getLast().compareTo(new BigDecimal("14744.9"))); - assertEquals(0, ticker.getAsk().compareTo(new BigDecimal("14744.81"))); - assertEquals(0, ticker.getBid().compareTo(new BigDecimal("14744.8"))); - assertEquals(0, ticker.getHigh().compareTo(new BigDecimal("14899.00000000"))); - assertEquals(0, ticker.getLow().compareTo(new BigDecimal("13409.97000000"))); - assertEquals(0, ticker.getOpen().compareTo(new BigDecimal("13609.53000000"))); - assertEquals(0, ticker.getVolume().compareTo(new BigDecimal("607.54445656"))); - assertNull(ticker.getVwap()); // not provided by COINBASE PRO - assertEquals(1508008776604L, (long) ticker.getTimestamp()); - - PowerMock.verifyAll(); - } - - @Test(expected = ExchangeNetworkException.class) - public void testGettingTickerHandlesExchangeNetworkException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD); - PowerMock.expectPrivate( - exchangeAdapter, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, eq(TICKER), eq(null)) - .andThrow( - new ExchangeNetworkException( - "Listen, Herr Mac, I don't know what kind of people you're used to dealing with, " - + "but nobody tells me what to do in my place.")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.getTicker(MARKET_ID); - PowerMock.verifyAll(); - } - - @Test(expected = TradingApiException.class) - public void testGettingTickerHandlesUnexpectedException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD); - PowerMock.expectPrivate( - exchangeAdapter, MOCKED_SEND_PUBLIC_REQUEST_TO_EXCHANGE_METHOD, eq(TICKER), eq(null)) - .andThrow( - new IllegalArgumentException( - "Indiana Jones. I always knew some day you'd come " - + "walking back through my door. I never doubted that. Something made it " - + "inevitable. So, what are you doing here in Nepal?")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.getTicker(MARKET_ID); - PowerMock.verifyAll(); - } - - // -------------------------------------------------------------------------- - // Non Exchange visiting tests - // -------------------------------------------------------------------------- - - @Test - public void testGettingExchangeSellingFeeIsAsExpected() { - PowerMock.replayAll(); - final CoinbaseProExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter(); - exchangeAdapter.init(exchangeConfig); - - final BigDecimal sellPercentageFee = - exchangeAdapter.getPercentageOfSellOrderTakenForExchangeFee(MARKET_ID); - assertEquals(0, sellPercentageFee.compareTo(new BigDecimal("0.0025"))); - PowerMock.verifyAll(); - } - - @Test - public void testGettingExchangeBuyingFeeIsAsExpected() { - PowerMock.replayAll(); - final CoinbaseProExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter(); - exchangeAdapter.init(exchangeConfig); - - final BigDecimal buyPercentageFee = - exchangeAdapter.getPercentageOfBuyOrderTakenForExchangeFee(MARKET_ID); - assertEquals(0, buyPercentageFee.compareTo(new BigDecimal("0.0025"))); - PowerMock.verifyAll(); - } - - @Test - public void testGettingImplNameIsAsExpected() { - PowerMock.replayAll(); - final CoinbaseProExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter(); - exchangeAdapter.init(exchangeConfig); - - assertEquals("COINBASE PRO REST API v1", exchangeAdapter.getImplName()); - PowerMock.verifyAll(); - } - - // -------------------------------------------------------------------------- - // Initialisation tests - // -------------------------------------------------------------------------- - - @Test - public void testExchangeAdapterInitialisesSuccessfully() { - PowerMock.replayAll(); - - final CoinbaseProExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter(); - exchangeAdapter.init(exchangeConfig); - assertNotNull(exchangeAdapter); - - PowerMock.verifyAll(); - } - - @Test(expected = IllegalArgumentException.class) - public void testExchangeAdapterThrowsExceptionIfPassphraseConfigIsMissing() { - PowerMock.reset(authenticationConfig); - expect(authenticationConfig.getItem("passphrase")).andReturn(null); - expect(authenticationConfig.getItem("key")).andReturn("your_client_key"); - expect(authenticationConfig.getItem("secret")).andReturn("your_client_secret"); - PowerMock.replayAll(); - - final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter(); - exchangeAdapter.init(exchangeConfig); - - PowerMock.verifyAll(); - } - - @Test(expected = IllegalArgumentException.class) - public void testExchangeAdapterThrowsExceptionIfPublicKeyConfigIsMissing() { - PowerMock.reset(authenticationConfig); - expect(authenticationConfig.getItem("passphrase")).andReturn("your_passphrase"); - expect(authenticationConfig.getItem("key")).andReturn(null); - expect(authenticationConfig.getItem("secret")).andReturn("your_client_secret"); - PowerMock.replayAll(); - - final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter(); - exchangeAdapter.init(exchangeConfig); - - PowerMock.verifyAll(); - } - - @Test(expected = IllegalArgumentException.class) - public void testExchangeAdapterThrowsExceptionIfSecretConfigIsMissing() { - PowerMock.reset(authenticationConfig); - expect(authenticationConfig.getItem("passphrase")).andReturn("your_passphrase"); - expect(authenticationConfig.getItem("key")).andReturn("your_client_key"); - expect(authenticationConfig.getItem("secret")).andReturn(null); - PowerMock.replayAll(); - - final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter(); - exchangeAdapter.init(exchangeConfig); - - PowerMock.verifyAll(); - } - - @Test(expected = IllegalArgumentException.class) - public void testExchangeAdapterThrowsExceptionIfBuyFeeIsMissing() { - PowerMock.reset(otherConfig); - expect(otherConfig.getItem("buy-fee")).andReturn(""); - expect(otherConfig.getItem("sell-fee")).andReturn("0.25"); - PowerMock.replayAll(); - - final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter(); - exchangeAdapter.init(exchangeConfig); - - PowerMock.verifyAll(); - } - - @Test(expected = IllegalArgumentException.class) - public void testExchangeAdapterThrowsExceptionIfSellFeeIsMissing() { - PowerMock.reset(otherConfig); - expect(otherConfig.getItem("buy-fee")).andReturn("0.25"); - expect(otherConfig.getItem("sell-fee")).andReturn(""); - - PowerMock.replayAll(); - final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter(); - exchangeAdapter.init(exchangeConfig); - - PowerMock.verifyAll(); - } - - @Test(expected = IllegalArgumentException.class) - public void testExchangeAdapterThrowsExceptionIfTimeoutConfigIsMissing() { - PowerMock.reset(networkConfig); - expect(networkConfig.getConnectionTimeout()).andReturn(0); - PowerMock.replayAll(); - - final ExchangeAdapter exchangeAdapter = new CoinbaseProExchangeAdapter(); - exchangeAdapter.init(exchangeConfig); - - PowerMock.verifyAll(); - } - - // -------------------------------------------------------------------------- - // Request sending tests - // - // "The rabbit-hole went straight on like a tunnel for some way, and then dipped suddenly down, - // so suddenly that Alice had not a moment to think about stopping herself before she found - // herself falling down what seemed to be a very deep well..." - // -------------------------------------------------------------------------- - - @Test - public void testSendingPublicRequestToExchangeSuccessfully() throws Exception { - final byte[] encoded = Files.readAllBytes(Paths.get(TICKER_JSON_RESPONSE)); - final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse = - new AbstractExchangeAdapter.ExchangeHttpResponse( - 200, "OK", new String(encoded, StandardCharsets.UTF_8)); - - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_MAKE_NETWORK_REQUEST_METHOD); - - final URL url = new URL(PUBLIC_API_BASE_URL + TICKER); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_MAKE_NETWORK_REQUEST_METHOD, - eq(url), - eq("GET"), - eq(null), - eq(new HashMap<>())) - .andReturn(exchangeResponse); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - final BigDecimal lastMarketPrice = exchangeAdapter.getLatestMarketPrice(MARKET_ID); - assertEquals(0, lastMarketPrice.compareTo(new BigDecimal("14744.9"))); - - PowerMock.verifyAll(); - } - - @Test(expected = ExchangeNetworkException.class) - public void testSendingPublicRequestToExchangeHandlesExchangeNetworkException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_MAKE_NETWORK_REQUEST_METHOD); - - final URL url = new URL(PUBLIC_API_BASE_URL + TICKER); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_MAKE_NETWORK_REQUEST_METHOD, - eq(url), - eq("GET"), - eq(null), - eq(new HashMap<>())) - .andThrow( - new ExchangeNetworkException("One wrong note eventually ruins the entire symphony.")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.getLatestMarketPrice(MARKET_ID); - - PowerMock.verifyAll(); - } - - @Test(expected = TradingApiException.class) - public void testSendingPublicRequestToExchangeHandlesTradingApiException() throws Exception { - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, MOCKED_MAKE_NETWORK_REQUEST_METHOD); - - final URL url = new URL(PUBLIC_API_BASE_URL + TICKER); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_MAKE_NETWORK_REQUEST_METHOD, - eq(url), - eq("GET"), - eq(null), - eq(new HashMap<>())) - .andThrow(new TradingApiException("Look on my works, ye Mighty, and despair.")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.getLatestMarketPrice(MARKET_ID); - - PowerMock.verifyAll(); - } - - @Test - @SuppressWarnings("unchecked") - public void testSendingAuthenticatedRequestToExchangeSuccessfully() throws Exception { - final byte[] encoded = Files.readAllBytes(Paths.get(NEW_SELL_ORDER_JSON_RESPONSE)); - final AbstractExchangeAdapter.ExchangeHttpResponse exchangeResponse = - new AbstractExchangeAdapter.ExchangeHttpResponse( - 200, "OK", new String(encoded, StandardCharsets.UTF_8)); - - final Map requestParamMap = new HashMap<>(); - requestParamMap.put( - "size", - new DecimalFormat("#.########", getDecimalFormatSymbols()).format(SELL_ORDER_QUANTITY)); - requestParamMap.put( - "price", new DecimalFormat("#.##", getDecimalFormatSymbols()).format(SELL_ORDER_PRICE)); - requestParamMap.put("side", "sell"); - requestParamMap.put("product_id", MARKET_ID); - - final Map requestHeaderMap = PowerMock.createPartialMock(HashMap.class, "put"); - expect(requestHeaderMap.put("Content-Type", "application/json")).andStubReturn(null); - expect(requestHeaderMap.put(eq("CB-ACCESS-KEY"), eq(KEY))).andStubReturn(null); - expect(requestHeaderMap.put(eq("CB-ACCESS-SIGN"), anyString())).andStubReturn(null); - expect(requestHeaderMap.put(eq("CB-ACCESS-TIMESTAMP"), anyString())).andStubReturn(null); - expect(requestHeaderMap.put(eq("CB-ACCESS-PASSPHRASE"), eq(PASSPHRASE))).andStubReturn(null); - PowerMock.replay(requestHeaderMap); // map needs to be in play early - - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, - MOCKED_MAKE_NETWORK_REQUEST_METHOD, - MOCKED_CREATE_REQUEST_HEADER_MAP_METHOD); - PowerMock.expectPrivate(exchangeAdapter, MOCKED_CREATE_REQUEST_HEADER_MAP_METHOD) - .andReturn(requestHeaderMap); - - final URL url = new URL(AUTHENTICATED_API_URL + NEW_ORDER); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_MAKE_NETWORK_REQUEST_METHOD, - eq(url), - eq("POST"), - eq(new GsonBuilder().create().toJson(requestParamMap)), - eq(requestHeaderMap)) - .andReturn(exchangeResponse); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - final String orderId = - exchangeAdapter.createOrder( - MARKET_ID, OrderType.SELL, SELL_ORDER_QUANTITY, SELL_ORDER_PRICE); - assertEquals("693d7ad9-e671-4d66-9911-7f75f6380134", orderId); - - PowerMock.verifyAll(); - } - - @Test(expected = ExchangeNetworkException.class) - @SuppressWarnings("unchecked") - public void testSendingAuthenticatedRequestToExchangeHandlesExchangeNetworkException() - throws Exception { - final Map requestParamMap = new HashMap<>(); - requestParamMap.put( - "size", - new DecimalFormat("#.########", getDecimalFormatSymbols()).format(SELL_ORDER_QUANTITY)); - requestParamMap.put( - "price", new DecimalFormat("#.##", getDecimalFormatSymbols()).format(SELL_ORDER_PRICE)); - requestParamMap.put("side", "sell"); - requestParamMap.put("product_id", MARKET_ID); - - final Map requestHeaderMap = PowerMock.createPartialMock(HashMap.class, "put"); - expect(requestHeaderMap.put("Content-Type", "application/json")).andStubReturn(null); - expect(requestHeaderMap.put(eq("CB-ACCESS-KEY"), eq(KEY))).andStubReturn(null); - expect(requestHeaderMap.put(eq("CB-ACCESS-SIGN"), anyString())).andStubReturn(null); - expect(requestHeaderMap.put(eq("CB-ACCESS-TIMESTAMP"), anyString())).andStubReturn(null); - expect(requestHeaderMap.put(eq("CB-ACCESS-PASSPHRASE"), eq(PASSPHRASE))).andStubReturn(null); - PowerMock.replay(requestHeaderMap); // map needs to be in play early - - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, - MOCKED_MAKE_NETWORK_REQUEST_METHOD, - MOCKED_CREATE_REQUEST_HEADER_MAP_METHOD); - PowerMock.expectPrivate(exchangeAdapter, MOCKED_CREATE_REQUEST_HEADER_MAP_METHOD) - .andReturn(requestHeaderMap); - - final URL url = new URL(AUTHENTICATED_API_URL + NEW_ORDER); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_MAKE_NETWORK_REQUEST_METHOD, - eq(url), - eq("POST"), - eq(new GsonBuilder().create().toJson(requestParamMap)), - eq(requestHeaderMap)) - .andThrow( - new ExchangeNetworkException( - "Allow me then a moment to consider. You seek your creator. " - + "I am looking at mine. I will serve you, yet you're human. " - + "You will die, I will not.")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.createOrder(MARKET_ID, OrderType.SELL, SELL_ORDER_QUANTITY, SELL_ORDER_PRICE); - - PowerMock.verifyAll(); - } - - @Test(expected = TradingApiException.class) - @SuppressWarnings("unchecked") - public void testSendingAuthenticatedRequestToExchangeHandlesTradingApiException() - throws Exception { - final Map requestParamMap = new HashMap<>(); - requestParamMap.put( - "size", - new DecimalFormat("#.########", getDecimalFormatSymbols()).format(SELL_ORDER_QUANTITY)); - requestParamMap.put( - "price", new DecimalFormat("#.##", getDecimalFormatSymbols()).format(SELL_ORDER_PRICE)); - requestParamMap.put("side", "sell"); - requestParamMap.put("product_id", MARKET_ID); - - final Map requestHeaderMap = PowerMock.createPartialMock(HashMap.class, "put"); - expect(requestHeaderMap.put("Content-Type", "application/json")).andStubReturn(null); - expect(requestHeaderMap.put(eq("CB-ACCESS-KEY"), eq(KEY))).andStubReturn(null); - expect(requestHeaderMap.put(eq("CB-ACCESS-SIGN"), anyString())).andStubReturn(null); - expect(requestHeaderMap.put(eq("CB-ACCESS-TIMESTAMP"), anyString())).andStubReturn(null); - expect(requestHeaderMap.put(eq("CB-ACCESS-PASSPHRASE"), eq(PASSPHRASE))).andStubReturn(null); - PowerMock.replay(requestHeaderMap); // map needs to be in play early - - final CoinbaseProExchangeAdapter exchangeAdapter = - PowerMock.createPartialMockAndInvokeDefaultConstructor( - CoinbaseProExchangeAdapter.class, - MOCKED_MAKE_NETWORK_REQUEST_METHOD, - MOCKED_CREATE_REQUEST_HEADER_MAP_METHOD); - PowerMock.expectPrivate(exchangeAdapter, MOCKED_CREATE_REQUEST_HEADER_MAP_METHOD) - .andReturn(requestHeaderMap); - - final URL url = new URL(AUTHENTICATED_API_URL + NEW_ORDER); - PowerMock.expectPrivate( - exchangeAdapter, - MOCKED_MAKE_NETWORK_REQUEST_METHOD, - eq(url), - eq("POST"), - eq(new GsonBuilder().create().toJson(requestParamMap)), - eq(requestHeaderMap)) - .andThrow(new TradingApiException("When you close your eyes do you dream of me?")); - - PowerMock.replayAll(); - exchangeAdapter.init(exchangeConfig); - - exchangeAdapter.createOrder(MARKET_ID, OrderType.SELL, SELL_ORDER_QUANTITY, SELL_ORDER_PRICE); - - PowerMock.verifyAll(); - } -} diff --git a/config/samples/coinbase-pro/email-alerts.yaml b/config/samples/coinbase-pro/email-alerts.yaml deleted file mode 100644 index 55b896c69..000000000 --- a/config/samples/coinbase-pro/email-alerts.yaml +++ /dev/null @@ -1,25 +0,0 @@ -############################################################################################ -# Email Alerts YAML config. -# -# - All fields are mandatory unless stated otherwise. -# - Only 1 emailAlerts block can be specified. -# - The email is sent using TLS. -# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML -# -# Sample config for using a Gmail account to send the email is shown below. -############################################################################################ ---- -emailAlerts: - - # If set to true, the bot will load the smtpConfig, and enable email alerts. - enabled: false - - # Set your SMTP details here. - smtpConfig: - host: smtp.gmail.com - tlsPort: 587 - accountUsername: your.account.username@gmail.com - accountPassword: your.account.password - fromAddress: from.addr@gmail.com - toAddress: to.addr@gmail.com - diff --git a/config/samples/coinbase-pro/engine.yaml b/config/samples/coinbase-pro/engine.yaml deleted file mode 100644 index 94731495a..000000000 --- a/config/samples/coinbase-pro/engine.yaml +++ /dev/null @@ -1,34 +0,0 @@ -############################################################################################ -# Trading Engine YAML config. -# -# - All fields are mandatory unless stated otherwise. -# - Only 1 engine block can be specified. -# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML -############################################################################################ ---- -engine: - - # A unique identifier for the bot. Value must be an alphanumeric string. - # Underscores and dashes are also permitted. - botId: my-coinbasepro-bot-1 - - # A friendly name for the bot. Value must be an alphanumeric string. Spaces are allowed. - botName: CoinbasePro Bot - - # This must be set to prevent catastrophic loss on the exchange. - # This is normally the currency you intend to hold a long position in. It should be set to the currency short code for the - # wallet, e.g. BTC, LTC, USD. This value can be case sensitive for some exchanges - check the Exchange Adapter documentation. - emergencyStopCurrency: BTC - - # This must be set to prevent a catastrophic loss on the exchange. - # The Trading Engine checks this value at the start of every trade cycle: if your emergencyStopCurrency balance on - # the trading drops below this value, the Trading Engine will stop trading on all markets and shutdown. - # Manual intervention is then required to restart the bot. You can set this value to 0 to override this check. - emergencyStopBalance: 0.7 - - # The is the interval in seconds that the Trading Engine will wait/sleep before executing - # the next trade cycle. The minimum value is 1 second. Some exchanges allow you to hit them harder than others. However, - # while their API documentation might say one thing, the reality is you might get socket timeouts and 5XX responses if you - # hit it too hard - you cannot perform ultra low latency trading over the public internet ;-) - # You'll need to experiment with the trade cycle interval for different exchanges. - tradeCycleInterval: 60 diff --git a/config/samples/coinbase-pro/exchange.yaml b/config/samples/coinbase-pro/exchange.yaml deleted file mode 100644 index 7ab94c1b0..000000000 --- a/config/samples/coinbase-pro/exchange.yaml +++ /dev/null @@ -1,66 +0,0 @@ -############################################################################################ -# Exchange Adapter YAML config. -# -# - Sample config below currently set to run against Coinbase Pro -# - All fields are mandatory unless stated otherwise. -# - BX-bot only supports running 1 exchange per bot. -# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML -# -# See the README "How do I write my own Exchange Adapter?" section for more details. -############################################################################################ ---- -exchange: - - # A friendly name for the Exchange. Value must be an alphanumeric string. Spaces are allowed. - name: Coinbase Pro - - # For the adapter value, you must specify the fully qualified name of your Exchange Adapter class so the Trading Engine - # can load and execute it. The class must be on the runtime classpath. - adapter: com.gazbert.bxbot.exchanges.CoinbaseProExchangeAdapter - - authenticationConfig: - # See: https://docs.pro.coinbase.com/#authentication to get your Coinbase Pro Trading API credentials. - passphrase: your-passphrase - key: your-api-key - secret: your-secret-key - - networkConfig: - # This value is in SECONDS. It is the timeout value that the exchange adapter will wait on socket connect/socket read - # when communicating with the exchange. Once this threshold has been breached, the exchange adapter will give up and - # throw a Trading API TimeoutException. - # - # The exchange adapter is single threaded: if one request gets blocked, it will block all subsequent requests from - # getting to the exchange. This timeout prevents an indefinite block. - # - # You'll need to experiment with values here. - connectionTimeout: 30 - - # Optional HTTP status codes that will trigger the adapter to throw a non-fatal ExchangeNetworkException - # if the exchange returns any of the below in an API call response: - nonFatalErrorCodes: [502, 503, 504, 520, 522, 525] - - # Optional java.io exception messages that will trigger the adapter to throw a non-fatal ExchangeNetworkException - # if the exchange returns any of the below in an API call response: - nonFatalErrorMessages: - - Connection reset - - Connection refused - - Remote host closed connection during handshake - - Unexpected end of file from server - - otherConfig: - # Exchange Taker Buy fee in % - # IMPORTANT - keep an eye on the fees: - # https://help.coinbase.com/en/pro/trading-and-funding/trading-rules-and-fees/fees.html - buy-fee: 0.5 - - # Exchange Taker Sell fee in % - # IMPORTANT - keep an eye on the fees: - # https://help.coinbase.com/en/pro/trading-and-funding/trading-rules-and-fees/fees.html - sell-fee: 0.5 - - # Amount of time in seconds to add to the locally calculated timestamp used to sign the message - # sent to the exchange. This allows for slight skew between the bot's local time and that - # of the exchange. See: https://docs.pro.coinbase.com/#selecting-a-timestamp - # Start with 0 and see how you get on... - time-server-bias: 0 - diff --git a/config/samples/coinbase-pro/markets.yaml b/config/samples/coinbase-pro/markets.yaml deleted file mode 100644 index 990f8c1eb..000000000 --- a/config/samples/coinbase-pro/markets.yaml +++ /dev/null @@ -1,34 +0,0 @@ -############################################################################################ -# Market YAML config. -# -# - All fields are mandatory unless stated otherwise. -# - Multiple market blocks can be listed. -# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML -############################################################################################ ---- -markets: - - # The id value is the market id as defined on the exchange, e.g. 'BTC-GBP'. - - id: BTC-GBP - - # A friendly name for the market. - # Value must be an alphanumeric string. Spaces are allowed. E.g. BTC/GBP - name: BTC/GBP - - # The baseCurrency value is the currency short code for the base currency in the currency pair. When you buy or sell a - # currency pair, you are performing that action on the base currency. The base currency is the commodity you are buying or - # selling. E.g. in a BTC/GBP market, the first currency (BTC) is the base currency and the second currency (GBP) is the - # counter currency. - baseCurrency: BTC - - # The counterCurrency value is the currency short code for the counter currency in the currency pair. This is also known - # as the quote currency. - counterCurrency: GBP - - # The enabled value allows you toggle trading on the market - config changes are only applied on startup. - enabled: true - - # The tradingStrategyId value must match a strategy id defined in your strategies.yaml config. - # Currently, BX-bot only supports 1 strategy per market. - tradingStrategyId: scalping-strategy - diff --git a/config/samples/coinbase-pro/strategies.yaml b/config/samples/coinbase-pro/strategies.yaml deleted file mode 100644 index aa953259d..000000000 --- a/config/samples/coinbase-pro/strategies.yaml +++ /dev/null @@ -1,45 +0,0 @@ -############################################################################################ -# Trading Strategy YAML config. -# -# - You configure the loading of your strategy using either a className or a beanName field. -# - All fields are mandatory unless stated otherwise. -# - Multiple strategy blocks can be listed. -# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML -# -# See the README "How do I write my own Trading Strategy?" section for full details. -############################################################################################ ---- -strategies: - - # A unique identifier for the strategy. The markets.yaml tradingStrategyId entries reference this. - # Value must be an alphanumeric string. Underscores and dashes are also permitted. E.g. my-macd-strat-1 - - id: scalping-strategy - - # A friendly name for the strategy. - # Value must be an alphanumeric string. Spaces are allowed. E.g. My Super MACD Strat - name: Basic Scalping Strat - - # The description value is optional. - description: > - A simple trend following scalper that buys at the current BID price, holds until current market price has reached - a configurable minimum percentage gain, and then sells at current ASK price, thereby taking profit from the spread. - Don't forget to factor in the exchange fees! - - # For the className value, you must specify the fully qualified name of your Strategy class for the - # Trading Engine to load and execute. This class must be on the runtime classpath. - # If you set this value to load your strategy, you cannot set the beanName value. - className: com.gazbert.bxbot.strategies.ExampleScalpingStrategy - - # For the beanName value, you must specify the Spring bean name of you Strategy component class - # for the Trading Engine to load and execute. - # You will also need to annotate your strategy class with `@Component("exampleScalpingStrategy")` - - # take a look at ExampleScalpingStrategy.java. This results in Spring injecting the bean. - # (see https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/stereotype/Component.html) - # If you set this value to load your strategy, you cannot set the className value. - #beanName: exampleScalpingStrategy - - # The configItems section is optional and allows you to set custom key/value pair config items. This config - # is passed to your Trading Strategy when the bot starts up. - configItems: - counter-currency-buy-order-amount: 20 - minimum-percentage-gain: 2 diff --git a/etc/spotbugs-exclude-filter.xml b/etc/spotbugs-exclude-filter.xml index 9efbadcfa..94bb5be95 100644 --- a/etc/spotbugs-exclude-filter.xml +++ b/etc/spotbugs-exclude-filter.xml @@ -46,12 +46,6 @@ - - - - - - @@ -76,14 +70,6 @@ - - - - - -