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

Java tutorial

Introduction

Here is the source code for com.gazbert.ada.adapter.CryptsyExchangeAdapter.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.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.Arrays;
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 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 Cryptsy exchange.
 * <p>
 * Cryptsy API details here: 
 * https://www.cryptsy.com/pages/api
 * <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 CryptsyExchangeAdapter implements TradingApi {
    /** Logger */
    private static final Logger LOG = Logger.getLogger(CryptsyExchangeAdapter.class);

    /** The Auth API URI */
    private static final String AUTH_API_URL = "https://api.cryptsy.com/api";

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

    /** Name of PUBLIC key prop in config file.*/
    private static final String PUBLIC_KEY_PROPERTY_NAME = "public-key";

    /** Name of PRIVATE key prop in config file. */
    private static final String PRIVATE_KEY_PROPERTY_NAME = "private-key";

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

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

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

    /*
     * Constructor gets the adapter ready for calling the Cryptsy API. 
     */
    public CryptsyExchangeAdapter() {
        // 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();
    }

    /////////////////////////////////////////////////////////////////////
    //
    // Cryptsy API Calls we adapt to the common Trading API.
    // See https://www.cryptsy.com/pages/privateapi
    //
    ///////////////////////////////////////////////////////////////////// 

    @Override
    public MarketOrderBook getMarketOrders(final String marketId) throws TradingApiException {
        final Map<String, String> params = new HashMap<String, String>();
        params.put("marketid", marketId);

        final String results = sendRequestToExchange("marketorders", params);

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

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

        //adapt BUYs
        final List<MarketOrder> buyOrders = new ArrayList<MarketOrder>();
        final CryptsyBuyOrder[] cryptsyBuyOrders = marketOrderWrapper.info.buyorders;
        for (int i = 0; i < cryptsyBuyOrders.length; i++) {
            final MarketOrder buyOrder = new MarketOrder(OrderType.BUY, cryptsyBuyOrders[i].buyprice,
                    cryptsyBuyOrders[i].quantity, cryptsyBuyOrders[i].total);
            buyOrders.add(buyOrder);
        }

        // adapt SELLs
        final List<MarketOrder> sellOrders = new ArrayList<MarketOrder>();
        final CryptsySellOrder[] cryptsySellOrders = marketOrderWrapper.info.sellorders;
        for (int i = 0; i < cryptsySellOrders.length; i++) {
            final MarketOrder sellOrder = new MarketOrder(OrderType.SELL, cryptsySellOrders[i].sellprice,
                    cryptsySellOrders[i].quantity, cryptsySellOrders[i].total);
            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("marketid", marketId);

        final String results = sendRequestToExchange("myorders", params);

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

        final CryptsyMyOpenOrders myOpenOrders = gson.fromJson(results, CryptsyMyOpenOrders.class);

        // adapt
        final List<OpenOrder> ordersToReturn = new ArrayList<OpenOrder>();
        final CryptsyOrder[] cryptsyOrders = myOpenOrders.orders;
        for (int i = 0; i < cryptsyOrders.length; i++) {
            OrderType orderType = null;
            final String cryptsyOrderType = cryptsyOrders[i].ordertype;
            if (cryptsyOrderType.equalsIgnoreCase(OrderType.BUY.getStringValue())) {
                orderType = OrderType.BUY;
            } else if (cryptsyOrderType.equalsIgnoreCase(OrderType.SELL.getStringValue())) {
                orderType = OrderType.SELL;
            } else {
                throw new TradingApiException(
                        "Unrecognised order type received in getYourOpenOrders(). Value: " + cryptsyOrderType);
            }

            final OpenOrder order = new OpenOrder(Long.toString(cryptsyOrders[i].orderid), cryptsyOrders[i].created,
                    marketId, orderType, cryptsyOrders[i].price, cryptsyOrders[i].quantity,
                    cryptsyOrders[i].orig_quantity, cryptsyOrders[i].total);

            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("marketid", marketId);
        params.put("ordertype", orderType);

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

        final String results = sendRequestToExchange("createorder", params);

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

        final CryptsyCreateOrderResponse createOrderResponse = gson.fromJson(results,
                CryptsyCreateOrderResponse.class);

        // Some logging for strange crypsy failures...
        if (createOrderResponse.success == 1) {
            return Long.toString(createOrderResponse.orderid);
        } else {
            LOG.error("Failed to place order on exchange. Error response: " + createOrderResponse.error
                    + " ***MoreInfo: " + createOrderResponse.moreinfo);
            return "0";
        }
    }

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

        final String results = sendRequestToExchange("cancelorder", params);

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

        final CryptsyStringResults cancelOrderResponse = gson.fromJson(results, CryptsyStringResults.class);
        return (cancelOrderResponse.success == 1);
    }

    /**
     * Fetches the latest price for a given market in BTC.
     *
     * @param marketId the id of the market.
     * @return the latest market price in BTC.
     * @throws TimeoutException if a timeout occurred trying to connect to the exchange. The timeout limit is 
     *         implementation specific for each Exchange Adapter; see the documentation for the adatper you are using.
     * @throws TradingApiException if the API call failed for any reason other than a timeout.
     */
    @Override
    public BigDecimal getLatestMarketPrice(final String marketId) throws TradingApiException {
        final Map<String, String> params = new HashMap<String, String>();
        params.put("marketid", marketId);

        final String results = sendRequestToExchange("markettrades", params);

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

        final CryptsyLatestTrades trades = gson.fromJson(results, CryptsyLatestTrades.class);

        // adapt
        final CryptsyTrade[] cryptsyTrades = trades.trades;
        if (cryptsyTrades.length == 0) {
            final String errorMsg = "Cryptsy 'markettrades' API call returned empty array of trades: "
                    + cryptsyTrades;
            throw new TradingApiException(errorMsg);
        }

        // just take the latest one
        final BigDecimal latestTradePrice = new BigDecimal(cryptsyTrades[0].tradeprice);
        return latestTradePrice;
    }

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

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

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

        // adapt      
        final BalanceInfo balanceInfo = new BalanceInfo(info.info.balances_available, info.info.balances_hold);
        return balanceInfo;
    }

    @Override
    public String getImplName() {
        return "Cryptsy Exchange";
    }

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

    @Override
    public BigDecimal getPercentageOfSellOrderTakenForExchangeFee() {
        // TODO get from Cryptsy API?
        return new BigDecimal(0.003); // 0.3%
    }

    ///////////////////////////////////////////////////////////////////// 
    //
    // GSON classes for JSON responses 
    // See https://www.cryptsy.com/pages/privateapi
    //
    /////////////////////////////////////////////////////////////////////    

    /**
     * GSON class for the Order Book wrapper.
     * @author gazbert
     */
    private static class CryptsyMarketOrderBookWrapper extends CryptsyMessageBase {
        // field names map to the JSON arg names
        @SerializedName("return")
        public CryptsyMarketOrderBook info;

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

    /**
     * GSON class for a market Order Book.
     * @author gazbert
     */
    private static class CryptsyMarketOrderBook {
        // field names map to the JSON arg names
        public CryptsySellOrder[] sellorders;
        public CryptsyBuyOrder[] buyorders;

        @Override
        public String toString() {
            return CryptsyMarketOrderBook.class.getSimpleName() + " [" + "sellorders=" + Arrays.toString(sellorders)
                    + ", buyorders=" + Arrays.toString(buyorders) + "]";
        }
    }

    /**
     * GSON class for a BUY order.
     * @author gazbert
     */
    private static class CryptsyBuyOrder {
        // field names map to the JSON arg names
        public BigDecimal buyprice;
        public BigDecimal quantity;
        public BigDecimal total;

        @Override
        public String toString() {
            return CryptsyBuyOrder.class.getSimpleName() + " [" + "buyprice=" + buyprice + ", quantity=" + quantity
                    + ", total=" + total + "]";
        }
    }

    /**
     * GSON class for a SELL order.
     * @author gazbert
     */
    private static class CryptsySellOrder {
        // field names map to the JSON arg names
        public BigDecimal sellprice;
        public BigDecimal quantity;
        public BigDecimal total;

        @Override
        public String toString() {
            return CryptsySellOrder.class.getSimpleName() + " [" + "sellprice=" + sellprice + ", quantity="
                    + quantity + ", total=" + total + "]";
        }
    }

    /**
     * GSON class used to receive response after creating an order.
     * @author gazbert
     */
    private static class CryptsyCreateOrderResponse extends CryptsyMessageBase {
        // field names map to the JSON arg names
        public long orderid;
        public String moreinfo;

        @Override
        public String toString() {
            return CryptsyCreateOrderResponse.class.getSimpleName() + " [" + "orderid=" + orderid + ", moreinfo="
                    + moreinfo + "]";
        }
    }

    /**
     * GSON class for receiving my open orders in API call response.
     * @author gazbert
     */
    private static class CryptsyMyOpenOrders extends CryptsyMessageBase {
        // field names map to the JSON arg names
        @SerializedName("return")
        public CryptsyOrder[] orders;

        @Override
        public String toString() {
            return CryptsyMyOpenOrders.class.getSimpleName() + " [" + "orders=" + Arrays.toString(orders) + "]";
        }
    }

    /**
     * GSON class for mapping returned orders from API call response.
     * @author gazbert
     */
    private static class CryptsyOrder {
        // field names map to the JSON arg names
        public long orderid;
        public Date created;
        public int marketid;
        public String ordertype;
        public BigDecimal price;
        public BigDecimal quantity;
        public BigDecimal orig_quantity;
        public BigDecimal total;

        @Override
        public String toString() {
            return CryptsyOrder.class.getSimpleName() + " [" + "orderid=" + orderid + ", created=" + created
                    + ", marketid=" + marketid + ", ordertype=" + ordertype + ", price=" + price + ", quantity="
                    + quantity + ", orig_quantity=" + orig_quantity + ", total=" + total + "]";
        }
    }

    /**
     * GSON class for mapping last completed Trades from API call response.
     * @author gazbert
     */
    private static class CryptsyLatestTrades extends CryptsyMessageBase {
        // field names map to the JSON arg names
        @SerializedName("return")
        public CryptsyTrade[] trades;
    }

    /**
     * GSON class for mapping a completed Trade from API call response.
     * @author gazbert
     */
    private static class CryptsyTrade {
        // field names map to the JSON arg names
        public long tradeid;
        public String tradetype;
        public Date datetime;
        public int marketid;
        public double tradeprice;
        public double quantity;
        public double fee;
        public double total;
        public String initiate_ordertype;
        public long order_id;

        @Override
        public String toString() {
            return CryptsyTrade.class.getSimpleName() + " [" + "tradeid=" + tradeid + ", tradetype=" + tradetype
                    + ", datetime=" + datetime + ", marketid=" + marketid + ", tradeprice=" + tradeprice
                    + ", quantity=" + quantity + ", fee=" + fee + ", total=" + total + ", initiate_ordertype="
                    + initiate_ordertype + ", order_id=" + order_id + "]";
        }
    }

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

        public CryptsyMessageBase() {
            error = "";
        }

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

    /**
     * GSON class for holding String results from API call requests/responses.
     * @author gazbert
     */
    private static class CryptsyStringResults extends CryptsyMessageBase {
        // field names map to the JSON arg names
        @SerializedName("return")
        public String info;
    }

    /**
     * GSON class for wrapping Cryptsy Info response from getInfo() API call.
     * @author gazbert
     */
    private static class CryptsyInfoWrapper extends CryptsyMessageBase {
        @SerializedName("return")
        public CryptsyInfo info;
    }

    /**
     * GSON class for holding Cryptsy info response from getInfo() API call.
     * @author gazbert
     */
    private static class CryptsyInfo {
        // field names map to the JSON arg names
        public CryptsyBalances balances_available;
        public CryptsyBalances balances_hold;
        public long servertimestamp;
        public String servertimezone;
        public Date serverdatetime;
        public int openordercount;

        @Override
        public String toString() {
            return CryptsyInfo.class.getSimpleName() + " [servertimestamp=" + servertimestamp + ", servertimezone="
                    + servertimezone + ", serverdatetime=" + serverdatetime + ", openordercount=" + openordercount
                    + "]";
        }
    }

    /** 
     * GSON class for holding altcoin wallet balances - basically a GSON enabled map.
     * @author gazbert
     */
    private static class CryptsyBalances extends HashMap<String, BigDecimal> {
        /** SID*/
        private static final long serialVersionUID = -4919716060747077759L;
    }

    /*
     * Needed because Cryptsy causes Gson to throw ISE under certain circumstances:
     * 
     * TODO show exception...
     */
    private class DateDeserializer implements JsonDeserializer<Date> {
        private SimpleDateFormat cryptsDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

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

    /*
     * Needed because Cryptsy causes Gson to throw ISE under certain circumstances:
     * 
     * Exception in thread "main" com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1
     * at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:176)
     * at com.google.gson.Gson.fromJson(Gson.java:803)
     * at com.google.gson.Gson.fromJson(Gson.java:768)
     * at com.google.gson.Gson.fromJson(Gson.java:717)
     * at com.google.gson.Gson.fromJson(Gson.java:689)
     * at com.gazbert.ada.adapter.CryptsyExchangeAdapter.getBalanceInfo(CryptsyExchangeAdapter.java:313)
     * at com.gazbert.ada.engine.TradingEngine.checkAndBailIfBtcBalanceOnExchangeIsBelowEmergencyStopLimit(TradingEngine.java:280)
     * at com.gazbert.ada.engine.TradingEngine.startTradeCycle(TradingEngine.java:170)
     * at com.gazbert.ada.engine.TradingEngine.start(TradingEngine.java:150)
     * at com.gazbert.ada.engine.Ada.main(Ada.java:60)
     * Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1
     * at com.google.gson.stream.JsonReader.beginObject(JsonReader.java:374)
     * at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:165)
     * ... 9 more
     * 
     */
    private class BalancesDeserializer implements JsonDeserializer<CryptsyBalances> {
        public CryptsyBalances deserialize(JsonElement json, Type type, JsonDeserializationContext context)
                throws JsonParseException {
            final CryptsyBalances balances = new CryptsyBalances();

            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 an API call to Cryptsy exchange.
     * 
     * @param apiMethod the API method to call
     * @param params the query param args to use in the API call
     * @return the response from Cryptsy.
     * @throws TradingApiException
     */
    private final String sendRequestToExchange(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 Cryptsy 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(AUTH_API_URL);
            exchangeConnection = url.openConnection();
            exchangeConnection.setUseCaches(false);
            exchangeConnection.setDoOutput(true);

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

            // 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.
             *
             * 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);

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

        // 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(privateKey.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. Private key seems invalid!";
            LOG.error(errorMsg, e);
            throw new IllegalArgumentException(errorMsg, e);
        }
    }

    /*
     * Loads Cryptsy 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 Cryptsy 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
             */
            publicKey = configEntries.getProperty(PUBLIC_KEY_PROPERTY_NAME);

            // WARNING: Don't log this on 'exposed' system...
            //LOG.info(PUBLIC_KEY_PROPERTY_NAME + ": " + publicKey);

            if (publicKey == null || publicKey.length() == 0) {
                final String errorMsg = PUBLIC_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
             */
            privateKey = configEntries.getProperty(PRIVATE_KEY_PROPERTY_NAME);

            // WARNING: Never log this unless testing in the green zone
            //LOG.info(PRIVATE_KEY_PROPERTY_NAME + ": " + privateKey);

            if (privateKey == null || privateKey.length() == 0) {
                final String errorMsg = PRIVATE_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 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(Date.class, new DateDeserializer());
        gsonBuilder.registerTypeAdapter(CryptsyBalances.class, new BalancesDeserializer());
        gson = gsonBuilder.create();
    }
}