de.andreas_rueckert.trade.site.mtgox.client.MtGoxClient.java Source code

Java tutorial

Introduction

Here is the source code for de.andreas_rueckert.trade.site.mtgox.client.MtGoxClient.java

Source

/**
 * Java implementation for cryptocoin trading.
 *
 * Copyright (c) 2013 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.mtgox.client;

import de.andreas_rueckert.MissingAccountDataException;
import de.andreas_rueckert.NotYetImplementedException;
import de.andreas_rueckert.persistence.PersistentProperty;
import de.andreas_rueckert.persistence.PersistentPropertyList;
import de.andreas_rueckert.trade.account.CryptoCoinAccountImpl;
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;
import de.andreas_rueckert.trade.CurrencyImpl;
import de.andreas_rueckert.trade.CurrencyNotSupportedException;
import de.andreas_rueckert.trade.CurrencyPair;
import de.andreas_rueckert.trade.CurrencyPairImpl;
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.Order;
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.TradeDataRequestNotAllowedException;
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.util.HttpUtils;
import de.andreas_rueckert.util.LogUtils;
import de.andreas_rueckert.util.TimeUtils;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
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 to handle mtgox requests.
 *
 * @author Andreas Rueckert <a_rueckert@gmx.net>
 *
 * @see https://en.bitcoin.it/wiki/MtGox/API/HTTP/v1#HTTP_API_version_1_methods
 */
public class MtGoxClient extends TradeSiteImpl implements TradeSite {

    // Static variables

    /**
     * The domain of the service.
     */
    public static String DOMAIN = "mtgox.com";

    // Instance variables

    /**
     * The currently used currency.
     */
    private Currency _currentCurrency = CurrencyImpl.USD;

    /**
     * The MtGox provided key.
     */
    private String _key;

    /**
     * The timestamp of the last requests for the request types, that have 
     * to be limited.
     */
    private long _lastRequestTimestampForDepth = -1L;
    private long _lastRequestTimestampForTicker = -1L;
    private long _lastRequestTimestampForTrades = -1L;

    /**
     * The MtGox provided secret.
     */
    private String _secret;

    /**
     * The microsecond base time (approximated from the millisecond timestamp).
     */
    private long _timestampBase = -1;

    /**
     * The nanosecond offset to add to the base.
     */
    private long _nanoTimeOffset;

    // Constructors

    /**
     * Create a new MtGox client.
     */
    public MtGoxClient() {
        super();

        _name = "MtGox";
        _url = "https://mtgox.com/";

        // Define the supported currency pairs for this trading site.
        _supportedCurrencyPairs = new CurrencyPair[1];
        _supportedCurrencyPairs[0] = new CurrencyPairImpl(CurrencyImpl.BTC, CurrencyImpl.USD);

        // Higher the log level.
        this._logLevel = LOGLEVEL_ERROR;
    }

    // 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 MtGox 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("MtGox query error: " + jsonResult.getString("error"));

                return null; // MtGox 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) {

        String url = "https://" + DOMAIN + "/api/1/" + order.getCurrencyPair().getCurrency().getName().toUpperCase()
                + order.getCurrencyPair().getPaymentCurrency().getName().toUpperCase() + "/private/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.

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

    /**
     * Get the accounts with the current funds on this trading site.
     *
     * @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 an array of Account objects.
     */
    public Collection<TradeSiteAccount> getAccounts(TradeSiteUserAccount userAccount) {

        JSONObject privateInfo = getPrivateInfo(userAccount);

        if (privateInfo != null) {

            // Get all the wallets.
            JSONObject jsonBalances = privateInfo.getJSONObject("Wallets");

            // An array for the parsed funds.
            ArrayList<TradeSiteAccount> result = new ArrayList<TradeSiteAccount>();

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

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

                JSONObject jsonBalance = jsonBalances.getJSONObject(currentCurrencyName);

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

                result.add(new TradeSiteAccountImpl(balance,
                        CurrencyImpl.findByString(currentCurrencyName.toUpperCase()), this));
            }

            return result; // Return the array with the accounts.
        } else {
            // System.out.println( "DEBUG: private info is null!");
        }

        return null;
    }

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

            if (!isSupportedCurrencyPair(order.getCurrencyPair())) {
                throw new CurrencyNotSupportedException("This currency pair is not support in MtGox orders: "
                        + order.getCurrencyPair().getCurrency().toString() + " and payment in "
                        + order.getCurrencyPair().getPaymentCurrency());
            }

            String url = "https://" + DOMAIN + "/api/1/"
                    + order.getCurrencyPair().getCurrency().getName().toUpperCase()
                    + order.getCurrencyPair().getPaymentCurrency().getName().toUpperCase() + "/private/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.
            parameters.put("amount", "" + order.getAmount());
            parameters.put("price", "" + order.getPrice()); // ToDo: format price and amount? Don't know, how picky mtgox is about the format...

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

        } else if (orderType == OrderType.DEPOSIT) { // This is a deposit order...

            DepositOrder depositOrder = (DepositOrder) order; // Just to avoid constant casting.

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

            // Check, if this currency is supported yet in this implementation.
            if (depositedCurrency.equals(CurrencyImpl.BTC)) {

                // Get the address for a deposit from the trade site.
                String depositAddress = getDepositAddress(depositedCurrency, order.getTradeSiteUserAccount());

                // Attach a new account for depositing to this order.
                depositOrder.setAccount(
                        new CryptoCoinAccountImpl(depositAddress, new BigDecimal("0"), depositedCurrency));

                // Now return a new order status to indicate, that the order was modified.
                return OrderStatus.DEPOSIT_ADDRESS_GENERATED;

            } else { // This currency is not supported yet.

                throw new CurrencyNotSupportedException("Depositing the currency " + depositedCurrency
                        + " is not supported yet in this implementation");
            }

        } else if (orderType == OrderType.WITHDRAW) { // This is a withdrawal order...

            throw new NotYetImplementedException("Withdrawal from MtGox is not yet implemented");

        }

        return null;
    }

    /**
     * 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 { // Use the default account from the API implementation.

            accountKey = _key;
            accountSecret = _secret;
        }

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

        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 all the cancelled trades of the current user.
     *
     * @param currencyPair The currency pair to use.
     * @param userAccount The account of the user on the exchange. Null, if the default account should be used.
     *
     * @return The cancelled trades as an array of Trade objects or null if the request failed.
     */
    Trade[] getCancelledTrades(CurrencyPair currencyPair, TradeSiteUserAccount userAccount) {

        // Query the MtGox server and fetch the result as a json object.
        JSONObject jsonResult = authenticatedQuery(
                "https://" + DOMAIN + "/api/1/" + currencyPair.getCurrency().getName()
                        + currencyPair.getPaymentCurrency().getName() + "/public/cancelledtrades",
                null, userAccount);

        if (jsonResult != null) {
            try {
                // Get the return value and convert it to a Trade array.
                JSONArray jsonTrades = jsonResult.getJSONArray("return");

                CryptoCoinTrade[] trades = new CryptoCoinTrade[jsonTrades.size()];

                try {
                    for (int i = 0; i < jsonTrades.size(); i++) {
                        trades[i] = new MtGoxTradeImpl(jsonTrades.getJSONObject(i), this, currencyPair);
                    }
                } catch (ParseException pe) { // Cannot parse the JSON trade.
                    System.err.println("Cannot parse JSON trade object: " + pe.toString());

                    return null; // Error => return null...
                }

                return trades; // Return the parsed trades.

            } catch (JSONException je) {
                System.err.println("Cannot parse trade objects: " + je.toString());
            }
        }

        return null; // The query failed.
    }

    /**
     * Get the currently set currency.
     *
     * @return The currently set currency.
     */
    public Currency getCurrentCurrency() {
        return _currentCurrency;
    }

    /**
     * Get an address to deposit coins at MtGox.
     *
     * @param currency The currency to deposit.
     * @param userAccount The account of the user on the exchange. Null, if the default account should be used.
     *
     * @return The deposit address as a string, or null if no address is found.
     */
    private String getDepositAddress(Currency currency, TradeSiteUserAccount userAccount) {

        // The URL to request the address from.
        String url = null;

        // Check, if the currency is supported
        if (currency.equals(CurrencyImpl.BTC)) {

            // Set the url to fetch the deposit address.
            url = "https://data.mtgox.com/api/1/generic/bitcoin/address";

            // Do a authenticated query for the deposit address.
            JSONObject result = authenticatedQuery(url, null, userAccount);

            // Get the return value and convert it to an object.
            JSONObject jsonAddress = result.getJSONObject("return");

            // Get the actual address as a string and return it.
            return jsonAddress.getString("addr");

        } else {

            throw new CurrencyNotSupportedException("Getting a deposit address for currency " + currency
                    + " is not yet supported by this implementation");
        }
    }

    /**
     * Get the trade data from mtgox.
     *
     * @param currencyPair The currency pair to query.
     *
     * @return The trade data as a Depth object.
     */
    public Depth getDepth(CurrencyPair currencyPair) {

        // System.out.println( "Fetching depth from MtGox for " + currencyPair.getName());

        // If the user wants intense logging, add some info.
        if (getLogLevel() > LOGLEVEL_WARNING) {
            LogUtils.getInstance().getLogger().info("Fetching depth from " + getName());
        }

        if (isRequestAllowed(TradeSiteRequestType.Depth)) {

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

            String requestUrl = "https://" + DOMAIN + "/api/1/" + currencyPair.getCurrency().getName()
                    + currencyPair.getPaymentCurrency().getName() + "/public/depth?raw";

            // System.out.println( "Fetching mtgox depth from: " + requestUrl);

            String requestResult = HttpUtils.httpGet(requestUrl);

            if (requestResult != null) { // Request sucessful?
                try {
                    // Convert the HTTP request return to JSON to parse further.
                    Depth depth = new MtGoxDepth(JSONObject.fromObject(requestResult), currencyPair, this);

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

                    return depth;
                } catch (JSONException je) {
                    System.err.println("Cannot parse mtgox depth return: " + je.toString());
                }
            }

            return null; // The depth request failed.
        }

        // The request is not allowed at the moment, so throw an exception.
        throw new TradeDataRequestNotAllowedException("Request for depth not allowed at the moment at MtGox site");
    }

    /**
     * Get the fee for an order in the resulting currency.
     * 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) {

        // For withdrawals and deposits, just use the default implementation for now..
        if ((order instanceof WithdrawOrder) || (order instanceof DepositOrder)) {

            return super.getFeeForOrder(order);

        } else { // This is a trade order.

            if ((order.getOrderType() == OrderType.SELL) // If this is a sell order for US dollar
                    && (order.getCurrencyPair().getPaymentCurrency().equals(CurrencyImpl.USD))) {

                // There's no fee for btc sales at the moment, it seems.
                return new Price("0", order.getCurrencyPair().getPaymentCurrency());

            } else { // Let the default implementation handle the fees.

                return super.getFeeForOrder(order);
            }
        }
    }

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

    /**
     * Get the open orders of the user.
     *
     * @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
     */
    public Collection<SiteOrder> getOpenOrders(TradeSiteUserAccount userAccount) {

        JSONObject jsonResult = authenticatedQuery("https://mtgox.com/api/1/generic/private/orders", null,
                userAccount);

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

            // Create a buffer for the result.
            ArrayList<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: MtGox order with site id " + currentSiteId + " is not in order book!");
                }
            }

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

        return null; // The query failed.
    }

    /**
     * Request the private info of a user.
     *
     * @param userAccount The account of the user on the exchange. Null, if the default account should be used.
     *
     * @return The private info of the current user.
     */
    public JSONObject getPrivateInfo(TradeSiteUserAccount userAccount) {

        JSONObject jsonResult = authenticatedQuery("https://mtgox.com/api/1/generic/private/info", null,
                userAccount);

        if (jsonResult != null) {
            JSONObject requestReturn = jsonResult.getJSONObject("return");

            return requestReturn;
        }

        return null; // Query failed...
    }

    /**
     * Get the section name in the global property file.
     */
    public String getPropertySectionName() {
        return "MtGox";
    }

    /**
     * Get the current settings of the MtGox client.
     *
     * @return The current settings of the MtGox client.
     */
    public PersistentPropertyList getSettings() {

        // Get the settings from the base class.
        PersistentPropertyList result = super.getSettings();

        result.add(new PersistentProperty("Key", null, _key, 5)); // The key is a parameter.
        result.add(new PersistentProperty("Secret", null, _secret, 4)); // The secret the next parameter.

        return result; // Return the map with the settings.
    }

    /**
     * Get the current ticker from MtGox API version 1.
     *
     * @param currencyPair The currency pair to use for the data.
     *
     * @return The current MtGox ticker.
     */
    public Ticker getTicker(CurrencyPair currencyPair) {

        if (isRequestAllowed(TradeSiteRequestType.Ticker)) {

            String url = "https://" + DOMAIN + "/api/1/" + currencyPair.getCurrency().getName()
                    + currencyPair.getPaymentCurrency().getName() + "/public/ticker";

            String requestResult = HttpUtils.httpGet(url);

            if (requestResult != null) { // Request successful?
                try {
                    // Convert the HTTP request return value to JSON to parse further.
                    JSONObject jsonResult = JSONObject.fromObject(requestResult);

                    // Get the result value to check for success
                    if (!"success".equals(jsonResult.getString("result"))) {
                        return null; // MtGox server did not return the ticker.
                    }
                    Ticker ticker = new MtGoxTicker(jsonResult.getJSONObject("return"), currencyPair, this);

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

                    return ticker;

                } catch (JSONException je) {
                    System.err.println("Cannot parse ticker object: " + je.toString());
                }
            }

            return null; // The ticker request failed.
        }

        // The request is not allowed at the moment, so throw an exception.
        throw new TradeDataRequestNotAllowedException("Request for ticker not allowed at the moment at MtGox site");
    }

    /**
     * Get a number of trades.
     *
     * @param tid The id of the first trade to get or just 0 to get all trades availables. The id is the
     * microsecond timestamp of the trade, so the tid is used as a timespan filter, too!
     * @param currencyPair The currency pair to query.
     *
     * @return The trades as an array of CryptoCoinTrade objects.
     */
    public CryptoCoinTrade[] getTrades(long tid, CurrencyPair currencyPair) {

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

        String requestUrl = "https://" + DOMAIN + "/api/1/" + currencyPair.getCurrency().getName()
                + currencyPair.getPaymentCurrency().getName() + "/public/trades?since=" + tid;

        return getTradesFromURL(requestUrl, currencyPair);
    }

    /**
     * Get a number of trades from a given URL.
     *
     * @param url The URL to fetch the trades from.
     * @param currencyPair The requested currency pair.
     *
     * @return The trades as an array of Trade objects or null if an error occured.
     */
    private CryptoCoinTrade[] getTradesFromURL(String url, CurrencyPair currencyPair) {

        if (isRequestAllowed(TradeSiteRequestType.Trades)) {

            ArrayList<CryptoCoinTrade> trades = new ArrayList<CryptoCoinTrade>();

            // System.out.println( "Fetching trades from url: " + url);

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

                    // Get the result value to check for success
                    if (!"success".equals(jsonResult.getString("result"))) {
                        return null; // MtGox server did not return the ticker.
                    }

                    // Convert the result to a JSON array.
                    JSONArray resultArray = jsonResult.getJSONArray("return");

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

                        try {
                            trades.add(new MtGoxTradeImpl(tradeObject, this, currencyPair)); // Add the new Trade object to the list.
                        } catch (ParseException pe) { // Cannot parse the JSON trade.
                            System.err.println("Cannot parse JSON trade object: " + pe.toString());

                            return null;
                        }
                    }

                    CryptoCoinTrade[] tradeArray = trades.toArray(new CryptoCoinTrade[trades.size()]); // Convert the list to an array.

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

                    return tradeArray; // And return the array.

                } catch (JSONException je) {
                    System.err.println("Cannot parse trade object: " + je.toString());
                }
            }
            return null; // An error occured.
        }

        // The request is not allowed at the moment, so throw an exception.
        throw new TradeDataRequestNotAllowedException("Request for trades not allowed at the moment at MtGox site");
    }

    /**
     * Get the interval, in which the trade site updates it's depth, ticker etc. 
     * in microseconds.
     *
     * @return The update interval in microseconds.
     */
    public long getUpdateInterval() {
        return 15L * 1000000L; // The default MtGox update happens every 15s, I think.
    }

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

        // MtGox is quite strict about the request limitations, so I have to check each request type separately...

        // Get the current time.
        long currentTimestamp = TimeUtils.getInstance().getCurrentGMTTimeMicros();

        switch (requestType) {
        case Depth:
            return (_lastRequestTimestampForDepth == -1L)
                    || (currentTimestamp > _lastRequestTimestampForDepth + 15000000L);
        case Ticker:
            return (_lastRequestTimestampForTicker == -1L)
                    || (currentTimestamp > _lastRequestTimestampForTicker + 15000000L);
        case Trades:
            return (_lastRequestTimestampForTrades == -1L)
                    || (currentTimestamp > _lastRequestTimestampForTrades + 15000000L);
        }

        return true; // I guess, we can always execute orders...
    }

    /**
     * Set a new MtGox key.
     *
     * @param key The new MtGox key.
     */
    private void setKey(String key) {
        _key = key;
    }

    /**
     * Set a new MtGox secret.
     *
     * @param secret The new MtGox secret.
     */
    private void setSecret(String secret) {
        _secret = secret;
    }

    /**
     * Set new settings for the client.
     *
     * @param settings The new settings for the client.
     */
    public void setSettings(PersistentPropertyList settings) {

        super.setSettings(settings);

        String key = settings.getStringProperty("Key");
        if (key != null) {
            setKey(key); // Get the API key from the settings.
        }
        String secret = settings.getStringProperty("Secret");
        if (secret != null) {
            setSecret(secret); // Get the secret from the settings.
        }
    }

    /**
     * Return a string for this site (just a name for now).
     * To be used in the project tree.
     */
    public String toString() {
        return getName();
    }

    /**
     * Update the timestamp of the last request.
     *
     * @param requestType The type of the last request.
     */
    public void updateLastRequest(TradeSiteRequestType requestType) {

        // I don't count the orders, since should be posted at any time, I think...
        switch (requestType) {
        case Depth:
            _lastRequestTimestampForDepth = TimeUtils.getInstance().getCurrentGMTTimeMicros();
            break;
        case Ticker:
            _lastRequestTimestampForTicker = TimeUtils.getInstance().getCurrentGMTTimeMicros();
            break;
        case Trades:
            _lastRequestTimestampForTrades = TimeUtils.getInstance().getCurrentGMTTimeMicros();
            break;
        }
    }
}