package com.msgilligan.bitcoinj.money;
import org.javamoney.moneta.convert.ExchangeRateBuilder;
import org.javamoney.moneta.spi.DefaultNumberValue;
import org.javamoney.moneta.spi.LazyBoundCurrencyConversion;
import org.javamoney.moneta.spi.base.BaseExchangeRateProvider;
import org.knowm.xchange.Exchange;
import org.knowm.xchange.ExchangeFactory;
import org.knowm.xchange.currency.CurrencyPair;
import org.knowm.xchange.dto.marketdata.Ticker;
import org.knowm.xchange.service.marketdata.MarketDataService;
import javax.money.convert.ConversionContext;
import javax.money.convert.ConversionQuery;
import javax.money.convert.CurrencyConversion;
import javax.money.convert.ExchangeRate;
import javax.money.convert.ProviderContext;
import javax.money.convert.RateType;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Base ExchangeRateProvider using XChange library
* Currently supports current DEFERRED rates only
*/
public abstract class BaseXChangeExchangeRateProvider extends BaseExchangeRateProvider
implements ObservableExchangeRateProvider {
protected final ProviderContext providerContext;
protected String name;
protected Exchange exchange;
protected MarketDataService marketDataService;
private ScheduledExecutorService stpe;
private ScheduledFuture<?> future;
private final Map<CurrencyUnitPair, MonitoredCurrency> monitoredCurrencies = new HashMap<>();
private static final int initialDelay = 0;
private static final int period = 60;
private volatile boolean stopping = false;
/**
* Construct using an XChange Exchange class object for a set of currencies
* @param exchangeClass
* @param pairs pairs to monitor
*/
protected BaseXChangeExchangeRateProvider(Class<? extends Exchange> exchangeClass,
CurrencyUnitPair... pairs) {
exchange = ExchangeFactory.INSTANCE.createExchange(exchangeClass.getName());
name = exchange.getExchangeSpecification().getExchangeName();
providerContext = ProviderContext.of(name, RateType.DEFERRED);
marketDataService = exchange.getMarketDataService();
for (CurrencyUnitPair pair : pairs) {
MonitoredCurrency monitoredCurrency = new MonitoredCurrency(pair, xchangePair(pair));
monitoredCurrencies.put(pair, monitoredCurrency);
}
start(); // starting here causes first ticker to be read before observers can be registered!!!
}
protected BaseXChangeExchangeRateProvider(Class<? extends Exchange> exchangeClass,
String... pairs) {
this(exchangeClass, pairsConvert(pairs));
}
private static CurrencyUnitPair[] pairsConvert(String[] strings) {
CurrencyUnitPair[] units = new CurrencyUnitPair[strings.length];
for (int i = 0 ; i < strings.length ; i++) {
units[i] = new CurrencyUnitPair(strings[i]);
}
return units;
}
/**
* Map from CurrencyUnitPair to XChange CurrencyPair
* Override to handle cases like ItBit that use "XBT" instead of "BTC"
* @param pair CurrencyUnitPair using JavaMoney CurrencyUnits
* @return XChange CurrencyPair
*/
protected CurrencyPair xchangePair(CurrencyUnitPair pair) {
return new CurrencyPair(pair.getBase().getCurrencyCode(), pair.getTarget().getCurrencyCode());
}
/**
* Start the polling thread
*/
@Override
public void start() {
stpe = Executors.newScheduledThreadPool(2);
final BaseXChangeExchangeRateProvider that = this;
Runnable task = new Runnable() {
@Override
public void run() {
that.poll();
}
};
future = stpe.scheduleWithFixedDelay(task, initialDelay, period, TimeUnit.SECONDS);
}
/**
* stop the polling thread
*/
@Override
public void stop() {
if (!stopping) {
stopping = true;
final ScheduledFuture<?> handle = future;
Runnable task = new Runnable() {
@Override
public void run() {
handle.cancel(true);
}
};
stpe.schedule(task, 0, TimeUnit.SECONDS);
stpe.shutdown();
}
}
/**
* Poll the exchange for updated Tickers
*/
protected void poll() {
for (Map.Entry<CurrencyUnitPair, MonitoredCurrency> entry : monitoredCurrencies.entrySet()) {
try {
MonitoredCurrency monitor = entry.getValue();
monitor.setTicker(marketDataService.getTicker(monitor.exchangePair));
notifyExchangeRateObservers(monitor);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void registerExchangeRateObserver(CurrencyUnitPair pair, ExchangeRateObserver observer) {
// TODO: validate rate as one this provider supports
MonitoredCurrency monitor = monitoredCurrencies.get(pair);
monitor.observerList.add(observer);
// If we've got data already, call observer immediately
if (monitor.isTickerAvailable()) {
observer.onExchangeRateChange(buildExchangeRateChange(monitor));
}
}
public void notifyExchangeRateObservers(MonitoredCurrency monitor) {
for (ExchangeRateObserver observer : monitor.observerList) {
observer.onExchangeRateChange(buildExchangeRateChange(monitor));
}
}
@Override
public ProviderContext getContext() {
return providerContext;
}
@Override
public boolean isAvailable(ConversionQuery conversionQuery) {
return getExchangeRate(conversionQuery) != null;
}
@Override
public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) {
CurrencyUnitPair pair = new CurrencyUnitPair(conversionQuery.getBaseCurrency(), conversionQuery.getCurrency());
MonitoredCurrency monitoredCurrency = monitoredCurrencies.get(pair);
if (monitoredCurrency == null) {
return null;
}
return buildExchangeRate(monitoredCurrency);
}
private ExchangeRateChange buildExchangeRateChange(MonitoredCurrency monitor) {
Date date = monitor.getTicker().getTimestamp();
// Not all exchanges provide a timestamp, default to 0 if it is null
long milliseconds = (date != null) ? date.getTime() : 0;
return new ExchangeRateChange(buildExchangeRate(monitor), milliseconds);
}
protected ExchangeRate buildExchangeRate(MonitoredCurrency monitoredCurrency) {
return new ExchangeRateBuilder(name, RateType.DEFERRED)
.setBase(monitoredCurrency.pair.getBase())
.setTerm(monitoredCurrency.pair.getTarget())
.setFactor(DefaultNumberValue.of(monitoredCurrency.getTicker().getLast()))
.build();
}
@Override
public CurrencyConversion getCurrencyConversion(ConversionQuery conversionQuery) {
return new LazyBoundCurrencyConversion(conversionQuery, this, ConversionContext
.of(getContext().getProviderName(), getContext().getRateTypes().iterator().next()));
}
protected static class MonitoredCurrency {
final CurrencyUnitPair pair; // Terminating (target) JavaMoney CurrencyUnit
final CurrencyPair exchangePair; // XChange currency pair (format used by XChange/exchange)
final List<ExchangeRateObserver> observerList = new ArrayList<>();
private final CountDownLatch tickerReady = new CountDownLatch(1);
private Ticker _ticker = null; // The '_' means use the getter and setter, please
public MonitoredCurrency(CurrencyUnitPair pair, CurrencyPair exchangePair) {
this.pair = pair;
this.exchangePair = exchangePair;
}
boolean isTickerAvailable() {
return _ticker != null;
}
Ticker getTicker() {
if (_ticker == null) {
// were we called before first poll completed?
try {
// wait for the CountdownLatch
boolean ready = tickerReady.await(60, TimeUnit.SECONDS);
if (!ready) {
throw new RuntimeException("timeout");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
return _ticker;
}
void setTicker(Ticker ticker) {
_ticker = ticker;
tickerReady.countDown();
}
}
}