/** * Copyright 2012 multibit.org * * Licensed under the MIT license (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://opensource.org/licenses/mit-license.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.multibit.exchange; import com.xeiam.xchange.Exchange; import com.xeiam.xchange.ExchangeFactory; import com.xeiam.xchange.ExchangeSpecification; import com.xeiam.xchange.currency.Currencies; import com.xeiam.xchange.currency.CurrencyPair; import com.xeiam.xchange.dto.marketdata.Ticker; import com.xeiam.xchange.service.polling.PollingMarketDataService; import org.joda.money.BigMoney; import org.joda.money.CurrencyUnit; import org.multibit.controller.Controller; import org.multibit.controller.exchange.ExchangeController; import org.multibit.model.exchange.ExchangeData; import org.multibit.model.exchange.ExchangeModel; import org.multibit.viewsystem.swing.MultiBitFrame; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Collection; import java.util.List; import java.util.TimerTask; /** * TimerTask to poll currency exchanges for ticker data process */ public class TickerTimerTask extends TimerTask { public static final int DEFAULT_REPEAT_RATE = 600000; // milliseconds public static final int INITIAL_DELAY = 0; // milliseconds public static final int TASK_SEPARATION = 1000; // milliseconds public static final int NUMBER_OF_SIGNIFICANT_DIGITS = 20; private static Logger log = LoggerFactory.getLogger(TickerTimerTask.class); private final Controller controller; private final ExchangeController exchangeController; private final MultiBitFrame mainFrame; // Is this the first row in the ticker (=true) or the second row (=false). private final boolean isFirstExchange; private String shortExchangeName; private String currency; private Exchange exchange; private PollingMarketDataService marketDataService; private List<CurrencyPair> exchangeSymbols; /** * Constructs the TickerTimerTask. */ public TickerTimerTask(ExchangeController exchangeController, MultiBitFrame mainFrame, boolean isFirstExchange) { this.exchangeController = exchangeController; this.controller = this.exchangeController; this.mainFrame = mainFrame; this.isFirstExchange = isFirstExchange; if (isFirstExchange) { currency = controller.getModel().getUserPreference(ExchangeModel.TICKER_FIRST_ROW_CURRENCY); if (currency == null || currency.length() == 0) { currency = ExchangeData.DEFAULT_CURRENCY; controller.getModel().setUserPreference(ExchangeModel.TICKER_FIRST_ROW_CURRENCY, currency); } shortExchangeName = controller.getModel().getUserPreference(ExchangeModel.TICKER_FIRST_ROW_EXCHANGE); } else { currency = controller.getModel().getUserPreference(ExchangeModel.TICKER_SECOND_ROW_CURRENCY); shortExchangeName = controller.getModel().getUserPreference(ExchangeModel.TICKER_SECOND_ROW_EXCHANGE); } } /** * When the timer executes, get the exchange data and pass to the * CurrencyConverter, which notifies parties of interest. */ @Override public void run() { // If this is the second row and is not showing, do not do anything. if (!isFirstExchange && !Boolean.TRUE.toString().equals( controller.getModel().getUserPreference(ExchangeModel.TICKER_SHOW_SECOND_ROW))) { return; } try { // Create exchange. synchronized (this) { if (exchange == null) { log.debug("exchange is null ... creating exchange ... (isFirstExchange = " + isFirstExchange + ")"); if (shortExchangeName == null) { log.debug("shortExchangeName is null, defaulting to " + ExchangeData.DEFAULT_EXCHANGE); shortExchangeName = ExchangeData.DEFAULT_EXCHANGE; } createExchangeObjects(shortExchangeName); if (exchange == null) { log.debug("Cannot create exchange (isFirstExchange = " + isFirstExchange + ")"); } } } if (marketDataService != null) { if (exchangeSymbols != null) { // Only get data from server if ticker is being shown if // currency conversion is switched on. // (This is to minimise the load on the remote servers). if (!Boolean.FALSE.toString().equals(controller.getModel().getUserPreference(ExchangeModel.TICKER_SHOW)) || !Boolean.FALSE.toString().equals( controller.getModel().getUserPreference(ExchangeModel.SHOW_BITCOIN_CONVERTED_TO_FIAT))) { // Get symbol ticker if it is one of the // currencies we are interested in. // (This is to save hitting the server for every // currency). boolean getItFromTheServer = false; // Is the amount quoted the reciprocal of number // of currency units per BTC boolean invertedRates = false; // Is the currency pair the other way round ie // base currency = other, counter currency = BTC boolean reverseRates = ExchangeData.doesExchangeUseReverseRates(shortExchangeName); CurrencyPair currencyPairToUse = null; for (CurrencyPair loopSymbolPair : exchangeSymbols) { if (ExchangeData.OPEN_EXCHANGE_RATES_EXCHANGE_NAME.equals(shortExchangeName)) { if (loopSymbolPair.baseCurrency.equals(currency)) { getItFromTheServer = true; invertedRates = true; currencyPairToUse = loopSymbolPair; break; } } else { if ("BTC".equals(loopSymbolPair.baseCurrency) && loopSymbolPair.counterCurrency.equals(currency)) { getItFromTheServer = true; currencyPairToUse = loopSymbolPair; break; } if ("BTC".equals(loopSymbolPair.counterCurrency) && loopSymbolPair.baseCurrency.equals(currency)) { getItFromTheServer = true; invertedRates = true; currencyPairToUse = loopSymbolPair; break; } } } if (getItFromTheServer) { BigMoney last = null; BigMoney bid = null; BigMoney ask = null; Ticker loopTicker; if (ExchangeData.OPEN_EXCHANGE_RATES_EXCHANGE_NAME.equals(shortExchangeName)) { log.debug("Getting loopTicker for " + currency + " USD"); loopTicker = marketDataService.getTicker(currency, "USD"); System.out.println("loopTicker = " + loopTicker); Ticker btcUsdTicker = null; log.debug("Getting btcUsdTicker for BTC/USD"); btcUsdTicker = marketDataService.getTicker(Currencies.BTC, Currencies.USD); System.out.println("btcUsdTicker = " + btcUsdTicker); BigMoney usdBtcRateMoney = btcUsdTicker.getLast(); BigDecimal usdBtcRate = null; if (usdBtcRateMoney != null) { usdBtcRate = usdBtcRateMoney.getAmount(); if (loopTicker.getLast() != null) { last = loopTicker.getLast().dividedBy(usdBtcRate, RoundingMode.HALF_EVEN); } if (loopTicker.getBid() != null) { bid = loopTicker.getBid().dividedBy(usdBtcRate, RoundingMode.HALF_EVEN); } if (loopTicker.getAsk() != null) { ask = loopTicker.getAsk().dividedBy(usdBtcRate, RoundingMode.HALF_EVEN); } } } else { log.debug("Getting ticker for " + currencyPairToUse.baseCurrency + " " + currencyPairToUse.counterCurrency); loopTicker = marketDataService.getTicker(currencyPairToUse.baseCurrency, currencyPairToUse.counterCurrency); log.debug("Got ticker for " + currencyPairToUse.baseCurrency + " " + currencyPairToUse.counterCurrency); last = loopTicker.getLast(); bid = loopTicker.getBid(); ask = loopTicker.getAsk(); if (invertedRates && !reverseRates) { if (last != null && last.getAmount() != BigDecimal.ZERO) { last = BigMoney.of(last.getCurrencyUnit(), BigDecimal.ONE.divide(last.getAmount(), NUMBER_OF_SIGNIFICANT_DIGITS, BigDecimal.ROUND_HALF_EVEN)); } else { last = null; } if (bid != null && bid.getAmount() != BigDecimal.ZERO) { bid = BigMoney.of(last.getCurrencyUnit(), BigDecimal.ONE.divide(bid.getAmount(), NUMBER_OF_SIGNIFICANT_DIGITS, BigDecimal.ROUND_HALF_EVEN)); } else { bid = null; } if (ask != null && ask.getAmount() != BigDecimal.ZERO) { ask = BigMoney.of(last.getCurrencyUnit(), BigDecimal.ONE.divide(ask.getAmount(), NUMBER_OF_SIGNIFICANT_DIGITS, BigDecimal.ROUND_HALF_EVEN)); } else { ask = null; } } if (invertedRates) { if (reverseRates) { // USD/ BTC, reciprocal rate currency = currencyPairToUse.baseCurrency; } else { // BTC/ USD, reciprocal rate currency = currencyPairToUse.counterCurrency; } } else { if (reverseRates) { // USD/ BTC, normal rate currency = currencyPairToUse.baseCurrency; } else { // BTC/ USD, normal rate currency = currencyPairToUse.counterCurrency; } } } this.exchangeController.getModel().getExchangeData(shortExchangeName).setLastPrice(currency, last); this.exchangeController.getModel().getExchangeData(shortExchangeName).setLastBid(currency, bid); this.exchangeController.getModel().getExchangeData(shortExchangeName).setLastAsk(currency, ask); log.debug("Exchange = " + shortExchangeName); // Put the exchange rate into the currency converter. if (isFirstExchange) { String newCurrencyCode = currency; if (ExchangeData.BITCOIN_CHARTS_EXCHANGE_NAME.equals(shortExchangeName)) { // Use only the last three characters - the // currency code. if (currency.length() >= 3) { newCurrencyCode = currency.substring(currency.length() - 3); } } CurrencyConverter.INSTANCE.setCurrencyUnit(CurrencyUnit.of(newCurrencyCode)); CurrencyConverter.INSTANCE.setRate(last.getAmount()); } } } } // Fire exchange rate data changed - used by rest of MultiBit. mainFrame.fireExchangeDataChanged(); } } catch (Exception e) { // Stop any xchange errors percolating out. log.error(e.getClass().getName() + " " + e.getMessage()); if (e.getCause() != null) { log.error(e.getCause().getClass().getName() + " " + e.getCause().getMessage()); } } } public void createExchangeObjects(String newExchangeName) { exchange = createExchange(newExchangeName); if (exchange != null) { // Interested in the public market data feed (no authentication). marketDataService = exchange.getPollingMarketDataService(); log.debug("marketDataService = " + marketDataService); // Get the list of available currencies. exchangeSymbols = marketDataService.getExchangeSymbols(); log.debug("exchangeSymbols = " + exchangeSymbols); if (exchangeSymbols != null) { Collection<String> availableCurrencies = new java.util.TreeSet<String>(); for (int i = 0; i < exchangeSymbols.size(); i++) { String baseCurrency = exchangeSymbols.get(i).baseCurrency; String counterCurrency = exchangeSymbols.get(i).counterCurrency; if (ExchangeData.OPEN_EXCHANGE_RATES_EXCHANGE_NAME.equals(newExchangeName)) { if ("USD".equalsIgnoreCase(baseCurrency) && !"BTC".equalsIgnoreCase(counterCurrency)) { if (!"EEK".equalsIgnoreCase(counterCurrency) && !"CLF".equalsIgnoreCase(counterCurrency) && !"JEP".equalsIgnoreCase(counterCurrency) && ! "SVC".equalsIgnoreCase(counterCurrency)) { availableCurrencies.add(counterCurrency); } } if ("USD".equalsIgnoreCase(counterCurrency) && !"BTC".equalsIgnoreCase(baseCurrency)) { if (!"EEK".equalsIgnoreCase(baseCurrency) && !"CLF".equalsIgnoreCase(baseCurrency) && !"JEP".equalsIgnoreCase(baseCurrency) && ! "SVC".equalsIgnoreCase(baseCurrency)) { availableCurrencies.add(baseCurrency); } } } else { if ("BTC".equalsIgnoreCase(baseCurrency)) { availableCurrencies.add(counterCurrency); } if ("BTC".equalsIgnoreCase(counterCurrency)) { availableCurrencies.add(baseCurrency); } } } ExchangeData.setAvailableCurrenciesForExchange(newExchangeName, availableCurrencies); } } } /** * Create the exchange specified by the exchange short name * * @param exchangeShortname The name of the exchange to create */ private Exchange createExchange(String exchangeShortname) { log.debug("creating exchange from exchangeShortname = " + exchangeShortname); if (exchangeShortname == null) { return null; } try { // Demonstrate the public market data service. // Use the factory to get the exchange API using default settings. String exchangeClassname = ExchangeData.convertExchangeShortNameToClassname(exchangeShortname); if (exchangeClassname == null) { return null; } Exchange exchangeToReturn; if (ExchangeData.OPEN_EXCHANGE_RATES_EXCHANGE_NAME.equalsIgnoreCase(exchangeShortname)) { ExchangeSpecification exchangeSpecification = new ExchangeSpecification(exchangeClassname); exchangeSpecification.setPlainTextUri("http://openexchangerates.org"); exchangeSpecification .setApiKey(controller.getModel().getUserPreference(ExchangeModel.OPEN_EXCHANGE_RATES_API_CODE)); exchangeToReturn = ExchangeFactory.INSTANCE.createExchange(exchangeSpecification); } else { exchangeToReturn = ExchangeFactory.INSTANCE.createExchange(exchangeClassname); } if (this.exchangeController.getModel().getExchangeData(shortExchangeName) == null) { ExchangeData exchangeData = new ExchangeData(); exchangeData.setShortExchangeName(shortExchangeName); this.exchangeController.getModel().getShortExchangeNameToExchangeMap().put(exchangeShortname, exchangeData); } return exchangeToReturn; } catch (com.xeiam.xchange.ExchangeException e) { // Probably xchange is not on classpath - ticker will not run // but error should not spread out from here to rest of MultiBit. log.error(e.getClass().getName() + " " + e.getMessage()); }catch (NoClassDefFoundError e) { // Probably xchange is not on classpath - ticker will not run // but error should not spread out from here to rest of MultiBit. log.error(e.getClass().getName() + " " + e.getMessage()); } catch (NullPointerException e) { log.error(e.getClass().getName() + " " + e.getMessage()); } return null; } /** * Get the exchange used by this TickerTimerTask. */ public Exchange getExchange() { return exchange; } public boolean isFirstExchange() { return isFirstExchange; } }