com.gazbert.ada.adapter.BtceExchangeAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.gazbert.ada.adapter.BtceExchangeAdapter.java

Source

package com.gazbert.ada.adapter;

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 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.
 */

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
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.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.log4j.Logger;

import com.gazbert.ada.trading.api.BalanceInfo;
import com.gazbert.ada.trading.api.MarketOrder;
import com.gazbert.ada.trading.api.MarketOrderBook;
import com.gazbert.ada.trading.api.OpenOrder;
import com.gazbert.ada.trading.api.OrderType;
import com.gazbert.ada.trading.api.TimeoutException;
import com.gazbert.ada.trading.api.TradingApi;
import com.gazbert.ada.trading.api.TradingApiException;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;

/**
 * Adapter for integrating with BTC-e exchange.
 * <p>
 * BTC-e API details here: 
 * https://btc-e.com/api/documentation
 * and
 * https://btc-e.com/page/2
 * <p>
 * This adapter is Single threaded: we <em>must</em> preserve trading transaction order... the URLConnection 
 * does this by blocking when waiting for input stream (response) for each API call. It is *not* thread safe in its
 * current form.
 * <p>
 * API calls will throw a {@link TimeoutException} if:
 * <ul>
 * <li>A {@link java.net.SocketTimeoutException} is thrown by the {@link URLConnection}</li>
 * <li>A {@link java.io.IOException} thrown by the {@link URLConnection} with HTTP status codes:
 *     502 Bad Gateway, 503 Servivce Unavailable, 504 Gateway Timeout.</li>
 * </ul>
 * 
 * @author gazbert
 *
 */
public final class BtceExchangeAdapter implements TradingApi {
    /** Logger */
    private static final Logger LOG = Logger.getLogger(BtceExchangeAdapter.class);

    /** 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/";

    /**
     * Your BTC-e API keys and connection timeout config.
     * This file *must* be on Ada's classpath.
     */
    private static final String CONFIG_LOCATION = "btce-config.properties";

    /** 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";

    /** Name of connection timeout prop in config file. */
    private static final String CONNECTION_TIMEOUT_PROPERTY_NAME = "connection-timeout";

    /** Nonce used for auth packets */
    private static long nonce = 0;

    /** The connection timeout in SECONDS for terminating hung connections to the exchange. */
    private int connectionTimeout;

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

    /** Our public key used in MAC authentication */
    private String key = "";

    /** Our secret used in MAC authentication */
    private String secret = "";

    /**
     * MAC - This class provides the functionality of a "Message Authentication Code" (MAC) algorithm.
     * Used to encrypt the hash of the entire message with our private key to ensure message integrity.
     */
    private Mac mac;

    /** GSON engine used for parsing JSON in BTC-e API call responses */
    private Gson gson;

    /*
     * Constructor gets the adapter ready for calling the BTC-e API. 
     */
    public BtceExchangeAdapter() {
        // set the initial nonce used in the secure messaging.
        nonce = System.currentTimeMillis() / 1000;

        // set up the secure messaging layer for payload integrity.
        initSecureMessageLayer();

        // Initialise GSON layer for processing the JSON in the API calls/responses.
        initGson();
    }

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

    @Override
    public MarketOrderBook getMarketOrders(final String marketId) throws TradingApiException {
        final String results = sendPublicRequestToExchange("depth", marketId);

        // TODO tmp debug
        //LOG.debug("BTC-e Order Book: " + results);

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

        //adapt BUYs
        final List<MarketOrder> buyOrders = new ArrayList<MarketOrder>();
        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), // quanity
                    order.get(0).multiply(order.get(1)));
            buyOrders.add(buyOrder);
        }

        // adapt SELLs
        final List<MarketOrder> sellOrders = new ArrayList<MarketOrder>();
        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);
        }

        final MarketOrderBook orderBook = new MarketOrderBook(marketId, sellOrders, buyOrders);
        return orderBook;
    }

    @Override
    public List<OpenOrder> getYourOpenOrders(final String marketId) throws TradingApiException {
        final Map<String, String> params = new HashMap<String, String>();
        params.put("pair", marketId);

        final String results = sendAuthenticatedRequestToExchange("ActiveOrders", params);

        // TODO tmp debug
        //LOG.debug("Open Orders: " + results);

        final BtceOpenOrderResponseWrapper myOpenOrders = gson.fromJson(results,
                BtceOpenOrderResponseWrapper.class);
        final List<OpenOrder> ordersToReturn = new ArrayList<OpenOrder>();

        // this sucks!
        if (myOpenOrders.success == 0 && 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 = null;
                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, new BigDecimal(0), //c.f. cryptsyOrders[i].orig_quantity - not provided by btce :-(
                        new BigDecimal(0) //c.f. cryptsyOrders[i].total - not provided by btce :-(
                );

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

    @Override
    public String createOrder(final String marketId, final String orderType, final BigDecimal quantity,
            final BigDecimal price) throws TradingApiException {
        final Map<String, String> params = new HashMap<String, String>();
        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 String results;
        if (orderType == OrderType.BUY.getStringValue()) {
            // buying BTC
            params.put("type", "buy");
            results = sendAuthenticatedRequestToExchange("Trade", params);
        } else if (orderType == OrderType.SELL.getStringValue()) {
            // selling BTC
            params.put("type", "sell");
            results = 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);
        }

        // TODO tmp debug
        LOG.debug("Order response: " + results);

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

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

                // we generate our own unique ID in the adaper for the order.
                // This ID won't match any existing orders, so next trade cycle in strat will assume (correctly) 
                // that order filled.
                return "INSTAFILL__" + UUID.randomUUID().toString();
            }
            return Long.toString(orderId);
        } else {
            LOG.error("Failed to place order on exchange. Error response: " + createOrderResponseWrapper.error);
            return "0";
        }
    }

    @Override
    public boolean cancelOrder(final String orderId) throws TradingApiException {
        final Map<String, String> params = new HashMap<String, String>();
        params.put("order_id", orderId);
        final String results = sendAuthenticatedRequestToExchange("CancelOrder", params);

        // TODO tmp debug to trap all the diff types of error response in JSON
        LOG.debug("cancelOrder() response: " + results);

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

        if (cancelOrderResponse.success == 0) {
            //game over!
            LOG.error("Failed to cancel order: " + cancelOrderResponse.error);
            return false;
        } else {
            return true;
        }
    }

    @Override
    public BigDecimal getLatestMarketPrice(final String marketId) throws TradingApiException {
        final String results = sendPublicRequestToExchange("ticker", marketId);

        // TODO tmp debug
        LOG.debug("BTCe ticker raw response: " + results);

        final BtceTickerWrapper btceTicker = gson.fromJson(results, BtceTickerWrapper.class);
        return btceTicker.ticker.last;
    }

    @Override
    public BalanceInfo getBalanceInfo() throws TradingApiException {
        final String results = sendAuthenticatedRequestToExchange("getInfo", null);

        // TODO tmp debug
        LOG.debug("BTC-e Balances: " + results);

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

        // adapt      
        final HashMap<String, BigDecimal> balancesAvailable = new HashMap<String, BigDecimal>();
        balancesAvailable.put("USD", info.info.funds.get("usd"));
        balancesAvailable.put("BTC", info.info.funds.get("btc"));

        // 2nd arg of reserved balances not provided by exchange.
        final BalanceInfo balanceInfo = new BalanceInfo(balancesAvailable, new HashMap<String, BigDecimal>());
        return balanceInfo;
    }

    @Override
    public String getImplName() {
        return "BTC-e Exchange";
    }

    @Override
    public BigDecimal getPercentageOfBuyOrderTakenForExchangeFee() {
        // TODO get from BTC-e API?
        return new BigDecimal(0.002); // 0.2%
    }

    @Override
    public BigDecimal getPercentageOfSellOrderTakenForExchangeFee() {
        // TODO get from BTC-e API?
        return new BigDecimal(0.002); // 0.2%
    }

    ////////////////////////////////////////////////////////////////////////// 
    //
    // 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.
     * @author gazbert
     */
    private static class BtceMessageBase {
        // field names map to the JSON arg names
        public int success;
        public String error;

        public BtceMessageBase() {
            error = "";
        }

        @Override
        public String toString() {
            return BtceMessageBase.class.getSimpleName() + " [" + "success=" + success + ", error=" + error + "]";
        }
    }

    /**
     * GSON class for wrapping BTC-e Info response from getInfo() API call.
     * <p>
     * JSON response:
     * <pre>
     * {
     *   "success":1,
     *   "return":
     *   {
     *     "funds":
     *     {
     *       "usd":325,
     *       "btc":23.998,
     *       "sc":121.998,
     *       "ltc":0,
     *       "ruc":0,
     *       "nmc":0
     *     },
     *     "rights":
     *     {
     *       "info":1,
     *       "trade":1
     *     },
     *     "transaction_count":80,
     *     "open_orders":1,
     *     "server_time":1342123547
     *   }
     * }
     * </pre>
     *
     * @author gazbert
     */
    private static class BtceInfoWrapper extends BtceMessageBase {
        @SerializedName("return")
        public BtceInfo info;
    }

    /**
     * GSON class for holding BTC-e info response from getInfo() API call.
     * @author gazbert
     */
    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 BtceInfo.class.getSimpleName() + " [funds=" + funds + ", rights=" + rights
                    + ", transaction_count=" + transaction_count + ", open_orders=" + open_orders + ", server_time="
                    + server_time + "]";
        }
    }

    /** 
     * 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.
     * 
     * @author gazbert
     */
    private static class BtceFunds extends HashMap<String, BigDecimal> {
        /** SID */
        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).
     * 
     * @author gazbert
     */
    private static class BtceRights extends HashMap<String, BigDecimal> {
        /** SID */
        private static final long serialVersionUID = 5353335214767688903L;
    }

    /**
     * GSON class for the Order Book wrapper.
     * <p>
     * JSON returned:
     * <pre>
     * {
     *   "btc_usd":
     *   {
     *     "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>
     * 
     * @author gazbert
     */
    private static class BtceMarketOrderBookWrapper extends BtceMessageBase {
        // field names map to the JSON arg names)
        @SerializedName("btc_usd")
        public BtceOrderBook orderBook;

        @Override
        public String toString() {
            return BtceMarketOrderBookWrapper.class.getSimpleName() + " [" + "orderBook=" + orderBook + "]";
        }
    }

    /**
     * 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.
     * 
     * @author gazbert
     */
    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 BtceOrderBook.class.getSimpleName() + " [" + "bids=" + bids + ", asks=" + asks + "]";
        }
    }

    /**
     * GSON class for holding BTC-e create order response wapper from Trade API call.
     * <p>
     * JSON response:
     * <pre>   
     * {
     *   "success":1,
     *   "return": {
     *     "received":0.1,
     *     "remains":0,
     *     "order_id":0,
     *     "funds": {
     *       "usd":325,
     *       "btc":2.498,
     *       "sc":121.998,
     *       "ltc":0,
     *       "ruc":0,
     *       "nmc":0
     *     }
     *   }
     * }    
     * </pre>
     * @author gazbert
     */
    private static class BtceCreateOrderResponseWrapper extends BtceMessageBase {
        // field names map to the JSON arg names
        @SerializedName("return")
        public BtceCreateOrderResponse orderResponse;

        @Override
        public String toString() {
            return BtceCreateOrderResponseWrapper.class.getSimpleName() + " [" + "orderResponse=" + orderResponse
                    + "]";
        }
    }

    /**
     * GSON class for holding BTC-e create order response details from Trade API call.
     * @author gazbert
     */
    private static class BtceCreateOrderResponse extends BtceCreateOrderResponseWrapper {
        // field names map to the JSON arg names
        public BigDecimal received;
        public BigDecimal remains;
        public long order_id;
        public BtceFunds funds;

        @Override
        public String toString() {
            return BtceCreateOrderResponse.class.getSimpleName() + " [received=" + received + ", remains=" + remains
                    + ", order_id=" + order_id + ", funds=" + funds + "]";
        }
    }

    /**
     * GSON class for holding BTC-e open order response wapper from ActiveOrders API call.
     * <p>
     * JSON response when we don't have open orders is:
     * <pre>
     * {"success":0,"error":"no orders"}
     * </pre>
     * <p>
     * Error? WTF???!!!
     * <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>
     * 
     * @author gazbert
     */
    private static class BtceOpenOrderResponseWrapper extends BtceMessageBase {
        // field names map to the JSON arg names
        @SerializedName("return")
        public BtceOpenOrders openOrders;

        @Override
        public String toString() {
            return BtceOpenOrderResponseWrapper.class.getSimpleName() + " [" + "openOrders=" + openOrders + "]";
        }
    }

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

    /**
     * GSON class for holding BTC-e open order response details from ActiveOrders API call.
     * @author gazbert
     */
    private static class BtceOpenOrder {
        // field names map to the JSON arg names
        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 BtceOpenOrder.class.getSimpleName() + " [pair=" + pair + ", type=" + type + ", amount=" + amount
                    + ", rate=" + rate + ", timestamp_created=" + timestamp_created + ", status=" + status + "]";
        }
    }

    /**
     * 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>
     * 
     * Unusual API - why the feck do I want to know about funds when I cancel an order?!! ;-)
     * 
     * @author gazbert
     */
    private static class BtceCancelledOrderWrapper extends BtceMessageBase {
        // field names map to the JSON arg names
        @SerializedName("return")
        public BtceCancelledOrder cancelledOrder;

        @Override
        public String toString() {
            return BtceCancelledOrderWrapper.class.getSimpleName() + " [" + "cancelledOrder=" + cancelledOrder
                    + "]";
        }
    }

    /** 
     * GSON class for cancelled order response.
     * @author gazbert
     */
    private static class BtceCancelledOrder {
        public long order_id;
        public BtceFunds funds;

        @Override
        public String toString() {
            return BtceCancelledOrder.class.getSimpleName() + " [" + "order_id=" + order_id + ", funds=" + funds
                    + "]";
        }
    }

    /**
     * GSON class for a BTC-e ticker response wrapper.
     * <p>
     * JSON looks like:
     * <pre>     
     * {
     *   "btc_usd":{
     *     "high":667,
     *     "low":606,
     *     "avg":636.5,
     *     "vol":6377783.27609,
     *     "vol_cur":10014.85647,
     *     "last":666.9,
     *     "buy":666.998,
     *     "sell":666.997,
     *     "updated":1401647163
     *   }
     * }
     * </pre>
     * @author gazbert
     */
    private static class BtceTickerWrapper {
        // field names map to the JSON arg names
        // 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 BtceTickerWrapper.class.getSimpleName() + " [" + "ticker=" + ticker + "]";
        }
    }

    /**
     * GSON class for a BTC-e ticker response.
     * @author gazbert
     */
    private static class BtceTicker {
        // field names map to the JSON arg names
        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 BtceTicker.class.getSimpleName() + " [" + "high=" + high + ", low=" + low + ", avg=" + avg
                    + ", vol=" + vol + ", vol_cur=" + vol_cur + ", last=" + last + ", buy=" + buy + ", sell=" + sell
                    + ", updated=" + updated + "]";
        }
    }

    /**
     * Needed for BTC-e open orders API call response:
     * <pre>
     * {
     *   "success":1,
     *   "return":{
     *     "253356918":{
     *       "pair":"btc_usd","type":"sell","amount":0.01000000,"rate":641.00000000,
     *       "timestamp_created":1401653697,"status":0
     *     }
     *   }
     * }
     * </pre>
     * 
     * @author gazbert
     */
    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();
                final Iterator<Entry<String, JsonElement>> openOrdersIterator = jsonObject.entrySet().iterator();
                while (openOrdersIterator.hasNext()) {
                    final Entry<String, JsonElement> jsonOrder = openOrdersIterator.next();
                    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;
        }
    }

    /**
     * Needed for BTC-e Date format.
     * 
     * Needed because BTC-e causes Gson to throw ISE under certain circumstances:
     * 
     * <pre>
     * </pre>
     * 
     * @author gazbert
     */
    private class DateDeserializer implements JsonDeserializer<Date> {
        private SimpleDateFormat btceDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        public Date deserialize(JsonElement json, Type type, JsonDeserializationContext context)
                throws JsonParseException {
            Date dateFromBtce = null;
            if (json.isJsonPrimitive()) {
                try {
                    dateFromBtce = btceDateFormat.parse(json.getAsString());
                } catch (ParseException e) {
                    // TODO should we barf big time here??
                    dateFromBtce = null;
                    final String errorMsg = "Failed to parse a BTC-e date";
                    LOG.error(errorMsg, e);
                }
            }
            return dateFromBtce;
        }
    }

    /**
     * Needed because BTC-e causes Gson to throw ISE under certain circumstances:
     * 
     * <pre>
     * </pre>
     * 
     * @author gazbert
     */
    private class BalancesDeserializer implements JsonDeserializer<BtceFunds> {
        public BtceFunds deserialize(JsonElement json, Type type, JsonDeserializationContext context)
                throws JsonParseException {
            final BtceFunds balances = new BtceFunds();

            if (json.isJsonObject()) {
                final JsonObject jsonObject = json.getAsJsonObject();
                final Iterator<Entry<String, JsonElement>> balancesIterator = jsonObject.entrySet().iterator();
                while (balancesIterator.hasNext()) {
                    final Entry<String, JsonElement> jsonOrder = balancesIterator.next();
                    final String currency = jsonOrder.getKey();
                    final BigDecimal balance = context.deserialize(jsonOrder.getValue(), BigDecimal.class);
                    balances.put(currency, balance);
                }
            }
            return balances;
        }
    }

    /////////////////////////////////////////////////////////////////////
    //
    // Private utils.
    // 
    /////////////////////////////////////////////////////////////////////     

    /*
     * Makes a public REST-like API call to BTC-e exchange. Uses HTTP GET.
     * 
     * @param apiMethod the API method to call
     * @param resource to use in the API call
     * @return the response from BTC-e.
     * @throws TradingApiException
     */
    private final String sendPublicRequestToExchange(final String apiMethod, final String resource)
            throws TradingApiException {
        // Connect to the exchange
        HttpURLConnection exchangeConnection = null;
        final StringBuffer exchangeResponse = new StringBuffer();

        try {
            final URL url = new URL(PUBLIC_API_BASE_URL + apiMethod + "/" + resource);
            if (LOG.isDebugEnabled()) {
                LOG.debug("Using following URL for API call: " + url);
            }

            exchangeConnection = (HttpURLConnection) url.openConnection();
            exchangeConnection.setUseCaches(false);
            exchangeConnection.setDoOutput(true);

            // TODO check this is correct format
            exchangeConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

            // Scare them with her name!!! Er, perhaps, I need to be a bit more stealth here...
            exchangeConnection.setRequestProperty("User-Agent",
                    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36");

            /*
             * Add a timeout so we don't get blocked indefinitley; Timeout is in milllis.
             *
             * Cryptsy gets stuck here for ~ 1 min once every half hour or so. Especially read timeouts.
             * 
             * Give it time before killing it off - to prevent me from having to write more horrible order state 
             * management code in the strategy ;-) 
             * 
             * connectionTimeout is in SECONDS and from config.
             */
            final int timeoutInMillis = connectionTimeout * 1000;
            exchangeConnection.setConnectTimeout(timeoutInMillis);
            exchangeConnection.setReadTimeout(timeoutInMillis);

            // Grab the response - we just block here as per Connection API
            final BufferedReader responseInputStream = new BufferedReader(
                    new InputStreamReader(exchangeConnection.getInputStream()));

            // Read the JSON response lines into our response buffer
            String responseLine = null;
            while ((responseLine = responseInputStream.readLine()) != null) {
                exchangeResponse.append(responseLine);
            }
            responseInputStream.close();

            // return the JSON response string
            return exchangeResponse.toString();
        } catch (MalformedURLException e) {
            final String errorMsg = "Failed to send request to Exchange.";
            LOG.error(errorMsg, e);
            throw new TradingApiException(errorMsg, e);
        } catch (SocketTimeoutException e) {
            final String errorMsg = "Failed to connect to Exchange due to socket timeout.";
            LOG.error(errorMsg, e);
            throw new TimeoutException(errorMsg, e);
        } catch (IOException e) {
            /*
             * Crypsy often fails with these codes, but recovers by next request...
             */
            if (e.getMessage().contains("502") || e.getMessage().contains("503")
                    || e.getMessage().contains("504")) {
                final String errorMsg = "Failed to connect to Exchange due to 5XX timeout.";
                LOG.error(errorMsg, e);
                throw new TimeoutException(errorMsg, e);
            } else {
                final String errorMsg = "Failed to connect to Exchange due to some other IO error.";
                LOG.error(errorMsg, e);
                throw new TradingApiException(errorMsg, e);
            }
        } finally {
            if (exchangeConnection != null) {
                exchangeConnection.disconnect();
            }
        }
    }

    /*
     * Makes an API call to 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 BTC-e.
     * @throws TradingApiException
     */
    private final String sendAuthenticatedRequestToExchange(final String apiMethod, Map<String, String> params)
            throws TradingApiException {
        if (!initializedMACAuthentication) {
            final String errorMsg = "MAC Message security layer has not been initialized.";
            LOG.error(errorMsg);
            throw new IllegalStateException(errorMsg);
        }

        // method is the API method name
        // nonce is required by BTC-e in every request
        if (params == null) {
            params = new HashMap<String, String>();
        }
        params.put("method", apiMethod);
        params.put("nonce", Long.toString(nonce));

        // increment ready for next call...
        // must be 1 higher for next call
        nonce++;

        // Build the URL with query param args in it
        String postData = "";
        for (final Iterator<String> paramsIterator = params.keySet().iterator(); paramsIterator.hasNext();) {
            final String param = paramsIterator.next();
            if (postData.length() > 0) {
                postData += "&";
            }
            postData += param + "=" + URLEncoder.encode(params.get(param));
        }

        // Connect to the exchange
        URLConnection exchangeConnection = null;
        final StringBuffer exchangeResponse = new StringBuffer();

        try {
            final URL url = new URL(AUTHENTICATED_API_URL);
            exchangeConnection = url.openConnection();
            exchangeConnection.setUseCaches(false);
            exchangeConnection.setDoOutput(true);

            // Add my public key
            exchangeConnection.setRequestProperty("Key", key);

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

            exchangeConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

            // Scare them with her name!!! Er, perhaps, I need to be a bit more stealth here...
            exchangeConnection.setRequestProperty("User-Agent",
                    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36");

            /*
             * Add a timeout so we don't get blocked indefinitley; Timeout is in milllis.
             * 
             * Give it time before killing it off - to prevent me from having to write more horrible order state 
             * management code in the strategy ;-) 
             * 
             * connectionTimeout is in SECONDS and from config.
             */
            final int timeoutInMillis = connectionTimeout * 1000;
            exchangeConnection.setConnectTimeout(timeoutInMillis);
            exchangeConnection.setReadTimeout(timeoutInMillis);

            // POST the request
            final OutputStreamWriter outputPostStream = new OutputStreamWriter(
                    exchangeConnection.getOutputStream());
            outputPostStream.write(postData);
            outputPostStream.close();

            // Grab the response - we just block here as per Connection API
            final BufferedReader responseInputStream = new BufferedReader(
                    new InputStreamReader(exchangeConnection.getInputStream()));

            // Read the JSON response lines into our response buffer
            String responseLine = null;
            while ((responseLine = responseInputStream.readLine()) != null) {
                exchangeResponse.append(responseLine);
            }
            responseInputStream.close();
        } catch (MalformedURLException e) {
            final String errorMsg = "Failed to send request to Exchange.";
            LOG.error(errorMsg, e);
            throw new TradingApiException(errorMsg, e);
        } catch (SocketTimeoutException e) {
            final String errorMsg = "Failed to connect to Exchange due to socket timeout.";
            LOG.error(errorMsg, e);
            throw new TimeoutException(errorMsg, e);
        } catch (IOException e) {
            /*
             * Cryptsy often fails with these codes, but recovers by next request...
             */
            if (e.getMessage().contains("502") || e.getMessage().contains("503")
                    || e.getMessage().contains("504")) {
                final String errorMsg = "Failed to connect to Exchange due to 5XX timeout.";
                LOG.error(errorMsg, e);
                throw new TimeoutException(errorMsg, e);
            } else {
                final String errorMsg = "Failed to connect to Exchange due to some other IO error.";
                LOG.error(errorMsg, e);
                throw new TradingApiException(errorMsg, e);
            }
        }

        // return the JSON response string
        return exchangeResponse.toString();
    }

    /*
     * Converts a given byte array to a hex String.
     */
    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 Security layer.
     * Loads API keys and sets up the MAC to encrypt the data we send to the exchange.
     * We fail hard n fast if any of this stuff blows.
     */
    private void initSecureMessageLayer() {
        // load API keys from config
        loadApiKeysAndOtherConfig();

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

    /*
     * Loads BTC-e API keys and other config.
     */
    private void loadApiKeysAndOtherConfig() {
        final Properties configEntries = new Properties();
        final InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(CONFIG_LOCATION);

        if (inputStream == null) {
            final String errorMsg = "Cannot find BTC-e config at: " + CONFIG_LOCATION
                    + " HINT: is it on Ada's classpath?";
            LOG.error(errorMsg);
            throw new IllegalStateException(errorMsg);
        }

        try {
            configEntries.load(inputStream);

            /*
             * Grab the public key
             */
            key = configEntries.getProperty(KEY_PROPERTY_NAME);

            // WARNING: Don't log this on 'exposed' system...
            //LOG.debug(KEY_PROPERTY_NAME + ": " + key);

            if (key == null || key.length() == 0) {
                final String errorMsg = KEY_PROPERTY_NAME + " cannot be null or zero length!"
                        + " HINT: is the value set in the " + CONFIG_LOCATION + "?";
                LOG.error(errorMsg);
                throw new IllegalArgumentException(errorMsg);
            }

            /*
             * Grab the private key
             */
            secret = configEntries.getProperty(SECRET_PROPERTY_NAME);

            // WARNING: Never log this unless testing in the green zone
            //LOG.debug(SECRET_PROPERTY_NAME + ": " + secret);

            if (secret == null || secret.length() == 0) {
                final String errorMsg = SECRET_PROPERTY_NAME + " cannot be null or zero length!"
                        + " HINT: is the value set in the " + CONFIG_LOCATION + "?";
                LOG.error(errorMsg);
                throw new IllegalArgumentException(errorMsg);
            }

            /*
             * Grab the connection timeout
             */
            connectionTimeout = Integer.parseInt( // will barf bigtime if not a number; we want this to fail fast.
                    configEntries.getProperty(CONNECTION_TIMEOUT_PROPERTY_NAME));
            if (connectionTimeout == 0) {
                final String errorMsg = CONNECTION_TIMEOUT_PROPERTY_NAME + " cannot be 0 value!"
                        + " HINT: is the value set in the " + CONFIG_LOCATION + "?";
                LOG.error(errorMsg);
                throw new IllegalArgumentException(errorMsg);
            }

            LOG.info(CONNECTION_TIMEOUT_PROPERTY_NAME + ": " + connectionTimeout);
        } catch (IOException e) {
            final String errorMsg = "Failed to load Exchange config: " + CONFIG_LOCATION;
            LOG.error(errorMsg, e);
            throw new IllegalStateException(errorMsg, e);
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                final String errorMsg = "Failed to close input stream for: " + CONFIG_LOCATION;
                LOG.error(errorMsg, e);
                throw new IllegalStateException(errorMsg, e);
            }
        }
    }

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