Skip to content

Commit

Permalink
Add redundancy to ARS price node
Browse files Browse the repository at this point in the history
 - Implement ExchangeRateTransformer (@alvasw)
 - Implement BlueLyticsApi (@alvasw)
 - Implement ArsBlueMarketGapProvider (@alvasw)
 - Implement ArsBlueRateTransformer (@alvasw)
 - merge @alvasw improvements
 - fixed bug lossing prices after merge
 - fix tests, all green
 - delete unused code
  • Loading branch information
alvasw authored and rodvar committed Sep 11, 2023
1 parent c70ea6b commit 04f37ba
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 211 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";
}
}
90 changes: 30 additions & 60 deletions src/main/java/bisq/price/spot/ExchangeRateService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@
package bisq.price.spot;

import bisq.common.util.Tuple2;

import bisq.core.util.InlierUtil;

import bisq.price.util.bluelytics.BlueLyticsService;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
Expand All @@ -40,17 +36,22 @@ 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 @@ -76,56 +77,6 @@ public Map<String, Object> getAllMarketPrices() {
return result;
}

/**
* Please do not call provider.get(), use this method instead to consider currencies with blue markets
* @param provider to get the rates from
* @return the exchange rates for the different currencies the provider supports considering bluemarket rates if any
*/
public Set<ExchangeRate> providerCurrentExchangeRates(ExchangeRateProvider provider) {
Map<String, Double> blueMarketGapForCurrency = this.getBlueMarketGapForCurrencies();
Set<ExchangeRate> originalExchangeRates = provider.get();
if (originalExchangeRates == null)
return null;

Set<ExchangeRate> exchangeRates = new HashSet<>();
boolean noOverlapBetweenCurrencies = Sets.intersection(originalExchangeRates.stream().map(ExchangeRate::getCurrency)
.collect(Collectors.toSet()), blueMarketGapForCurrency.keySet())
.isEmpty();

if (provider.alreadyConsidersBlueMarkets() || noOverlapBetweenCurrencies) {
exchangeRates.addAll(originalExchangeRates);
} else {
this.addRatesUpdatingBlueGaps(blueMarketGapForCurrency, provider, exchangeRates, originalExchangeRates);
}
return exchangeRates;
}

private void addRatesUpdatingBlueGaps(Map<String, Double> blueMarketGapForCurrency, ExchangeRateProvider provider, Set<ExchangeRate> exchangeRates, Set<ExchangeRate> originalExchangeRates) {
originalExchangeRates.forEach(er -> {
// default to original rate
ExchangeRate exchangeRateToUse = er;
if (blueMarketGapForCurrency.containsKey(er.getCurrency())) {
ExchangeRate updatedExchangeRate = provider.maybeUpdateBlueMarketPriceGapForRate(er, blueMarketGapForCurrency.get(er.getCurrency()));
if (updatedExchangeRate != null) {
exchangeRateToUse = updatedExchangeRate;
// this.log.info(String.format("Replaced original %s rate of $%s to $%s", er.getCurrency(), BigDecimal.valueOf(er.getPrice()).toEngineeringString(), BigDecimal.valueOf(updatedExchangeRate.getPrice()).toEngineeringString()));
}
}
exchangeRates.add(exchangeRateToUse);
});
}

private Map<String, Double> getBlueMarketGapForCurrencies() {
Map<String, Double> blueMarketGapForCurrencies = new HashMap<>();
// ARS
Double arsBlueMultiplier = BlueLyticsService.getInstance().blueGapMultiplier();
if (!arsBlueMultiplier.isNaN()) {
// this.log.info("Updated ARS/USD multiplier is " + arsBlueMultiplier);
blueMarketGapForCurrencies.put("ARS", arsBlueMultiplier);
}
return blueMarketGapForCurrencies;
}

/**
* For each currency, create an aggregate {@link ExchangeRate} based on the currency's
* rates from all providers. If multiple providers have rates for the currency, then
Expand Down Expand Up @@ -212,20 +163,39 @@ private double getOutlierStdDeviation() {
private Map<String, List<ExchangeRate>> getCurrencyCodeToExchangeRates() {
Map<String, List<ExchangeRate>> currencyCodeToExchangeRates = new HashMap<>();
for (ExchangeRateProvider p : providers) {
Set<ExchangeRate> exchangeRates = providerCurrentExchangeRates(p);
Set<ExchangeRate> exchangeRates = p.get();
if (exchangeRates == null)
continue;
for (ExchangeRate exchangeRate : exchangeRates) {
String currencyCode = exchangeRate.getCurrency();

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

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

public interface ExchangeRateTransformer {
Optional<ExchangeRate> apply(ExchangeRateProvider provider, ExchangeRate exchangeRate);

String supportedCurrency();
}
21 changes: 21 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,21 @@
/*
* 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;

public interface BlueRateProvider {
}
2 changes: 1 addition & 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 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;
}
}
41 changes: 41 additions & 0 deletions src/main/java/bisq/price/util/bluelytics/BlueLyticsApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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 org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.OptionalDouble;

public class BlueLyticsApi {
private static final String API_URL = "https://api.bluelytics.com.ar/v2/latest";
private final WebClient webClient = WebClient.create();

public OptionalDouble getSellGapMultiplier() {
BlueLyticsDto blueLyticsDto = webClient.get()
.uri(API_URL)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(BlueLyticsDto.class)
.block(Duration.of(30, ChronoUnit.SECONDS));

return blueLyticsDto == null ? OptionalDouble.empty() : blueLyticsDto.gapSellMultiplier();
}
}
23 changes: 10 additions & 13 deletions src/main/java/bisq/price/util/bluelytics/BlueLyticsDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,25 @@
import lombok.Setter;

import java.util.Date;
import java.util.OptionalDouble;

@Getter
@Setter
public class BlueLyticsDto {
@Getter
@Setter
public static class USDRate {
Double value_avg;
Double value_sell;
Double value_buy;
double value_avg;
double value_sell;
double value_buy;
}

BlueLyticsDto.USDRate oficial;
BlueLyticsDto.USDRate blue;
Date last_update;
private BlueLyticsDto.USDRate oficial;
private BlueLyticsDto.USDRate blue;
private Date last_update;

/**
*
* @return the sell multiplier to go from oficial to blue market for ARS/USD
* if its not available, returns NaN
*/
public Double gapSellMultiplier() {
return this.blue.value_sell / this.oficial.value_sell;
public OptionalDouble gapSellMultiplier() {
double sellMultiplier = blue.value_sell / oficial.value_sell;
return Double.isNaN(sellMultiplier) ? OptionalDouble.empty() : OptionalDouble.of(sellMultiplier);
}
}
Loading

0 comments on commit 04f37ba

Please sign in to comment.