Java tutorial
package com.gazbert.ada.adapter; /* * The MIT License (MIT) * * Copyright (c) 2014 Gareth Jon Lynch * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.lang.reflect.Type; import java.math.BigDecimal; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.text.DecimalFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.UUID; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.apache.log4j.Logger; import com.gazbert.ada.trading.api.BalanceInfo; import com.gazbert.ada.trading.api.MarketOrder; import com.gazbert.ada.trading.api.MarketOrderBook; import com.gazbert.ada.trading.api.OpenOrder; import com.gazbert.ada.trading.api.OrderType; import com.gazbert.ada.trading.api.TimeoutException; import com.gazbert.ada.trading.api.TradingApi; import com.gazbert.ada.trading.api.TradingApiException; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; /** * Adapter for integrating with BTC-e exchange. * <p> * BTC-e API details here: * https://btc-e.com/api/documentation * and * https://btc-e.com/page/2 * <p> * This adapter is Single threaded: we <em>must</em> preserve trading transaction order... the URLConnection * does this by blocking when waiting for input stream (response) for each API call. It is *not* thread safe in its * current form. * <p> * API calls will throw a {@link TimeoutException} if: * <ul> * <li>A {@link java.net.SocketTimeoutException} is thrown by the {@link URLConnection}</li> * <li>A {@link java.io.IOException} thrown by the {@link URLConnection} with HTTP status codes: * 502 Bad Gateway, 503 Servivce Unavailable, 504 Gateway Timeout.</li> * </ul> * * @author gazbert * */ public final class BtceExchangeAdapter implements TradingApi { /** Logger */ private static final Logger LOG = Logger.getLogger(BtceExchangeAdapter.class); /** The Authenticated API URI */ private static final String AUTHENTICATED_API_URL = "https://btc-e.com/tapi"; /** The public API URI */ private static final String PUBLIC_API_BASE_URL = "https://btc-e.com/api/3/"; /** * Your BTC-e API keys and connection timeout config. * This file *must* be on Ada's classpath. */ private static final String CONFIG_LOCATION = "btce-config.properties"; /** Name of PUBLIC key prop in config file.*/ private static final String KEY_PROPERTY_NAME = "key"; /** Name of secret prop in config file. */ private static final String SECRET_PROPERTY_NAME = "secret"; /** Name of connection timeout prop in config file. */ private static final String CONNECTION_TIMEOUT_PROPERTY_NAME = "connection-timeout"; /** Nonce used for auth packets */ private static long nonce = 0; /** The connection timeout in SECONDS for terminating hung connections to the exchange. */ private int connectionTimeout; /** Used to indicate if we have initialised the MAC authentication protocol */ private boolean initializedMACAuthentication = false; /** Our public key used in MAC authentication */ private String key = ""; /** Our secret used in MAC authentication */ private String secret = ""; /** * MAC - This class provides the functionality of a "Message Authentication Code" (MAC) algorithm. * Used to encrypt the hash of the entire message with our private key to ensure message integrity. */ private Mac mac; /** GSON engine used for parsing JSON in BTC-e API call responses */ private Gson gson; /* * Constructor gets the adapter ready for calling the BTC-e API. */ public BtceExchangeAdapter() { // set the initial nonce used in the secure messaging. nonce = System.currentTimeMillis() / 1000; // set up the secure messaging layer for payload integrity. initSecureMessageLayer(); // Initialise GSON layer for processing the JSON in the API calls/responses. initGson(); } ///////////////////////////////////////////////////////////////////////// // // BTC-e API Calls we adapt to the common Trading API. // See https://btc-e.com/api/documentation and https://btc-e.com/page/2 // ///////////////////////////////////////////////////////////////////////// @Override public MarketOrderBook getMarketOrders(final String marketId) throws TradingApiException { final String results = sendPublicRequestToExchange("depth", marketId); // TODO tmp debug //LOG.debug("BTC-e Order Book: " + results); final BtceMarketOrderBookWrapper marketOrderWrapper = gson.fromJson(results, BtceMarketOrderBookWrapper.class); //adapt BUYs final List<MarketOrder> buyOrders = new ArrayList<MarketOrder>(); final List<List<BigDecimal>> btceBuyOrders = marketOrderWrapper.orderBook.bids; for (final List<BigDecimal> order : btceBuyOrders) { final MarketOrder buyOrder = new MarketOrder(OrderType.BUY, order.get(0), // price order.get(1), // quanity order.get(0).multiply(order.get(1))); buyOrders.add(buyOrder); } // adapt SELLs final List<MarketOrder> sellOrders = new ArrayList<MarketOrder>(); final List<List<BigDecimal>> btceSellOrders = marketOrderWrapper.orderBook.asks; for (final List<BigDecimal> order : btceSellOrders) { final MarketOrder sellOrder = new MarketOrder(OrderType.SELL, order.get(0), // price order.get(1), // quantity order.get(0).multiply(order.get(1))); sellOrders.add(sellOrder); } final MarketOrderBook orderBook = new MarketOrderBook(marketId, sellOrders, buyOrders); return orderBook; } @Override public List<OpenOrder> getYourOpenOrders(final String marketId) throws TradingApiException { final Map<String, String> params = new HashMap<String, String>(); params.put("pair", marketId); final String results = sendAuthenticatedRequestToExchange("ActiveOrders", params); // TODO tmp debug //LOG.debug("Open Orders: " + results); final BtceOpenOrderResponseWrapper myOpenOrders = gson.fromJson(results, BtceOpenOrderResponseWrapper.class); final List<OpenOrder> ordersToReturn = new ArrayList<OpenOrder>(); // this sucks! if (myOpenOrders.success == 0 && myOpenOrders.error.equalsIgnoreCase("no orders")) { LOG.debug("No Open Orders"); } else { LOG.debug("BtceOpenOrderResponseWrapper: " + myOpenOrders); // adapt (and we really have to this time... jeez... final BtceOpenOrders btceOrders = myOpenOrders.openOrders; final Set<Entry<Long, BtceOpenOrder>> entries = btceOrders.entrySet(); for (final Entry<Long, BtceOpenOrder> entry : entries) { final Long orderId = entry.getKey(); final BtceOpenOrder orderDetails = entry.getValue(); OrderType orderType = null; final String btceOrderType = orderDetails.type; if (btceOrderType.equalsIgnoreCase(OrderType.BUY.getStringValue())) { orderType = OrderType.BUY; } else if (btceOrderType.equalsIgnoreCase(OrderType.SELL.getStringValue())) { orderType = OrderType.SELL; } else { throw new TradingApiException( "Unrecognised order type received in getYourOpenOrders(). Value: " + btceOrderType); } final OpenOrder order = new OpenOrder(Long.toString(orderId), new Date(orderDetails.timestamp_created), marketId, orderType, orderDetails.rate, orderDetails.amount, new BigDecimal(0), //c.f. cryptsyOrders[i].orig_quantity - not provided by btce :-( new BigDecimal(0) //c.f. cryptsyOrders[i].total - not provided by btce :-( ); ordersToReturn.add(order); } } return ordersToReturn; } @Override public String createOrder(final String marketId, final String orderType, final BigDecimal quantity, final BigDecimal price) throws TradingApiException { final Map<String, String> params = new HashMap<String, String>(); params.put("pair", marketId); // note we need to limit to 8 decimal places else exchange will barf params.put("amount", new DecimalFormat("#.########").format(quantity)); params.put("rate", new DecimalFormat("#.########").format(price)); final String results; if (orderType == OrderType.BUY.getStringValue()) { // buying BTC params.put("type", "buy"); results = sendAuthenticatedRequestToExchange("Trade", params); } else if (orderType == OrderType.SELL.getStringValue()) { // selling BTC params.put("type", "sell"); results = sendAuthenticatedRequestToExchange("Trade", params); } else { final String errorMsg = "Invalid order type: " + orderType + " - Can only be " + OrderType.BUY.getStringValue() + " or " + OrderType.SELL.getStringValue(); LOG.error(errorMsg); throw new IllegalArgumentException(errorMsg); } // TODO tmp debug LOG.debug("Order response: " + results); final BtceCreateOrderResponseWrapper createOrderResponseWrapper = gson.fromJson(results, BtceCreateOrderResponseWrapper.class); if (createOrderResponseWrapper.success == 1) { final long orderId = createOrderResponseWrapper.orderResponse.order_id; if (orderId == 0) { // PATCH for BTCe returning 0 orderIds during 17-24 July 2014... even though order was successful! // This is becoz my EMA strat buys @ ASK price and sells @ BID price so orders fill immediately... // BTCe returns 0 becoz order no longer exists - it is failled instantly and therefore has no ID. LOG.error("BTCe returned 0 for orderId - must have been instantly filled! ;-)"); // we generate our own unique ID in the adaper for the order. // This ID won't match any existing orders, so next trade cycle in strat will assume (correctly) // that order filled. return "INSTAFILL__" + UUID.randomUUID().toString(); } return Long.toString(orderId); } else { LOG.error("Failed to place order on exchange. Error response: " + createOrderResponseWrapper.error); return "0"; } } @Override public boolean cancelOrder(final String orderId) throws TradingApiException { final Map<String, String> params = new HashMap<String, String>(); params.put("order_id", orderId); final String results = sendAuthenticatedRequestToExchange("CancelOrder", params); // TODO tmp debug to trap all the diff types of error response in JSON LOG.debug("cancelOrder() response: " + results); final BtceCancelledOrderWrapper cancelOrderResponse = gson.fromJson(results, BtceCancelledOrderWrapper.class); if (cancelOrderResponse.success == 0) { //game over! LOG.error("Failed to cancel order: " + cancelOrderResponse.error); return false; } else { return true; } } @Override public BigDecimal getLatestMarketPrice(final String marketId) throws TradingApiException { final String results = sendPublicRequestToExchange("ticker", marketId); // TODO tmp debug LOG.debug("BTCe ticker raw response: " + results); final BtceTickerWrapper btceTicker = gson.fromJson(results, BtceTickerWrapper.class); return btceTicker.ticker.last; } @Override public BalanceInfo getBalanceInfo() throws TradingApiException { final String results = sendAuthenticatedRequestToExchange("getInfo", null); // TODO tmp debug LOG.debug("BTC-e Balances: " + results); final BtceInfoWrapper info = gson.fromJson(results, BtceInfoWrapper.class); // adapt final HashMap<String, BigDecimal> balancesAvailable = new HashMap<String, BigDecimal>(); balancesAvailable.put("USD", info.info.funds.get("usd")); balancesAvailable.put("BTC", info.info.funds.get("btc")); // 2nd arg of reserved balances not provided by exchange. final BalanceInfo balanceInfo = new BalanceInfo(balancesAvailable, new HashMap<String, BigDecimal>()); return balanceInfo; } @Override public String getImplName() { return "BTC-e Exchange"; } @Override public BigDecimal getPercentageOfBuyOrderTakenForExchangeFee() { // TODO get from BTC-e API? return new BigDecimal(0.002); // 0.2% } @Override public BigDecimal getPercentageOfSellOrderTakenForExchangeFee() { // TODO get from BTC-e API? return new BigDecimal(0.002); // 0.2% } ////////////////////////////////////////////////////////////////////////// // // GSON classes for JSON responses // See https://btc-e.com/api/documentation and https://btc-e.com/page/2 // ////////////////////////////////////////////////////////////////////////// /** * GSON base class for API call requests and responses. * @author gazbert */ private static class BtceMessageBase { // field names map to the JSON arg names public int success; public String error; public BtceMessageBase() { error = ""; } @Override public String toString() { return BtceMessageBase.class.getSimpleName() + " [" + "success=" + success + ", error=" + error + "]"; } } /** * GSON class for wrapping BTC-e Info response from getInfo() API call. * <p> * JSON response: * <pre> * { * "success":1, * "return": * { * "funds": * { * "usd":325, * "btc":23.998, * "sc":121.998, * "ltc":0, * "ruc":0, * "nmc":0 * }, * "rights": * { * "info":1, * "trade":1 * }, * "transaction_count":80, * "open_orders":1, * "server_time":1342123547 * } * } * </pre> * * @author gazbert */ private static class BtceInfoWrapper extends BtceMessageBase { @SerializedName("return") public BtceInfo info; } /** * GSON class for holding BTC-e info response from getInfo() API call. * @author gazbert */ private static class BtceInfo { // field names map to the JSON arg names public BtceFunds funds; public BtceRights rights; public int transaction_count; public int open_orders; public String server_time; @Override public String toString() { return BtceInfo.class.getSimpleName() + " [funds=" + funds + ", rights=" + rights + ", transaction_count=" + transaction_count + ", open_orders=" + open_orders + ", server_time=" + server_time + "]"; } } /** * GSON class for holding fund balances - basically a GSON enabled map. * Keys into map are 'usd' and 'btc' for the bits we're interested in. * Values are BigDec's. * * @author gazbert */ private static class BtceFunds extends HashMap<String, BigDecimal> { /** SID */ private static final long serialVersionUID = 5516523641453401953L; } /** * GSON class for holding access rights - basically a GSON enabled map. * Keys are 'info' and 'trade'. We expect both to be set to 1 (i.e. true). * * @author gazbert */ private static class BtceRights extends HashMap<String, BigDecimal> { /** SID */ private static final long serialVersionUID = 5353335214767688903L; } /** * GSON class for the Order Book wrapper. * <p> * JSON returned: * <pre> * { * "btc_usd": * { * "asks":[[567.711,1.97874609],[568.226,0.018],[569,1.192759],[569.234,0.04312865],[576.418,28.68]], * "bids":[[567.37,0.03607374],[566.678,0.04321508],[566.615,0.5],[566.502,0.02489897],[557.8,0.363]] * } * } * </pre> * * @author gazbert */ private static class BtceMarketOrderBookWrapper extends BtceMessageBase { // field names map to the JSON arg names) @SerializedName("btc_usd") public BtceOrderBook orderBook; @Override public String toString() { return BtceMarketOrderBookWrapper.class.getSimpleName() + " [" + "orderBook=" + orderBook + "]"; } } /** * GSON class for holding BTC-e Order Book response from depth API call. * <p> * JSON looks like: * <pre> * { * "asks":[[567.711,1.97874609],[568.226,0.018],[569,1.192759],[569.234,0.04312865],[576.418,28.68]], * "bids":[[567.37,0.03607374],[566.678,0.04321508],[566.615,0.5],[566.502,0.02489897],[557.8,0.363]] * } * </pre> * Each is a list of open orders and each order is represented as a list of price and amount. * * @author gazbert */ private static class BtceOrderBook { // field names map to the JSON arg names public List<List<BigDecimal>> bids; public List<List<BigDecimal>> asks; @Override public String toString() { return BtceOrderBook.class.getSimpleName() + " [" + "bids=" + bids + ", asks=" + asks + "]"; } } /** * GSON class for holding BTC-e create order response wapper from Trade API call. * <p> * JSON response: * <pre> * { * "success":1, * "return": { * "received":0.1, * "remains":0, * "order_id":0, * "funds": { * "usd":325, * "btc":2.498, * "sc":121.998, * "ltc":0, * "ruc":0, * "nmc":0 * } * } * } * </pre> * @author gazbert */ private static class BtceCreateOrderResponseWrapper extends BtceMessageBase { // field names map to the JSON arg names @SerializedName("return") public BtceCreateOrderResponse orderResponse; @Override public String toString() { return BtceCreateOrderResponseWrapper.class.getSimpleName() + " [" + "orderResponse=" + orderResponse + "]"; } } /** * GSON class for holding BTC-e create order response details from Trade API call. * @author gazbert */ private static class BtceCreateOrderResponse extends BtceCreateOrderResponseWrapper { // field names map to the JSON arg names public BigDecimal received; public BigDecimal remains; public long order_id; public BtceFunds funds; @Override public String toString() { return BtceCreateOrderResponse.class.getSimpleName() + " [received=" + received + ", remains=" + remains + ", order_id=" + order_id + ", funds=" + funds + "]"; } } /** * GSON class for holding BTC-e open order response wapper from ActiveOrders API call. * <p> * JSON response when we don't have open orders is: * <pre> * {"success":0,"error":"no orders"} * </pre> * <p> * Error? WTF???!!! * <p> * JSON response when we have open orders is: * <pre> * { * "success":1, * "return":{ * "253356918":{ * "pair":"btc_usd","type":"sell","amount":0.01000000,"rate":641.00000000, * "timestamp_created":1401653697,"status":0 * } * } * } * </pre> * * @author gazbert */ private static class BtceOpenOrderResponseWrapper extends BtceMessageBase { // field names map to the JSON arg names @SerializedName("return") public BtceOpenOrders openOrders; @Override public String toString() { return BtceOpenOrderResponseWrapper.class.getSimpleName() + " [" + "openOrders=" + openOrders + "]"; } } /** * GSON class for holding open orders - basically a GSON enabled map. * Keys into map is the orderId. * * @author gazbert */ private static class BtceOpenOrders extends HashMap<Long, BtceOpenOrder> { /** SID */ private static final long serialVersionUID = 2298531396505507808L; } /** * GSON class for holding BTC-e open order response details from ActiveOrders API call. * @author gazbert */ private static class BtceOpenOrder { // field names map to the JSON arg names public String pair; // market id public String type; // buy|sell public BigDecimal amount; public BigDecimal rate; // price public Long timestamp_created; public int status; // ?? @Override public String toString() { return BtceOpenOrder.class.getSimpleName() + " [pair=" + pair + ", type=" + type + ", amount=" + amount + ", rate=" + rate + ", timestamp_created=" + timestamp_created + ", status=" + status + "]"; } } /** * GSON class for holding BTC-e cancel order response. * * <pre> * { * "success":1, * "return":{ * "order_id":343154, * "funds":{ * "usd":325, * "btc":24.998, * "sc":121.998, * "ltc":0, * "ruc":0, * "nmc":0 * } * } * } * </pre> * * Unusual API - why the feck do I want to know about funds when I cancel an order?!! ;-) * * @author gazbert */ private static class BtceCancelledOrderWrapper extends BtceMessageBase { // field names map to the JSON arg names @SerializedName("return") public BtceCancelledOrder cancelledOrder; @Override public String toString() { return BtceCancelledOrderWrapper.class.getSimpleName() + " [" + "cancelledOrder=" + cancelledOrder + "]"; } } /** * GSON class for cancelled order response. * @author gazbert */ private static class BtceCancelledOrder { public long order_id; public BtceFunds funds; @Override public String toString() { return BtceCancelledOrder.class.getSimpleName() + " [" + "order_id=" + order_id + ", funds=" + funds + "]"; } } /** * GSON class for a BTC-e ticker response wrapper. * <p> * JSON looks like: * <pre> * { * "btc_usd":{ * "high":667, * "low":606, * "avg":636.5, * "vol":6377783.27609, * "vol_cur":10014.85647, * "last":666.9, * "buy":666.998, * "sell":666.997, * "updated":1401647163 * } * } * </pre> * @author gazbert */ private static class BtceTickerWrapper { // field names map to the JSON arg names // TODO fix this - can GSON take wildcard types or will I have to code my own deserializer? @SerializedName("btc_usd") public BtceTicker ticker; @Override public String toString() { return BtceTickerWrapper.class.getSimpleName() + " [" + "ticker=" + ticker + "]"; } } /** * GSON class for a BTC-e ticker response. * @author gazbert */ private static class BtceTicker { // field names map to the JSON arg names public BigDecimal high; public BigDecimal low; public BigDecimal avg; public BigDecimal vol; public BigDecimal vol_cur; public BigDecimal last; public BigDecimal buy; public BigDecimal sell; public long updated; @Override public String toString() { return BtceTicker.class.getSimpleName() + " [" + "high=" + high + ", low=" + low + ", avg=" + avg + ", vol=" + vol + ", vol_cur=" + vol_cur + ", last=" + last + ", buy=" + buy + ", sell=" + sell + ", updated=" + updated + "]"; } } /** * Needed for BTC-e open orders API call response: * <pre> * { * "success":1, * "return":{ * "253356918":{ * "pair":"btc_usd","type":"sell","amount":0.01000000,"rate":641.00000000, * "timestamp_created":1401653697,"status":0 * } * } * } * </pre> * * @author gazbert */ private class OpenOrdersDeserializer implements JsonDeserializer<BtceOpenOrders> { public BtceOpenOrders deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { final BtceOpenOrders openOrders = new BtceOpenOrders(); if (json.isJsonObject()) { final JsonObject jsonObject = json.getAsJsonObject(); final Iterator<Entry<String, JsonElement>> openOrdersIterator = jsonObject.entrySet().iterator(); while (openOrdersIterator.hasNext()) { final Entry<String, JsonElement> jsonOrder = openOrdersIterator.next(); final Long orderId = new Long(jsonOrder.getKey()); // will barf hard n fast if NaN final BtceOpenOrder openOrder = context.deserialize(jsonOrder.getValue(), BtceOpenOrder.class); openOrders.put(orderId, openOrder); } } return openOrders; } } /** * Needed for BTC-e Date format. * * Needed because BTC-e causes Gson to throw ISE under certain circumstances: * * <pre> * </pre> * * @author gazbert */ private class DateDeserializer implements JsonDeserializer<Date> { private SimpleDateFormat btceDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public Date deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { Date dateFromBtce = null; if (json.isJsonPrimitive()) { try { dateFromBtce = btceDateFormat.parse(json.getAsString()); } catch (ParseException e) { // TODO should we barf big time here?? dateFromBtce = null; final String errorMsg = "Failed to parse a BTC-e date"; LOG.error(errorMsg, e); } } return dateFromBtce; } } /** * Needed because BTC-e causes Gson to throw ISE under certain circumstances: * * <pre> * </pre> * * @author gazbert */ private class BalancesDeserializer implements JsonDeserializer<BtceFunds> { public BtceFunds deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { final BtceFunds balances = new BtceFunds(); if (json.isJsonObject()) { final JsonObject jsonObject = json.getAsJsonObject(); final Iterator<Entry<String, JsonElement>> balancesIterator = jsonObject.entrySet().iterator(); while (balancesIterator.hasNext()) { final Entry<String, JsonElement> jsonOrder = balancesIterator.next(); final String currency = jsonOrder.getKey(); final BigDecimal balance = context.deserialize(jsonOrder.getValue(), BigDecimal.class); balances.put(currency, balance); } } return balances; } } ///////////////////////////////////////////////////////////////////// // // Private utils. // ///////////////////////////////////////////////////////////////////// /* * Makes a public REST-like API call to BTC-e exchange. Uses HTTP GET. * * @param apiMethod the API method to call * @param resource to use in the API call * @return the response from BTC-e. * @throws TradingApiException */ private final String sendPublicRequestToExchange(final String apiMethod, final String resource) throws TradingApiException { // Connect to the exchange HttpURLConnection exchangeConnection = null; final StringBuffer exchangeResponse = new StringBuffer(); try { final URL url = new URL(PUBLIC_API_BASE_URL + apiMethod + "/" + resource); if (LOG.isDebugEnabled()) { LOG.debug("Using following URL for API call: " + url); } exchangeConnection = (HttpURLConnection) url.openConnection(); exchangeConnection.setUseCaches(false); exchangeConnection.setDoOutput(true); // TODO check this is correct format exchangeConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // Scare them with her name!!! Er, perhaps, I need to be a bit more stealth here... exchangeConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36"); /* * Add a timeout so we don't get blocked indefinitley; Timeout is in milllis. * * Cryptsy gets stuck here for ~ 1 min once every half hour or so. Especially read timeouts. * * Give it time before killing it off - to prevent me from having to write more horrible order state * management code in the strategy ;-) * * connectionTimeout is in SECONDS and from config. */ final int timeoutInMillis = connectionTimeout * 1000; exchangeConnection.setConnectTimeout(timeoutInMillis); exchangeConnection.setReadTimeout(timeoutInMillis); // Grab the response - we just block here as per Connection API final BufferedReader responseInputStream = new BufferedReader( new InputStreamReader(exchangeConnection.getInputStream())); // Read the JSON response lines into our response buffer String responseLine = null; while ((responseLine = responseInputStream.readLine()) != null) { exchangeResponse.append(responseLine); } responseInputStream.close(); // return the JSON response string return exchangeResponse.toString(); } catch (MalformedURLException e) { final String errorMsg = "Failed to send request to Exchange."; LOG.error(errorMsg, e); throw new TradingApiException(errorMsg, e); } catch (SocketTimeoutException e) { final String errorMsg = "Failed to connect to Exchange due to socket timeout."; LOG.error(errorMsg, e); throw new TimeoutException(errorMsg, e); } catch (IOException e) { /* * Crypsy often fails with these codes, but recovers by next request... */ if (e.getMessage().contains("502") || e.getMessage().contains("503") || e.getMessage().contains("504")) { final String errorMsg = "Failed to connect to Exchange due to 5XX timeout."; LOG.error(errorMsg, e); throw new TimeoutException(errorMsg, e); } else { final String errorMsg = "Failed to connect to Exchange due to some other IO error."; LOG.error(errorMsg, e); throw new TradingApiException(errorMsg, e); } } finally { if (exchangeConnection != null) { exchangeConnection.disconnect(); } } } /* * Makes an API call to BTC-e exchange. * * @param apiMethod the API method to call * @param params the query param args to use in the API call * @return the response from BTC-e. * @throws TradingApiException */ private final String sendAuthenticatedRequestToExchange(final String apiMethod, Map<String, String> params) throws TradingApiException { if (!initializedMACAuthentication) { final String errorMsg = "MAC Message security layer has not been initialized."; LOG.error(errorMsg); throw new IllegalStateException(errorMsg); } // method is the API method name // nonce is required by BTC-e in every request if (params == null) { params = new HashMap<String, String>(); } params.put("method", apiMethod); params.put("nonce", Long.toString(nonce)); // increment ready for next call... // must be 1 higher for next call nonce++; // Build the URL with query param args in it String postData = ""; for (final Iterator<String> paramsIterator = params.keySet().iterator(); paramsIterator.hasNext();) { final String param = paramsIterator.next(); if (postData.length() > 0) { postData += "&"; } postData += param + "=" + URLEncoder.encode(params.get(param)); } // Connect to the exchange URLConnection exchangeConnection = null; final StringBuffer exchangeResponse = new StringBuffer(); try { final URL url = new URL(AUTHENTICATED_API_URL); exchangeConnection = url.openConnection(); exchangeConnection.setUseCaches(false); exchangeConnection.setDoOutput(true); // Add my public key exchangeConnection.setRequestProperty("Key", key); // Sign the payload with my private key exchangeConnection.setRequestProperty("Sign", toHex(mac.doFinal(postData.getBytes("UTF-8")))); exchangeConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // Scare them with her name!!! Er, perhaps, I need to be a bit more stealth here... exchangeConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36"); /* * Add a timeout so we don't get blocked indefinitley; Timeout is in milllis. * * Give it time before killing it off - to prevent me from having to write more horrible order state * management code in the strategy ;-) * * connectionTimeout is in SECONDS and from config. */ final int timeoutInMillis = connectionTimeout * 1000; exchangeConnection.setConnectTimeout(timeoutInMillis); exchangeConnection.setReadTimeout(timeoutInMillis); // POST the request final OutputStreamWriter outputPostStream = new OutputStreamWriter( exchangeConnection.getOutputStream()); outputPostStream.write(postData); outputPostStream.close(); // Grab the response - we just block here as per Connection API final BufferedReader responseInputStream = new BufferedReader( new InputStreamReader(exchangeConnection.getInputStream())); // Read the JSON response lines into our response buffer String responseLine = null; while ((responseLine = responseInputStream.readLine()) != null) { exchangeResponse.append(responseLine); } responseInputStream.close(); } catch (MalformedURLException e) { final String errorMsg = "Failed to send request to Exchange."; LOG.error(errorMsg, e); throw new TradingApiException(errorMsg, e); } catch (SocketTimeoutException e) { final String errorMsg = "Failed to connect to Exchange due to socket timeout."; LOG.error(errorMsg, e); throw new TimeoutException(errorMsg, e); } catch (IOException e) { /* * Cryptsy often fails with these codes, but recovers by next request... */ if (e.getMessage().contains("502") || e.getMessage().contains("503") || e.getMessage().contains("504")) { final String errorMsg = "Failed to connect to Exchange due to 5XX timeout."; LOG.error(errorMsg, e); throw new TimeoutException(errorMsg, e); } else { final String errorMsg = "Failed to connect to Exchange due to some other IO error."; LOG.error(errorMsg, e); throw new TradingApiException(errorMsg, e); } } // return the JSON response string return exchangeResponse.toString(); } /* * Converts a given byte array to a hex String. */ private String toHex(byte[] byteArrayToConvert) throws UnsupportedEncodingException { final StringBuilder hexString = new StringBuilder(); for (final byte aByte : byteArrayToConvert) { hexString.append(String.format("%02x", aByte & 0xff)); } return hexString.toString(); } /* * Initialises the Security layer. * Loads API keys and sets up the MAC to encrypt the data we send to the exchange. * We fail hard n fast if any of this stuff blows. */ private void initSecureMessageLayer() { // load API keys from config loadApiKeysAndOtherConfig(); // Setup the MAC try { final SecretKeySpec keyspec = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA512"); mac = Mac.getInstance("HmacSHA512"); mac.init(keyspec); initializedMACAuthentication = true; } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) { final String errorMsg = "Failed to setup MAC security. HINT: Is HMAC-SHA512 installed?"; LOG.error(errorMsg, e); throw new IllegalStateException(errorMsg, e); } catch (InvalidKeyException e) { final String errorMsg = "Failed to setup MAC security. Secret key seems invalid!"; LOG.error(errorMsg, e); throw new IllegalArgumentException(errorMsg, e); } } /* * Loads BTC-e API keys and other config. */ private void loadApiKeysAndOtherConfig() { final Properties configEntries = new Properties(); final InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(CONFIG_LOCATION); if (inputStream == null) { final String errorMsg = "Cannot find BTC-e config at: " + CONFIG_LOCATION + " HINT: is it on Ada's classpath?"; LOG.error(errorMsg); throw new IllegalStateException(errorMsg); } try { configEntries.load(inputStream); /* * Grab the public key */ key = configEntries.getProperty(KEY_PROPERTY_NAME); // WARNING: Don't log this on 'exposed' system... //LOG.debug(KEY_PROPERTY_NAME + ": " + key); if (key == null || key.length() == 0) { final String errorMsg = KEY_PROPERTY_NAME + " cannot be null or zero length!" + " HINT: is the value set in the " + CONFIG_LOCATION + "?"; LOG.error(errorMsg); throw new IllegalArgumentException(errorMsg); } /* * Grab the private key */ secret = configEntries.getProperty(SECRET_PROPERTY_NAME); // WARNING: Never log this unless testing in the green zone //LOG.debug(SECRET_PROPERTY_NAME + ": " + secret); if (secret == null || secret.length() == 0) { final String errorMsg = SECRET_PROPERTY_NAME + " cannot be null or zero length!" + " HINT: is the value set in the " + CONFIG_LOCATION + "?"; LOG.error(errorMsg); throw new IllegalArgumentException(errorMsg); } /* * Grab the connection timeout */ connectionTimeout = Integer.parseInt( // will barf bigtime if not a number; we want this to fail fast. configEntries.getProperty(CONNECTION_TIMEOUT_PROPERTY_NAME)); if (connectionTimeout == 0) { final String errorMsg = CONNECTION_TIMEOUT_PROPERTY_NAME + " cannot be 0 value!" + " HINT: is the value set in the " + CONFIG_LOCATION + "?"; LOG.error(errorMsg); throw new IllegalArgumentException(errorMsg); } LOG.info(CONNECTION_TIMEOUT_PROPERTY_NAME + ": " + connectionTimeout); } catch (IOException e) { final String errorMsg = "Failed to load Exchange config: " + CONFIG_LOCATION; LOG.error(errorMsg, e); throw new IllegalStateException(errorMsg, e); } finally { try { inputStream.close(); } catch (IOException e) { final String errorMsg = "Failed to close input stream for: " + CONFIG_LOCATION; LOG.error(errorMsg, e); throw new IllegalStateException(errorMsg, e); } } } /* * Initialises the GSON layer. */ private void initGson() { final GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(BtceOpenOrders.class, new OpenOrdersDeserializer()); gson = gsonBuilder.create(); } }