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

Java tutorial

Introduction

Here is the source code for com.gazbert.bxbot.exchanges.BtceExchangeAdapter.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.*;
import com.google.gson.annotations.SerializedName;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.util.*;
import java.util.Map.Entry;

/**
 * <p>
 * Exchange Adapter for integrating with the BTC-e exchange.
 * The BTC-e API is documented <a href="https://btc-e.com/api/documentation">here</a> and
 * <a href="https://btc-e.com/page/2">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 BTC-e, it has only been unit tested up until the point of calling the
 * {@link #sendPublicRequestToExchange(String, String)} and {@link #sendAuthenticatedRequestToExchange(String, Map)}
 * methods. Use it at our own risk!
 * </strong>
 * </p>
 * <p>
 * The BTC-e trading API is 'unique' and difficult to adapt! Apologies for the shocking code coming up... ;-o
 * </p>
 * <p>
 * This 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 BtceExchangeAdapter extends AbstractExchangeAdapter implements ExchangeAdapter {

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

    /**
     * The Authenticated API URI.
     */
    private static final String AUTHENTICATED_API_URL = "https://btc-e.com/tapi";

    /**
     * The Public API URI.
     */
    private static final String PUBLIC_API_BASE_URL = "https://btc-e.com/api/3/";

    /**
     * Used for reporting unexpected errors.
     */
    private static final String UNEXPECTED_ERROR_MSG = "Unexpected error has occurred in BTC-e 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 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";

    /**
     * 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 public key used in MAC authentication.
     */
    private String key = "";

    /**
     * The secret used in MAC authentication.
     */
    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 BTC-e API call responses.
     */
    private Gson gson;

    @Override
    public void init(ExchangeConfig config) {

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

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

    // ------------------------------------------------------------------------------------------------
    // BTC-e API Calls adapted to the Trading API.
    // See https://btc-e.com/api/documentation and https://btc-e.com/page/2
    // ------------------------------------------------------------------------------------------------

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

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

            final BtceMarketOrderBookWrapper marketOrderWrapper = gson.fromJson(response.getPayload(),
                    BtceMarketOrderBookWrapper.class);

            final List<MarketOrder> buyOrders = new ArrayList<>();
            final List<List<BigDecimal>> btceBuyOrders = marketOrderWrapper.orderBook.bids;
            for (final List<BigDecimal> order : btceBuyOrders) {
                final MarketOrder buyOrder = new MarketOrder(OrderType.BUY, order.get(0), // price
                        order.get(1), // quantity
                        order.get(0).multiply(order.get(1)));
                buyOrders.add(buyOrder);
            }

            final List<MarketOrder> sellOrders = new ArrayList<>();
            final List<List<BigDecimal>> btceSellOrders = marketOrderWrapper.orderBook.asks;
            for (final List<BigDecimal> order : btceSellOrders) {
                final MarketOrder sellOrder = new MarketOrder(OrderType.SELL, order.get(0), // price
                        order.get(1), // quantity
                        order.get(0).multiply(order.get(1)));
                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 Map<String, String> params = getRequestParamMap();
            params.put("pair", marketId);

            final ExchangeHttpResponse response = sendAuthenticatedRequestToExchange("ActiveOrders", params);
            LOG.debug(() -> "Open Orders response: " + response);

            final BtceOpenOrderResponseWrapper myOpenOrders = gson.fromJson(response.getPayload(),
                    BtceOpenOrderResponseWrapper.class);

            if (myOpenOrders.success == 1) {

                final List<OpenOrder> ordersToReturn = new ArrayList<>();

                // this sucks!
                if (myOpenOrders.error.equalsIgnoreCase("no orders")) {
                    LOG.debug(() -> "No Open Orders");

                } else {
                    LOG.debug(() -> "BtceOpenOrderResponseWrapper: " + myOpenOrders);

                    // adapt - and we really have to this time... jeez...
                    final BtceOpenOrders btceOrders = myOpenOrders.openOrders;
                    final Set<Entry<Long, BtceOpenOrder>> entries = btceOrders.entrySet();
                    for (final Entry<Long, BtceOpenOrder> entry : entries) {
                        final Long orderId = entry.getKey();
                        final BtceOpenOrder orderDetails = entry.getValue();

                        OrderType orderType;
                        final String btceOrderType = orderDetails.type;
                        if (btceOrderType.equalsIgnoreCase(OrderType.BUY.getStringValue())) {
                            orderType = OrderType.BUY;
                        } else if (btceOrderType.equalsIgnoreCase(OrderType.SELL.getStringValue())) {
                            orderType = OrderType.SELL;
                        } else {
                            throw new TradingApiException(
                                    "Unrecognised order type received in getYourOpenOrders(). Value: "
                                            + btceOrderType);
                        }

                        final OpenOrder order = new OpenOrder(Long.toString(orderId),
                                new Date(orderDetails.timestamp_created), marketId, orderType, orderDetails.rate,
                                orderDetails.amount, null, // orig_quantity - not provided by btce :-(
                                orderDetails.rate.multiply(orderDetails.amount) // total - not provided by btce :-(
                        );

                        ordersToReturn.add(order);
                    }
                }
                return ordersToReturn;

            } else {

                final String errorMsg = "Failed to get Open Orders from exchange. Error response: " + 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 String createOrder(String marketId, OrderType orderType, BigDecimal quantity, BigDecimal price)
            throws TradingApiException, ExchangeNetworkException {

        try {
            final Map<String, String> params = getRequestParamMap();
            params.put("pair", marketId);

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

            final ExchangeHttpResponse response;
            if (orderType == OrderType.BUY) {
                // buying BTC
                params.put("type", "buy");
                response = sendAuthenticatedRequestToExchange("Trade", params);
            } else if (orderType == OrderType.SELL) {
                // selling BTC
                params.put("type", "sell");
                response = sendAuthenticatedRequestToExchange("Trade", params);
            } 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);
            }

            LOG.debug(() -> "Create Order response: " + response);

            final BtceCreateOrderResponseWrapper createOrderResponseWrapper = gson.fromJson(response.getPayload(),
                    BtceCreateOrderResponseWrapper.class);

            if (createOrderResponseWrapper.success == 1) {
                final long orderId = createOrderResponseWrapper.orderResponse.order_id;
                if (orderId == 0) {
                    // PATCH for BTC-e returning 0 for orderIds during 17-24 July 2014... even though order was successful!
                    // This is because my EMA strat buys @ ASK price and sells @ BID price so orders fill immediately...
                    // BTC-e returns 0 because order no longer exists - it has filled instantly and therefore has no ID.
                    LOG.error("PATCH code: BTC-e returned 0 for orderId - it must have been instantly filled!");

                    // we generate our own unique ID in the adapter for the order.
                    // This ID won't match any existing orders, so next trade cycle in strategy will assume (correctly)
                    // that order has filled.
                    return "INSTAFILL__" + UUID.randomUUID().toString();
                }
                return Long.toString(orderId);

            } else {
                final String errorMsg = "Failed to place order on exchange. Error response: "
                        + createOrderResponseWrapper.error;
                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 Map<String, String> params = getRequestParamMap();
            params.put("order_id", orderId);

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

            final BtceCancelledOrderWrapper cancelOrderResponse = gson.fromJson(response.getPayload(),
                    BtceCancelledOrderWrapper.class);

            if (cancelOrderResponse.success == 0) {
                LOG.error("Failed to cancel order: " + cancelOrderResponse);
                return false;
            } else {
                return true;
            }
        } 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 TradingApiException, ExchangeNetworkException {

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

            final BtceTickerWrapper btceTicker = gson.fromJson(response.getPayload(), BtceTickerWrapper.class);
            return btceTicker.ticker.last;

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

            final BtceInfoWrapper info = gson.fromJson(response.getPayload(), BtceInfoWrapper.class);

            if (info.success == 1) {
                final HashMap<String, BigDecimal> balancesAvailable = new HashMap<>();
                for (final Entry<String, BigDecimal> fund : info.info.funds.entrySet()) {
                    balancesAvailable.put(fund.getKey().toUpperCase(), fund.getValue());
                }

                // 2nd arg of reserved balances not provided by exchange.
                return new BalanceInfo(balancesAvailable, new HashMap<>());

            } else {
                final String errorMsg = "Failed to get Balance Info from exchange. Error response: " + 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 getPercentageOfBuyOrderTakenForExchangeFee(String marketId)
            throws TradingApiException, ExchangeNetworkException {

        try {
            final ExchangeHttpResponse response = sendPublicRequestToExchange("fee", marketId);
            LOG.debug(() -> "Buy Fee response: " + response);

            final BtceFees fees = gson.fromJson(response.getPayload(), BtceFees.class);

            // adapt the % into BigDecimal format
            return fees.get(marketId).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 = sendPublicRequestToExchange("fee", marketId);
            LOG.debug(() -> "Sell Fee response: " + response);

            final BtceFees fees = gson.fromJson(response.getPayload(), BtceFees.class);

            // adapt the % into BigDecimal format
            return fees.get(marketId).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 "BTC-e API v1";
    }

    // ------------------------------------------------------------------------------------------------
    //  GSON classes for JSON responses.
    //  See https://btc-e.com/api/documentation and  https://btc-e.com/page/2
    // ------------------------------------------------------------------------------------------------

    /**
     * GSON base class for API call requests and responses.
     */
    private static class BtceMessageBase {
        // field names map to the JSON arg names
        public int success;
        public String error;

        BtceMessageBase() {
            error = "";
        }

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

    /**
     * GSON class for wrapping BTC-e Info response from getInfo() API call.
     */
    private static class BtceInfoWrapper extends BtceMessageBase {
        @SerializedName("return")
        public BtceInfo info;

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

    /**
     * GSON class for holding BTC-e info response from getInfo() API call.
     */
    private static class BtceInfo {

        // field names map to the JSON arg names
        public BtceFunds funds;
        public BtceRights rights;
        public int transaction_count;
        public int open_orders;
        public String server_time;

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("funds", funds).add("rights", rights)
                    .add("transaction_count", transaction_count).add("open_orders", open_orders)
                    .add("server_time", server_time).toString();
        }
    }

    /**
     * GSON class for holding fund balances - basically a GSON enabled map.
     * Keys into map are 'usd' and 'btc' for the bits we're interested in.
     * Values are BigDec's.
     */
    private static class BtceFunds extends HashMap<String, BigDecimal> {
        private static final long serialVersionUID = 5516523641453401953L;
    }

    /**
     * GSON class for holding access rights - basically a GSON enabled map.
     * Keys are 'info' and 'trade'. We expect both to be set to 1 (i.e. true).
     */
    private static class BtceRights extends HashMap<String, BigDecimal> {
        private static final long serialVersionUID = 5353335214767688903L;
    }

    /**
     * GSON class for the Order Book wrapper.
     */
    private static class BtceMarketOrderBookWrapper extends BtceMessageBase {
        // field names map to the JSON arg names)
        @SerializedName("btc_usd")
        BtceOrderBook orderBook;

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

    /**
     * GSON class for holding BTC-e Order Book response from depth API call.
     * <p>
     * JSON looks like:
     * <pre>
     *   {
     *     "asks":[[567.711,1.97874609],[568.226,0.018],[569,1.192759],[569.234,0.04312865],[576.418,28.68]],
     *     "bids":[[567.37,0.03607374],[566.678,0.04321508],[566.615,0.5],[566.502,0.02489897],[557.8,0.363]]
     *   }
     * </pre>
     * Each is a list of open orders and each order is represented as a list of price and amount.
     */
    private static class BtceOrderBook {
        // field names map to the JSON arg names
        public List<List<BigDecimal>> bids;
        public List<List<BigDecimal>> asks;

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

    /**
     * GSON class for holding BTC-e create order response wrapper from Trade API call.
     */
    private static class BtceCreateOrderResponseWrapper extends BtceMessageBase {
        @SerializedName("return")
        BtceCreateOrderResponse orderResponse;

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

    /**
     * GSON class for holding BTC-e create order response details from Trade API call.
     */
    private static class BtceCreateOrderResponse extends BtceCreateOrderResponseWrapper {

        public BigDecimal received;
        public BigDecimal remains;
        public long order_id;
        public BtceFunds funds;

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("received", received).add("remains", remains)
                    .add("order_id", order_id).add("funds", funds).toString();
        }
    }

    /**
     * <p>
     * GSON class for holding BTC-e open order response wrapper from ActiveOrders API call.
     * </p>
     * <p>
     * <p>
     * JSON response when we don't have open orders is:
     * <pre>
     * {"success":0,"error":"no orders"}
     * </pre>
     * Error? WTF?!
     * </p>
     * <p>
     * <p>
     * JSON response when we have open orders is:
     * <pre>
     * {
     *   "success":1,
     *   "return":{
     *     "253356918":{
     *       "pair":"btc_usd","type":"sell","amount":0.01000000,"rate":641.00000000,
     *       "timestamp_created":1401653697,"status":0
     *     }
     *   }
     * }
     * </pre>
     * </p>
     */
    private static class BtceOpenOrderResponseWrapper extends BtceMessageBase {

        @SerializedName("return")
        public BtceOpenOrders openOrders;

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

    /**
     * GSON class for holding open orders - basically a GSON enabled map.
     * Keys into map is the orderId.
     */
    private static class BtceOpenOrders extends HashMap<Long, BtceOpenOrder> {
        private static final long serialVersionUID = 2298531396505507808L;
    }

    /**
     * GSON class for holding BTC-e open order response details from ActiveOrders API call.
     */
    private static class BtceOpenOrder {

        public String pair; // market id
        public String type; // buy|sell
        public BigDecimal amount;
        public BigDecimal rate; // price
        public Long timestamp_created;
        public int status; // ??

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("pair", pair).add("type", type).add("amount", amount)
                    .add("rate", rate).add("timestamp_created", timestamp_created).add("status", status).toString();
        }
    }

    /**
     * <p>
     * GSON class for holding BTC-e cancel order response.
     * <pre>
     * {
     *   "success":1,
     *   "return":{
     *     "order_id":343154,
     *     "funds":{
     *       "usd":325,
     *       "btc":24.998,
     *       "sc":121.998,
     *       "ltc":0,
     *       "ruc":0,
     *       "nmc":0
     *     }
     *   }
     * }
     * </pre>
     * </p>
     * Unusual API - why the frak do I want to know about funds when I cancel an order?
     */
    private static class BtceCancelledOrderWrapper extends BtceMessageBase {

        @SerializedName("return")
        BtceCancelledOrder cancelledOrder;

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

    /**
     * GSON class for cancelled order response.
     */
    private static class BtceCancelledOrder {

        public long order_id;
        public BtceFunds funds;

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

    /**
     * GSON class for a BTC-e ticker response wrapper.
     */
    private static class BtceTickerWrapper {
        // TODO fix this - can GSON take wildcard types or will I have to code my own deserializer?
        @SerializedName("btc_usd")
        public BtceTicker ticker;

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

    /**
     * GSON class for a BTC-e ticker response.
     */
    private static class BtceTicker {

        public BigDecimal high;
        public BigDecimal low;
        public BigDecimal avg;
        public BigDecimal vol;
        public BigDecimal vol_cur;
        public BigDecimal last;
        public BigDecimal buy;
        public BigDecimal sell;
        public long updated;

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("high", high).add("low", low).add("avg", avg)
                    .add("vol", vol).add("vol_cur", vol_cur).add("last", last).add("buy", buy).add("sell", sell)
                    .add("updated", updated).toString();
        }
    }

    /**
     * GSON class for holding fee response.
     * <p>
     * Basically a GSON enabled map.
     * Keys into map is the market id.
     * Values are BigDec's.
     */
    private static class BtceFees extends HashMap<String, BigDecimal> {
        private static final long serialVersionUID = 5516523641423401953L;
    }

    /**
     * Deserializer needed for BTC-e open orders API call response:
     */
    private class OpenOrdersDeserializer implements JsonDeserializer<BtceOpenOrders> {
        public BtceOpenOrders deserialize(JsonElement json, Type type, JsonDeserializationContext context)
                throws JsonParseException {
            final BtceOpenOrders openOrders = new BtceOpenOrders();

            if (json.isJsonObject()) {
                final JsonObject jsonObject = json.getAsJsonObject();
                for (Entry<String, JsonElement> jsonOrder : jsonObject.entrySet()) {
                    final Long orderId = new Long(jsonOrder.getKey()); // will barf hard n fast if NaN
                    final BtceOpenOrder openOrder = context.deserialize(jsonOrder.getValue(), BtceOpenOrder.class);
                    openOrders.put(orderId, openOrder);
                }
            }
            return openOrders;
        }
    }

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

    /**
     * Makes a public API call to the BTC-e exchange.
     *
     * @param apiMethod the API method to call.
     * @param resource  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 sendPublicRequestToExchange(String apiMethod, String resource)
            throws TradingApiException, ExchangeNetworkException {

        // 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 + "/" + resource);
            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);
        }
    }

    /**
     * Makes an authenticated API call to the BTC-e exchange.
     *
     * @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, String> params)
            throws TradingApiException, ExchangeNetworkException {

        if (!initializedMACAuthentication) {
            final String errorMsg = "MAC Message security layer has not been initialized.";
            LOG.error(errorMsg);
            throw new IllegalStateException(errorMsg);
        }

        try {

            // Setup common params for the API call
            if (params == null) {
                params = new HashMap<>();
            }
            params.put("method", apiMethod);

            // must be higher for next call, even if a failure occurred on previous call
            params.put("nonce", Long.toString(++nonce));

            // Build the URL with query param args in it - yuk!
            String postData = "";
            for (final String param : params.keySet()) {
                if (postData.length() > 0) {
                    postData += "&";
                }
                //noinspection deprecation
                postData += param + "=" + URLEncoder.encode(params.get(param));
            }

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

            // Add public key
            requestHeaders.put("Key", key);

            // Sign the payload with private key
            requestHeaders.put("Sign", toHex(mac.doFinal(postData.getBytes("UTF-8"))));

            final URL url = new URL(AUTHENTICATED_API_URL);
            return sendNetworkRequest(url, "POST", postData, 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"), "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);
        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();
        gsonBuilder.registerTypeAdapter(BtceOpenOrders.class, new OpenOrdersDeserializer());
        gson = gsonBuilder.create();
    }

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