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

Java tutorial

Introduction

Here is the source code for com.gazbert.ada.adapter.BitstampExchangeAdapter.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.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.JsonParseException;

/**
 * Adapter for integrating with Bitstamp exchange.
 * <p>
 * Bitstamp API details here: 
 * https://www.bitstamp.net/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 BitstampExchangeAdapter implements TradingApi {
    /** Logger */
    private static final Logger LOG = Logger.getLogger(BitstampExchangeAdapter.class);

    /** The Auth API URI */
    private static final String API_BASE_URL = "https://www.bitstamp.net/api/";

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

    /** Name of client id prop in config file. */
    private static final String CLIENT_ID_PROPERTY_NAME = "client-id";

    /** Name of 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 is a regular integer number. It must be increasing with every request we make. 
     * We use unix time for inititialising the first nonce param.
     */
    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 client id */
    private String clientId = "";

    /**
     * Our key used in the MAC message:
     * message = nonce + client_id + api_key
     */
    private String key = "";

    /** 
     * Our secret used for signing MAC message 
     * signature = hmac.new(API_SECRET, msg=message, digestmod=hashlib.sha256).hexdigest().upper()
     */
    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.
     * 
     * Signature is a HMAC-SHA256 encoded message containing: nonce, client ID and API key. 
     * The HMAC-SHA256 code must be generated using a secret key that was generated with your API key. 
     * This code must be converted to it's hexadecimal representation (64 uppercase characters).
     */
    private Mac mac;

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

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

    /////////////////////////////////////////////////////////////////////
    //
    // Bitstamp API Calls we adapt to the common Trading API.
    // See https://www.bitstamp.net/api/
    //
    ///////////////////////////////////////////////////////////////////// 

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

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

        final BitstampOrderBook bitstampOrderBook = gson.fromJson(results, BitstampOrderBook.class);

        //adapt BUYs
        final List<MarketOrder> buyOrders = new ArrayList<MarketOrder>();
        final List<List<BigDecimal>> bitstampBuyOrders = bitstampOrderBook.bids;
        for (final List<BigDecimal> order : bitstampBuyOrders) {
            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>> bitstampSellOrders = bitstampOrderBook.asks;
        for (final List<BigDecimal> order : bitstampSellOrders) {
            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(String marketId) throws TradingApiException {
        final String results = sendAuthenticatedRequestToExchange("open_orders", null);

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

        final BitstampOrderResponse[] myOpenOrders = gson.fromJson(results, BitstampOrderResponse[].class);

        // adapt
        final List<OpenOrder> ordersToReturn = new ArrayList<OpenOrder>();
        for (final BitstampOrderResponse openOrder : myOpenOrders) {
            OrderType orderType = null;
            if (openOrder.type == 0) {
                orderType = OrderType.BUY;
            } else if (openOrder.type == 1) {
                orderType = OrderType.SELL;
            } else {
                throw new TradingApiException(
                        "Unrecognised order type received in getYourOpenOrders(). Value: " + openOrder.type);
            }

            final OpenOrder order = new OpenOrder(Long.toString(openOrder.id), openOrder.datetime, marketId,
                    orderType, openOrder.price, openOrder.amount, new BigDecimal(0), //cryptsyOrders[i].orig_quantity - not provided by stamp :-(
                    new BigDecimal(0) //cryptsyOrders[i].total - not provided by stamp :-(
            );

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

    @Override
    public String createOrder(String marketId, String orderType, BigDecimal quantity, BigDecimal price)
            throws TradingApiException {
        final Map<String, String> params = new HashMap<String, String>();

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

        final String results;
        if (orderType == OrderType.BUY.getStringValue()) {
            // buying BTC
            results = sendAuthenticatedRequestToExchange("buy", params);
        } else if (orderType == OrderType.SELL.getStringValue()) {
            // selling BTC
            results = sendAuthenticatedRequestToExchange("sell", 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 to trap all the diff types of error response in JSON
        LOG.debug("createOrder() response: " + results);

        final BitstampOrderResponse createOrderResponse = gson.fromJson(results, BitstampOrderResponse.class);
        return Long.toString(createOrderResponse.id);
    }

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

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

        // Returns 'true' if order has been found and canceled.   
        if (results.equalsIgnoreCase("true")) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Fetches the latest price for a given market in USD.
     *
     * @param marketId the id of the market.
     * @return the latest market price in USD.
     * @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(String marketId) throws TradingApiException {
        final String results = sendPublicRequestToExchange("ticker", null);

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

        final BitstampTicker bitstampTicker = gson.fromJson(results, BitstampTicker.class);
        return bitstampTicker.last;
    }

    @Override
    public BigDecimal getPercentageOfBuyOrderTakenForExchangeFee() {
        // TODO get from API!
        return new BigDecimal(0.005); // 0.5%
    }

    @Override
    public BigDecimal getPercentageOfSellOrderTakenForExchangeFee() {
        // TODO get from API!
        return new BigDecimal(0.005); // 0.5%
    }

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

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

        final BitstampBalance balances = gson.fromJson(results, BitstampBalance.class);

        // adapt
        final Map<String, BigDecimal> balancesAvailable = new HashMap<String, BigDecimal>();
        balancesAvailable.put("BTC", balances.btc_available);
        balancesAvailable.put("USD", balances.usd_available);

        final Map<String, BigDecimal> balancesOnOrder = new HashMap<String, BigDecimal>();
        balancesOnOrder.put("BTC", balances.btc_reserved);
        balancesOnOrder.put("USD", balances.usd_reserved);

        final BalanceInfo balanceInfo = new BalanceInfo(balancesAvailable, balancesOnOrder);
        return balanceInfo;
    }

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

    ///////////////////////////////////////////////////////////////////// 
    //
    // GSON classes for JSON responses 
    // See https://www.bitstamp.net/api/
    //
    /////////////////////////////////////////////////////////////////////    

    /**
     * GSON class for holding Bitstamp Balance response from balance API call.
     * <p>
     * JSON looks like:
     * <pre>
     * {
     *   "btc_reserved": "0", "fee": "0.5000", "btc_available": "0",
     *   "usd_reserved": "0", "btc_balance": "0", "usd_balance": "0.00", "usd_available": "0.00"
     * }
     * </pre>
     * @author gazbert
     */
    private static class BitstampBalance {
        // field names map to the JSON arg names
        public BigDecimal btc_reserved;
        public BigDecimal fee;
        public BigDecimal btc_available;
        public BigDecimal usd_reserved;
        public BigDecimal usd_balance;
        public BigDecimal usd_available;

        @Override
        public String toString() {
            return BitstampBalance.class.getSimpleName() + " [btc_reserved=" + btc_reserved + ", fee=" + fee
                    + ", btc_available=" + btc_available + ", usd_reserved=" + usd_reserved + ", usd_balance="
                    + usd_balance + ", usd_available=" + usd_available + "]";
        }
    }

    /**
     * GSON class for holding Bitstamp Order Book response from order_book API call.
     * <p>
     * JSON looks like:
     * <pre>
     * {
     *   "timestamp": "1400943488",
     *   "bids": [["521.86", "0.00017398"], ["519.58", "0.25100000"], ["0.01", "38820.00000000"]],
     *   "asks": [["521.88", "10.00000000"], ["522.00", "310.24504478"], ["522.13", "0.02852084"]]
     * }
     * </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 BitstampOrderBook {
        // field names map to the JSON arg names
        public long timestamp; //unix timestamp date and time
        public List<List<BigDecimal>> bids;
        public List<List<BigDecimal>> asks;

        @Override
        public String toString() {
            return BitstampOrderBook.class.getSimpleName() + " [timestamp=" + timestamp + ", bids=" + bids
                    + ", asks=" + asks + "]";
        }
    }

    /**
     * GSON class for a Bitstamp ticker response.
     * <p>
     * JSON looks like:
     * <pre>
     * {
     *   "high": "586.34", "last": "576.55", "timestamp": "1401031033", "bid": "575.02", "vwap": "560.61", 
     *   "volume": "18412.67728589", "low": "517.55", "ask": "576.55"
     * }
     * </pre>
     * @author gazbert
     */
    private static class BitstampTicker {
        // field names map to the JSON arg names
        public BigDecimal high;
        public BigDecimal last;
        public long timestamp;
        public BigDecimal bid;
        public BigDecimal vwap;
        public BigDecimal volume;
        public BigDecimal low;
        public BigDecimal ask;

        @Override
        public String toString() {
            return BitstampTicker.class.getSimpleName() + " [high=" + high + ", last=" + last + ", timestamp="
                    + timestamp + ", bid=" + bid + ", vwap=" + vwap + ", volume=" + volume + ", low=" + low
                    + ", ask=" + ask + "]";
        }
    }

    /**
     * GSON class for Bitstamp create order response.
     * <p>
     * JSON looks like:
     * <pre>
     * [{"price": "569.50", "amount": "0.00893170", "type": 0, "id": 25716384, "datetime": "2014-05-25 19:45:59"}]
     * </pre>
     * @author gazbert
     */
    private static class BitstampOrderResponse {
        // field names map to the JSON arg names
        public long id;
        public Date datetime;
        public int type; // 0 = buy; 1 = sell
        public BigDecimal price;
        public BigDecimal amount;

        @Override
        public String toString() {
            return BitstampOrderResponse.class.getSimpleName() + " [" + "id=" + id + ", datetime=" + datetime
                    + ", type=" + type + ", price=" + price + ", amount=" + amount + "]";
        }
    }

    /**
     * Needed because stamp Date format is different in open_order response and causes default Gson parsing to barf:
     * <pre>
     * [main] 2014-05-25 20:51:31,074 ERROR BitstampExchangeAdapter  - Failed to parse a Bitstamp date
     * java.text.ParseException: Unparseable date: "2014-05-25 19:50:32"
     * at java.text.DateFormat.parse(DateFormat.java:357)
     * at com.gazbert.ada.adapter.BitstampExchangeAdapter$DateDeserializer.deserialize(BitstampExchangeAdapter.java:596)
     * at com.gazbert.ada.adapter.BitstampExchangeAdapter$DateDeserializer.deserialize(BitstampExchangeAdapter.java:1)
     * at com.google.gson.TreeTypeAdapter.read(TreeTypeAdapter.java:58)
     * </pre>
     * @author gazbert
     */
    private class BitstampDateDeserializer implements JsonDeserializer<Date> {
        private SimpleDateFormat bitstampDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

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

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

    /*
     * Makes a public API call to Bitstamp exchange. Uses HTTP GET.
     * 
     * @param apiMethod the API method to call
     * @param params the query param args to use in the API call
     * @return the response from Bitstamp.
     * @throws TradingApiException
     */
    private final String sendPublicRequestToExchange(final String apiMethod, Map<String, String> params)
            throws TradingApiException {
        // Connect to the exchange
        HttpURLConnection exchangeConnection = null;
        final StringBuffer exchangeResponse = new StringBuffer();

        try {
            if (params == null) {
                params = new HashMap<String, String>();
            }

            // Build the URL with query param args in it
            String queryString = "";
            if (params.size() > 0) {
                queryString = "?";
            }

            for (final Iterator<String> paramsIterator = params.keySet().iterator(); paramsIterator.hasNext();) {
                final String param = paramsIterator.next();
                if (queryString.length() > 0) {
                    queryString += "&";
                }
                queryString += param + "=" + URLEncoder.encode(params.get(param));
            }

            // MUST have the trailing slash even if no params...
            final URL url = new URL(API_BASE_URL + apiMethod + "/" + queryString);
            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 authenticated (private) API call to Bitstamp exchange. Uses HTTP POST.
     * 
     * @param apiMethod the API method to call
     * @param params the query param args to use in the API call
     * @return the response from Bitstamp.
     * @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);
        }

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

        try {
            if (params == null) {
                params = new HashMap<String, String>();
            }

            params.put("key", key);
            params.put("nonce", Long.toString(nonce));

            // Create MAC message for signature
            // message = nonce + client_id + api_key
            mac.reset(); // force reset
            mac.update(String.valueOf(nonce).getBytes());
            mac.update(clientId.getBytes());
            mac.update(key.getBytes());

            // signature = hmac.new(API_SECRET, msg=message, digestmod=hashlib.sha256).hexdigest().upper()
            final String signature = toHex(mac.doFinal()).toUpperCase();
            //final String signature = String.format("%064x", new BigInteger(1, mac.doFinal())).toUpperCase();        
            params.put("signature", signature);

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

            final URL url = new URL(API_BASE_URL + apiMethod + "/"); // MUST have the trailing slash...
            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);

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

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

    /*
     * 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"), "HmacSHA256");
            mac = Mac.getInstance("HmacSHA256");
            mac.init(keyspec);
            initializedMACAuthentication = true;
        } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
            final String errorMsg = "Failed to setup MAC security. HINT: Is HMAC-SHA256 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 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 Bistamp config at: " + CONFIG_LOCATION
                    + " HINT: is it on Ada's classpath?";
            LOG.error(errorMsg);
            throw new IllegalStateException(errorMsg);
        }

        try {
            configEntries.load(inputStream);

            /*
             * Grab the client id
             */
            clientId = configEntries.getProperty(CLIENT_ID_PROPERTY_NAME);

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

            if (clientId == null || clientId.length() == 0) {
                final String errorMsg = CLIENT_ID_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 key
             */
            key = configEntries.getProperty(KEY_PROPERTY_NAME);

            // WARNING: Don't log this on 'exposed' system...
            //LOG.info(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 secret
             */
            secret = configEntries.getProperty(SECRET_PROPERTY_NAME);

            // WARNING: Never log this unless testing in the green zone
            //LOG.info(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(Date.class, new BitstampDateDeserializer());
        gson = gsonBuilder.create();
    }
}