Integrate initial set of ExchangeRateProviders

Add support for a few exchanges to demonstrate and test the pricenode
aggregate rates.

The chose exchanges were selected because they each provide a varied
list of fiat and altcoins, with a substantial overlap between them. This
 provides a robust initial set of datapoints and scenarios for aggregate
  rates.
This commit is contained in:
cd2357 2020-06-16 21:23:31 +02:00
parent f650115580
commit 671e80929a
No known key found for this signature in database
GPG Key ID: F26C56748514D0D3
11 changed files with 456 additions and 54 deletions

View File

@ -57,7 +57,7 @@ configure(subprojects) {
junitVersion = '4.12' junitVersion = '4.12'
jupiterVersion = '5.3.2' jupiterVersion = '5.3.2'
kotlinVersion = '1.3.41' kotlinVersion = '1.3.41'
knowmXchangeVersion = '4.3.3' knowmXchangeVersion = '5.0.0'
langVersion = '3.8' langVersion = '3.8'
logbackVersion = '1.1.11' logbackVersion = '1.1.11'
loggingVersion = '1.2' loggingVersion = '1.2'
@ -458,6 +458,10 @@ configure(project(':pricenode')) {
dependencies { dependencies {
compile project(":core") compile project(":core")
compileOnly "org.projectlombok:lombok:$lombokVersion"
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
implementation "com.google.code.gson:gson:$gsonVersion" implementation "com.google.code.gson:gson:$gsonVersion"
implementation "commons-codec:commons-codec:$codecVersion" implementation "commons-codec:commons-codec:$codecVersion"
implementation "org.apache.httpcomponents:httpcore:$httpcoreVersion" implementation "org.apache.httpcomponents:httpcore:$httpcoreVersion"
@ -465,13 +469,18 @@ configure(project(':pricenode')) {
exclude(module: 'commons-codec') exclude(module: 'commons-codec')
} }
compile("org.knowm.xchange:xchange-bitcoinaverage:$knowmXchangeVersion") compile("org.knowm.xchange:xchange-bitcoinaverage:$knowmXchangeVersion")
compile("org.knowm.xchange:xchange-binance:$knowmXchangeVersion")
compile("org.knowm.xchange:xchange-bitfinex:$knowmXchangeVersion")
compile("org.knowm.xchange:xchange-coinmarketcap:$knowmXchangeVersion") compile("org.knowm.xchange:xchange-coinmarketcap:$knowmXchangeVersion")
compile("org.knowm.xchange:xchange-kraken:$knowmXchangeVersion")
compile("org.knowm.xchange:xchange-poloniex:$knowmXchangeVersion") compile("org.knowm.xchange:xchange-poloniex:$knowmXchangeVersion")
compile("org.springframework.boot:spring-boot-starter-web:$springBootVersion") compile("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
compile("org.springframework.boot:spring-boot-starter-actuator") compile("org.springframework.boot:spring-boot-starter-actuator")
testCompile "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" testCompile "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
testCompile "org.junit.jupiter:junit-jupiter-params:$jupiterVersion" testCompile "org.junit.jupiter:junit-jupiter-params:$jupiterVersion"
testRuntime("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion") testRuntime("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion")
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
} }
test { test {

View File

@ -19,9 +19,24 @@ package bisq.price.spot;
import bisq.price.PriceProvider; import bisq.price.PriceProvider;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.TradeCurrency;
import org.knowm.xchange.Exchange;
import org.knowm.xchange.ExchangeFactory;
import org.knowm.xchange.currency.Currency;
import org.knowm.xchange.currency.CurrencyPair;
import org.knowm.xchange.dto.marketdata.Ticker;
import org.knowm.xchange.exceptions.CurrencyPairNotValidException;
import org.knowm.xchange.service.marketdata.MarketDataService;
import java.time.Duration; import java.time.Duration;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
/** /**
* Abstract base class for providers of bitcoin {@link ExchangeRate} data. Implementations * Abstract base class for providers of bitcoin {@link ExchangeRate} data. Implementations
@ -60,4 +75,82 @@ public abstract class ExchangeRateProvider extends PriceProvider<Set<ExchangeRat
.filter(e -> "USD".equals(e.getCurrency()) || "LTC".equals(e.getCurrency())) .filter(e -> "USD".equals(e.getCurrency()) || "LTC".equals(e.getCurrency()))
.forEach(e -> log.info("BTC/{}: {}", e.getCurrency(), e.getPrice())); .forEach(e -> log.info("BTC/{}: {}", e.getCurrency(), e.getPrice()));
} }
/**
* @param exchangeClass Class of the {@link Exchange} for which the rates should be polled
* @return Exchange rates for Bisq-supported fiat currencies and altcoins in the specified {@link Exchange}
*
* @see CurrencyUtil#getAllSortedFiatCurrencies()
* @see CurrencyUtil#getAllSortedCryptoCurrencies()
*/
protected Set<ExchangeRate> doGet(Class<? extends Exchange> exchangeClass) {
Set<ExchangeRate> result = new HashSet<ExchangeRate>();
// Initialize XChange objects
Exchange exchange = ExchangeFactory.INSTANCE.createExchange(exchangeClass.getName());
MarketDataService marketDataService = exchange.getMarketDataService();
// Retrieve all currency pairs supported by the exchange
List<CurrencyPair> currencyPairs = exchange.getExchangeSymbols();
Set<String> supportedCryptoCurrencies = CurrencyUtil.getAllSortedCryptoCurrencies().stream()
.map(TradeCurrency::getCode)
.collect(Collectors.toSet());
Set<String> supportedFiatCurrencies = CurrencyUtil.getAllSortedFiatCurrencies().stream()
.map(TradeCurrency::getCode)
.collect(Collectors.toSet());
// Filter the supported fiat currencies (currency pair format is BTC-FIAT)
currencyPairs.stream()
.filter(cp -> cp.base.equals(Currency.BTC))
.filter(cp -> supportedFiatCurrencies.contains(cp.counter.getCurrencyCode()))
.forEach(cp -> {
try {
Ticker t = marketDataService.getTicker(new CurrencyPair(cp.base, cp.counter));
result.add(new ExchangeRate(
cp.counter.getCurrencyCode(),
t.getLast(),
// Some exchanges do not provide timestamps
t.getTimestamp() == null ? new Date() : t.getTimestamp(),
this.getName()
));
} catch (CurrencyPairNotValidException cpnve) {
// Some exchanges support certain currency pairs for other services but not for spot markets
// In that case, trying to retrieve the market ticker for that pair may fail with this specific type of exception
log.info("Currency pair " + cp + " not supported in Spot Markets: " + cpnve.getMessage());
} catch (Exception e) {
// Catch any other type of generic exception (IO, network level, rate limit reached, etc)
log.info("Exception encountered while retrieving rate for currency pair " + cp + ": " + e.getMessage());
}
});
// Filter the supported altcoins (currency pair format is ALT-BTC)
currencyPairs.stream()
.filter(cp -> cp.counter.equals(Currency.BTC))
.filter(cp -> supportedCryptoCurrencies.contains(cp.base.getCurrencyCode()))
.forEach(cp -> {
try {
Ticker t = marketDataService.getTicker(new CurrencyPair(cp.base, cp.counter));
result.add(new ExchangeRate(
cp.base.getCurrencyCode(),
t.getLast(),
// Some exchanges do not provide timestamps
t.getTimestamp() == null ? new Date() : t.getTimestamp(),
this.getName()
));
} catch (CurrencyPairNotValidException cpnve) {
// Some exchanges support certain currency pairs for other services but not for spot markets
// In that case, trying to retrieve the market ticker for that pair may fail with this specific type of exception
log.info("Currency pair " + cp + " not supported in Spot Markets: " + cpnve.getMessage());
} catch (Exception e) {
// Catch any other type of generic exception (IO, network level, rate limit reached, etc)
log.info("Exception encountered while retrieving rate for currency pair " + cp + ": " + e.getMessage());
}
});
return result;
}
} }

View File

@ -0,0 +1,46 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq 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 GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.price.spot.providers;
import bisq.price.spot.ExchangeRate;
import bisq.price.spot.ExchangeRateProvider;
import org.knowm.xchange.binance.BinanceExchange;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Set;
@Component
@Order(5)
public class Binance extends ExchangeRateProvider {
public Binance() {
super("BINANCE", "binance", Duration.ofMinutes(1));
}
@Override
public Set<ExchangeRate> doGet() {
// Supported fiat: EUR, NGN, RUB, TRY, ZAR
// Supported alts: BEAM, DASH, DCR, DOGE, ETC, ETH, LTC, NAV, PIVX, XMR, XZC, ZEC, ZEN
return doGet(BinanceExchange.class);
}
}

View File

@ -0,0 +1,46 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq 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 GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.price.spot.providers;
import bisq.price.spot.ExchangeRate;
import bisq.price.spot.ExchangeRateProvider;
import org.knowm.xchange.bitfinex.BitfinexExchange;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Set;
@Component
@Order(6)
public class Bitfinex extends ExchangeRateProvider {
public Bitfinex() {
super("BITFINEX", "bitfinex", Duration.ofMinutes(1));
}
@Override
public Set<ExchangeRate> doGet() {
// Supported fiat: EUR, GBP, JPY, USD
// Supported alts: DAI, ETC, ETH, LTC, XMR, ZEC
return doGet(BitfinexExchange.class);
}
}

View File

@ -0,0 +1,46 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq 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 GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.price.spot.providers;
import bisq.price.spot.ExchangeRate;
import bisq.price.spot.ExchangeRateProvider;
import org.knowm.xchange.kraken.KrakenExchange;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Set;
@Component
@Order(7)
public class Kraken extends ExchangeRateProvider {
public Kraken() {
super("KRAKEN", "kraken", Duration.ofMinutes(1));
}
@Override
public Set<ExchangeRate> doGet() {
// Supported fiat: AUD, CAD, CHF, EUR, GBP, JPY, USD
// Supported alts: DASH, DOGE, ETC, ETH, LTC, XMR, ZEC
return doGet(KrakenExchange.class);
}
}

View File

@ -19,33 +19,19 @@ package bisq.price.spot.providers;
import bisq.price.spot.ExchangeRate; import bisq.price.spot.ExchangeRate;
import bisq.price.spot.ExchangeRateProvider; import bisq.price.spot.ExchangeRateProvider;
import bisq.price.util.Altcoins;
import org.knowm.xchange.currency.Currency; import org.knowm.xchange.poloniex.PoloniexExchange;
import org.knowm.xchange.currency.CurrencyPair;
import org.knowm.xchange.poloniex.dto.marketdata.PoloniexMarketData;
import org.knowm.xchange.poloniex.dto.marketdata.PoloniexTicker;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.RequestEntity;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.time.Duration; import java.time.Duration;
import java.util.Date;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Component @Component
@Order(4) @Order(4)
class Poloniex extends ExchangeRateProvider { public class Poloniex extends ExchangeRateProvider {
private final RestTemplate restTemplate = new RestTemplate();
public Poloniex() { public Poloniex() {
super("POLO", "poloniex", Duration.ofMinutes(1)); super("POLO", "poloniex", Duration.ofMinutes(1));
@ -53,42 +39,8 @@ class Poloniex extends ExchangeRateProvider {
@Override @Override
public Set<ExchangeRate> doGet() { public Set<ExchangeRate> doGet() {
Date timestamp = new Date(); // Poloniex tickers don't include their own timestamp // Supported fiat: -
// Supported alts: DASH, DCR, DOGE, ETC, ETH, GRIN, LTC, XMR, ZEC
return getTickers() return doGet(PoloniexExchange.class);
.filter(t -> t.getCurrencyPair().base.equals(Currency.BTC))
.filter(t -> Altcoins.ALL_SUPPORTED.contains(t.getCurrencyPair().counter.getCurrencyCode()))
.map(t ->
new ExchangeRate(
t.getCurrencyPair().counter.getCurrencyCode(),
t.getPoloniexMarketData().getLast(),
timestamp,
this.getName()
)
)
.collect(Collectors.toSet());
}
private Stream<PoloniexTicker> getTickers() {
return getTickersKeyedByCurrencyPair().entrySet().stream()
.map(e -> {
String pair = e.getKey();
PoloniexMarketData data = e.getValue();
String[] symbols = pair.split("_"); // e.g. BTC_USD => [BTC, USD]
return new PoloniexTicker(data, new CurrencyPair(symbols[0], symbols[1]));
});
}
private Map<String, PoloniexMarketData> getTickersKeyedByCurrencyPair() {
return restTemplate.exchange(
RequestEntity
.get(UriComponentsBuilder
.fromUriString("https://poloniex.com/public?command=returnTicker").build()
.toUri())
.build(),
new ParameterizedTypeReference<Map<String, PoloniexMarketData>>() {
}
).getBody();
} }
} }

View File

@ -0,0 +1,74 @@
package bisq.price;
import bisq.price.spot.ExchangeRate;
import bisq.price.spot.ExchangeRateProvider;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.TradeCurrency;
import com.google.common.collect.Sets;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import static org.junit.Assert.assertTrue;
@Slf4j
public class ExchangeTestBase {
protected void doGet_successfulCall(ExchangeRateProvider exchangeProvider) {
// Use the XChange library to call the provider API, in order to retrieve the
// exchange rates. If the API call fails, or the response body cannot be parsed,
// the test will fail with an exception
Set<ExchangeRate> retrievedExchangeRates = exchangeProvider.doGet();
// Log the valid exchange rates which were retrieved
// Useful when running the tests, to easily identify which exchanges provide useful pairs
retrievedExchangeRates.forEach(e -> log.info("Found exchange rate " + e.toString()));
// Sanity checks
assertTrue(retrievedExchangeRates.size() > 0);
checkProviderCurrencyPairs(retrievedExchangeRates);
}
/**
* Check that every retrieved currency pair is between BTC and either
* A) a fiat currency on the list of Bisq-supported fiat currencies, or
* B) an altcoin on the list of Bisq-supported altcoins
*
* @param retrievedExchangeRates Exchange rates retrieved from the provider
*/
private void checkProviderCurrencyPairs(Set<ExchangeRate> retrievedExchangeRates) {
Set<String> retrievedRatesCurrencies = retrievedExchangeRates.stream()
.map(ExchangeRate::getCurrency)
.collect(Collectors.toSet());
Set<String> supportedCryptoCurrencies = CurrencyUtil.getAllSortedCryptoCurrencies().stream()
.map(TradeCurrency::getCode)
.collect(Collectors.toSet());
Set<String> supportedFiatCurrencies = CurrencyUtil.getAllSortedFiatCurrencies().stream()
.map(TradeCurrency::getCode)
.collect(Collectors.toSet());
Set<String> supportedFiatCurrenciesRetrieved = supportedFiatCurrencies.stream()
.filter(f -> retrievedRatesCurrencies.contains(f))
.collect(Collectors.toCollection(TreeSet::new));
log.info("Retrieved rates for supported fiat currencies: " + supportedFiatCurrenciesRetrieved);
Set<String> supportedCryptoCurrenciesRetrieved = supportedCryptoCurrencies.stream()
.filter(c -> retrievedRatesCurrencies.contains(c))
.collect(Collectors.toCollection(TreeSet::new));
log.info("Retrieved rates for supported altcoins: " + supportedCryptoCurrenciesRetrieved);
Set<String> supportedCurrencies = Sets.union(supportedCryptoCurrencies, supportedFiatCurrencies);
Set unsupportedCurrencies = Sets.difference(retrievedRatesCurrencies, supportedCurrencies);
assertTrue("Retrieved exchange rates contain unsupported currencies: " + unsupportedCurrencies,
unsupportedCurrencies.isEmpty());
}
}

View File

@ -0,0 +1,34 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq 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 GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.price.spot.providers;
import bisq.price.ExchangeTestBase;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
@Slf4j
public class BinanceTest extends ExchangeTestBase {
@Test
public void doGet_successfulCall() {
doGet_successfulCall(new Binance());
}
}

View File

@ -0,0 +1,34 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq 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 GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.price.spot.providers;
import bisq.price.ExchangeTestBase;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
@Slf4j
public class BitfinexTest extends ExchangeTestBase {
@Test
public void doGet_successfulCall() {
doGet_successfulCall(new Bitfinex());
}
}

View File

@ -0,0 +1,34 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq 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 GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.price.spot.providers;
import bisq.price.ExchangeTestBase;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
@Slf4j
public class KrakenTest extends ExchangeTestBase {
@Test
public void doGet_successfulCall() {
doGet_successfulCall(new Kraken());
}
}

View File

@ -0,0 +1,34 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq 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 GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.price.spot.providers;
import bisq.price.ExchangeTestBase;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
@Slf4j
public class PoloniexTest extends ExchangeTestBase {
@Test
public void doGet_successfulCall() {
doGet_successfulCall(new Poloniex());
}
}