diff --git a/.gitignore b/.gitignore index 9f43113..b83be85 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist/ bin/ build/ +.idea diff --git a/server_extensions_api/src/com/generalbytes/batm/server/extensions/ICurrencies.java b/server_extensions_api/src/com/generalbytes/batm/server/extensions/ICurrencies.java index 7ecb407..76f5a32 100644 --- a/server_extensions_api/src/com/generalbytes/batm/server/extensions/ICurrencies.java +++ b/server_extensions_api/src/com/generalbytes/batm/server/extensions/ICurrencies.java @@ -20,6 +20,7 @@ package com.generalbytes.batm.server.extensions; public interface ICurrencies { public static final String BTC = "BTC"; + public static final String XBT = "XBT"; //alternate BTC code public static final String LTC = "LTC"; public static final String DOGE = "DOGE"; public static final String MAX = "MAX"; diff --git a/server_extensions_extra/libs/guava-18.0.jar b/server_extensions_extra/libs/guava-18.0.jar new file mode 100644 index 0000000..8f89e49 Binary files /dev/null and b/server_extensions_extra/libs/guava-18.0.jar differ diff --git a/server_extensions_extra/libs/xchange-bitfinex-2.2.0-SNAPSHOT.jar b/server_extensions_extra/libs/xchange-bitfinex-2.2.0-SNAPSHOT.jar deleted file mode 100644 index f7d47fd..0000000 Binary files a/server_extensions_extra/libs/xchange-bitfinex-2.2.0-SNAPSHOT.jar and /dev/null differ diff --git a/server_extensions_extra/libs/xchange-bitfinex-4.0.0-SNAPSHOT.jar b/server_extensions_extra/libs/xchange-bitfinex-4.0.0-SNAPSHOT.jar new file mode 100644 index 0000000..aee9799 Binary files /dev/null and b/server_extensions_extra/libs/xchange-bitfinex-4.0.0-SNAPSHOT.jar differ diff --git a/server_extensions_extra/libs/xchange-core-2.2.0-SNAPSHOT.jar b/server_extensions_extra/libs/xchange-core-2.2.0-SNAPSHOT.jar deleted file mode 100644 index 0cf64a9..0000000 Binary files a/server_extensions_extra/libs/xchange-core-2.2.0-SNAPSHOT.jar and /dev/null differ diff --git a/server_extensions_extra/libs/xchange-core-4.0.0-SNAPSHOT.jar b/server_extensions_extra/libs/xchange-core-4.0.0-SNAPSHOT.jar new file mode 100644 index 0000000..70279d9 Binary files /dev/null and b/server_extensions_extra/libs/xchange-core-4.0.0-SNAPSHOT.jar differ diff --git a/server_extensions_extra/libs/xchange-itbit-4.0.0-SNAPSHOT.jar b/server_extensions_extra/libs/xchange-itbit-4.0.0-SNAPSHOT.jar new file mode 100644 index 0000000..b033d99 Binary files /dev/null and b/server_extensions_extra/libs/xchange-itbit-4.0.0-SNAPSHOT.jar differ diff --git a/server_extensions_extra/res/batm-extensions.xml b/server_extensions_extra/res/batm-extensions.xml index bfe3194..ac8981b 100644 --- a/server_extensions_extra/res/batm-extensions.xml +++ b/server_extensions_extra/res/batm-extensions.xml @@ -27,6 +27,9 @@ BTC + + BTC + BTC @@ -42,6 +45,13 @@ BTC + + + + + + BTC + diff --git a/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/BitcoinExtension.java b/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/BitcoinExtension.java index 0e88dbc..7ef3590 100644 --- a/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/BitcoinExtension.java +++ b/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/BitcoinExtension.java @@ -19,6 +19,7 @@ package com.generalbytes.batm.server.extensions.extra.bitcoin; import com.generalbytes.batm.server.extensions.*; import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitfinex.BitfinexExchange; +import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.itbit.ItBitExchange; import com.generalbytes.batm.server.extensions.extra.bitcoin.paymentprocessors.bitcoinpay.BitcoinPayPP; import com.generalbytes.batm.server.extensions.extra.bitcoin.paymentprocessors.coinofsale.CoinOfSalePP; import com.generalbytes.batm.server.extensions.extra.bitcoin.sources.BitcoinAverageRateSource; @@ -47,6 +48,12 @@ public class BitcoinExtension implements IExtension{ String apiKey = paramTokenizer.nextToken(); String apiSecret = paramTokenizer.nextToken(); return new BitfinexExchange(apiKey, apiSecret); + } else if ("itbit".equalsIgnoreCase(prefix)) { + String userId = paramTokenizer.nextToken(); + String walletId = paramTokenizer.nextToken(); + String clientKey = paramTokenizer.nextToken(); + String clientSecret = paramTokenizer.nextToken(); + return new ItBitExchange(userId, walletId, clientKey, clientSecret); } } return null; @@ -144,6 +151,8 @@ public class BitcoinExtension implements IExtension{ return new FixPriceRateSource(rate,preferedFiatCurrency); }else if ("bitfinex".equalsIgnoreCase(exchangeType)) { return new BitfinexExchange("**","**"); + }else if ("itbit".equalsIgnoreCase(exchangeType)) { + return new ItBitExchange(); } } return null; diff --git a/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/XChangeExchange.java b/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/XChangeExchange.java new file mode 100644 index 0000000..b54cc1f --- /dev/null +++ b/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/XChangeExchange.java @@ -0,0 +1,730 @@ +/************************************************************************************* + * Copyright (C) 2014 GENERAL BYTES s.r.o. All rights reserved. + *

+ * This software may be distributed and modified under the terms of the GNU + * General Public License version 2 (GPL2) as published by the Free Software + * Foundation and appearing in the file GPL2.TXT included in the packaging of + * this file. Please note that GPL2 Section 2[b] requires that all works based + * on this software must also be made publicly available under the terms of + * the GPL2 ("Copyleft"). + *

+ * Contact information + * ------------------- + *

+ * GENERAL BYTES s.r.o. + * Web : http://www.generalbytes.com + *

+ * Other information: + *

+ * This implementation was created in cooperation with Orillia BVBA + ************************************************************************************/ +package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges; + +import com.generalbytes.batm.server.extensions.IExchangeAdvanced; +import com.generalbytes.batm.server.extensions.IRateSourceAdvanced; +import com.generalbytes.batm.server.extensions.ITask; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.util.concurrent.RateLimiter; +import com.xeiam.xchange.Exchange; +import com.xeiam.xchange.ExchangeFactory; +import com.xeiam.xchange.ExchangeSpecification; +import com.xeiam.xchange.currency.CurrencyPair; +import com.xeiam.xchange.dto.Order.OrderType; +import com.xeiam.xchange.dto.marketdata.OrderBook; +import com.xeiam.xchange.dto.marketdata.Ticker; +import com.xeiam.xchange.dto.trade.LimitOrder; +import com.xeiam.xchange.dto.trade.MarketOrder; +import com.xeiam.xchange.dto.trade.OpenOrders; +import com.xeiam.xchange.service.polling.account.PollingAccountService; +import com.xeiam.xchange.service.polling.marketdata.PollingMarketDataService; +import com.xeiam.xchange.service.polling.trade.PollingTradeService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +public abstract class XChangeExchange implements IExchangeAdvanced, IRateSourceAdvanced { + + private static final long cacheRefreshSeconds = 30; + private static final Cache balanceCache = createCache(); + private static final Cache rateCache = createCache(); + + private static Cache createCache() { + return CacheBuilder + .newBuilder() + .expireAfterWrite(cacheRefreshSeconds, TimeUnit.SECONDS) + .build(); + } + + private final Exchange exchange; + private final String name; + private final Logger log; + private final RateLimiter rateLimiter; + + public XChangeExchange(ExchangeSpecification specification) { + exchange = ExchangeFactory.INSTANCE.createExchange(specification); + name = exchange.getExchangeSpecification().getExchangeName(); + log = LoggerFactory.getLogger("batm.master." + name); + rateLimiter = RateLimiter.create(getAllowedCallsPerSecond()); + } + + protected abstract boolean isWithdrawSuccessful(String result); + protected abstract double getAllowedCallsPerSecond(); + + // exchanges that use non standard currency codes can override this + protected String mapCurrency(String currency) { + return currency; + } + + private String checkSupportedCrypto(String currency) { + currency = mapCurrency(currency); + if (!getCryptoCurrencies().contains(currency)) { + log.debug("{} exchange doesn't support cryptocurrency '{}'", name, currency); + return null; + } + return currency; + } + + private String checkSupportedFiat(String currency) { + currency = mapCurrency(currency); + if (!getCryptoCurrencies().contains(currency)) { + log.debug("{} exchange doesn't support fiat currency '{}'", name, currency); + return null; + } + return currency; + } + + @Override + public BigDecimal getExchangeRateLast(String cryptoCurrency, String fiatCurrency) { + String key = getKey(cryptoCurrency, fiatCurrency); + try { + BigDecimal result = rateCache.get(key, new RateCaller(key)); + log.debug("{} exchange rate request: {} = {}", name, key, result); + return result; + } catch (ExecutionException e) { + log.error("{} exchange rate request: {}", name, key, e); + return null; + } + } + + private String getKey(String cryptoCurrency, String fiatCurrency) { + return String.format("%s:%s", cryptoCurrency, fiatCurrency); + } + + private String[] getKeyParts(String key) { + return key.split(":"); + } + + @Override + public BigDecimal getCryptoBalance(String cryptoCurrency) { + cryptoCurrency = checkSupportedCrypto(cryptoCurrency); + return cryptoCurrency == null ? BigDecimal.ZERO : getBalance(cryptoCurrency); + } + + @Override + public BigDecimal getFiatBalance(String fiatCurrency) { + fiatCurrency = checkSupportedFiat(fiatCurrency); + return fiatCurrency == null ? BigDecimal.ZERO : getBalance(fiatCurrency); + } + + private BigDecimal getBalance(String currency) { + try { + BigDecimal balance = balanceCache.get(currency, new BalanceCaller(currency)); + log.debug("{} exchange balance request: {} = {}", name, currency, balance); + return balance; + } catch (ExecutionException e) { + log.error("{} exchange balance request: {}", name, currency, e); + return null; + } + } + + public final String sendCoins(String destinationAddress, BigDecimal amount, String cryptoCurrency, String description) { + cryptoCurrency = checkSupportedCrypto(cryptoCurrency); + if (cryptoCurrency == null) { + return null; + } + + log.info("{} exchange withdrawing {} {} to {}", name, amount, cryptoCurrency, destinationAddress); + + PollingAccountService accountService = exchange.getPollingAccountService(); + try { + String result = accountService.withdrawFunds(cryptoCurrency, amount, destinationAddress); + if (isWithdrawSuccessful(result)) { + log.debug("{} exchange withdrawal completed with result: {}", name, result); + return "success"; + } else { + log.error("{} exchange withdrawal failed with result: '{}'", name, result); + } + } catch (IOException e) { + log.error("{} exchange withdrawal failed", name, e); + } + return null; + } + + public String purchaseCoins(BigDecimal amount, String cryptoCurrency, String fiatCurrencyToUse, String description) { + cryptoCurrency = checkSupportedCrypto(cryptoCurrency); + fiatCurrencyToUse = checkSupportedFiat(fiatCurrencyToUse); + if (cryptoCurrency == null || fiatCurrencyToUse == null) { + return null; + } + + PollingAccountService accountService = exchange.getPollingAccountService(); + PollingMarketDataService marketService = exchange.getPollingMarketDataService(); + PollingTradeService tradeService = exchange.getPollingTradeService(); + + try { + log.debug("AccountInfo as String: {}", accountService.getAccountInfo()); + + CurrencyPair currencyPair = new CurrencyPair(cryptoCurrency, fiatCurrencyToUse); + + Ticker ticker = marketService.getTicker(currencyPair); + + LimitOrder order = new LimitOrder.Builder(OrderType.BID, currencyPair) + .limitPrice(ticker.getAsk()) + .tradableAmount(amount) + .build(); + log.debug("marketOrder = {}", order); + + String orderId = tradeService.placeLimitOrder(order); + log.debug("orderId = {} {}", orderId, order); + + try { + Thread.sleep(2000); //give exchange 2 seconds to reflect open order in order book + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // get open orders + log.debug("Open orders:"); + boolean orderProcessed = false; + int numberOfChecks = 0; + while (!orderProcessed && numberOfChecks < 10) { + boolean orderFound = false; + OpenOrders openOrders = tradeService.getOpenOrders(); + for (LimitOrder openOrder : openOrders.getOpenOrders()) { + log.debug("openOrder = {}", openOrder); + if (orderId.equals(openOrder.getId())) { + orderFound = true; + break; + } + } + if (orderFound) { + log.debug("Waiting for order to be processed."); + try { + Thread.sleep(3000); //don't get your ip address banned + } catch (InterruptedException e) { + e.printStackTrace(); + } + } else { + orderProcessed = true; + } + numberOfChecks++; + } + if (orderProcessed) { + return orderId; + } + } catch (IOException e) { + log.error(String.format("{} exchange purchase coins failed", name), e); + } + return null; + } + + @Override + public ITask createPurchaseCoinsTask(BigDecimal amount, String cryptoCurrency, String fiatCurrencyToUse, String description) { + cryptoCurrency = checkSupportedCrypto(cryptoCurrency); + fiatCurrencyToUse = checkSupportedFiat(fiatCurrencyToUse); + if (cryptoCurrency == null || fiatCurrencyToUse == null) { + return null; + } + return new PurchaseCoinsTask(amount, cryptoCurrency, fiatCurrencyToUse, description); + } + + @Override + public String getDepositAddress(String cryptoCurrency) { + cryptoCurrency = checkSupportedCrypto(cryptoCurrency); + if (cryptoCurrency == null) { + return null; + } + + PollingAccountService accountService = exchange.getPollingAccountService(); + try { + return accountService.requestDepositAddress(cryptoCurrency); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + @Override + public String sellCoins(BigDecimal cryptoAmount, String cryptoCurrency, String fiatCurrencyToUse, String description) { + cryptoCurrency = checkSupportedCrypto(cryptoCurrency); + fiatCurrencyToUse = checkSupportedFiat(fiatCurrencyToUse); + if (cryptoCurrency == null || fiatCurrencyToUse == null) { + return null; + } + + log.info("Calling {} exchange (sell {} {})", name, cryptoAmount, cryptoCurrency); + PollingAccountService accountService = exchange.getPollingAccountService(); + PollingTradeService tradeService = exchange.getPollingTradeService(); + + try { + log.debug("AccountInfo as String: {}", accountService.getAccountInfo()); + + CurrencyPair currencyPair = new CurrencyPair(cryptoCurrency, fiatCurrencyToUse); + + MarketOrder order = new MarketOrder(OrderType.ASK, cryptoAmount, currencyPair); + log.debug("marketOrder = {}", order); + + String orderId = tradeService.placeMarketOrder(order); + log.debug("orderId = {} {}", orderId, order); + + try { + Thread.sleep(2000); //give exchange 2 seconds to reflect open order in order book + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // get open orders + log.debug("Open orders:"); + boolean orderProcessed = false; + int numberOfChecks = 0; + while (!orderProcessed && numberOfChecks < 10) { + boolean orderFound = false; + OpenOrders openOrders = tradeService.getOpenOrders(); + for (LimitOrder openOrder : openOrders.getOpenOrders()) { + log.debug("openOrder = {}", openOrder); + if (orderId.equals(openOrder.getId())) { + orderFound = true; + break; + } + } + if (orderFound) { + log.debug("Waiting for order to be processed."); + try { + Thread.sleep(3000); //don't get your ip address banned + } catch (InterruptedException e) { + e.printStackTrace(); + } + }else{ + orderProcessed = true; + } + numberOfChecks++; + } + if (orderProcessed) { + return orderId; + } + } catch (IOException e) { + log.error("{} exchange sell coins failed", name, e); + } + return null; + } + + @Override + public ITask createSellCoinsTask(BigDecimal amount, String cryptoCurrency, String fiatCurrencyToUse, String description) { + cryptoCurrency = checkSupportedCrypto(cryptoCurrency); + fiatCurrencyToUse = checkSupportedFiat(fiatCurrencyToUse); + if (cryptoCurrency == null || fiatCurrencyToUse == null) { + return null; + } + return new SellCoinsTask(amount, cryptoCurrency, fiatCurrencyToUse, description); + } + + @Override + public BigDecimal getExchangeRateForBuy(String cryptoCurrency, String fiatCurrency) { + BigDecimal result = calculateBuyPrice(cryptoCurrency, fiatCurrency, BigDecimal.TEN); + if (result != null) { + return result.divide(BigDecimal.TEN, 2, BigDecimal.ROUND_UP); + } + return null; + } + + @Override + public BigDecimal getExchangeRateForSell(String cryptoCurrency, String fiatCurrency) { + BigDecimal result = calculateSellPrice(cryptoCurrency, fiatCurrency, BigDecimal.TEN); + if (result != null) { + return result.divide(BigDecimal.TEN, 2, BigDecimal.ROUND_DOWN); + } + return null; + } + + @Override + public BigDecimal calculateBuyPrice(String cryptoCurrency, String fiatCurrency, BigDecimal cryptoAmount) { + cryptoCurrency = checkSupportedCrypto(cryptoCurrency); + fiatCurrency = checkSupportedFiat(fiatCurrency); + if (cryptoCurrency == null || fiatCurrency == null) { + return null; + } + + rateLimiter.acquire(); + PollingMarketDataService marketDataService = exchange.getPollingMarketDataService(); + try { + CurrencyPair currencyPair = new CurrencyPair(cryptoCurrency, fiatCurrency); + OrderBook orderBook = marketDataService.getOrderBook(currencyPair); + List asks = orderBook.getAsks(); + BigDecimal targetAmount = cryptoAmount; + BigDecimal asksTotal = BigDecimal.ZERO; + BigDecimal tradableLimit = null; + Collections.sort(asks, new Comparator() { + @Override + public int compare(LimitOrder lhs, LimitOrder rhs) { + return lhs.getLimitPrice().compareTo(rhs.getLimitPrice()); + } + }); + +// log.debug("Selected asks:"); + for (LimitOrder ask : asks) { +// log.debug("ask = " + ask); + asksTotal = asksTotal.add(ask.getTradableAmount()); + if (targetAmount.compareTo(asksTotal) <= 0) { + tradableLimit = ask.getLimitPrice(); + break; + } + } + + if (tradableLimit != null) { + log.debug("Called {} exchange for BUY rate: {}:{} = {}", name, cryptoCurrency, fiatCurrency, tradableLimit); + return tradableLimit.multiply(cryptoAmount); + } + } catch (Throwable e) { + log.error("{} exchange failed to calculate buy price", name, e); + } + return null; + } + + @Override + public BigDecimal calculateSellPrice(String cryptoCurrency, String fiatCurrency, BigDecimal cryptoAmount) { + cryptoCurrency = checkSupportedCrypto(cryptoCurrency); + fiatCurrency = checkSupportedFiat(fiatCurrency); + if (cryptoCurrency == null || fiatCurrency == null) { + return null; + } + + rateLimiter.acquire(); + PollingMarketDataService marketDataService = exchange.getPollingMarketDataService(); + try { + CurrencyPair currencyPair = new CurrencyPair(cryptoCurrency, fiatCurrency); + + OrderBook orderBook = marketDataService.getOrderBook(currencyPair); + List bids = orderBook.getBids(); + + BigDecimal targetAmount = cryptoAmount; + BigDecimal bidsTotal = BigDecimal.ZERO; + BigDecimal tradableLimit = null; + + Collections.sort(bids, new Comparator() { + @Override + public int compare(LimitOrder lhs, LimitOrder rhs) { + return rhs.getLimitPrice().compareTo(lhs.getLimitPrice()); + } + }); + + for (LimitOrder bid : bids) { + bidsTotal = bidsTotal.add(bid.getTradableAmount()); + if (targetAmount.compareTo(bidsTotal) <= 0) { + tradableLimit = bid.getLimitPrice(); + break; + } + } + + if (tradableLimit != null) { + log.debug("Called {} exchange for SELL rate: {}:{} = {}", name, cryptoCurrency, fiatCurrency, tradableLimit); + return tradableLimit.multiply(cryptoAmount); + } + } catch (Throwable e) { + log.error("{} exchange failed to calculate sell price", name, e); + } + return null; + + } + + class RateCaller implements Callable { + private final String key; + + RateCaller(String key) { + this.key = key; + } + + @Override + public BigDecimal call() throws Exception { + String[] keyParts = getKeyParts(key); + return exchange.getPollingMarketDataService() + .getTicker(new CurrencyPair(keyParts[0], keyParts[1])) + .getLast(); + } + } + + class BalanceCaller implements Callable { + private final String currency; + + BalanceCaller(String currency) { + this.currency = currency; + } + + @Override + public BigDecimal call() throws Exception { + return exchange.getPollingAccountService() + .getAccountInfo() + .getWallet(currency) + .getBalance(); + } + } + + class PurchaseCoinsTask implements ITask { + private long MAXIMUM_TIME_TO_WAIT_FOR_ORDER_TO_FINISH = 5 * 60 * 60 * 1000; //5 hours + + private BigDecimal amount; + private String cryptoCurrency; + private String fiatCurrencyToUse; + private String description; + + private String orderId; + private String result; + private boolean finished; + + PurchaseCoinsTask(BigDecimal amount, String cryptoCurrency, String fiatCurrencyToUse, String description) { + this.amount = amount; + this.cryptoCurrency = cryptoCurrency; + this.fiatCurrencyToUse = fiatCurrencyToUse; + this.description = description; + } + + @Override + public boolean onCreate() { + log.debug("{} exchange purchase {} {}", name, amount, cryptoCurrency); + PollingAccountService accountService = exchange.getPollingAccountService(); + PollingMarketDataService marketService = exchange.getPollingMarketDataService(); + PollingTradeService tradeService = exchange.getPollingTradeService(); + + try { + log.debug("AccountInfo as String: {}", accountService.getAccountInfo()); + + CurrencyPair currencyPair = new CurrencyPair(cryptoCurrency, fiatCurrencyToUse); + + Ticker ticker = marketService.getTicker(currencyPair); + LimitOrder order = new LimitOrder.Builder(OrderType.BID, currencyPair) + .limitPrice(ticker.getAsk()) + .tradableAmount(amount) + .build(); + + log.debug("limitOrder = {}", order); + + orderId = tradeService.placeLimitOrder(order); + log.debug("orderId = {} {}", orderId, order); + + try { + Thread.sleep(2000); //give exchange 2 seconds to reflect open order in order book + } catch (InterruptedException e) { + e.printStackTrace(); + } + } catch (IOException e) { + e.printStackTrace(); + log.error("{} exchange purchase task failed", name, e); + } + return (orderId != null); + } + + @Override + public boolean onDoStep() { + if (orderId == null) { + log.debug("Giving up on waiting for trade to complete. Because it did not happen"); + finished = true; + result = "Skipped"; + return false; + } + PollingTradeService tradeService = exchange.getPollingTradeService(); + // get open orders + boolean orderProcessed = false; + long checkTillTime = System.currentTimeMillis() + MAXIMUM_TIME_TO_WAIT_FOR_ORDER_TO_FINISH; + if (System.currentTimeMillis() > checkTillTime) { + log.debug("Giving up on waiting for trade {} to complete", orderId); + finished = true; + return false; + } + + log.debug("Open orders:"); + boolean orderFound = false; + try { + OpenOrders openOrders = tradeService.getOpenOrders(); + for (LimitOrder openOrder : openOrders.getOpenOrders()) { + log.debug("openOrder = {}", openOrder); + if (orderId.equals(openOrder.getId())) { + orderFound = true; + break; + } + } + } catch (IOException e) { + e.printStackTrace(); + } + + if (orderFound) { + log.debug("Waiting for order to be processed."); + } else { + orderProcessed = true; + } + + if (orderProcessed) { + result = orderId; + finished = true; + } + + return result != null; + } + + @Override + public boolean isFinished() { + return finished; + } + + @Override + public String getResult() { + return result; + } + + @Override + public boolean isFailed() { + return finished && result == null; + } + + @Override + public void onFinish() { + log.debug("Purchase task finished."); + } + + @Override + public long getShortestTimeForNexStepInvocation() { + return 5 * 1000; //it doesn't make sense to run step sooner than after 5 seconds + } + } + + class SellCoinsTask implements ITask { + private long MAXIMUM_TIME_TO_WAIT_FOR_ORDER_TO_FINISH = 5 * 60 * 60 * 1000; //5 hours + + private BigDecimal cryptoAmount; + private String cryptoCurrency; + private String fiatCurrencyToUse; + private String description; + + private String orderId; + private String result; + private boolean finished; + + SellCoinsTask(BigDecimal cryptoAmount, String cryptoCurrency, String fiatCurrencyToUse, String description) { + this.cryptoAmount = cryptoAmount; + this.cryptoCurrency = cryptoCurrency; + this.fiatCurrencyToUse = fiatCurrencyToUse; + this.description = description; + } + + @Override + public boolean onCreate() { + log.info("Calling {} exchange (sell {} {})", name, cryptoAmount, cryptoCurrency); + PollingAccountService accountService = exchange.getPollingAccountService(); + PollingTradeService tradeService = exchange.getPollingTradeService(); + + try { + log.debug("AccountInfo as String: {}", accountService.getAccountInfo()); + + CurrencyPair currencyPair = new CurrencyPair(cryptoCurrency, fiatCurrencyToUse); + + MarketOrder order = new MarketOrder(OrderType.ASK, cryptoAmount, currencyPair); + log.debug("marketOrder = {}", order); + + orderId = tradeService.placeMarketOrder(order); + log.debug("orderId = {} {}", orderId, order); + + try { + Thread.sleep(2000); //give exchange 2 seconds to reflect open order in order book + } catch (InterruptedException e) { + e.printStackTrace(); + } + } catch (IOException e) { + e.printStackTrace(); + log.error("{} exchange sell coins task failed", name, e); + } catch (Throwable e) { + e.printStackTrace(); + } + return (orderId != null); + } + + @Override + public boolean onDoStep() { + if (orderId == null) { + log.debug("Giving up on waiting for trade to complete. Because it did not happen"); + finished = true; + result = "Skipped"; + return false; + } + PollingTradeService tradeService = exchange.getPollingTradeService(); + // get open orders + boolean orderProcessed = false; + long checkTillTime = System.currentTimeMillis() + MAXIMUM_TIME_TO_WAIT_FOR_ORDER_TO_FINISH; + if (System.currentTimeMillis() > checkTillTime) { + log.debug("Giving up on waiting for trade {} to complete", orderId); + finished = true; + return false; + } + + log.debug("Open orders:"); + boolean orderFound = false; + try { + OpenOrders openOrders = tradeService.getOpenOrders(); + for (LimitOrder openOrder : openOrders.getOpenOrders()) { + log.debug("openOrder = {}", openOrder); + if (orderId.equals(openOrder.getId())) { + orderFound = true; + break; + } + } + } catch (IOException e) { + e.printStackTrace(); + } + + if (orderFound) { + log.debug("Waiting for order to be processed."); + }else{ + orderProcessed = true; + } + + if (orderProcessed) { + result = orderId; + finished = true; + } + + return result != null; + } + + @Override + public boolean isFinished() { + return finished; + } + + @Override + public String getResult() { + return result; + } + + @Override + public boolean isFailed() { + return finished && result == null; + } + + @Override + public void onFinish() { + log.debug("Sell task finished."); + } + + @Override + public long getShortestTimeForNexStepInvocation() { + return 5 * 1000; //it doesn't make sense to run step sooner than after 5 seconds + } + } + +} diff --git a/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitfinex/BitfinexExchange.java b/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitfinex/BitfinexExchange.java index 98cd67d..d5fc61d 100644 --- a/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitfinex/BitfinexExchange.java +++ b/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitfinex/BitfinexExchange.java @@ -25,7 +25,7 @@ import java.math.BigDecimal; import java.util.*; import com.generalbytes.batm.server.extensions.*; -import com.xeiam.xchange.ExchangeException; +import com.xeiam.xchange.exceptions.ExchangeException; import com.xeiam.xchange.dto.marketdata.OrderBook; import com.xeiam.xchange.dto.trade.LimitOrder; import com.xeiam.xchange.dto.trade.OpenOrders; @@ -39,9 +39,9 @@ import com.xeiam.xchange.currency.CurrencyPair; import com.xeiam.xchange.dto.Order.OrderType; import com.xeiam.xchange.dto.marketdata.Ticker; import com.xeiam.xchange.dto.trade.MarketOrder; -import com.xeiam.xchange.service.polling.PollingAccountService; -import com.xeiam.xchange.service.polling.PollingMarketDataService; -import com.xeiam.xchange.service.polling.PollingTradeService; +import com.xeiam.xchange.service.polling.account.PollingAccountService; +import com.xeiam.xchange.service.polling.marketdata.PollingMarketDataService; +import com.xeiam.xchange.service.polling.trade.PollingTradeService; import org.slf4j.spi.LocationAwareLogger; public class BitfinexExchange implements IExchangeAdvanced, IRateSourceAdvanced { diff --git a/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/itbit/ItBitExchange.java b/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/itbit/ItBitExchange.java new file mode 100644 index 0000000..1bf65d2 --- /dev/null +++ b/server_extensions_extra/src/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/itbit/ItBitExchange.java @@ -0,0 +1,89 @@ +/************************************************************************************* + * Copyright (C) 2014 GENERAL BYTES s.r.o. All rights reserved. + *

+ * This software may be distributed and modified under the terms of the GNU + * General Public License version 2 (GPL2) as published by the Free Software + * Foundation and appearing in the file GPL2.TXT included in the packaging of + * this file. Please note that GPL2 Section 2[b] requires that all works based + * on this software must also be made publicly available under the terms of + * the GPL2 ("Copyleft"). + *

+ * Contact information + * ------------------- + *

+ * GENERAL BYTES s.r.o. + * Web : http://www.generalbytes.com + *

+ * Other information: + *

+ * This implementation was created in cooperation with Orillia BVBA + ************************************************************************************/ +package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.itbit; + +import com.generalbytes.batm.server.extensions.ICurrencies; +import com.generalbytes.batm.server.extensions.IExchangeAdvanced; +import com.generalbytes.batm.server.extensions.IRateSource; +import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.XChangeExchange; +import com.xeiam.xchange.ExchangeSpecification; + +import java.util.HashSet; +import java.util.Set; + +public class ItBitExchange extends XChangeExchange implements IExchangeAdvanced, IRateSource { + + private static ExchangeSpecification getDefaultSpecification() { + return new com.xeiam.xchange.itbit.v1.ItBitExchange().getDefaultExchangeSpecification(); + } + + private static ExchangeSpecification getSpecification(String userId, String walletId, String clientKey, String clientSecret) { + ExchangeSpecification spec = getDefaultSpecification(); + spec.setExchangeSpecificParametersItem("userId", userId); + spec.setExchangeSpecificParametersItem("walletId", walletId); + spec.setApiKey(clientKey); + spec.setSecretKey(clientSecret); + return spec; + } + + public ItBitExchange() { + super(getDefaultSpecification()); + } + + public ItBitExchange(String userId, String walletId, String clientKey, String clientSecret) { + super(getSpecification(userId, walletId, clientKey, clientSecret)); + } + + @Override + public Set getCryptoCurrencies() { + Set cryptoCurrencies = new HashSet(); + cryptoCurrencies.add(ICurrencies.XBT); + return cryptoCurrencies; + } + + @Override + public Set getFiatCurrencies() { + Set fiatCurrencies = new HashSet(); + fiatCurrencies.add(ICurrencies.USD); + fiatCurrencies.add(ICurrencies.EUR); + return fiatCurrencies; + } + + @Override + public String getPreferredFiatCurrency() { + return ICurrencies.USD; + } + + @Override + protected boolean isWithdrawSuccessful(String result) { + return true; + } + + @Override + protected double getAllowedCallsPerSecond() { + return 10; + } + + @Override + protected String mapCurrency(String currency) { + return currency.equals(ICurrencies.BTC) ? ICurrencies.XBT : currency; + } +} \ No newline at end of file