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

Java tutorial

Introduction

Here is the source code for com.gazbert.bxbot.exchanges.BitfinexExchangeAdapter.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.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.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.time.Instant;
import java.util.*;

/**
 * <p>
 * Exchange Adapter for integrating with the Bitfinex exchange.
 * The Bitfinex API is documented <a href="https://www.bitfinex.com/pages/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 Bitfinex, it has only been unit tested up until the point of calling the
 * {@link #sendPublicRequestToExchange(String)} and {@link #sendAuthenticatedRequestToExchange(String, Map)}
 * methods. Use it at our own risk!
 * </strong>
 * </p>
 * <p>
 * The adapter uses v1 of the Bitfinex API - it is limited to 60 API calls per minute. It only supports 'exchange'
 * accounts; it does <em>not</em> support 'trading' (margin trading) accounts or 'deposit' (liquidity SWAPs) accounts.
 * Furthermore, the adapter does not support sending 'hidden' orders.
 * </p>
 * <p>
 * There are different exchange fees for Takers and Makers - see <a href="https://www.bitfinex.com/pages/fees">here.</a>
 * This adapter will use the <em>Taker</em> fees to keep things simple for now.
 * </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 BitfinexExchangeAdapter extends AbstractExchangeAdapter implements ExchangeAdapter {

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

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

    /**
     * The public API URI.
     */
    private static final String PUBLIC_API_BASE_URL = "https://api.bitfinex.com/" + BITFINEX_API_VERSION + "/";

    /**
     * The Authenticated API URI - it is the same as the Authenticated URL as of 8 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 Bitfinex 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 PUBLIC key prop in config file.
     */
    private static final String KEY_PROPERTY_NAME = "key";

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

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

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

    /**
     * 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 Bitfinex API call responses.
     */
    private Gson gson;

    @Override
    public void init(ExchangeConfig config) {

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

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

    // ------------------------------------------------------------------------------------------------
    // Bitfinex API Calls adapted to the Trading API.
    // See https://www.bitfinex.com/pages/api
    // ------------------------------------------------------------------------------------------------

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

        try {
            final ExchangeHttpResponse response = sendPublicRequestToExchange("book/" + marketId);
            LOG.debug(() -> "Market Orders response: " + response);

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

            final List<MarketOrder> buyOrders = new ArrayList<>();
            for (BitfinexMarketOrder bitfinexBuyOrder : orderBook.bids) {
                final MarketOrder buyOrder = new MarketOrder(OrderType.BUY, bitfinexBuyOrder.price,
                        bitfinexBuyOrder.amount, bitfinexBuyOrder.price.multiply(bitfinexBuyOrder.amount));
                buyOrders.add(buyOrder);
            }

            final List<MarketOrder> sellOrders = new ArrayList<>();
            for (BitfinexMarketOrder bitfinexSellOrder : orderBook.asks) {
                final MarketOrder sellOrder = new MarketOrder(OrderType.SELL, bitfinexSellOrder.price,
                        bitfinexSellOrder.amount, bitfinexSellOrder.price.multiply(bitfinexSellOrder.amount));
                sellOrders.add(sellOrder);
            }

            return new MarketOrderBook(marketId, sellOrders, buyOrders);

        } 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<OpenOrder> getYourOpenOrders(String marketId) throws TradingApiException, ExchangeNetworkException {

        try {
            final ExchangeHttpResponse response = sendAuthenticatedRequestToExchange("orders", null);
            LOG.debug(() -> "Open Orders response: " + response);

            final BitfinexOpenOrders bitfinexOpenOrders = gson.fromJson(response.getPayload(),
                    BitfinexOpenOrders.class);

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

                final OpenOrder order = new OpenOrder(Long.toString(bitfinexOpenOrder.id),
                        // for some reason 'finex adds decimal point to long date value, e.g. "1442073766.0"  - grrrr!
                        Date.from(Instant
                                .ofEpochMilli(Integer.parseInt(bitfinexOpenOrder.timestamp.split("\\.")[0]))),
                        marketId, orderType, bitfinexOpenOrder.price, bitfinexOpenOrder.remaining_amount,
                        bitfinexOpenOrder.original_amount,
                        bitfinexOpenOrder.price.multiply(bitfinexOpenOrder.original_amount) // total - not provided by finex :-(
                );

                ordersToReturn.add(order);
            }
            return ordersToReturn;

        } catch (ExchangeNetworkException | TradingApiException e) {
            throw e;
        } catch (Exception e) {
            LOG.error(UNEXPECTED_ERROR_MSG, e);
            throw new TradingApiException(UNEXPECTED_ERROR_MSG, e);
        }
    }

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

        try {
            final Map<String, Object> params = getRequestParamMap();

            params.put("symbol", marketId);

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

            params.put("exchange", "bitfinex");

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

            // 'type' is either "market" / "limit" / "stop" / "trailing-stop" / "fill-or-kill" / "exchange market" /
            // "exchange limit" / "exchange stop" / "exchange trailing-stop" / "exchange fill-or-kill".
            // (type starting by "exchange " are exchange orders, others are margin trading orders)

            // this adapter only supports 'exchange limit orders'
            params.put("type", "exchange limit");

            // This adapter does not currently support hidden orders.
            // Exchange API notes: "true if the order should be hidden. Default is false."
            // If you try and set "is_hidden" to false, the exchange barfs and sends a 401 back. Nice.
            //params.put("is_hidden", "false");

            final ExchangeHttpResponse response = sendAuthenticatedRequestToExchange("order/new", params);
            LOG.debug(() -> "Create Order response: " + response);

            final BitfinexNewOrderResponse createOrderResponse = gson.fromJson(response.getPayload(),
                    BitfinexNewOrderResponse.class);
            final long id = createOrderResponse.order_id;
            if (id == 0) {
                final String errorMsg = "Failed to place order on exchange. Error response: " + response;
                LOG.error(errorMsg);
                throw new TradingApiException(errorMsg);
            } else {
                return Long.toString(createOrderResponse.order_id);
            }

        } 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 Map<String, Object> params = getRequestParamMap();
            params.put("order_id", Long.parseLong(orderId));

            final ExchangeHttpResponse response = sendAuthenticatedRequestToExchange("order/cancel", params);
            LOG.debug(() -> "Cancel Order response: " + response);

            // Exchange returns order id and other details if successful, a 400 HTTP Status if the order id was not recognised.
            gson.fromJson(response.getPayload(), BitfinexCancelOrderResponse.class);
            return true;

        } catch (ExchangeNetworkException | TradingApiException e) {
            if (e.getCause() != null && e.getCause().getMessage().contains("400")) {
                final String errorMsg = "Failed to cancel order on exchange. Did not recognise Order Id: "
                        + orderId;
                LOG.error(errorMsg, e);
                return false;
            } else {
                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 TradingApiException, ExchangeNetworkException {

        try {
            final ExchangeHttpResponse response = sendPublicRequestToExchange("pubticker/" + marketId);
            LOG.debug(() -> "Latest Market Price response: " + response);

            final BitfinexTicker ticker = gson.fromJson(response.getPayload(), BitfinexTicker.class);
            return ticker.last_price;

        } 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("balances", null);
            LOG.debug(() -> "Balance Info response: " + response);

            final BitfinexBalances allAccountBalances = gson.fromJson(response.getPayload(),
                    BitfinexBalances.class);
            final HashMap<String, BigDecimal> balancesAvailable = new HashMap<>();

            /*
             * The adapter only fetches the 'exchange' account balance details - this is the Bitfinex 'exchange' account,
             * i.e. the limit order trading account balance.
             */
            allAccountBalances.stream().filter(accountBalance -> accountBalance.type.equalsIgnoreCase("exchange"))
                    .forEach(accountBalance -> {
                        if (accountBalance.currency.equalsIgnoreCase("usd")) {
                            balancesAvailable.put("USD", accountBalance.available);
                        } else if (accountBalance.currency.equalsIgnoreCase("btc")) {
                            balancesAvailable.put("BTC", accountBalance.available);
                        }
                    });

            // 2nd arg of BalanceInfo constructor for reserved/on-hold balances is not provided by exchange.
            return new BalanceInfo(balancesAvailable, new HashMap<>());

        } 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 getPercentageOfBuyOrderTakenForExchangeFee(String marketId)
            throws TradingApiException, ExchangeNetworkException {

        try {
            final ExchangeHttpResponse response = sendAuthenticatedRequestToExchange("account_infos", null);
            LOG.debug(() -> "Buy Fee response: " + response);

            // Nightmare to adapt! Just take the top-level taker fees.
            final BitfinexAccountInfos bitfinexAccountInfos = gson.fromJson(response.getPayload(),
                    BitfinexAccountInfos.class);
            final BigDecimal fee = bitfinexAccountInfos.get(0).taker_fees;

            // adapt the % into BigDecimal format
            return fee.divide(new BigDecimal("100"), 8, BigDecimal.ROUND_HALF_UP);

        } 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 getPercentageOfSellOrderTakenForExchangeFee(String marketId)
            throws TradingApiException, ExchangeNetworkException {

        try {
            final ExchangeHttpResponse response = sendAuthenticatedRequestToExchange("account_infos", null);
            LOG.debug(() -> "Sell Fee response: " + response);

            // Nightmare to adapt! Just take the top-level taker fees.
            final BitfinexAccountInfos bitfinexAccountInfos = gson.fromJson(response.getPayload(),
                    BitfinexAccountInfos.class);
            final BigDecimal fee = bitfinexAccountInfos.get(0).taker_fees;

            // adapt the % into BigDecimal format
            return fee.divide(new BigDecimal("100"), 8, BigDecimal.ROUND_HALF_UP);

        } catch (ExchangeNetworkException | TradingApiException e) {
            throw e;
        } catch (Exception e) {
            LOG.error(UNEXPECTED_ERROR_MSG, e);
            throw new TradingApiException(UNEXPECTED_ERROR_MSG, e);
        }
    }

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

    // ------------------------------------------------------------------------------------------------
    //  GSON classes for JSON responses.
    //  See https://www.bitfinex.com/pages/api
    // ------------------------------------------------------------------------------------------------

    /**
     * GSON class for a market Order Book.
     */
    private static class BitfinexOrderBook {

        // field names map to the JSON arg names
        public BitfinexMarketOrder[] bids;
        public BitfinexMarketOrder[] asks;

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

    /**
     * GSON class for a Market Order.
     */
    private static class BitfinexMarketOrder {

        // field names map to the JSON arg names
        public BigDecimal price;
        public BigDecimal amount;
        public String timestamp;

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("price", price).add("amount", amount)
                    .add("timestamp", timestamp).toString();
        }
    }

    /**
     * GSON class for receiving your open orders in 'orders' API call response.
     */
    private static class BitfinexOpenOrders extends ArrayList<BitfinexOpenOrder> {
        private static final long serialVersionUID = 5516523641153401953L;
    }

    /**
     * GSON class for mapping returned order from 'orders' API call response.
     */
    private static class BitfinexOpenOrder {

        public long id;
        public String symbol;
        public String exchange;
        public BigDecimal price;
        public BigDecimal avg_execution_price;
        public String side; // e.g. "sell"
        public String type; // e.g. "exchange limit"
        public String timestamp;
        public boolean is_live;
        public boolean is_cancelled;
        public boolean is_hidden;
        public boolean was_forced;
        public BigDecimal original_amount;
        public BigDecimal remaining_amount;
        public BigDecimal executed_amount;

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("id", id).add("symbol", symbol).add("exchange", exchange)
                    .add("price", price).add("avg_execution_price", avg_execution_price).add("side", side)
                    .add("type", type).add("timestamp", timestamp).add("is_live", is_live)
                    .add("is_cancelled", is_cancelled).add("is_hidden", is_hidden).add("was_forced", was_forced)
                    .add("original_amount", original_amount).add("remaining_amount", remaining_amount)
                    .add("executed_amount", executed_amount).toString();
        }
    }

    /**
     * GSON class for a Bitfinex 'pubticker' API call response.
     */
    private static class BitfinexTicker {

        public BigDecimal mid;
        public BigDecimal bid;
        public BigDecimal ask;
        public BigDecimal last_price;
        public BigDecimal low;
        public BigDecimal high;
        public BigDecimal volume;
        public String timestamp;

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("mid", mid).add("bid", bid).add("ask", ask)
                    .add("last_price", last_price).add("low", low).add("high", high).add("volume", volume)
                    .add("timestamp", timestamp).toString();
        }
    }

    /**
     * GSON class for holding Bitfinex response from 'account_infos' API call.
     * <p>
     * This is a lot of work to just get the exchange fees!
     * <p>
     * We want the taker fees.
     * <p>
     * <pre>
     *  [
     *      {
     *          "maker_fees": "0.1",
     *          "taker_fees": "0.2",
     *          "fees": [
     *              {
     *                  "pairs": "BTC",
     *                  "maker_fees": "0.1",
     *                  "taker_fees": "0.2"
     *              },
     *              {
     *                  "pairs": "LTC",
     *                  "maker_fees": "0.1",
     *                  "taker_fees": "0.2"
     *              },
     *              {
     *                  "pairs": "DRK",
     *                  "maker_fees": "0.1",
     *                  "taker_fees": "0.2"
     *              }
     *          ]
     *      }
     *  ]
     * </pre>
     */
    private static class BitfinexAccountInfos extends ArrayList<BitfinexAccountInfo> {
        private static final long serialVersionUID = 5516521641453401953L;
    }

    /**
     * GSON class for holding Bitfinex Account Info.
     */
    private static class BitfinexAccountInfo {

        public BigDecimal maker_fees;
        public BigDecimal taker_fees;
        public BitfinexPairFees fees;

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("maker_fees", maker_fees).add("taker_fees", taker_fees)
                    .add("fees", fees).toString();
        }
    }

    /**
     * GSON class for holding Bitfinex Pair Fees.
     */
    private static class BitfinexPairFees extends ArrayList<BitfinexPairFee> {
        private static final long serialVersionUID = 1516526641473401953L;
    }

    /**
     * GSON class for holding Bitfinex Pair Fee.
     */
    private static class BitfinexPairFee {

        public String pairs;
        public BigDecimal maker_fees;
        public BigDecimal taker_fees;

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("pairs", pairs).add("maker_fees", maker_fees)
                    .add("taker_fees", taker_fees).toString();
        }
    }

    /**
     * GSON class for holding Bitfinex response from 'balances' API call.
     * <p>
     * Basically an array of BitfinexAccountBalance types.
     * <p>
     * <pre>
     * [
     * {"type":"deposit","currency":"btc","amount":"0.12347175","available":"0.001"},
     * {"type":"deposit","currency":"usd","amount":"0.0","available":"0.0"},
     * {"type":"exchange","currency":"btc","amount":"0.0","available":"0.0"},
     * {"type":"exchange","currency":"usd","amount":"0.0","available":"0.0"},
     * {"type":"trading","currency":"btc","amount":"0.0","available":"0.0"},
     * {"type":"trading","currency":"usd","amount":"0.0","available":"0.0"}
     * ]
     * </pre>
     */
    private static class BitfinexBalances extends ArrayList<BitfinexAccountBalance> {
        private static final long serialVersionUID = 5516523641953401953L;
    }

    /**
     * GSON class for holding a Bitfinex account type balance info.
     * <p>
     * There are 3 types of account: 'deposit' (swaps), 'exchange' (limit orders), 'trading' (margin).
     * <pre>
     * [
     * {"type":"deposit","currency":"btc","amount":"0.12347175","available":"0.001"},
     * {"type":"deposit","currency":"usd","amount":"0.0","available":"0.0"},
     * {"type":"exchange","currency":"btc","amount":"0.0","available":"0.0"},
     * {"type":"exchange","currency":"usd","amount":"0.0","available":"0.0"},
     * {"type":"trading","currency":"btc","amount":"0.0","available":"0.0"},
     * {"type":"trading","currency":"usd","amount":"0.0","available":"0.0"}
     * ]
     * </pre>
     */
    private static class BitfinexAccountBalance {

        // field names map to the JSON arg names
        public String type;
        public String currency;
        public BigDecimal amount;
        public BigDecimal available;

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("type", type).add("currency", currency)
                    .add("amount", amount).add("available", available).toString();
        }
    }

    /**
     * GSON class for Bitfinex 'order/new' response.
     */
    private static class BitfinexNewOrderResponse {

        public long id; // same as order_id
        public String symbol;
        public String exchange;
        public BigDecimal price;
        public BigDecimal avg_execution_price;
        public String side; // e.g. "sell"
        public String type; // e.g. "exchange limit"
        public String timestamp;
        public boolean is_live;
        public boolean is_cancelled;
        public boolean is_hidden;
        public boolean was_forced;
        public BigDecimal original_amount;
        public BigDecimal remaining_amount;
        public BigDecimal executed_amount;
        public long order_id; // same as id

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("id", id).add("symbol", symbol).add("exchange", exchange)
                    .add("price", price).add("avg_execution_price", avg_execution_price).add("side", side)
                    .add("type", type).add("timestamp", timestamp).add("is_live", is_live)
                    .add("is_cancelled", is_cancelled).add("is_hidden", is_hidden).add("was_forced", was_forced)
                    .add("original_amount", original_amount).add("remaining_amount", remaining_amount)
                    .add("executed_amount", executed_amount).add("order_id", order_id).toString();
        }
    }

    /**
     * GSON class for Bitfinex 'order/cancel' response.
     */
    private static class BitfinexCancelOrderResponse {

        public long id; // only get this param; there is no order_id
        public String symbol;
        public String exchange;
        public BigDecimal price;
        public BigDecimal avg_execution_price;
        public String side; // e.g. "sell"
        public String type; // e.g. "exchange limit"
        public String timestamp;
        public boolean is_live;
        public boolean is_cancelled;
        public boolean is_hidden;
        public boolean was_forced;
        public BigDecimal original_amount;
        public BigDecimal remaining_amount;
        public BigDecimal executed_amount;

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("id", id).add("symbol", symbol).add("exchange", exchange)
                    .add("price", price).add("avg_execution_price", avg_execution_price).add("side", side)
                    .add("type", type).add("timestamp", timestamp).add("is_live", is_live)
                    .add("is_cancelled", is_cancelled).add("is_hidden", is_hidden).add("was_forced", was_forced)
                    .add("original_amount", original_amount).add("remaining_amount", remaining_amount)
                    .add("executed_amount", executed_amount).toString();
        }
    }

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

    /**
     * Makes a public API call to the Bitfinex 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 Bitfinex exchange.
     * </p>
     * <p>
     * <pre>
     * Bitfinex Example:
     *
     * POST https://api.bitfinex.com/v1/order/new
     *
     * With JSON payload of:
     *
     * {
     *    "request": "/v1/<request-type>
     *    "nonce": "1234",
     *    "other-params : "for the request if any..."
     * }
     *
     * To authenticate a request, we must calculate the following:
     *
     * payload = request-parameters-dictionary -> JSON encode -> base64
     * signature = HMAC-SHA384(payload, api-secret) as hexadecimal in lowercase (MUST be lowercase)
     * send (api-key, payload, signature)
     *
     * These are sent as HTTP headers named:
     *
     * X-BFX-APIKEY
     * X-BFX-PAYLOAD
     * X-BFX-SIGNATURE
     * </pre>
     *
     * @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 apiMethod, Map<String, Object> 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, e.g. "balances"
                params = new HashMap<>();
            }

            // nonce is required by Bitfinex in every request
            params.put("nonce", Long.toString(nonce));
            nonce++; // increment ready for next call.

            // must include the method in request param too
            params.put("request", "/" + BITFINEX_API_VERSION + "/" + apiMethod);

            // JSON-ify the param dictionary
            final String paramsInJson = gson.toJson(params);

            // Need to base64 encode payload as per API
            final String base64payload = DatatypeConverter.printBase64Binary(paramsInJson.getBytes());

            // Request headers required by Exchange
            final Map<String, String> requestHeaders = new HashMap<>();
            // Add the public key
            requestHeaders.put("X-BFX-APIKEY", key);

            // Add Base64 encoded JSON payload
            requestHeaders.put("X-BFX-PAYLOAD", base64payload);

            // Add the signature
            mac.reset(); // force reset
            mac.update(base64payload.getBytes());

            /*
             * signature = HMAC-SHA384(payload, api-secret) as hexadecimal - MUST be in LOWERCASE else signature fails.
             * See: http://bitcoin.stackexchange.com/questions/25835/bitfinex-api-call-returns-400-bad-request
             */
            final String signature = toHex(mac.doFinal()).toLowerCase();
            requestHeaders.put("X-BFX-SIGNATURE", signature);

            // payload is JSON for this exchange
            requestHeaders.put("Content-Type", "application/json");

            final URL url = new URL(AUTHENTICATED_API_URL + apiMethod);
            return sendNetworkRequest(url, "POST", paramsInJson, requestHeaders);

        } catch (MalformedURLException | UnsupportedEncodingException e) {

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

    /**
     * Converts a given byte array to a hex String.
     *
     * @param byteArrayToConvert byte array to convert.
     * @return the string representation of the given byte array.
     * @throws UnsupportedEncodingException if the byte array encoding is not recognised.
     */
    private String toHex(byte[] byteArrayToConvert) throws UnsupportedEncodingException {

        final StringBuilder hexString = new StringBuilder();

        for (final byte aByte : byteArrayToConvert) {
            hexString.append(String.format("%02x", aByte & 0xff));
        }
        return hexString.toString();
    }

    /**
     * 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() {

        // Setup the MAC
        try {
            final SecretKeySpec keyspec = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA384");
            mac = Mac.getInstance("HmacSHA384");
            mac.init(keyspec);
            initializedMACAuthentication = true;
        } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
            final String errorMsg = "Failed to setup MAC security. HINT: Is HMAC-SHA384 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);
        key = getAuthenticationConfigItem(authenticationConfig, KEY_PROPERTY_NAME);
        secret = getAuthenticationConfigItem(authenticationConfig, SECRET_PROPERTY_NAME);
    }

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

    /**
     * Initialises the GSON layer.
     */
    private void initGson() {
        final GsonBuilder gsonBuilder = new GsonBuilder();
        gson = gsonBuilder.create();
    }

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