/* * The MtGox-Java API is free software: you can redistribute it and/or modify * it under the terms of the Lesser GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * The MtGox-Java API is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * Lesser GNU General Public License for more details. * * You should have received a copy of the Lesser GNU General Public License * along with the MtGox-Java API . If not, see <http://www.gnu.org/licenses/>. */ package to.sparks.mtgox.example; import java.math.BigDecimal; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.collections.comparators.ReverseComparator; import org.apache.commons.lang.ArrayUtils; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.core.task.SimpleAsyncTaskExecutor; import to.sparks.mtgox.MtGoxHTTPClient; import to.sparks.mtgox.event.StreamEvent; import to.sparks.mtgox.event.TickerEvent; import to.sparks.mtgox.event.TradeEvent; import to.sparks.mtgox.model.*; /** * A trading bot that will maintain a sequence of bid and ask orders. * * It should maintain a staggered set of orders that are cancelled and reordered * if they move too far from their calculated price or volume. * The bids are for varying amounts of bitcoins, according to a configured array * of percentages. * The orders are staggered above the lowest ask price and below the highest bid * price. * The total balance of the account is used in placing the orders. * * @author SparksG */ public class TradingBot implements ApplicationListener<StreamEvent> { /* The logger */ static final Logger logger = Logger.getLogger(TradingBot.class.getName()); /* The percentage of total balance bought or sold in each staggered order */ static final double[] percentagesOrderPriceSpread = new double[]{0.08D, 0.16D, 0.45D, 0.14D, 0.05D}; /* The percentage above ot below last price that each order should be staggered */ static final double[] percentagesAboveOrBelowPrice = new double[]{0.003D, 0.005D, 0.008D, 0.0120D, 0.016D}; /* The percentage of price that an order is allowed to deviate before re-ordering * at the newly calculated prices */ static final BigDecimal percentAllowedPriceDeviation = BigDecimal.valueOf(0.0015D); private SimpleAsyncTaskExecutor taskExecutor; private MtGoxHTTPClient mtgoxAPI; private CurrencyInfo baseCurrency; private Ticker lastTicker; private Date timeOfLastOrder = Calendar.getInstance().getTime(); public TradingBot(SimpleAsyncTaskExecutor taskExecutor, MtGoxHTTPClient mtgoxAPI) throws Exception { this.mtgoxAPI = mtgoxAPI; this.taskExecutor = taskExecutor; AccountInfo info = mtgoxAPI.getAccountInfo(); logger.log(Level.INFO, "Logged into account: {0}", info.getLogin()); baseCurrency = mtgoxAPI.getCurrencyInfo(mtgoxAPI.getBaseCurrency()); String currencyCode = baseCurrency.getCurrency().getCurrencyCode(); logger.log(Level.INFO, "Configured base currency: {0}", currencyCode); lastTicker = mtgoxAPI.getTicker(); taskExecutor.execute(new Logic()); logger.info("Waiting for trade events to trigger bot activity..."); } public static void main(String[] args) throws Exception { ApplicationContext context = new ClassPathXmlApplicationContext("to/sparks/mtgox/example/TradingBot.xml"); TradingBot me = context.getBean("tradingBot", TradingBot.class); } class Logic implements Runnable { @Override public void run() { try { MtGoxFiatCurrency buyPrice = lastTicker.getBuy().getPriceValue(); MtGoxFiatCurrency sellPrice = lastTicker.getSell().getPriceValue(); Order[] openOrders = mtgoxAPI.getOpenOrders(); MtGoxFiatCurrency[] optimumBidPrices = new MtGoxFiatCurrency[percentagesAboveOrBelowPrice.length]; for (int i = 0; i < percentagesAboveOrBelowPrice.length; i++) { optimumBidPrices[i] = getPriceAtOrderIndex(MtGoxHTTPClient.OrderType.Bid, buyPrice, i); } MtGoxFiatCurrency[] optimumAskPrices = new MtGoxFiatCurrency[percentagesAboveOrBelowPrice.length]; for (int i = 0; i < percentagesAboveOrBelowPrice.length; i++) { optimumAskPrices[i] = getPriceAtOrderIndex(MtGoxHTTPClient.OrderType.Ask, sellPrice, i); } if (isOrdersValid(optimumBidPrices, optimumAskPrices, openOrders)) { logger.info("The current orders remain valid."); } else { logger.info("There are invalid bid or ask orders, or none exist."); cancelOrders(mtgoxAPI, openOrders); AccountInfo info = mtgoxAPI.getAccountInfo(); Wallet fiatWallet = info.getWallets().get(baseCurrency.getCurrency().getCurrencyCode()); try { MtGoxBitcoin numBTCtoBuy = new MtGoxBitcoin(fiatWallet.getBalance().divide(buyPrice)); logger.log(Level.INFO, "Trying to buy a total of {0} bitcoins.", numBTCtoBuy.toPlainString()); for (int i = 0; i < optimumBidPrices.length; i++) { MtGoxBitcoin vol = new MtGoxBitcoin(numBTCtoBuy.multiply(BigDecimal.valueOf(percentagesOrderPriceSpread[i]))); String ref = mtgoxAPI.placeOrder(MtGoxHTTPClient.OrderType.Bid, optimumBidPrices[i], vol); logger.log(Level.FINE, "Bid order placed at price: {0}{1} amount: {2} ref: {3}", new Object[]{optimumBidPrices[i].getCurrencyInfo().getCurrency().getCurrencyCode(), optimumBidPrices[i].getNumUnits(), vol.toPlainString(), ref}); } } catch (Exception ex) { Logger.getLogger(TradingBot.class.getName()).log(Level.SEVERE, null, ex); } Wallet btcWallet = info.getWallets().get("BTC"); try { MtGoxBitcoin numBTCtoSell = (MtGoxBitcoin) btcWallet.getBalance(); logger.log(Level.INFO, "Trying to sell a total of {0} bitcoins.", numBTCtoSell.toPlainString()); for (int i = 0; i < optimumAskPrices.length; i++) { MtGoxBitcoin vol = new MtGoxBitcoin(numBTCtoSell.multiply(BigDecimal.valueOf(percentagesOrderPriceSpread[i]))); String ref = mtgoxAPI.placeOrder(MtGoxHTTPClient.OrderType.Ask, optimumAskPrices[i], vol); logger.log(Level.FINE, "Ask order placed at price: {0}{1} amount: {2} ref: {3}", new Object[]{optimumAskPrices[i].getCurrencyInfo().getCurrency().getCurrencyCode(), optimumAskPrices[i].getNumUnits(), vol.toPlainString(), ref}); } } catch (Exception ex) { Logger.getLogger(TradingBot.class.getName()).log(Level.SEVERE, null, ex); } logger.log(Level.INFO, "Account balance: {0} BTC + {2}{3}{1} = Total current value: {2}{3}{4}", new Object[]{btcWallet.getBalance().toPlainString(), fiatWallet.getBalance().toPlainString(), lastTicker.getLast().getCurrencyInfo().getCurrency().getCurrencyCode(), lastTicker.getLast().getCurrencyInfo().getSymbol(), fiatWallet.getBalance().add(btcWallet.getBalance().multiply(lastTicker.getLast().getNumUnits()))}); } } catch (Exception ex) { Logger.getLogger(TradingBot.class.getName()).log(Level.SEVERE, null, ex); } } private void cancelOrders(MtGoxHTTPClient mtGoxAPI, Order[] orders) throws Exception { if (ArrayUtils.isNotEmpty(orders)) { for (Order order : orders) { logger.log(Level.FINE, "Cancelling order: {0}", order.getOid()); mtGoxAPI.cancelOrder(order); } } else { logger.fine("There are no orders to cancel."); } } } @Override public void onApplicationEvent(StreamEvent event) { try { if (event instanceof TradeEvent) { Trade trade = (Trade) event.getPayload(); if (trade.getPrice().getCurrencyInfo().equals(baseCurrency)) { if (trade.getAmount().compareTo(new MtGoxBitcoin(0.9D)) > 0) { logger.log(Level.INFO, "Market-making trade event: {0}${1} volume: {2}", new Object[]{trade.getPrice_currency(), trade.getPrice().toPlainString(), trade.getAmount().toPlainString()}); Calendar thirtySecondsAgo = Calendar.getInstance(); thirtySecondsAgo.add(Calendar.SECOND, -30); if (timeOfLastOrder.before(thirtySecondsAgo.getTime())) { taskExecutor.execute(new Logic()); timeOfLastOrder = Calendar.getInstance().getTime(); } else { logger.info("Ignoring order because too soon."); } } else { logger.log(Level.FINE, "Insufficient sized trade event: {0}${1} volume: {2}", new Object[]{trade.getPrice_currency(), trade.getPrice().toPlainString(), trade.getAmount().toPlainString()}); } } } else if (event instanceof TickerEvent) { if (((Ticker) event.getPayload()).getCurrencyCode().equalsIgnoreCase(baseCurrency.getCurrency().getCurrencyCode())) { lastTicker = (Ticker) event.getPayload(); logger.log(Level.FINE, "Ticker Last: {0}{1}{2} Volume: {3} Buy: {0}{4} Sell: {0}{5}", new Object[]{ lastTicker.getVwap().getCurrencyInfo().getCurrency().getCurrencyCode(), lastTicker.getVwap().getCurrencyInfo().getSymbol(), lastTicker.getLast().toPlainString(), lastTicker.getVol().toPlainString(), lastTicker.getBuy().getDisplay(), lastTicker.getSell().getDisplay() }); } } } catch (Exception ex) { logger.log(Level.SEVERE, null, ex); } } private static boolean isOrdersValid(MtGoxFiatCurrency[] optimumBidPrices, MtGoxFiatCurrency[] optimumAskPrices, Order[] orders) { boolean bRet = false; if (ArrayUtils.isNotEmpty(orders) && orders.length == percentagesOrderPriceSpread.length * 2 && orders.length == percentagesAboveOrBelowPrice.length * 2) { return isWithinAllowedDeviation(percentAllowedPriceDeviation, orders, optimumBidPrices, optimumAskPrices); } return bRet; } /** * Return the calculated price for the order at the given index in the * orders arrays. * * @param index The index of the order price/volume in the orders arrays. * @return The calulated price of the order at the given index */ private static MtGoxFiatCurrency getPriceAtOrderIndex(MtGoxHTTPClient.OrderType orderType, MtGoxFiatCurrency lastPrice, int index) { MtGoxFiatCurrency price; if (orderType == MtGoxHTTPClient.OrderType.Bid) { price = new MtGoxFiatCurrency(lastPrice.subtract(lastPrice.multiply(BigDecimal.valueOf(percentagesAboveOrBelowPrice[index]))), lastPrice.getCurrencyInfo()); } else { price = new MtGoxFiatCurrency(lastPrice.add(lastPrice.multiply(BigDecimal.valueOf(percentagesAboveOrBelowPrice[index]))), lastPrice.getCurrencyInfo()); } return price; } /** * Have the orders deviated from the calculated prices? * The list must be sorted by descending price or this comparison will not * work. */ private static boolean isWithinAllowedDeviation(BigDecimal percentAllowedPriceDeviation, Order[] allOrders, MtGoxFiatCurrency[] optimumBidPrices, MtGoxFiatCurrency[] optimumAskPrices) { boolean bRet = true; List<Order> bidOrders = new ArrayList<>(); List<Order> askOrders = new ArrayList<>(); for (Order order : allOrders) { if (order.getType() == MtGoxHTTPClient.OrderType.Bid) { bidOrders.add(order); } else { askOrders.add(order); } } // Sort the bid orders by price descending, so that they can be compared to the optimum prices array which is also in that order. Collections.sort(bidOrders, new ReverseComparator(new Comparator<Order>() { @Override public int compare(Order o1, Order o2) { return o1.getPrice().getPriceValue().compareTo(o2.getPrice().getPriceValue().getNumUnits()); } })); // Sort the ask orders by price ascending, so that they can be compared to the optimum prices array which is also in that order. Collections.sort(askOrders, new Comparator<Order>() { @Override public int compare(Order o1, Order o2) { return o1.getPrice().getPriceValue().compareTo(o2.getPrice().getPriceValue().getNumUnits()); } }); for (int i = 0; i < bidOrders.size(); i++) { BigDecimal actualPrice = bidOrders.get(i).getPrice().getPriceValue().getNumUnits(); BigDecimal optimumPrice = optimumBidPrices[i].getNumUnits(); if (isDiffTooLarge(actualPrice, optimumPrice)) { bRet = false; break; } } for (int i = 0; i < askOrders.size(); i++) { BigDecimal actualPrice = askOrders.get(i).getPrice().getPriceValue().getNumUnits(); BigDecimal optimumPrice = optimumAskPrices[i].getNumUnits(); if (isDiffTooLarge(actualPrice, optimumPrice)) { bRet = false; break; } } return bRet; } private static boolean isDiffTooLarge(BigDecimal actualPrice, BigDecimal optimumPrice) { BigDecimal diff; if (actualPrice.compareTo(optimumPrice) < 0) { diff = optimumPrice.subtract(actualPrice); } else { diff = actualPrice.subtract(optimumPrice); } return diff.compareTo(optimumPrice.multiply(percentAllowedPriceDeviation)) > 0; } }