de.andreas_rueckert.trade.site.anx.client.ANXClient.java Source code

Java tutorial

Introduction

Here is the source code for de.andreas_rueckert.trade.site.anx.client.ANXClient.java

Source

/**
 * Java implementation for cryptocoin trading.
 *
 * Copyright (c) 2014 the authors:
 * 
 * @author Andreas Rueckert <mail@andreas-rueckert.de>
 *
 * Permission is hereby granted, free of charge, to any person obtaining 
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the Software
 * is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all 
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 
 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package de.andreas_rueckert.trade.site.anx.client;

import de.andreas_rueckert.MissingAccountDataException;
import de.andreas_rueckert.NotYetImplementedException;
import de.andreas_rueckert.trade.account.TradeSiteAccount;
import de.andreas_rueckert.trade.account.TradeSiteAccountImpl;
import de.andreas_rueckert.trade.CryptoCoinTrade;
import de.andreas_rueckert.trade.currency.Currency;
import de.andreas_rueckert.trade.currency.CurrencyImpl;
import de.andreas_rueckert.trade.currency.CurrencyNotSupportedException;
import de.andreas_rueckert.trade.currency.CurrencyPair;
import de.andreas_rueckert.trade.currency.CurrencyPairImpl;
import de.andreas_rueckert.trade.currency.CurrencyProvider;
import de.andreas_rueckert.trade.Depth;
import de.andreas_rueckert.trade.order.CryptoCoinOrderBook;
import de.andreas_rueckert.trade.order.DepositOrder;
import de.andreas_rueckert.trade.order.OrderNotInOrderBookException;
import de.andreas_rueckert.trade.order.OrderStatus;
import de.andreas_rueckert.trade.order.OrderType;
import de.andreas_rueckert.trade.order.SiteOrder;
import de.andreas_rueckert.trade.order.WithdrawOrder;
import de.andreas_rueckert.trade.Price;
import de.andreas_rueckert.trade.site.TradeSite;
import de.andreas_rueckert.trade.site.TradeSiteImpl;
import de.andreas_rueckert.trade.site.TradeSiteRequestType;
import de.andreas_rueckert.trade.site.TradeSiteUserAccount;
import de.andreas_rueckert.trade.Ticker;
import de.andreas_rueckert.trade.Trade;
import de.andreas_rueckert.trade.TradeDataNotAvailableException;
import de.andreas_rueckert.util.HttpUtils;
import de.andreas_rueckert.util.LogUtils;
import de.andreas_rueckert.util.TimeUtils;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import net.sf.json.JSONArray;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.apache.commons.codec.binary.Base64;

/**
 * Main class for the ANX API.
 *
 * @see <a href="http://docs.anxv2.apiary.io/">ANX API</a>
 */
public class ANXClient extends TradeSiteImpl implements TradeSite {

    // Inner classes

    // Static variables

    /**
     * This flag is mainly for debugging and deactivates the actual trading.
     */
    private final static boolean SIMULATION = true;

    // Instance variables

    /**
     * A mapping from the ANX currency pair code to a CurrencyPair object.
     */
    private Map<String, CurrencyPair> _currencyPairCodeMap = null;

    // Constructors

    /**
     * Create a new ANX client instance.
     */
    public ANXClient() {

        _name = "ANX"; // Set the name of this exchange.

        _url = "https://anxpro.com/api/2/"; // Base URL for API calls.

        // Set the supported currency pairs manualls, since it seems there's no API method to fetch them?
        List<CurrencyPair> supportedCurrencyPairs = new ArrayList<CurrencyPair>();
        supportedCurrencyPairs.add(new CurrencyPairImpl("BTC", "USD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("BTC", "HKD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("BTC", "EUR"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("BTC", "CAD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("BTC", "AUD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("BTC", "SGD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("BTC", "JPY"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("BTC", "CHF"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("BTC", "GBP"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("BTC", "NZD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("LTC", "BTC"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("LTC", "USD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("LTC", "HKD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("LTC", "EUR"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("LTC", "CAD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("LTC", "AUD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("LTC", "SGD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("LTC", "JPY"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("LTC", "CHF"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("LTC", "GBP"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("LTC", "NZD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("PPC", "BTC"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("PPC", "LTC"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("PPC", "USD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("PPC", "HKD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("PPC", "EUR"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("PPC", "CAD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("PPC", "AUD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("PPC", "SGD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("PPC", "JPY"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("PPC", "CHF"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("PPC", "GBP"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("PPC", "NZD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("NMC", "BTC"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("NMC", "LTC"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("NMC", "USD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("NMC", "HKD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("NMC", "EUR"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("NMC", "CAD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("NMC", "AUD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("NMC", "SGD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("NMC", "JPY"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("NMC", "CHF"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("NMC", "GBP"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("NMC", "NZD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("DOGE", "BTC"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("DOGE", "USD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("DOGE", "HKD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("DOGE", "EUR"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("DOGE", "CAD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("DOGE", "AUD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("DOGE", "SGD"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("DOGE", "JPY"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("DOGE", "CHF"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("DOGE", "GBP"));
        supportedCurrencyPairs.add(new CurrencyPairImpl("DOGE", "NZD"));

        // Copy the list to the instance array (defined in super class).
        _supportedCurrencyPairs = supportedCurrencyPairs.toArray(new CurrencyPair[supportedCurrencyPairs.size()]);
    }

    // Methods

    /**
     * Perform a query with user authentication and return the HTTP post reply as a string.
     *
     * @param requestUrl The URL to query.
     * @param parameters Additional parameters for the request.
     * @param userAccount The account of the user on the exchange. Null, if the default account should be used.
     *
     * @return The request result as a JSONObject, if the JSON result object has a success value. null otherwise
     */
    private final JSONObject authenticatedQuery(String requestUrl, Map<String, String> parameters,
            TradeSiteUserAccount userAccount) {

        // Create the post data to identify the logged in user.
        String postData = "nonce=" + TimeUtils.getInstance().getCurrentGMTTimeMicros();

        // Add the otional parameters to the query
        if (parameters != null) {

            // Loop over the parameters
            for (Map.Entry<String, String> entry : parameters.entrySet()) {

                // Add each parameter with it's value to the list of parameters.
                postData += "&" + entry.getKey() + "=" + entry.getValue();
            }
        }

        // Query the exchange server and return the HTTP result as a string.
        String requestResult = HttpUtils.httpPost(requestUrl, getAuthenticationHeader(postData, userAccount),
                postData);

        try {
            // Convert the HTTP request return value to JSON to parse further.
            JSONObject jsonResult = JSONObject.fromObject(requestResult);

            // Get the result value to check for an error
            if ("error".equals(jsonResult.getString("result"))) {

                // Output the error to the error stream.
                System.err.println(_name + " query error: " + jsonResult.getString("error"));

                return null; // Query did not success, so just return null.
            }

            // Return the entire json result object.
            return jsonResult;

        } catch (JSONException je) {
            System.err.println("Cannot parse json request result: " + je.toString());

            return null; // An error occured...
        }
    }

    /**
     * Cancel an order on the trade site.
     *
     * @param order The order to cancel.
     *
     * @return true, if the order was canceled. False otherwise.
     */
    public boolean cancelOrder(SiteOrder order) {

        // Create the URL for the request.
        String url = _url + getANXCurrencyPairString(order.getCurrencyPair()) + "/money/order/cancel";

        // The parameters for the HTTP post call.
        HashMap<String, String> parameter = new HashMap<String, String>();

        // Get the site id of this order.
        String site_id = order.getSiteId();

        // If there is no site id, we cannot cancel the order.
        if (site_id == null) {

            return false;

        }

        parameter.put("oid", order.getSiteId()); // Pass the site id of the order.

        // Do the actual request.
        JSONObject jsonResponse = authenticatedQuery(url, parameter, order.getTradeSiteUserAccount());

        if (jsonResponse == null) {

            LogUtils.getInstance().getLogger()
                    .error("No response from " + getName() + " while attempting to cancel an order");

            return false;

        } else { // Check, if the tradesite signals success.

            String tradeSiteResult = jsonResponse.getString("result");

            if (tradeSiteResult.equalsIgnoreCase("success")) { // If the tradesite signals success..

                return true; // canceling worked!

            } else if (tradeSiteResult.equalsIgnoreCase("error")) {

                LogUtils.getInstance().getLogger().error("Tradesite " + getName()
                        + " signaled error while trying to cancel an order: " + jsonResponse.getString("error"));

                return false;

            } else { // This is an unknown condition. Should never be reached, if this implementation is complete and the API did not change.

                LogUtils.getInstance().getLogger().error("Tradesite " + getName()
                        + " signaled an unknown condition while trying to cancel an order.");

                return false;
            }
        }
    }

    /**
     * Execute an order on the trade site.
     * Synchronize this method, since several users might execute orders in parallel via an API implementation instance.
     *
     * @param order The order to execute.
     *
     * @return The new status of the order.
     */
    public synchronized OrderStatus executeOrder(SiteOrder order) {

        OrderType orderType = order.getOrderType(); // Get the type of this order.

        if ((orderType == OrderType.BUY) || (orderType == OrderType.SELL)) { // If this is a buy or sell order, run the trade code.

            // Get the traded currency pair.
            CurrencyPair tradedPair = order.getCurrencyPair();

            if (!isSupportedCurrencyPair(tradedPair)) {

                throw new CurrencyNotSupportedException(
                        _name + " does not support this currency pair: " + tradedPair.toString());
            }

            // Create the URL for the request.
            String url = _url + getANXCurrencyPairString(tradedPair) + "/money/order/add";

            // The parameters for the HTTP post call.
            HashMap<String, String> parameters = new HashMap<String, String>();

            parameters.put("type", order.getOrderType() == OrderType.BUY ? "bid" : "ask"); // Indicate buy or sell.

            // The amount seems to be formatted with 10^8 with the current pairs:
            // http://docs.anxv2.apiary.io/#Multiplier
            parameters.put("amount_int", "" + order.getAmount().multiply(new BigDecimal("100000000")).longValue());

            // The price is multiplied with 10000 if the currency is BTC or LTC. 100000000 otherwise.
            BigDecimal multiplier = tradedPair.getCurrency().hasCode(new String[] { "BTC", "LTC" })
                    ? new BigDecimal("10000")
                    : new BigDecimal("100000000");

            parameters.put("price_int", "" + order.getPrice().multiply(multiplier).longValue());

            // If this is a simulation, don't execute the query
            if (SIMULATION) {

                System.out.println(
                        "Simulation mode for " + _name + ".\nWould make request to " + url + " with parameters:");
                for (Map.Entry parameterEntry : parameters.entrySet()) {

                    System.out.println(
                            "Parameter " + parameterEntry.getKey() + " has value " + parameterEntry.getValue());
                }

                return OrderStatus.UNKNOWN; // What else can we say?
            }

            // Query the server and fetch the result as a json object.
            JSONObject jsonResult = authenticatedQuery(url, parameters, order.getTradeSiteUserAccount());

            if (jsonResult != null) {

                if ("success".equalsIgnoreCase(jsonResult.getString("result"))) { // The order was executed.

                    // Now store the site id of the order in the order object,
                    // so we can check the status of the order later.
                    order.setSiteId(jsonResult.getString("return"));

                    // We could check the status of the order now, or just return, that it is unknown.
                    order.setStatus(OrderStatus.UNKNOWN);

                    return order.getStatus();

                } else {

                    order.setStatus(OrderStatus.ERROR);

                    return order.getStatus();
                }
            }
        }

        // The other order types are not yet implemented.
        throw new NotYetImplementedException("Executing this order type is not yet implemented for " + _name);
    }

    /**
     * Get the current funds of the user via the API.
     *
     * @param userAccount The account of the user on the exchange. Null, if the default account should be used.
     *
     * @return The accounts with the current balance as a collection of Account objects, or null if the request failed.
     */
    public Collection<TradeSiteAccount> getAccounts(TradeSiteUserAccount userAccount) {

        // Create the URL for the request.
        String url = _url + "money/info";

        // Do the actual request.
        JSONObject jsonResponse = authenticatedQuery(url, null, userAccount);

        if (jsonResponse == null) {

            LogUtils.getInstance().getLogger()
                    .error("No response from " + getName() + " while attempting to get the funds");

            return null;

        } else {

            String tradeSiteResult = jsonResponse.getString("result");

            if (tradeSiteResult.equalsIgnoreCase("success")) { // If the tradesite signals success..

                // Get the wallets as a JSON object.
                JSONObject jsonWallet = jsonResponse.getJSONObject("data").getJSONObject("Wallets");

                // A buffer for the parsed funds.
                List<TradeSiteAccount> result = new ArrayList<TradeSiteAccount>();

                // Now iterate over all the currencies in the funds.
                for (Iterator currencyIterator = jsonWallet.keys(); currencyIterator.hasNext();) {

                    String currentCurrency = (String) currencyIterator.next(); // Get the next currency.

                    // Get the balance for this currency.
                    BigDecimal balance = new BigDecimal(
                            jsonWallet.getJSONObject(currentCurrency).getJSONObject("Balance").getString("value"));

                    // Create an account and add it to the result.
                    result.add(new TradeSiteAccountImpl(balance,
                            CurrencyProvider.getInstance().getCurrencyForCode(currentCurrency.toUpperCase()),
                            this));
                }

                return result; // Return the list with the accounts.

            } else if (tradeSiteResult.equalsIgnoreCase("error")) {

                LogUtils.getInstance().getLogger().error("Tradesite " + getName()
                        + " signaled error while trying to get the funds: " + jsonResponse.getString("error"));

                return null;
            }
        }

        return null; // Should never be reached.
    }

    /**
     * Get the ANX code for a currency pair.
     *
     * @param currencyPair The currency pair.
     *
     * @return The ANX code for the currency pair as a string.
     */
    private final String getANXCurrencyPairString(CurrencyPair currencyPair) {

        return currencyPair.getCurrency().getCode() + currencyPair.getPaymentCurrency().getCode();
    }

    /**
     * Create authentication entries for a HTTP post header.
     *
     * @param postData The data to post via HTTP.
     * @param userAccount The account of the user on the exchange. Null, if the default account should be used.
     *
     * @return The header entries as a map or null if an error occured.
     */
    Map<String, String> getAuthenticationHeader(String postData, TradeSiteUserAccount userAccount) {

        HashMap<String, String> result = new HashMap<String, String>();
        Mac mac;
        String accountKey = null;
        String accountSecret = null;

        // Try to get user account and secret.
        if (userAccount != null) {

            accountKey = userAccount.getAPIkey();
            accountSecret = userAccount.getSecret();

        } else { // Throw an error.

            throw new MissingAccountDataException("No user account given for " + _name + " request");
        }

        // Check, if key and secret are available for the request.
        if (accountKey == null) {
            throw new MissingAccountDataException("Key not available for authenticated request to " + _name);
        }
        if (accountSecret == null) {
            throw new MissingAccountDataException("Secret not available for authenticated request to " + _name);
        }

        result.put("Rest-Key", accountKey);

        // Create a new secret key
        SecretKeySpec key = new SecretKeySpec(Base64.decodeBase64(accountSecret), "HmacSHA512");

        // Create a new mac
        try {

            mac = Mac.getInstance("HmacSHA512");

        } catch (NoSuchAlgorithmException nsae) {

            System.err.println("No such algorithm exception: " + nsae.toString());

            return null;
        }

        // Init mac with key.
        try {

            mac.init(key);

        } catch (InvalidKeyException ike) {

            System.err.println("Invalid key exception: " + ike.toString());

            return null;
        }

        // Encode the post data by the secret and encode the result as base64.
        try {

            result.put("Rest-Sign", Base64.encodeBase64String(mac.doFinal(postData.getBytes("UTF-8"))));

        } catch (UnsupportedEncodingException uee) {

            System.err.println("Unsupported encoding exception: " + uee.toString());

            return null;
        }

        return result;
    }

    /**
     * Get the currency pair object for a given currency pair code.
     *
     * @param currencyPairCode The ANX code for a currency pair (i.e. DOGEUSD).
     *
     * @return The currency pair as a CurrencyPair object or null of no such pair was found.
     */
    private final CurrencyPair getCurrencyPairForANXCode(String currencyPairCode) {

        // If there is no currency pair mapping yet, create one.
        if (_currencyPairCodeMap == null) {

            // Create a new hash map for the mapping.
            _currencyPairCodeMap = new HashMap<String, CurrencyPair>();

            // Loop over the currency pairs 
            for (CurrencyPair currentCurrencyPair : getSupportedCurrencyPairs()) {

                // Generate the ANX code for the pair as the key and add the pair to the mapping.
                _currencyPairCodeMap.put(getANXCurrencyPairString(currentCurrencyPair), currentCurrencyPair);
            }
        }

        // Fetch the pair for the given code from the map.
        return _currencyPairCodeMap.get(currencyPairCode);
    }

    /**
      * Get the market depth as a Depth object.
      *
      * @param currencyPair The queried currency pair.
      *
      * @throws TradeDataNotAvailableException if the depth is not available.
      */
    public Depth getDepth(CurrencyPair currencyPair) throws TradeDataNotAvailableException {

        if (!isSupportedCurrencyPair(currencyPair)) {
            throw new CurrencyNotSupportedException(
                    "Currency pair: " + currencyPair.toString() + " is currently not supported on " + _name);
        }

        // Create the URL for the depth request.
        String url = _url + getANXCurrencyPairString(currencyPair) + "/money/depth/full";

        // Do the actual request.
        String requestResult = HttpUtils.httpGet(url);

        if (requestResult != null) { // Request sucessful?

            try {

                // Convert the result to JSON.
                JSONObject requestResultJSON = (JSONObject) JSONObject.fromObject(requestResult);

                // Check the result field for success.
                String resultField = requestResultJSON.getString("result");

                if (resultField.equalsIgnoreCase("success")) { // Check, if the request worked ok.

                    // Get the data and convert them to a depth object.
                    return new ANXDepth(requestResultJSON.getJSONObject("data"), currencyPair, this);

                } else {

                    // Get the error message.
                    String errorField = requestResultJSON.getString("error");

                    // Write the first error to the log for now. Hopefully it should explain the problem.
                    LogUtils.getInstance().getLogger().error("Error while fetching the depth from " + _name
                            + ". Error message is: " + requestResultJSON.getString("error"));

                    throw new TradeDataNotAvailableException("Error while fetching the depth from " + this._name);
                }

            } catch (JSONException je) {

                LogUtils.getInstance().getLogger()
                        .error("Cannot parse " + this._name + " depth return: " + je.toString());

                throw new TradeDataNotAvailableException("cannot parse depth data from " + this._name);
            }

        }

        // The server return was null...
        throw new TradeDataNotAvailableException(this._name + " server did not respond to depth request");
    }

    /**
     * Get the current market depths (minimal data of the orders) for a given list of currency pairs.
     *
     * @param currencyPairs The currency pairs to query.
     *
     * @return The current market depths for the given currency pairs.
     *
     * @throws TradeDataNotAvailableException if to many depths are not available.
     */
    public List<Depth> getDepths(CurrencyPair[] currencyPairs) throws TradeDataNotAvailableException {

        // ANX has an optional argument for extra currency pairs, that could be used to fetch
        // many depths at once.
        // @see: http://docs.anxv2.apiary.io/#get-%2Fapi%2F2%2F{currency_pair}%2Fmoney%2Fdepth%2Ffull

        // Create the URL for the depth request.
        String url = _url + getANXCurrencyPairString(currencyPairs[0]) + "/money/depth/full?extraCcyPairs=";

        // Append the extra currency pairs to the URL.
        for (int currentPairIndex = 1; currentPairIndex < currencyPairs.length; ++currentPairIndex) {

            if (currentPairIndex > 1) { // Separate the pairs with colons.
                url += ",";
            }

            // Append the pair to the URL.
            url += getANXCurrencyPairString(currencyPairs[currentPairIndex]);
        }

        // Do the actual request.
        String requestResult = HttpUtils.httpGet(url);

        if (requestResult != null) { // Request sucessful?

            try {

                // Convert the result to JSON.
                JSONObject requestResultJSON = (JSONObject) JSONObject.fromObject(requestResult);

                // Check the result field for success.
                String resultField = requestResultJSON.getString("result");

                if (resultField.equalsIgnoreCase("success")) { // Check, if the request worked ok.

                    // This is an JSON object, but represents an array with the currency pair data.
                    JSONObject currencyPairData = requestResultJSON.getJSONObject("data");

                    // Create a buffer for the resulting depth array.
                    List<Depth> resultBuffer = new ArrayList<Depth>();

                    // Loop over the currency pairs.
                    for (Iterator<String> keys = currencyPairData.keys(); keys.hasNext();) {

                        // Get the ANX code for the next currency pair.
                        String currentPairCode = (String) keys.next();

                        // Try to get the currency pair for this code.
                        CurrencyPair currentPair = getCurrencyPairForANXCode(currentPairCode);

                        if (currentPair == null) { // If there is no pair, this code is unknown.

                            throw new CurrencyNotSupportedException(
                                    "Cannot find a currency pair for the ANX code: " + currentPairCode);

                        } else {

                            // Convert the data of this pair to a Depth object and add it to the result buffer.
                            resultBuffer.add(new ANXDepth(currencyPairData.getJSONObject(currentPairCode),
                                    currentPair, this));
                        }
                    }

                    // Return the result buffer.
                    return resultBuffer;

                } else {

                    // Get the error message.
                    String errorField = requestResultJSON.getString("error");

                    // Write the first error to the log for now. Hopefully it should explain the problem.
                    LogUtils.getInstance().getLogger().error("Error while fetching the depth from " + _name
                            + ". Error message is: " + requestResultJSON.getString("error"));

                    throw new TradeDataNotAvailableException("Error while fetching the depth from " + this._name);
                }
            } catch (JSONException je) {

                LogUtils.getInstance().getLogger()
                        .error("Cannot parse " + this._name + " depth return: " + je.toString());

                throw new TradeDataNotAvailableException("cannot parse depth data from " + this._name);
            }
        }

        // The server return was null...
        throw new TradeDataNotAvailableException(this._name + " server did not respond to depth request");
    }

    /**
     * Get the fee for an order.
     * Synchronize this method, since several users might use this method with different
     * accounts and therefore different fees via a single API implementation instance.
     *
     * @param order The order to use for the fee computation.
     *
     * @return The fee in the resulting currency (currency value for buy, payment currency value for sell).
     */
    public synchronized Price getFeeForOrder(SiteOrder order) {

        if (order instanceof WithdrawOrder) { // If this is a withdraw order

            // Get the withdrawn currency.
            Currency withdrawnCurrency = order.getCurrencyPair().getCurrency();

            if (withdrawnCurrency.hasCode(new String[] { "BTC", "LTC", "NMC", "PPC", "DOGE" })) {

                // Withdrawing cryptocoins is free.
                return new Price("0", withdrawnCurrency);

            } else {

                // Since I cannot check, if FIAT is send via wire or SEPA, I just leave it away for now.
                throw new CurrencyNotSupportedException(
                        "The fees for FIAT withdraw are currently not implemented for " + _name);
            }

        } else if (order instanceof DepositOrder) { // If this is a deposit order

            // Get the deposited currency.
            Currency depositedCurrency = order.getCurrencyPair().getCurrency();

            if (depositedCurrency.hasCode(new String[] { "BTC", "LTC", "NMC", "PPC", "DOGE" })) {

                // Depositing cryptocoins is free.
                return new Price("0", depositedCurrency);

            } else {

                // Since I cannot check, if FIAT is send via wire or SEPA, I just leave it away for now.
                throw new CurrencyNotSupportedException(
                        "The fees for FIAT deposit are currently not implemented for " + _name);
            }

        } else if (order.getOrderType() == OrderType.BUY) { // Is this a buy trade order?

            // @see: https://anxpro.com/faq#tab1
            // It says 0.05% fee?
            return new Price(order.getAmount().multiply(new BigDecimal("0.0005")),
                    order.getCurrencyPair().getCurrency());

        } else if (order.getOrderType() == OrderType.SELL) { // This is a sell trade order

            // A sell order has the payment currency as the target currency.
            return new Price(order.getAmount().multiply(order.getPrice()).multiply(new BigDecimal("0.0005")),
                    order.getCurrencyPair().getPaymentCurrency());

        } else { // This is an unknown order type?

            return null; // Should never happen...
        }
    }

    /**
     * Get the shortest allowed requet interval in microseconds.
     *
     * @return The shortest allowed request interval in microseconds.
     */
    public long getMinimumRequestInterval() {

        return 15L * 1000000L; // Just a default of 15s for now.
    }

    /**
     * Get the open orders on this trade site.
     *
     * @param userAccount The account of the user on the exchange. Null, if the default account should be used.
     *
     * @return The open orders as a collection, or null if the request failed.
     */
    public Collection<SiteOrder> getOpenOrders(TradeSiteUserAccount userAccount) {

        // Create a URL to request the open orders.
        // Note 1: no matter what the currency pair is, orders from _all_ pairs will be returned! 
        // ( @see http://docs.anxv2.apiary.io/#post-%2Fapi%2F2%2F{currency_pair}%2Fmoney%2Forders )
        // So I just use the first supported currency pair as a dummy for now.
        JSONObject jsonResult = authenticatedQuery(_url + _supportedCurrencyPairs[0] + "/money/orders", null,
                userAccount);

        if (jsonResult != null) {

            JSONArray requestReturn = jsonResult.getJSONArray("return"); // Get the return value as an array

            // Create a buffer for the result.
            List<SiteOrder> result = new ArrayList<SiteOrder>();

            // Now loop over the open orders.
            for (Iterator orderIterator = requestReturn.iterator(); orderIterator.hasNext();) {

                JSONObject currentJSONOrder = (JSONObject) orderIterator.next();

                // Get the site id from this order.
                String currentSiteId = currentJSONOrder.getString("oid");

                // Since we know the tradesite and the site id now, we can query the order book for the order.
                SiteOrder currentOrder = CryptoCoinOrderBook.getInstance().getOrder(this, currentSiteId);

                if (currentOrder != null) { // If the order book returned an order,

                    result.add(currentOrder); // add it to the result buffer.

                } else { // It seems, this order is not in the order book. I can consider this an error at the moment,
                         // since every order should go through the order book.

                    throw new OrderNotInOrderBookException(
                            "Error: " + _name + " order with site id " + currentSiteId + " is not in order book!");
                }
            }

            return result; // Return the buffer with the orders.
        }

        return null; // The query failed.
    }

    /**
     * Get the section name in the global property file.
     *
     * @return The name of the property section as a String.
     */
    public String getPropertySectionName() {

        return _name;
    }

    /**
     * Get the current ticker from the API.
     *
     * @param currencyPair The currency pair to query.
     *
     * @return The current ticker.
     *
     * @throws TradeDataNotAvailableException if the ticker is not available.
     */
    public Ticker getTicker(CurrencyPair currencyPair) throws TradeDataNotAvailableException {

        if (!isSupportedCurrencyPair(currencyPair)) {
            throw new CurrencyNotSupportedException(
                    "Currency pair: " + currencyPair.toString() + " is currently not supported on " + _name);
        }

        // Create the URL to fetch the ticker
        String url = _url + getANXCurrencyPairString(currencyPair) + "/money/ticker";

        // Do the actual request.
        String requestResult = HttpUtils.httpGet(url);

        if (requestResult != null) { // Request sucessful?

            try {

                // Convert the result to JSON.
                JSONObject requestResultJSON = (JSONObject) JSONObject.fromObject(requestResult);

                // Check the result field for success.
                String resultField = requestResultJSON.getString("result");

                if (resultField.equalsIgnoreCase("success")) { // Check, if the request worked ok.

                    // Get the data and convert them to a depth object.
                    return new ANXTicker(requestResultJSON.getJSONObject("data"), currencyPair, this);

                } else {

                    // Get the error message.
                    String errorField = requestResultJSON.getString("error");

                    // Write the error to the log. Hopefully it should explain the problem.
                    LogUtils.getInstance().getLogger().error("Error while fetching the depth from " + _name
                            + ". Error message is: " + requestResultJSON.getString("error"));

                    throw new TradeDataNotAvailableException("Error while fetching the ticker from " + this._name);
                }

            } catch (JSONException je) {

                LogUtils.getInstance().getLogger()
                        .error("Cannot parse " + this._name + " ticker return: " + je.toString());

                throw new TradeDataNotAvailableException("cannot parse depth data from " + this._name);
            }
        }

        // The server return was null...
        throw new TradeDataNotAvailableException(this._name + " server did not respond to ticker request");
    }

    /**
     * Get a list of recent trades.
     *
     * @param since_micros The GMT-relative epoch in microseconds.
     * @param currencyPair The currency pair to query.
     *
     * @return The trades as a list of Trade objects.
     *
     * @throws TradeDataNotAvailableException if the ticker is not available.
     */
    public List<Trade> getTrades(long since_micros, CurrencyPair currencyPair)
            throws TradeDataNotAvailableException {

        if (!isSupportedCurrencyPair(currencyPair)) {
            throw new CurrencyNotSupportedException(
                    "Currency pair: " + currencyPair.toString() + " is currently not supported on " + _name);
        }

        // Create the URL to fetch the trades.
        String url = _url + getANXCurrencyPairString(currencyPair) + "/money/trade/fetch?since="
                + (since_micros / 1000);

        // Do the actual request.
        String requestResult = HttpUtils.httpGet(url);

        if (requestResult != null) { // Request sucessful?

            try {

                // Convert the HTTP request return value to JSON to parse further.
                JSONObject jsonResult = JSONObject.fromObject(requestResult);

                // Create a buffer for the result.
                List<Trade> trades = new ArrayList<Trade>();

                // Get the result value to check for success
                if (!"success".equals(jsonResult.getString("result"))) {

                    // Write the error to the log. Hopefully it should explain the problem.
                    LogUtils.getInstance().getLogger().error("Error while fetching the trades from " + _name
                            + ". Error message is: " + jsonResult.getString("error"));

                    throw new TradeDataNotAvailableException(
                            this._name + " reported an error while fetching the trades.");
                }

                // Convert the trade data to a JSON array.
                JSONArray jsonData = jsonResult.getJSONArray("data");

                // Iterate over the json array and convert each trade from json to a Trade object.
                for (int i = 0; i < jsonData.size(); ++i) {

                    JSONObject tradeObject = jsonData.getJSONObject(i);

                    try {

                        trades.add(new ANXTrade(tradeObject, this, currencyPair)); // Add the new Trade object to the list.

                    } catch (JSONException je) { // Cannot parse the JSON trade.

                        // Write the error to the log. Hopefully it should explain the problem.
                        LogUtils.getInstance().getLogger().error("Error while parsing a trades from " + _name
                                + ". Error message is: " + je.toString());

                        throw new TradeDataNotAvailableException(
                                this._name + " reported an error while parsing a trade.");
                    }
                }

                // updateLastRequest( TradeSiteRequestType.Trades);  // Update the timestamp of the last request.

                return trades; // And return the list of trades.

            } catch (JSONException je) {

                System.err.println("Cannot parse trade object: " + je.toString());
            }
        }

        // The server return was null...
        throw new TradeDataNotAvailableException(this._name + " server did not respond to trades request");
    }

    /**
     * Get the interval, in which the trade site updates it's depth, ticker etc. 
     * in microseconds.
     *
     * @return The update interval in microseconds.
     */
    public final long getUpdateInterval() {

        return 15L * 1000000L; // 15s should work for most exchanges as a default.
    }

    /**
     * Check, if some request type is allowed at the moment. Most
     * trade site have limits on the number of request per time interval.
     *
     * @param requestType The type of request (trades, depth, ticker, order etc).
     *
     * @return true, if the given type of request is possible at the moment.
     */
    public boolean isRequestAllowed(TradeSiteRequestType requestType) {

        return true; // Just a dummy for now.
    }
}