diff --git a/src/main/java/bisq/core/provider/price/PriceFeedService.java b/src/main/java/bisq/core/provider/price/PriceFeedService.java index bbbf7c50..a5197352 100644 --- a/src/main/java/bisq/core/provider/price/PriceFeedService.java +++ b/src/main/java/bisq/core/provider/price/PriceFeedService.java @@ -36,6 +36,7 @@ import com.google.inject.Inject; +import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.SettableFuture; @@ -49,13 +50,23 @@ import java.time.Instant; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; +import java.util.stream.Collector; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -242,15 +253,6 @@ public MarketPrice getMarketPrice(String currencyCode) { return cache.getOrDefault(currencyCode, null); } - private void setBisqMarketPrice(String currencyCode, Price price) { - if (!cache.containsKey(currencyCode) || !cache.get(currencyCode).isExternallyProvidedPrice()) { - cache.put(currencyCode, new MarketPrice(currencyCode, - MathUtils.scaleDownByPowerOf10(price.getValue(), CurrencyUtil.isCryptoCurrency(currencyCode) ? 8 : 4), - 0, - false)); - updateCounter.set(updateCounter.get() + 1); - } - } /////////////////////////////////////////////////////////////////////////////////////////// // Setter @@ -302,28 +304,34 @@ public Date getLastRequestTimeStampCoinmarketcap() { return new Date(); } - public void applyLatestBisqMarketPrice(Set tradeStatisticsSet) { - // takes about 10 ms for 5000 items - Map> mapByCurrencyCode = new HashMap<>(); - tradeStatisticsSet.forEach(e -> { - final List list; - final String currencyCode = e.getCurrencyCode(); - if (mapByCurrencyCode.containsKey(currencyCode)) { - list = mapByCurrencyCode.get(currencyCode); - } else { - list = new ArrayList<>(); - mapByCurrencyCode.put(currencyCode, list); - } - list.add(e); - }); + public final void applyLatestBisqMarketPrice(Collection stats) { + Set snapshot = ImmutableSet.copyOf(stats); - mapByCurrencyCode.values().stream() - .filter(list -> !list.isEmpty()) - .forEach(list -> { - list.sort((o1, o2) -> o1.getTradeDate().compareTo(o2.getTradeDate())); - TradeStatistics2 tradeStatistics = list.get(list.size() - 1); - setBisqMarketPrice(tradeStatistics.getCurrencyCode(), tradeStatistics.getTradePrice()); - }); + Map> statsByCode = snapshot.parallelStream() + .collect(Collectors.groupingByConcurrent( + TradeStatistics2::getCurrencyCode, toSortedSetOfTradeStatistics2())); + + Collection> groupedCodes = statsByCode.values(); + + Iterable updates = groupedCodes.parallelStream() + .map(SortedSet::last) + .collect(Collectors.toCollection(ConcurrentLinkedQueue::new)); + + updates.forEach(last -> setBisqMarketPrice(last.getCurrencyCode(), last.getTradePrice())); + } + + private static Collector> toSortedSetOfTradeStatistics2() { + Comparator byDate = Comparator.comparing(TradeStatistics2::getTradeDate); + return Collectors.toCollection(() -> new ConcurrentSkipListSet<>(byDate)); + } + + private void setBisqMarketPrice(String code, Price price) { + if (!cache.containsKey(code) || !cache.get(code).isExternallyProvidedPrice()) { + int exponent = CurrencyUtil.isCryptoCurrency(code) ? 8 : 4; + double value = MathUtils.scaleDownByPowerOf10(price.getValue(), exponent); + cache.put(code, new MarketPrice(code, value, 0L, false)); + updateCounter.set(updateCounter.get() + 1); + } } diff --git a/src/test/java/bisq/core/provider/price/PriceFeedServiceTest.java b/src/test/java/bisq/core/provider/price/PriceFeedServiceTest.java new file mode 100644 index 00000000..1734ec30 --- /dev/null +++ b/src/test/java/bisq/core/provider/price/PriceFeedServiceTest.java @@ -0,0 +1,67 @@ +package bisq.core.provider.price; + +import bisq.core.monetary.Price; +import bisq.core.provider.ProvidersRepository; +import bisq.core.trade.statistics.TradeStatistics2; +import bisq.core.user.Preferences; + +import bisq.network.http.HttpClient; + +import com.google.common.collect.Lists; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({Preferences.class, TradeStatistics2.class, Price.class}) +public class PriceFeedServiceTest { + @Test + public void testApplyLatestBisqMarketPrice() { + long initialTime = new Date().getTime(); + + List obsoletes = Lists.newArrayList( + mockTradeStatistics("a", new Date(initialTime), 5), + mockTradeStatistics("b", new Date(initialTime), 6), + mockTradeStatistics("b", new Date(initialTime), 7), + mockTradeStatistics("a", new Date(initialTime), 8)); + + List stats = new ArrayList<>(obsoletes); + stats.add(mockTradeStatistics("a", new Date(initialTime + 100), 8)); + stats.add(mockTradeStatistics("b", new Date(initialTime + 200), 9)); + + Collections.shuffle(stats); + + PriceFeedService service = new PriceFeedService(mock(HttpClient.class), + mock(ProvidersRepository.class), mock(Preferences.class)); + service.applyLatestBisqMarketPrice(stats); + + assertThat(service.getMarketPrice("a"), notNullValue()); + assertThat(service.getMarketPrice("b"), notNullValue()); + + // verify that trade price is queried only once - during mock setup + obsoletes.forEach(st -> verify(st).getTradePrice()); + } + + private static TradeStatistics2 mockTradeStatistics(String code, Date tradeDate, long value) { + TradeStatistics2 result = mock(TradeStatistics2.class, RETURNS_DEEP_STUBS); + when(result.getTradeDate()).thenReturn(tradeDate); + when(result.getCurrencyCode()).thenReturn(code); + when(result.getTradePrice().getValue()).thenReturn(value); + return result; + } +}