Skip to content

Commit

Permalink
Merge pull request #17 from rodvar/feature/ars_pricenode_redundancy
Browse files Browse the repository at this point in the history
Feature/ars pricenode redundancy
  • Loading branch information
gabernard authored Sep 12, 2023
2 parents 72082ea + d67f1e2 commit a1a10a7
Show file tree
Hide file tree
Showing 14 changed files with 379 additions and 57 deletions.
61 changes: 61 additions & 0 deletions src/main/java/bisq/price/spot/ArsBlueRateTransformer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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;

import bisq.price.spot.providers.BlueRateProvider;
import bisq.price.util.bluelytics.ArsBlueMarketGapProvider;
import org.springframework.stereotype.Component;

import java.util.Optional;
import java.util.OptionalDouble;

@Component
public class ArsBlueRateTransformer implements ExchangeRateTransformer {
private final ArsBlueMarketGapProvider blueMarketGapProvider;

public ArsBlueRateTransformer(ArsBlueMarketGapProvider blueMarketGapProvider) {
this.blueMarketGapProvider = blueMarketGapProvider;
}

@Override
public Optional<ExchangeRate> apply(ExchangeRateProvider provider, ExchangeRate originalExchangeRate) {
if (provider instanceof BlueRateProvider) {
return Optional.of(originalExchangeRate);
}

OptionalDouble sellGapMultiplier = blueMarketGapProvider.get();
if (sellGapMultiplier.isEmpty()) {
return Optional.empty();
}

double blueRate = originalExchangeRate.getPrice() * sellGapMultiplier.getAsDouble();

ExchangeRate newExchangeRate = new ExchangeRate(
originalExchangeRate.getCurrency(),
blueRate,
originalExchangeRate.getTimestamp(),
originalExchangeRate.getProvider()
);
return Optional.of(newExchangeRate);
}

@Override
public String supportedCurrency() {
return "ARS";
}
}
2 changes: 2 additions & 0 deletions src/main/java/bisq/price/spot/ExchangeRateProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
import org.knowm.xchange.service.marketdata.params.Params;
import org.springframework.core.env.Environment;

import javax.annotation.Nullable;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.*;
import java.util.function.Predicate;
Expand Down
47 changes: 37 additions & 10 deletions src/main/java/bisq/price/spot/ExchangeRateService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@
package bisq.price.spot;

import bisq.common.util.Tuple2;

import bisq.core.util.InlierUtil;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
Expand All @@ -36,20 +34,24 @@
@Service
class ExchangeRateService {
protected final Logger log = LoggerFactory.getLogger(this.getClass());

private final Environment env;
private final List<ExchangeRateProvider> providers;
private final List<ExchangeRateTransformer> transformers;

/**
* Construct an {@link ExchangeRateService} with a list of all
* {@link ExchangeRateProvider} implementations discovered via classpath scanning.
*
* @param providers all {@link ExchangeRateProvider} implementations in ascending
* order of precedence
* @param providers all {@link ExchangeRateProvider} implementations in ascending
* order of precedence
* @param transformers all {@link ExchangeRateTransformer} implementations
*/
public ExchangeRateService(Environment env, List<ExchangeRateProvider> providers) {
public ExchangeRateService(Environment env,
List<ExchangeRateProvider> providers,
List<ExchangeRateTransformer> transformers) {
this.env = env;
this.providers = providers;
this.transformers = transformers;
}

public Map<String, Object> getAllMarketPrices() {
Expand All @@ -64,6 +66,7 @@ public Map<String, Object> getAllMarketPrices() {
metadata.putAll(getMetadata(p));
});


LinkedHashMap<String, Object> result = new LinkedHashMap<>(metadata);
// Use a sorted list by currency code to make comparison of json data between
// different price nodes easier
Expand Down Expand Up @@ -160,19 +163,43 @@ private double getOutlierStdDeviation() {
private Map<String, List<ExchangeRate>> getCurrencyCodeToExchangeRates() {
Map<String, List<ExchangeRate>> currencyCodeToExchangeRates = new HashMap<>();
for (ExchangeRateProvider p : providers) {
if (p.get() == null)
Set<ExchangeRate> exchangeRates = p.get();
if (exchangeRates == null)
continue;
for (ExchangeRate exchangeRate : p.get()) {
for (ExchangeRate exchangeRate : exchangeRates) {
String currencyCode = exchangeRate.getCurrency();

List<ExchangeRate> transformedExchangeRates = transformers.stream()
.filter(transformer -> transformer.supportedCurrency()
.equalsIgnoreCase(currencyCode)
)
.map(t -> t.apply(p, exchangeRate))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());

if (!transformedExchangeRates.isEmpty()) {
log.info(String.format("%s transformed from %s to %s", currencyCode, exchangeRate.getPrice(), transformedExchangeRates.get(0).getPrice()));
}

if (currencyCodeToExchangeRates.containsKey(currencyCode)) {
List<ExchangeRate> l = new ArrayList<>(currencyCodeToExchangeRates.get(currencyCode));
l.add(exchangeRate);
if (transformedExchangeRates.isEmpty()) {
l.add(exchangeRate);
} else {
l.addAll(transformedExchangeRates);
}
currencyCodeToExchangeRates.put(currencyCode, l);
} else {
currencyCodeToExchangeRates.put(currencyCode, List.of(exchangeRate));
if (transformedExchangeRates.isEmpty()) {
currencyCodeToExchangeRates.put(currencyCode, List.of(exchangeRate));
} else {
currencyCodeToExchangeRates.put(currencyCode, transformedExchangeRates);
}
}
}
}

return currencyCodeToExchangeRates;
}

Expand Down
31 changes: 31 additions & 0 deletions src/main/java/bisq/price/spot/ExchangeRateTransformer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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;

import java.util.Optional;

/**
* An ExchangeRateTransformer allows to apply a transformation on a particular ExchangeRate
* for particular supported currencies. This is useful for countries with currency controls
* that have a "blue" market in place for real/free trades.
*/
public interface ExchangeRateTransformer {
Optional<ExchangeRate> apply(ExchangeRateProvider provider, ExchangeRate exchangeRate);

String supportedCurrency();
}
24 changes: 24 additions & 0 deletions src/main/java/bisq/price/spot/providers/BlueRateProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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;

/**
* Tag for providers that provide a "blue" market exchange rate (unofficial real/free traded)
*/
public interface BlueRateProvider {
}
28 changes: 13 additions & 15 deletions src/main/java/bisq/price/spot/providers/CoinGecko.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@

package bisq.price.spot.providers;

import bisq.asset.Coin;
import bisq.price.spot.ExchangeRate;
import bisq.price.spot.ExchangeRateProvider;
import bisq.price.util.coingecko.CoinGeckoMarketData;

import bisq.price.util.coingecko.CoinGeckoTicker;
import lombok.Getter;
import lombok.Setter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.env.Environment;
import org.springframework.http.RequestEntity;
Expand All @@ -33,16 +37,13 @@
import java.math.BigDecimal;
import java.math.RoundingMode;

import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

@Component
class CoinGecko extends ExchangeRateProvider {

private static final String GET_EXCHANGE_RATES_URL = "https://api.coingecko.com/api/v3/exchange_rates";
private final RestTemplate restTemplate = new RestTemplate();

public CoinGecko(Environment env) {
Expand All @@ -60,18 +61,16 @@ public Set<ExchangeRate> doGet() {
Predicate<Map.Entry> isDesiredFiatPair = t -> getSupportedFiatCurrencies().contains(t.getKey());
Predicate<Map.Entry> isDesiredCryptoPair = t -> getSupportedCryptoCurrencies().contains(t.getKey());

getMarketData().getRates().entrySet().stream()
Map<String, CoinGeckoTicker> rates = getMarketData().getRates();
rates.entrySet().stream()
.filter(isDesiredFiatPair.or(isDesiredCryptoPair))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
.forEach((key, ticker) -> {

boolean useInverseRate = false;
if (getSupportedCryptoCurrencies().contains(key)) {
// Use inverse rate for alts, because the API returns the
// conversion rate in the opposite direction than what we need
// API returns the BTC/Alt rate, we need the Alt/BTC rate
useInverseRate = true;
}
boolean useInverseRate = getSupportedCryptoCurrencies().contains(key);
// Use inverse rate for alts, because the API returns the
// conversion rate in the opposite direction than what we need
// API returns the BTC/Alt rate, we need the Alt/BTC rate

BigDecimal rate = ticker.getValue();
// Find the inverse rate, while using enough decimals to reflect very
Expand All @@ -87,15 +86,14 @@ public Set<ExchangeRate> doGet() {
this.getName()
));
});

return result;
}

private CoinGeckoMarketData getMarketData() {
return restTemplate.exchange(
RequestEntity
.get(UriComponentsBuilder
.fromUriString("https://api.coingecko.com/api/v3/exchange_rates").build()
.fromUriString(CoinGecko.GET_EXCHANGE_RATES_URL).build()
.toUri())
.build(),
new ParameterizedTypeReference<CoinGeckoMarketData>() {
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/bisq/price/spot/providers/CryptoYa.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
* This ExchangeRateProvider provides a real market rate (black or "blue") for ARS/BTC
*/
@Component
class CryptoYa extends ExchangeRateProvider {
class CryptoYa extends ExchangeRateProvider implements BlueRateProvider {

private static final String CRYPTO_YA_BTC_ARS_API_URL = "https://criptoya.com/api/btc/ars/0.1";

Expand All @@ -49,6 +49,11 @@ public CryptoYa(Environment env) {
super(env, "CRYPTOYA", "cryptoya", Duration.ofMinutes(1));
}

/**
*
* @return average price buy/sell price averaging different providers suported by cryptoya api
* which uses the free market (or blue, or unofficial) ARS price for BTC
*/
@Override
public Set<ExchangeRate> doGet() {
CryptoYaMarketData cryptoYaMarketData = fetchArsBlueMarketData();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.util.bluelytics;

import bisq.price.PriceProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.util.Optional;
import java.util.OptionalDouble;

@Slf4j
@Component
public class ArsBlueMarketGapProvider extends PriceProvider<OptionalDouble> {
public interface Listener {
void onUpdate(OptionalDouble sellGapMultiplier);
}

private static final Duration REFRESH_INTERVAL = Duration.ofHours(1);

private final BlueLyticsApi blueLyticsApi = new BlueLyticsApi();
private final Optional<Listener> onUpdateListener;

public ArsBlueMarketGapProvider() {
super(REFRESH_INTERVAL);
this.onUpdateListener = Optional.empty();
}

public ArsBlueMarketGapProvider(Listener onUpdateListener) {
super(REFRESH_INTERVAL);
this.onUpdateListener = Optional.of(onUpdateListener);
}

@Override
protected OptionalDouble doGet() {
OptionalDouble sellGapMultiplier = blueLyticsApi.getSellGapMultiplier();
onUpdateListener.ifPresent(listener -> listener.onUpdate(sellGapMultiplier));
return sellGapMultiplier;
}
}
Loading

0 comments on commit a1a10a7

Please sign in to comment.