com.gazbert.bxbot.exchanges.ItBitExchangeAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.gazbert.bxbot.exchanges.ItBitExchangeAdapter.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 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 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.trading.api.*;
import com.google.common.base.MoreObjects;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.time.Instant;
import java.util.*;

/**
 * <p>
 * Exchange Adapter for integrating with the itBit exchange.
 * The itBit API is documented <a href="https://www.itbit.com/h/api">here</a>.
 * </p>
 * <p>
 * <strong>
 * DISCLAIMER:
 * This Exchange Adapter is provided as-is; it might have bugs in it and you could lose money. Despite running live
 * on itBit, it has only been unit tested up until the point of calling the
 * {@link #sendPublicRequestToExchange(String)} and {@link #sendAuthenticatedRequestToExchange(String, String, Map)}
 * methods. Use it at our own risk!
 * </strong>
 * </p>
 * <p>
 * The adapter only supports the REST implementation of the <a href="https://api.itbit.com/docs">Trading API</a>.
 * </p>
 * <p>The itBit exchange uses XBT for the Bitcoin currency code instead of the usual BTC. So, if you were to call
 * {@link #getBalanceInfo()}, you would need to use XBT (instead of BTC) as the key when fetching your Bitcoin balance
 * info from the returned maps.</p>
 * <p>
 * The adapter also assumes that only 1 exchange account wallet has been created on the exchange. If there is more
 * than 1, it will use the first one it finds when performing the {@link #getBalanceInfo()} call.
 * </p>
 * <p>
 * Exchange fees are loaded from the exchange.xml file on startup; they are not fetched from the exchange at
 * runtime as the itBit REST API v1 does not support this. The fees are used across all markets. Make sure you keep an
 * eye on the <a href="https://www.itbit.com/h/fees">exchange fees</a> and update the config accordingly.
 * There are different exchange fees for <a href="https://www.itbit.com/h/fees-maker-taker-model">Takers and Makers</a>
 * - this adapter will use the <em>Taker</em> fees to keep things simple for now.
 * </p>
 * <p>
 * NOTE: ItBit requires all price values to be limited to 2 decimal places and amount values to be limited to 4 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 same is done for
 * the order amount, but to 4 decimal places.
 * </p>
 * <p>
 * The Exchange Adapter is <em>not</em> 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.
 * </p>
 * <p>
 * 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 <em>all</em> other failures.
 * </p>
 *
 * @author gazbert
 * @since 1.0
 */
public final class ItBitExchangeAdapter extends AbstractExchangeAdapter implements ExchangeAdapter {

    private static final Logger LOG = LogManager.getLogger();

    /**
     * The version of the itBit API being used.
     */
    private static final String ITBIT_API_VERSION = "v1";

    /**
     * The public API URI.
     * The itBit Production Host is: https://api.itbit.com/v1/
     */
    private static final String PUBLIC_API_BASE_URL = "https://api.itbit.com/" + ITBIT_API_VERSION + "/";

    /**
     * The Authenticated API URI - it is the same as the Authenticated URL as of 25 Sep 2015.
     */
    private static final String AUTHENTICATED_API_URL = PUBLIC_API_BASE_URL;

    /**
     * Used for reporting unexpected errors.
     */
    private static final String UNEXPECTED_ERROR_MSG = "Unexpected error has occurred in itBit Exchange Adapter. ";

    /**
     * Unexpected IO error message for logging.
     */
    private static final String UNEXPECTED_IO_ERROR_MSG = "Failed to connect to Exchange due to unexpected IO error.";

    /**
     * Name of user id property in config file.
     */
    private static final String USER_ID_PROPERTY_NAME = "userId";

    /**
     * Name of PUBLIC key property in config file.
     */
    private static final String KEY_PROPERTY_NAME = "key";

    /**
     * Name of secret property in config file.
     */
    private static final String SECRET_PROPERTY_NAME = "secret";

    /**
     * Name of buy fee property in config file.
     */
    private static final String BUY_FEE_PROPERTY_NAME = "buy-fee";

    /**
     * Name of sell fee property in config file.
     */
    private static final String SELL_FEE_PROPERTY_NAME = "sell-fee";

    /**
     * Nonce used for sending authenticated messages to the exchange.
     */
    private static long nonce = 0;

    /**
     * The UUID of the wallet in use on the exchange.
     */
    private String walletId;

    /**
     * Exchange buy fees in % in {@link BigDecimal} format.
     */
    private BigDecimal buyFeePercentage;

    /**
     * Exchange sell fees in % in {@link BigDecimal} format.
     */
    private BigDecimal sellFeePercentage;

    /**
     * Used to indicate if we have initialised the MAC authentication protocol.
     */
    private boolean initializedMACAuthentication = false;

    /**
     * The user id.
     */
    private String userId = "";

    /**
     * The key used in the MAC message.
     */
    private String key = "";

    /**
     * The secret used for signing MAC message.
     */
    private String secret = "";

    /**
     * Provides the "Message Authentication Code" (MAC) algorithm used for the secure messaging layer.
     * Used to encrypt the hash of the entire message with the private key to ensure message integrity.
     */
    private Mac mac;

    /**
     * GSON engine used for parsing JSON in itBit API call responses.
     */
    private Gson gson;

    @Override
    public void init(ExchangeConfig config) {

        LOG.info(() -> "About to initialise itBit ExchangeConfig: " + config);
        setAuthenticationConfig(config);
        setNetworkConfig(config);
        setOtherConfig(config);

        nonce = System.currentTimeMillis() / 1000; // set the initial nonce used in the secure messaging.
        initSecureMessageLayer();
        initGson();
    }

    // ------------------------------------------------------------------------------------------------
    // itBit REST Trade API Calls adapted to the Trading API.
    // See https://api.itbit.com/docs
    // ------------------------------------------------------------------------------------------------

    @Override
    public String createOrder(String marketId, OrderType orderType, BigDecimal quantity, BigDecimal price)
            throws TradingApiException, ExchangeNetworkException {

        ExchangeHttpResponse response = null;

        try {

            if (walletId == null) {
                // need to fetch walletId if first API call
                getBalanceInfo();
            }

            final Map<String, String> params = getRequestParamMap();
            params.put("type", "limit");

            // note we need to limit amount to 4 decimal places else exchange will barf
            params.put("amount", new DecimalFormat("#.####").format(quantity));

            // Display param seems to be optional as per the itBit sample code:
            // https://github.com/itbit/itbit-restapi-python/blob/master/itbit_api.py - def create_order
            // params.put("display", new DecimalFormat("#.####").format(quantity)); // use the same as amount

            // note we need to limit price to 2 decimal places else exchange will barf
            params.put("price", new DecimalFormat("#.##").format(price));

            params.put("instrument", marketId);

            // This param is unique for itBit - no other Exchange Adapter I've coded requires this :-/
            // A bit hacky below, but I'm not tweaking the Trading API createOrder() call just for itBit.
            params.put("currency", marketId.substring(0, 3));

            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);
            }

            // Adapter does not using optional clientOrderIdentifier and clientOrderIdentifier params.
            // params.put("metadata", "{}");
            // params.put("clientOrderIdentifier", "id_123");

            response = sendAuthenticatedRequestToExchange("POST", "wallets/" + walletId + "/orders", params);
            if (LOG.isDebugEnabled()) {
                LOG.debug("Create Order response: " + response);
            }

            if (response.getStatusCode() == HttpURLConnection.HTTP_CREATED) {
                final ItBitNewOrderResponse itBitNewOrderResponse = gson.fromJson(response.getPayload(),
                        ItBitNewOrderResponse.class);
                return itBitNewOrderResponse.id;
            } 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) {
            final String unexpectedErrorMsg = UNEXPECTED_ERROR_MSG
                    + (response == null ? "NULL RESPONSE" : response);
            LOG.error(unexpectedErrorMsg, e);
            throw new TradingApiException(unexpectedErrorMsg, e);
        }
    }

    /*
     * marketId is not needed for cancelling orders on this exchange.
     */
    @Override
    public boolean cancelOrder(String orderId, String marketIdNotNeeded)
            throws TradingApiException, ExchangeNetworkException {

        ExchangeHttpResponse response = null;

        try {

            if (walletId == null) {
                // need to fetch walletId if first API call
                getBalanceInfo();
            }

            response = sendAuthenticatedRequestToExchange("DELETE", "wallets/" + walletId + "/orders/" + orderId,
                    null);
            if (LOG.isDebugEnabled()) {
                LOG.debug("Cancel Order response: " + response);
            }

            if (response.getStatusCode() == HttpURLConnection.HTTP_ACCEPTED) {
                gson.fromJson(response.getPayload(), ItBitCancelOrderResponse.class);
                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) {
            final String unexpectedErrorMsg = UNEXPECTED_ERROR_MSG
                    + (response == null ? "NULL RESPONSE" : response);
            LOG.error(unexpectedErrorMsg, e);
            throw new TradingApiException(unexpectedErrorMsg, e);
        }
    }

    @Override
    public List<OpenOrder> getYourOpenOrders(String marketId) throws TradingApiException, ExchangeNetworkException {

        ExchangeHttpResponse response = null;

        try {

            if (walletId == null) {
                // need to fetch walletId if first API call
                getBalanceInfo();
            }

            final Map<String, String> params = getRequestParamMap();
            params.put("status", "open"); // we only want open orders

            response = sendAuthenticatedRequestToExchange("GET", "wallets/" + walletId + "/orders", params);
            if (LOG.isDebugEnabled()) {
                LOG.debug("Open Orders response: " + response);
            }

            if (response.getStatusCode() == HttpURLConnection.HTTP_OK) {

                final ItBitYourOrder[] itBitOpenOrders = gson.fromJson(response.getPayload(),
                        ItBitYourOrder[].class);

                // adapt
                final List<OpenOrder> ordersToReturn = new ArrayList<>();
                for (final ItBitYourOrder itBitOpenOrder : itBitOpenOrders) {
                    OrderType orderType;
                    switch (itBitOpenOrder.side) {
                    case "buy":
                        orderType = OrderType.BUY;
                        break;
                    case "sell":
                        orderType = OrderType.SELL;
                        break;
                    default:
                        throw new TradingApiException(
                                "Unrecognised order type received in getYourOpenOrders(). Value: "
                                        + itBitOpenOrder.side);
                    }

                    final OpenOrder order = new OpenOrder(itBitOpenOrder.id,
                            Date.from(Instant.parse(itBitOpenOrder.createdTime)), // format: 2015-10-01T18:10:39.3930000Z
                            marketId, orderType, itBitOpenOrder.price,
                            itBitOpenOrder.amount.subtract(itBitOpenOrder.amountFilled), // remaining - not provided by itBit
                            itBitOpenOrder.amount, itBitOpenOrder.price.multiply(itBitOpenOrder.amount)); // total - not provided by itBit

                    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) {
            final String unexpectedErrorMsg = UNEXPECTED_ERROR_MSG
                    + (response == null ? "NULL RESPONSE" : response);
            LOG.error(unexpectedErrorMsg, e);
            throw new TradingApiException(unexpectedErrorMsg, e);
        }
    }

    @Override
    public MarketOrderBook getMarketOrders(String marketId) throws TradingApiException, ExchangeNetworkException {

        ExchangeHttpResponse response = null;

        try {
            response = sendPublicRequestToExchange("markets/" + marketId + "/order_book");
            if (LOG.isDebugEnabled()) {
                LOG.debug("Market Orders response: " + response);
            }

            if (response.getStatusCode() == HttpURLConnection.HTTP_OK) {

                final ItBitOrderBookWrapper orderBook = gson.fromJson(response.getPayload(),
                        ItBitOrderBookWrapper.class);

                final List<MarketOrder> buyOrders = new ArrayList<>();
                for (ItBitMarketOrder itBitBuyOrder : orderBook.bids) {
                    final MarketOrder buyOrder = new MarketOrder(OrderType.BUY, itBitBuyOrder.get(0),
                            itBitBuyOrder.get(1), itBitBuyOrder.get(0).multiply(itBitBuyOrder.get(1)));
                    buyOrders.add(buyOrder);
                }

                final List<MarketOrder> sellOrders = new ArrayList<>();
                for (ItBitMarketOrder itBitSellOrder : orderBook.asks) {
                    final MarketOrder sellOrder = new MarketOrder(OrderType.SELL, itBitSellOrder.get(0),
                            itBitSellOrder.get(1), itBitSellOrder.get(0).multiply(itBitSellOrder.get(1)));
                    sellOrders.add(sellOrder);
                }

                return new MarketOrderBook(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) {
            final String unexpectedErrorMsg = UNEXPECTED_ERROR_MSG
                    + (response == null ? "NULL RESPONSE" : response);
            LOG.error(unexpectedErrorMsg, e);
            throw new TradingApiException(unexpectedErrorMsg, e);
        }
    }

    @Override
    public BigDecimal getLatestMarketPrice(String marketId) throws TradingApiException, ExchangeNetworkException {

        ExchangeHttpResponse response = null;

        try {

            response = sendPublicRequestToExchange("markets/" + marketId + "/ticker");
            if (LOG.isDebugEnabled()) {
                LOG.debug("Latest Market Price response: " + response);
            }

            if (response.getStatusCode() == HttpURLConnection.HTTP_OK) {

                final ItBitTicker itBitTicker = gson.fromJson(response.getPayload(), ItBitTicker.class);
                return itBitTicker.lastPrice;
            } 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) {
            final String unexpectedErrorMsg = UNEXPECTED_ERROR_MSG
                    + (response == null ? "NULL RESPONSE" : response);
            LOG.error(unexpectedErrorMsg, e);
            throw new TradingApiException(unexpectedErrorMsg, e);
        }
    }

    @Override
    public BalanceInfo getBalanceInfo() throws TradingApiException, ExchangeNetworkException {

        ExchangeHttpResponse response = null;

        try {

            final Map<String, String> params = getRequestParamMap();
            params.put("userId", userId);

            response = sendAuthenticatedRequestToExchange("GET", "wallets", params);
            if (LOG.isDebugEnabled()) {
                LOG.debug("Balance Info response: " + response);
            }

            if (response.getStatusCode() == HttpURLConnection.HTTP_OK) {

                final ItBitWallet[] itBitWallets = gson.fromJson(response.getPayload(), ItBitWallet[].class);

                // assume only 1 trading account wallet being used on exchange
                final ItBitWallet exchangeWallet = itBitWallets[0];

                /*
                 * If this is the first time to fetch the balance/wallet info, store the wallet UUID for future calls.
                 * The Trading Engine will always call this method first, before any user Trading Strategies are invoked,
                 * so any of the other Trading API methods that rely on the wallet UUID will be satisfied.
                 */
                if (walletId == null) {
                    walletId = exchangeWallet.id;
                }

                // adapt
                final Map<String, BigDecimal> balancesAvailable = new HashMap<>();
                final List<ItBitBalance> balances = exchangeWallet.balances;
                for (final ItBitBalance balance : balances) {
                    balancesAvailable.put(balance.currency, balance.availableBalance);
                }

                // 2nd arg of BalanceInfo constructor for reserved/on-hold balances is not provided by exchange.
                return new BalanceInfo(balancesAvailable, new HashMap<>());
            } 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) {
            final String unexpectedErrorMsg = UNEXPECTED_ERROR_MSG
                    + (response == null ? "NULL RESPONSE" : response);
            LOG.error(unexpectedErrorMsg, e);
            throw new TradingApiException(unexpectedErrorMsg, e);
        }
    }

    @Override
    public BigDecimal getPercentageOfBuyOrderTakenForExchangeFee(String marketId)
            throws TradingApiException, ExchangeNetworkException {

        // itBit does not provide API call for fetching % buy fee; it only provides the fee monetary value for a
        // given order via /wallets/{walletId}/trades API call. We load the % fee statically from exchange.xml file.
        return buyFeePercentage;
    }

    @Override
    public BigDecimal getPercentageOfSellOrderTakenForExchangeFee(String marketId)
            throws TradingApiException, ExchangeNetworkException {

        // itBit does not provide API call for fetching % sell fee; it only provides the fee monetary value for a
        // given order via/wallets/{walletId}/trades API call. We load the % fee statically from exchange.xml file.
        return sellFeePercentage;
    }

    @Override
    public String getImplName() {
        return "itBit REST API v1";
    }

    // ------------------------------------------------------------------------------------------------
    //  GSON classes for JSON responses.
    //  See https://api.itbit.com/docs
    // ------------------------------------------------------------------------------------------------

    /**
     * <p>
     * GSON class for holding itBit order returned from:
     * "Cancel Order" /wallets/{walletId}/orders/{orderId} API call.
     * </p>
     * <p>
     * <p>
     * No payload returned by exchange on success.
     * </p>
     */
    private static class ItBitCancelOrderResponse {
    }

    /**
     * <p>
     * GSON class for holding itBit new order response from:
     * "Create New Order" POST /wallets/{walletId}/orders API call.
     * </p>
     * <p>
     * <p>
     * It is exactly the same as order returned in Get Orders response.
     * </p>
     */
    private static class ItBitNewOrderResponse extends ItBitYourOrder {
    }

    /**
     * GSON class for holding itBit order returned from:
     * "Get Orders" /wallets/{walletId}/orders{?instrument,page,perPage,status} API call.
     */
    private static class ItBitYourOrder {

        public String id;
        public String walletId;
        public String side; // 'buy' or 'sell'
        public String instrument; // the marketId e.g. 'XBTUSD'
        public String type; // order type e.g. "limit"
        public BigDecimal amount; // the original amount
        public BigDecimal displayAmount; // ??? not documented in the REST API
        public BigDecimal price;
        public BigDecimal volumeWeightedAveragePrice;
        public BigDecimal amountFilled;
        public String createdTime; // e.g. "2015-10-01T18:10:39.3930000Z"
        public String status; // e.g. "open"
        public ItBitOrderMetadata metadata; // {} value returned - no idea what this is
        public String clientOrderIdentifier; // cool - broker support :-)

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("id", id).add("walletId", walletId).add("side", side)
                    .add("instrument", instrument).add("type", type).add("amount", amount)
                    .add("displayAmount", displayAmount).add("price", price)
                    .add("volumeWeightedAveragePrice", volumeWeightedAveragePrice).add("amountFilled", amountFilled)
                    .add("createdTime", createdTime).add("status", status).add("metadata", metadata)
                    .add("clientOrderIdentifier", clientOrderIdentifier).toString();
        }
    }

    /**
     * GSON class for holding Your Order metadata. No idea what this is / or gonna be...
     */
    private static class ItBitOrderMetadata {
    }

    /**
     * GSON class for holding itBit ticker returned from:
     * "Get Order Book" /markets/{tickerSymbol}/order_book API call.
     */
    private static class ItBitOrderBookWrapper {

        public List<ItBitMarketOrder> bids;
        public List<ItBitMarketOrder> asks;

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("bids", bids).add("asks", asks).toString();
        }
    }

    /**
     * GSON class for holding Market Orders. First element in array is price, second element is amount.
     */
    private static class ItBitMarketOrder extends ArrayList<BigDecimal> {
        private static final long serialVersionUID = -4959711260747077759L;
    }

    /**
     * GSON class for holding itBit ticker returned from:
     * "Get Ticker" /markets/{tickerSymbol}/ticker API call.
     */
    private static class ItBitTicker {

        // field names map to the JSON arg names
        public String pair; // e.g. XBTUSD
        public BigDecimal bid;
        public BigDecimal bidAmt;
        public BigDecimal ask;
        public BigDecimal askAmt;
        public BigDecimal lastPrice; // we only wants this precious
        public BigDecimal lastAmt;
        public BigDecimal lastvolume24hAmt;
        public BigDecimal volumeToday;
        public BigDecimal high24h;
        public BigDecimal low24h;
        public BigDecimal highToday;
        public BigDecimal lowToday;
        public BigDecimal openToday;
        public BigDecimal vwapToday;
        public BigDecimal vwap24h;
        public String serverTimeUTC;

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("pair", pair).add("bid", bid).add("bidAmt", bidAmt)
                    .add("ask", ask).add("askAmt", askAmt).add("lastPrice", lastPrice).add("lastAmt", lastAmt)
                    .add("lastvolume24hAmt", lastvolume24hAmt).add("volumeToday", volumeToday)
                    .add("high24h", high24h).add("low24h", low24h).add("highToday", highToday)
                    .add("lowToday", lowToday).add("openToday", openToday).add("vwapToday", vwapToday)
                    .add("vwap24h", vwap24h).add("serverTimeUTC", serverTimeUTC).toString();
        }
    }

    /**
     * GSON class for holding itBit wallets returned from:
     * "Get All Wallets" /wallets{?userId,page,perPage} API call.
     */
    private static class ItBitWallet {

        public String id;
        public String userId;
        public String name;
        public List<ItBitBalance> balances;

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("id", id).add("userId", userId).add("name", name)
                    .add("balances", balances).toString();
        }
    }

    /**
     * GSON class for holding itBit wallet balances.
     */
    private static class ItBitBalance {

        public BigDecimal availableBalance;
        public BigDecimal totalBalance;
        public String currency; // e.g. USD

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("availableBalance", availableBalance)
                    .add("totalBalance", totalBalance).add("currency", currency).toString();
        }
    }

    // ------------------------------------------------------------------------------------------------
    //  Transport layer
    // ------------------------------------------------------------------------------------------------

    /**
     * Makes a public API call to the itBit exchange.
     *
     * @param apiMethod the API method to call.
     * @return the response from the exchange.
     * @throws ExchangeNetworkException if there is a network issue connecting to exchange.
     * @throws TradingApiException      if anything unexpected happens.
     */
    private ExchangeHttpResponse sendPublicRequestToExchange(String apiMethod)
            throws ExchangeNetworkException, TradingApiException {

        // Request headers required by Exchange
        final Map<String, String> requestHeaders = new HashMap<>();
        requestHeaders.put("Content-Type", "application/x-www-form-urlencoded");

        try {

            final URL url = new URL(PUBLIC_API_BASE_URL + apiMethod);
            return sendNetworkRequest(url, "GET", null, requestHeaders);

        } catch (MalformedURLException e) {
            final String errorMsg = UNEXPECTED_IO_ERROR_MSG;
            LOG.error(errorMsg, e);
            throw new TradingApiException(errorMsg, e);
        }
    }

    /**
     * <p>
     * Makes an authenticated API call to the itBit exchange.
     * </p>
     * <p>
     * <p>
     * Quite complex, but well documented
     * <a href="https://api.itbit.com/docs#faq-2.-how-do-i-sign-a-request?">here.</a>
     * </p>
     *
     * @param httpMethod the HTTP method to use, e.g. GET, POST, DELETE
     * @param apiMethod  the API method to call.
     * @param params     the query param args to use in the API call.
     * @return the response from the exchange.
     * @throws ExchangeNetworkException if there is a network issue connecting to exchange.
     * @throws TradingApiException      if anything unexpected happens.
     */
    private ExchangeHttpResponse sendAuthenticatedRequestToExchange(String httpMethod, String apiMethod,
            Map<String, String> 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 {

            // Generate new UNIX time in secs
            final String unixTime = Long.toString(System.currentTimeMillis());

            // increment nonce for use in this call
            nonce++;

            if (params == null) {
                // create empty map for non-param API calls
                params = new HashMap<>();
            }

            /*
             * Construct an array of UTF-8 encoded strings. That array should contain, in order,
             * the http verb of the request being signed (e.g. GET?), the full url of the request,
             * the body of the message being sent, the nonce as a string, and the timestamp as a string.
             * If the request has no body, an empty string should be used.
             */
            final String invocationUrl;
            String requestBody = "";
            String requestBodyForSignature = "";
            final List<String> signatureParamList = new ArrayList<>();
            signatureParamList.add(httpMethod);

            switch (httpMethod) {

            case "GET":
                LOG.debug(() -> "Building secure GET request...");

                // Build (optional) query param string
                final StringBuilder queryParamBuilder = new StringBuilder();
                for (final String param : params.keySet()) {
                    if (queryParamBuilder.length() > 0) {
                        queryParamBuilder.append("&");
                    }
                    //noinspection deprecation
                    // Don't URL encode as it messed up the UUID params, e.g. wallet id
                    //queryParams += param + "=" + URLEncoder.encode(params.get(param));
                    queryParamBuilder.append(param).append("=").append(params.get(param));
                }

                final String queryParams = queryParamBuilder.toString();
                LOG.debug(() -> "Query param string: " + queryParams);

                if (params.isEmpty()) {
                    invocationUrl = AUTHENTICATED_API_URL + apiMethod;
                    signatureParamList.add(invocationUrl);
                } else {
                    invocationUrl = AUTHENTICATED_API_URL + apiMethod + "?" + queryParams;
                    signatureParamList.add(invocationUrl);
                }

                signatureParamList.add(requestBodyForSignature); // request body is empty JSON string for a GET
                break;

            case "POST":
                LOG.debug(() -> "Building secure POST request...");

                invocationUrl = AUTHENTICATED_API_URL + apiMethod;
                signatureParamList.add(invocationUrl);

                requestBody = gson.toJson(params);
                signatureParamList.add(requestBody);
                break;

            case "DELETE":
                LOG.debug(() -> "Building secure DELETE request...");

                invocationUrl = AUTHENTICATED_API_URL + apiMethod;
                signatureParamList.add(invocationUrl);
                signatureParamList.add(requestBodyForSignature); // request body is empty JSON string for a DELETE
                break;

            default:
                throw new IllegalArgumentException("Don't know how to build secure [" + httpMethod + "] request!");
            }

            // Add the nonce
            signatureParamList.add(Long.toString(nonce));

            // Add the UNIX time
            signatureParamList.add(unixTime);

            /*
             * Convert that array to JSON, encoded as UTF-8. The resulting JSON should contain no spaces or other
             * whitespace characters. For example, a valid JSON-encoded array might look like:
             * '["GET","https://api.itbit.com/v1/wallets/7e037345-1288-4c39-12fe-d0f99a475a98","","5","1405385860202"]'
             */
            final String signatureParamsInJson = gson.toJson(signatureParamList);
            LOG.debug(() -> "Signature params in JSON: " + signatureParamsInJson);

            // Prepend the string version of the nonce to the JSON-encoded array string
            final String noncePrependedToJson = Long.toString(nonce) + signatureParamsInJson;

            // Construct the SHA-256 hash of the noncePrependedToJson. Call this the message hash.
            final MessageDigest md = MessageDigest.getInstance("SHA-256");
            md.update(noncePrependedToJson.getBytes());
            final byte[] messageHash = md.digest();

            // Prepend the UTF-8 encoded request URL to the message hash.
            // Generate the SHA-512 HMAC of the prependRequestUrlToMsgHash using your API secret as the key.
            mac.reset(); // force reset
            mac.update(invocationUrl.getBytes());
            mac.update(messageHash);

            final String signature = DatatypeConverter.printBase64Binary(mac.doFinal());

            // Request headers required by Exchange
            final Map<String, String> requestHeaders = new HashMap<>();
            requestHeaders.put("Content-Type", "application/json");

            // Add Authorization header
            // Generate the authorization header by concatenating the client key with a colon separator (:)
            // and the signature. The resulting string should look like "clientkey:signature".
            requestHeaders.put("Authorization", key + ":" + signature);

            requestHeaders.put("X-Auth-Timestamp", unixTime);
            requestHeaders.put("X-Auth-Nonce", Long.toString(nonce));

            final URL url = new URL(invocationUrl);
            return sendNetworkRequest(url, httpMethod, requestBody, requestHeaders);

        } catch (MalformedURLException e) {
            final String errorMsg = UNEXPECTED_IO_ERROR_MSG;
            LOG.error(errorMsg, e);
            throw new TradingApiException(errorMsg, e);

        } catch (NoSuchAlgorithmException e) {
            final String errorMsg = "Failed to create SHA-256 digest when building message signature.";
            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.
     * We fail hard n fast if any of this stuff blows.
     */
    private void initSecureMessageLayer() {

        try {
            final SecretKeySpec keyspec = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA512");
            mac = Mac.getInstance("HmacSHA512");
            mac.init(keyspec);
            initializedMACAuthentication = true;
        } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
            final String errorMsg = "Failed to setup MAC security. HINT: Is HMAC-SHA512 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);
        userId = getAuthenticationConfigItem(authenticationConfig, USER_ID_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,
                BigDecimal.ROUND_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,
                BigDecimal.ROUND_HALF_UP);
        LOG.info(() -> "Sell fee % in BigDecimal format: " + sellFeePercentage);
    }

    // ------------------------------------------------------------------------------------------------
    //  Util methods
    // ------------------------------------------------------------------------------------------------

    /**
     * Initialises the GSON layer.
     */
    private void initGson() {

        // We need to disable HTML escaping for this adapter else GSON will change = to unicode for query strings, e.g.
        // https://api.itbit.com/v1/wallets?userId=56DA621F --> https://api.itbit.com/v1/wallets?userId\u003d56DA621F
        final GsonBuilder gsonBuilder = new GsonBuilder().disableHtmlEscaping();
        gson = gsonBuilder.create();
    }

    /*
     * Hack for unit-testing map params passed to transport layer.
     */
    private Map<String, String> getRequestParamMap() {
        return new HashMap<>();
    }
}