diff --git a/common/src/main/java/bisq/common/util/ComparableExt.java b/common/src/main/java/bisq/common/util/ComparableExt.java
new file mode 100644
index 0000000000..3e360deeaf
--- /dev/null
+++ b/common/src/main/java/bisq/common/util/ComparableExt.java
@@ -0,0 +1,65 @@
+/*
+ * 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 .
+ */
+
+package bisq.common.util;
+
+import java.util.NavigableSet;
+import java.util.function.Predicate;
+
+import javax.annotation.Nullable;
+
+import static bisq.common.util.Preconditions.checkComparatorNullOrNatural;
+
+/**
+ * A {@link Comparable} which may be compared with adhoc mark/delimiter objects, in
+ * addition to objects of the same type, to support searches for adjacent elements in
+ * a sorted collection without having to use dummy objects for this purpose. For example,
+ * one may wish to find the smallest object after a given date, in a collection sorted by
+ * date. This is to work round the limitation that {@link java.util.SortedSet} and
+ * {@link java.util.SortedMap} only support comparison with other keys when searching for
+ * elements rather than allowing general binary searches with a predicate.
+ *
+ *
Implementations should define {@link Comparable#compareTo(Object)} like follows:
+ *
{@code
+ * public int compareTo(@NotNull ComparableExt o) {
+ * return o instanceof Foo ? this.normalCompareTo((Foo) o) : -o.compareTo(this);
+ * }
+ * }
+ * @param
+ */
+public interface ComparableExt extends Comparable> {
+ @SuppressWarnings("unchecked")
+ @Nullable
+ static > E lower(NavigableSet set, Predicate super E> filter) {
+ checkComparatorNullOrNatural(set.comparator(), "Set must be naturally ordered");
+ return (E) ((NavigableSet>) set).lower(Mark.of(filter));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Nullable
+ static > E higher(NavigableSet set, Predicate super E> filter) {
+ checkComparatorNullOrNatural(set.comparator(), "Set must be naturally ordered");
+ return (E) ((NavigableSet>) set).higher(Mark.of(filter));
+ }
+
+ interface Mark extends ComparableExt {
+ @SuppressWarnings("unchecked")
+ static Mark of(Predicate super T> filter) {
+ return x -> x instanceof Mark ? 0 : filter.test((T) x) ? -1 : 1;
+ }
+ }
+}
diff --git a/common/src/main/java/bisq/common/util/MathUtils.java b/common/src/main/java/bisq/common/util/MathUtils.java
index 77c702b65d..44212a935d 100644
--- a/common/src/main/java/bisq/common/util/MathUtils.java
+++ b/common/src/main/java/bisq/common/util/MathUtils.java
@@ -101,7 +101,7 @@ public class MathUtils {
return BigDecimal.valueOf(value1).multiply(BigDecimal.valueOf(value2)).doubleValue();
}
- public static long getMedian(Long[] list) {
+ public static long getMedian(long[] list) {
if (list.length == 0) {
return 0L;
}
diff --git a/common/src/main/java/bisq/common/util/Preconditions.java b/common/src/main/java/bisq/common/util/Preconditions.java
index e78215d540..63af13c3e3 100644
--- a/common/src/main/java/bisq/common/util/Preconditions.java
+++ b/common/src/main/java/bisq/common/util/Preconditions.java
@@ -1,7 +1,12 @@
package bisq.common.util;
+import com.google.common.collect.Ordering;
+
import java.io.File;
+import java.util.Comparator;
+
+import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
/**
@@ -29,4 +34,10 @@ public class Preconditions {
return dir;
}
+
+ // needed since Guava makes it impossible to create an ImmutableSorted[Set|Map] with a null comparator:
+ public static void checkComparatorNullOrNatural(Comparator> comparator, Object errorMessage) {
+ checkArgument(comparator == null || comparator.equals(Ordering.natural()) ||
+ comparator.equals(Comparator.naturalOrder()), errorMessage);
+ }
}
diff --git a/common/src/main/java/bisq/common/util/RangeUtils.java b/common/src/main/java/bisq/common/util/RangeUtils.java
new file mode 100644
index 0000000000..80e87ae511
--- /dev/null
+++ b/common/src/main/java/bisq/common/util/RangeUtils.java
@@ -0,0 +1,82 @@
+/*
+ * 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 .
+ */
+
+package bisq.common.util;
+
+import com.google.common.collect.BoundType;
+import com.google.common.collect.Range;
+
+import java.util.Collections;
+import java.util.NavigableSet;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public final class RangeUtils {
+ private RangeUtils() {
+ }
+
+ public static > NavigableSet subSet(NavigableSet set,
+ Predicate super E> fromFilter,
+ Predicate super E> toFilter) {
+ E fromElement = ComparableExt.higher(set, fromFilter);
+ E toElement = ComparableExt.lower(set, toFilter);
+ return fromElement != null && toElement != null && fromElement.compareTo(toElement) <= 0 ?
+ set.subSet(fromElement, true, toElement, true) : Collections.emptyNavigableSet();
+ }
+
+ public static > SubCollection, E> subSet(NavigableSet set) {
+ return new SubCollection<>() {
+ @Override
+ public > WithKeyFunction, K> withKey(Function increasingKeyFn) {
+ return (Range range) -> {
+ var fromToFilter = boundFilters(increasingKeyFn, range);
+ return subSet(set, fromToFilter.first, fromToFilter.second);
+ };
+ }
+ };
+ }
+
+ private static > Tuple2, Predicate> boundFilters(Function keyFn,
+ Range keyRange) {
+ Predicate fromFilter, toFilter;
+ if (keyRange.hasLowerBound()) {
+ K fromKey = keyRange.lowerEndpoint();
+ fromFilter = keyRange.lowerBoundType() == BoundType.CLOSED
+ ? (E e) -> fromKey.compareTo(keyFn.apply(e)) <= 0
+ : (E e) -> fromKey.compareTo(keyFn.apply(e)) < 0;
+ } else {
+ fromFilter = e -> true;
+ }
+ if (keyRange.hasUpperBound()) {
+ K toKey = keyRange.upperEndpoint();
+ toFilter = keyRange.upperBoundType() == BoundType.CLOSED
+ ? (E e) -> toKey.compareTo(keyFn.apply(e)) < 0
+ : (E e) -> toKey.compareTo(keyFn.apply(e)) <= 0;
+ } else {
+ toFilter = e -> false;
+ }
+ return new Tuple2<>(fromFilter, toFilter);
+ }
+
+ public interface SubCollection {
+ > WithKeyFunction withKey(Function increasingKeyFn);
+ }
+
+ public interface WithKeyFunction> {
+ C overRange(Range range);
+ }
+}
diff --git a/common/src/test/java/bisq/common/util/RangeUtilsTest.java b/common/src/test/java/bisq/common/util/RangeUtilsTest.java
new file mode 100644
index 0000000000..ce0afa090e
--- /dev/null
+++ b/common/src/test/java/bisq/common/util/RangeUtilsTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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 .
+ */
+
+package bisq.common.util;
+
+import com.google.common.collect.ImmutableSortedSet;
+
+import java.util.Comparator;
+import java.util.NavigableSet;
+import java.util.stream.IntStream;
+
+import org.jetbrains.annotations.NotNull;
+
+import org.junit.jupiter.api.Test;
+
+import static bisq.common.util.RangeUtils.subSet;
+import static com.google.common.collect.Range.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class RangeUtilsTest {
+ @Test
+ public void subSetWithStrictlyIncreasingKey() {
+ var subSetWithValue = subSet(range(0, 10)).withKey(n -> n.value);
+
+ assertEquals(range(0, 10), subSetWithValue.overRange(all()));
+ assertEquals(range(0, 6), subSetWithValue.overRange(atMost(5)));
+ assertEquals(range(5, 10), subSetWithValue.overRange(atLeast(5)));
+ assertEquals(range(0, 5), subSetWithValue.overRange(lessThan(5)));
+ assertEquals(range(6, 10), subSetWithValue.overRange(greaterThan(5)));
+ assertEquals(range(3, 8), subSetWithValue.overRange(closed(3, 7)));
+ assertEquals(range(3, 7), subSetWithValue.overRange(closedOpen(3, 7)));
+ assertEquals(range(4, 8), subSetWithValue.overRange(openClosed(3, 7)));
+ assertEquals(range(4, 7), subSetWithValue.overRange(open(3, 7)));
+ assertEquals(range(5, 6), subSetWithValue.overRange(singleton(5)));
+ assertEquals(range(0, 1), subSetWithValue.overRange(singleton(0)));
+ assertEquals(range(0, 0), subSetWithValue.overRange(singleton(-1)));
+ assertEquals(range(9, 10), subSetWithValue.overRange(singleton(9)));
+ assertEquals(range(0, 0), subSetWithValue.overRange(singleton(10)));
+ assertEquals(range(0, 0), subSetWithValue.overRange(closedOpen(5, 5)));
+ assertEquals(range(0, 10), subSetWithValue.overRange(closed(-1, 10)));
+ }
+
+ @Test
+ public void subSetWithNonStrictlyIncreasingKey() {
+ var subSetWithValueDiv3 = subSet(range(0, 10)).withKey(n -> n.value / 3);
+
+ assertEquals(range(0, 10), subSetWithValueDiv3.overRange(closed(0, 3)));
+ assertEquals(range(0, 9), subSetWithValueDiv3.overRange(closedOpen(0, 3)));
+ assertEquals(range(3, 10), subSetWithValueDiv3.overRange(openClosed(0, 3)));
+ assertEquals(range(3, 9), subSetWithValueDiv3.overRange(open(0, 3)));
+ assertEquals(range(0, 3), subSetWithValueDiv3.overRange(singleton(0)));
+ assertEquals(range(3, 6), subSetWithValueDiv3.overRange(singleton(1)));
+ assertEquals(range(9, 10), subSetWithValueDiv3.overRange(singleton(3)));
+ }
+
+ private static NavigableSet range(int startInclusive, int endExclusive) {
+ return IntStream.range(startInclusive, endExclusive)
+ .mapToObj(TestInteger::new)
+ .collect(ImmutableSortedSet.toImmutableSortedSet(Comparator.naturalOrder()));
+ }
+
+ private static final class TestInteger implements ComparableExt {
+ int value;
+
+ TestInteger(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int compareTo(@NotNull ComparableExt o) {
+ return o instanceof TestInteger ? Integer.compare(value, ((TestInteger) o).value) : -o.compareTo(this);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return this == o || o instanceof TestInteger && value == ((TestInteger) o).value;
+ }
+
+ @Override
+ public int hashCode() {
+ return Integer.hashCode(value);
+ }
+
+ @Override
+ public String toString() {
+ return Integer.toString(value);
+ }
+ }
+}
diff --git a/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java b/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java
index 7bfbe8fdb5..d32843536c 100644
--- a/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java
+++ b/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java
@@ -90,6 +90,7 @@ public class BurningManPresentationService implements DaoStateListener {
private int currentChainHeight;
private Optional burnTarget = Optional.empty();
private final Map burningManCandidatesByName = new HashMap<>();
+ private Long accumulatedDecayedBurnedAmount;
private final Set reimbursements = new HashSet<>();
private Optional averageDistributionPerCycle = Optional.empty();
private Set myCompensationRequestNames = null;
@@ -132,6 +133,7 @@ public class BurningManPresentationService implements DaoStateListener {
burningManCandidatesByName.clear();
reimbursements.clear();
burnTarget = Optional.empty();
+ accumulatedDecayedBurnedAmount = null;
myCompensationRequestNames = null;
averageDistributionPerCycle = Optional.empty();
legacyBurningManDPT = Optional.empty();
@@ -188,8 +190,7 @@ public class BurningManPresentationService implements DaoStateListener {
long lowerBaseTarget = Math.round(burnTarget * maxCompensationShare);
double maxBoostedCompensationShare = burningManCandidate.getMaxBoostedCompensationShare();
long upperBaseTarget = Math.round(boostedBurnTarget * maxBoostedCompensationShare);
- Collection burningManCandidates = getBurningManCandidatesByName().values();
- long totalBurnedAmount = burnTargetService.getAccumulatedDecayedBurnedAmount(burningManCandidates, currentChainHeight);
+ long totalBurnedAmount = getAccumulatedDecayedBurnedAmount();
if (totalBurnedAmount == 0) {
// The first BM would reach their max burn share by 5.46 BSQ already. But we suggest the lowerBaseTarget
@@ -344,7 +345,6 @@ public class BurningManPresentationService implements DaoStateListener {
receiverAddressesByBurningManName.get(name).addAll(burningManCandidate.getAllAddresses());
});
-
Map map = new HashMap<>();
receiverAddressesByBurningManName
.forEach((name, addresses) -> addresses
@@ -371,4 +371,12 @@ public class BurningManPresentationService implements DaoStateListener {
proofOfBurnOpReturnTxOutputByHash.putAll(burningManService.getProofOfBurnOpReturnTxOutputByHash(currentChainHeight));
return proofOfBurnOpReturnTxOutputByHash;
}
+
+ private long getAccumulatedDecayedBurnedAmount() {
+ if (accumulatedDecayedBurnedAmount == null) {
+ Collection burningManCandidates = getBurningManCandidatesByName().values();
+ accumulatedDecayedBurnedAmount = burnTargetService.getAccumulatedDecayedBurnedAmount(burningManCandidates, currentChainHeight);
+ }
+ return accumulatedDecayedBurnedAmount;
+ }
}
diff --git a/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java b/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java
index 6898843e35..3605eb981d 100644
--- a/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java
+++ b/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java
@@ -32,6 +32,7 @@ import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.core.monetary.Price;
+import bisq.core.trade.statistics.TradeStatistics3;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.core.util.AveragePriceUtil;
@@ -49,6 +50,8 @@ import javax.inject.Singleton;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
+import javafx.collections.SetChangeListener;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@@ -58,6 +61,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -87,6 +91,7 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
private final Preferences preferences;
private final Map averageBsqPriceByMonth = new HashMap<>(getHistoricalAverageBsqPriceByMonth());
+ private boolean averagePricesValid;
@Getter
private final Map balanceModelByBurningManName = new HashMap<>();
@Getter
@@ -116,13 +121,13 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
@Override
public void addListeners() {
+ tradeStatisticsManager.getObservableTradeStatisticsSet().addListener(
+ (SetChangeListener) observable -> averagePricesValid = false);
}
@Override
public void start() {
UserThread.execute(() -> isProcessing.set(true));
- // Create the map from now back to the last entry of the historical data (April 2019-Nov. 2022).
- averageBsqPriceByMonth.putAll(getAverageBsqPriceByMonth(new Date(), 2022, 10));
updateBalanceModelByAddress();
CompletableFuture.runAsync(() -> {
@@ -180,8 +185,11 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
}
public Map getAverageBsqPriceByMonth() {
- getAverageBsqPriceByMonth(new Date(), HIST_BSQ_PRICE_LAST_DATE_YEAR, HIST_BSQ_PRICE_LAST_DATE_MONTH)
- .forEach((key, value) -> averageBsqPriceByMonth.put(new Date(key.getTime()), Price.valueOf("BSQ", value.getValue())));
+ if (!averagePricesValid) {
+ // Fill the map from now back to the last entry of the historical data (April 2019-Nov. 2022).
+ averageBsqPriceByMonth.putAll(getAverageBsqPriceByMonth(new Date(), HIST_BSQ_PRICE_LAST_DATE_YEAR, HIST_BSQ_PRICE_LAST_DATE_MONTH));
+ averagePricesValid = true;
+ }
return averageBsqPriceByMonth;
}
@@ -202,21 +210,7 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
Map averageBsqPriceByMonth = getAverageBsqPriceByMonth();
return getReceivedBtcBalanceEntryListExcludingLegacyBM().stream()
.filter(e -> e.getType() == BalanceEntry.Type.BTC_TRADE_FEE_TX)
- .map(balanceEntry -> {
- Date month = balanceEntry.getMonth();
- Optional price = Optional.ofNullable(averageBsqPriceByMonth.get(month));
- long receivedBtc = balanceEntry.getAmount();
- Optional receivedBtcAsBsq;
- if (price.isEmpty() || price.get().getValue() == 0) {
- receivedBtcAsBsq = Optional.empty();
- } else {
- long volume = price.get().getVolumeByAmount(Coin.valueOf(receivedBtc)).getValue();
- receivedBtcAsBsq = Optional.of(MathUtils.roundDoubleToLong(MathUtils.scaleDownByPowerOf10(volume, 6)));
- }
- return receivedBtcAsBsq;
- })
- .filter(Optional::isPresent)
- .mapToLong(Optional::get)
+ .flatMapToLong(balanceEntry -> receivedBtcAsBsq(balanceEntry, averageBsqPriceByMonth).stream())
.sum();
}
@@ -231,45 +225,29 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
Map averageBsqPriceByMonth = getAverageBsqPriceByMonth();
return getReceivedBtcBalanceEntryListExcludingLegacyBM().stream()
.filter(e -> e.getType() == BalanceEntry.Type.DPT_TX)
- .map(balanceEntry -> {
- Date month = balanceEntry.getMonth();
- Optional price = Optional.ofNullable(averageBsqPriceByMonth.get(month));
- long receivedBtc = balanceEntry.getAmount();
- Optional receivedBtcAsBsq;
- if (price.isEmpty() || price.get().getValue() == 0) {
- receivedBtcAsBsq = Optional.empty();
- } else {
- long volume = price.get().getVolumeByAmount(Coin.valueOf(receivedBtc)).getValue();
- receivedBtcAsBsq = Optional.of(MathUtils.roundDoubleToLong(MathUtils.scaleDownByPowerOf10(volume, 6)));
- }
- return receivedBtcAsBsq;
- })
- .filter(Optional::isPresent)
- .mapToLong(Optional::get)
+ .flatMapToLong(balanceEntry -> receivedBtcAsBsq(balanceEntry, averageBsqPriceByMonth).stream())
.sum();
}
public long getTotalAmountOfDistributedBsq() {
Map averageBsqPriceByMonth = getAverageBsqPriceByMonth();
return getReceivedBtcBalanceEntryListExcludingLegacyBM().stream()
- .map(balanceEntry -> {
- Date month = balanceEntry.getMonth();
- Optional price = Optional.ofNullable(averageBsqPriceByMonth.get(month));
- long receivedBtc = balanceEntry.getAmount();
- Optional receivedBtcAsBsq;
- if (price.isEmpty() || price.get().getValue() == 0) {
- receivedBtcAsBsq = Optional.empty();
- } else {
- long volume = price.get().getVolumeByAmount(Coin.valueOf(receivedBtc)).getValue();
- receivedBtcAsBsq = Optional.of(MathUtils.roundDoubleToLong(MathUtils.scaleDownByPowerOf10(volume, 6)));
- }
- return receivedBtcAsBsq;
- })
- .filter(Optional::isPresent)
- .mapToLong(Optional::get)
+ .flatMapToLong(balanceEntry -> receivedBtcAsBsq(balanceEntry, averageBsqPriceByMonth).stream())
.sum();
}
+ private static OptionalLong receivedBtcAsBsq(ReceivedBtcBalanceEntry balanceEntry,
+ Map averageBsqPriceByMonth) {
+ Date month = balanceEntry.getMonth();
+ long receivedBtc = balanceEntry.getAmount();
+ Price price = averageBsqPriceByMonth.get(month);
+ if (price == null) {
+ return OptionalLong.empty();
+ }
+ long volume = price.getVolumeByAmount(Coin.valueOf(receivedBtc)).getValue();
+ return OptionalLong.of(MathUtils.roundDoubleToLong(MathUtils.scaleDownByPowerOf10(volume, 6)));
+ }
+
private List getReceivedBtcBalanceEntryListExcludingLegacyBM() {
if (!receivedBtcBalanceEntryListExcludingLegacyBM.isEmpty()) {
return receivedBtcBalanceEntryListExcludingLegacyBM;
@@ -329,18 +307,18 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
private void addAccountingBlockToBalanceModel(Map balanceModelByBurningManName,
AccountingBlock accountingBlock) {
- accountingBlock.getTxs().forEach(tx -> {
- tx.getOutputs().forEach(txOutput -> {
- String name = txOutput.getName();
- balanceModelByBurningManName.putIfAbsent(name, new BalanceModel());
- balanceModelByBurningManName.get(name).addReceivedBtcBalanceEntry(new ReceivedBtcBalanceEntry(tx.getTruncatedTxId(),
- txOutput.getValue(),
- new Date(accountingBlock.getDate()),
- toBalanceEntryType(tx.getType())));
- });
- });
+ accountingBlock.getTxs().forEach(tx ->
+ tx.getOutputs().forEach(txOutput -> {
+ String name = txOutput.getName();
+ balanceModelByBurningManName.putIfAbsent(name, new BalanceModel());
+ balanceModelByBurningManName.get(name).addReceivedBtcBalanceEntry(new ReceivedBtcBalanceEntry(tx.getTruncatedTxId(),
+ txOutput.getValue(),
+ new Date(accountingBlock.getDate()),
+ toBalanceEntryType(tx.getType())));
+ }));
}
+ @SuppressWarnings("SameParameterValue")
private Map getAverageBsqPriceByMonth(Date from, int backToYear, int backToMonth) {
Map averageBsqPriceByMonth = new HashMap<>();
Calendar calendar = new GregorianCalendar();
diff --git a/core/src/main/java/bisq/core/monetary/AltcoinExchangeRate.java b/core/src/main/java/bisq/core/monetary/AltcoinExchangeRate.java
index 4064c2c288..4e3f53402e 100644
--- a/core/src/main/java/bisq/core/monetary/AltcoinExchangeRate.java
+++ b/core/src/main/java/bisq/core/monetary/AltcoinExchangeRate.java
@@ -60,34 +60,39 @@ public class AltcoinExchangeRate {
* @throws ArithmeticException if the converted altcoin amount is too high or too low.
*/
public Altcoin coinToAltcoin(Coin convertCoin) {
- BigInteger converted = BigInteger.valueOf(coin.value)
- .multiply(BigInteger.valueOf(convertCoin.value))
- .divide(BigInteger.valueOf(altcoin.value));
- if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
- || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
- throw new ArithmeticException("Overflow");
- return Altcoin.valueOf(altcoin.currencyCode, converted.longValue());
+ long converted;
+ if ((int) coin.value == coin.value && (int) convertCoin.value == convertCoin.value) {
+ // Short circuit in a common case where long arithmetic won't overflow.
+ converted = coin.value * convertCoin.value / altcoin.value;
+ } else {
+ // Otherwise use BigInteger to maintain full precision without overflowing.
+ converted = BigInteger.valueOf(coin.value)
+ .multiply(BigInteger.valueOf(convertCoin.value))
+ .divide(BigInteger.valueOf(altcoin.value))
+ .longValueExact();
+ }
+ return Altcoin.valueOf(altcoin.currencyCode, converted);
}
/**
- * Convert a altcoin amount to a coin amount using this exchange rate.
+ * Convert an altcoin amount to a coin amount using this exchange rate.
*
* @throws ArithmeticException if the converted coin amount is too high or too low.
*/
public Coin altcoinToCoin(Altcoin convertAltcoin) {
checkArgument(convertAltcoin.currencyCode.equals(altcoin.currencyCode), "Currency mismatch: %s vs %s",
convertAltcoin.currencyCode, altcoin.currencyCode);
- // Use BigInteger because it's much easier to maintain full precision without overflowing.
- BigInteger converted = BigInteger.valueOf(altcoin.value)
- .multiply(BigInteger.valueOf(convertAltcoin.value))
- .divide(BigInteger.valueOf(coin.value));
- if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
- || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
- throw new ArithmeticException("Overflow");
- try {
- return Coin.valueOf(converted.longValue());
- } catch (IllegalArgumentException x) {
- throw new ArithmeticException("Overflow: " + x.getMessage());
+ long converted;
+ if ((int) altcoin.value == altcoin.value && (int) convertAltcoin.value == convertAltcoin.value) {
+ // Short circuit in a common case where long arithmetic won't overflow.
+ converted = altcoin.value * convertAltcoin.value / coin.value;
+ } else {
+ // Otherwise use BigInteger to maintain full precision without overflowing.
+ converted = BigInteger.valueOf(altcoin.value)
+ .multiply(BigInteger.valueOf(convertAltcoin.value))
+ .divide(BigInteger.valueOf(coin.value))
+ .longValueExact();
}
+ return Coin.valueOf(converted);
}
}
diff --git a/core/src/main/java/bisq/core/monetary/Price.java b/core/src/main/java/bisq/core/monetary/Price.java
index e07a896efe..6e0677fe30 100644
--- a/core/src/main/java/bisq/core/monetary/Price.java
+++ b/core/src/main/java/bisq/core/monetary/Price.java
@@ -25,9 +25,6 @@ import org.bitcoinj.core.Monetary;
import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import org.jetbrains.annotations.NotNull;
/**
@@ -39,12 +36,11 @@ import org.jetbrains.annotations.NotNull;
* those classes, like {@link Fiat} or {@link Altcoin}.
*/
public class Price extends MonetaryWrapper implements Comparable {
- private static final Logger log = LoggerFactory.getLogger(Price.class);
/**
* Create a new {@code Price} from specified {@code Monetary}.
*
- * @param monetary
+ * @param monetary The monetary (either {@link Altcoin} or {@link Fiat}) to wrap
*/
public Price(Monetary monetary) {
super(monetary);
@@ -82,23 +78,41 @@ public class Price extends MonetaryWrapper implements Comparable {
public Volume getVolumeByAmount(Coin amount) {
if (monetary instanceof Fiat)
- return new Volume(new ExchangeRate((Fiat) monetary).coinToFiat(amount));
+ return new Volume(coinToFiat(new ExchangeRate((Fiat) monetary), amount));
else if (monetary instanceof Altcoin)
return new Volume(new AltcoinExchangeRate((Altcoin) monetary).coinToAltcoin(amount));
else
throw new IllegalStateException("Monetary must be either of type Fiat or Altcoin");
}
+ // Short circuit BigInteger logic in ExchangeRate.coinToFiat in a common case where long arithmetic won't overflow.
+ private static Fiat coinToFiat(ExchangeRate rate, Coin convertCoin) {
+ if ((int) convertCoin.value == convertCoin.value && (int) rate.fiat.value == rate.fiat.value) {
+ long converted = convertCoin.value * rate.fiat.value / rate.coin.value;
+ return Fiat.valueOf(rate.fiat.currencyCode, converted);
+ }
+ return rate.coinToFiat(convertCoin);
+ }
+
public Coin getAmountByVolume(Volume volume) {
Monetary monetary = volume.getMonetary();
if (monetary instanceof Fiat && this.monetary instanceof Fiat)
- return new ExchangeRate((Fiat) this.monetary).fiatToCoin((Fiat) monetary);
+ return fiatToCoin(new ExchangeRate((Fiat) this.monetary), (Fiat) monetary);
else if (monetary instanceof Altcoin && this.monetary instanceof Altcoin)
return new AltcoinExchangeRate((Altcoin) this.monetary).altcoinToCoin((Altcoin) monetary);
else
return Coin.ZERO;
}
+ // Short circuit BigInteger logic in ExchangeRate.fiatToCoin in a common case where long arithmetic won't overflow.
+ private static Coin fiatToCoin(ExchangeRate rate, Fiat convertFiat) {
+ if ((int) convertFiat.value == convertFiat.value && (int) rate.coin.value == rate.coin.value) {
+ long converted = convertFiat.value * rate.coin.value / rate.fiat.value;
+ return Coin.valueOf(converted);
+ }
+ return rate.fiatToCoin(convertFiat);
+ }
+
public String getCurrencyCode() {
return monetary instanceof Altcoin ? ((Altcoin) monetary).getCurrencyCode() : ((Fiat) monetary).getCurrencyCode();
}
diff --git a/core/src/main/java/bisq/core/offer/availability/DisputeAgentSelection.java b/core/src/main/java/bisq/core/offer/availability/DisputeAgentSelection.java
index eaec9dcbe9..fe15585858 100644
--- a/core/src/main/java/bisq/core/offer/availability/DisputeAgentSelection.java
+++ b/core/src/main/java/bisq/core/offer/availability/DisputeAgentSelection.java
@@ -26,8 +26,6 @@ import bisq.common.util.Tuple2;
import com.google.common.annotations.VisibleForTesting;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
@@ -35,6 +33,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
@@ -62,16 +61,11 @@ public class DisputeAgentSelection {
DisputeAgentManager disputeAgentManager,
boolean isMediator) {
// We take last 100 entries from trade statistics
- List list = new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet());
- list.sort(Comparator.comparing(TradeStatistics3::getDateAsLong));
- Collections.reverse(list);
- if (!list.isEmpty()) {
- int max = Math.min(list.size(), LOOK_BACK_RANGE);
- list = list.subList(0, max);
- }
+ Stream stream = tradeStatisticsManager.getNavigableTradeStatisticsSet().descendingSet().stream()
+ .limit(LOOK_BACK_RANGE);
// We stored only first 4 chars of disputeAgents onion address
- List lastAddressesUsedInTrades = list.stream()
+ List lastAddressesUsedInTrades = stream
.map(tradeStatistics3 -> isMediator ? tradeStatistics3.getMediator() : tradeStatistics3.getRefundAgent())
.filter(Objects::nonNull)
.collect(Collectors.toList());
diff --git a/core/src/main/java/bisq/core/payment/TradeLimits.java b/core/src/main/java/bisq/core/payment/TradeLimits.java
index 2d3c5c5688..6bd38f38de 100644
--- a/core/src/main/java/bisq/core/payment/TradeLimits.java
+++ b/core/src/main/java/bisq/core/payment/TradeLimits.java
@@ -19,9 +19,9 @@ package bisq.core.payment;
import bisq.core.dao.governance.param.Param;
import bisq.core.dao.governance.period.PeriodService;
+import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService;
-
-import bisq.common.util.MathUtils;
+import bisq.core.dao.state.model.blockchain.Block;
import org.bitcoinj.core.Coin;
@@ -37,7 +37,7 @@ import javax.annotation.Nullable;
@Slf4j
@Singleton
-public class TradeLimits {
+public class TradeLimits implements DaoStateListener {
@Nullable
@Getter
private static TradeLimits INSTANCE;
@@ -45,10 +45,14 @@ public class TradeLimits {
private final DaoStateService daoStateService;
private final PeriodService periodService;
+ private volatile Coin cachedMaxTradeLimit;
+
@Inject
public TradeLimits(DaoStateService daoStateService, PeriodService periodService) {
this.daoStateService = daoStateService;
this.periodService = periodService;
+
+ daoStateService.addDaoStateListener(this);
INSTANCE = this;
}
@@ -58,6 +62,10 @@ public class TradeLimits {
// guice.
}
+ @Override
+ public void onParseBlockCompleteAfterBatchProcessing(Block block) {
+ cachedMaxTradeLimit = null;
+ }
/**
* The default trade limits defined as statics in PaymentMethod are only used until the DAO
@@ -67,7 +75,11 @@ public class TradeLimits {
* @return the maximum trade limit set by the DAO.
*/
public Coin getMaxTradeLimit() {
- return daoStateService.getParamValueAsCoin(Param.MAX_TRADE_LIMIT, periodService.getChainHeight());
+ Coin limit = cachedMaxTradeLimit;
+ if (limit == null) {
+ cachedMaxTradeLimit = limit = daoStateService.getParamValueAsCoin(Param.MAX_TRADE_LIMIT, periodService.getChainHeight());
+ }
+ return limit;
}
// We possibly rounded value for the first month gets multiplied by 4 to get the trade limit after the account
@@ -98,8 +110,6 @@ public class TradeLimits {
long smallestLimit = maxLimit / (4 * riskFactor); // e.g. 100000000 / 32 = 3125000
// We want to avoid more than 4 decimal places (100000000 / 32 = 3125000 or 1 BTC / 32 = 0.03125 BTC).
// We want rounding to 0.0313 BTC
- double decimalForm = MathUtils.scaleDownByPowerOf10((double) smallestLimit, 8);
- double rounded = MathUtils.roundDouble(decimalForm, 4);
- return MathUtils.roundDoubleToLong(MathUtils.scaleUpByPowerOf10(rounded, 8));
+ return ((smallestLimit + 5000L) / 10000L) * 10000L;
}
}
diff --git a/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java b/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java
index 32d212d344..877c19849f 100644
--- a/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java
+++ b/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java
@@ -26,11 +26,13 @@ import bisq.common.proto.persistable.PersistablePayload;
import org.bitcoinj.core.Coin;
-import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Comparator;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@@ -196,8 +198,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable paymentMethods = new ArrayList<>(Arrays.asList(
+ private static final List PAYMENT_METHODS = Stream.of(
// EUR
SEPA = new PaymentMethod(SEPA_ID, 6 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK),
SEPA_INSTANT = new PaymentMethod(SEPA_INSTANT_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK),
@@ -277,18 +278,15 @@ public final class PaymentMethod implements PersistablePayload, Comparable m.id.equals(CLEAR_X_CHANGE_ID) ? "ZELLE" : m.id)
+ ).collect(Collectors.toUnmodifiableList());
- static {
- paymentMethods.sort((o1, o2) -> {
- String id1 = o1.getId();
- if (id1.equals(CLEAR_X_CHANGE_ID))
- id1 = "ZELLE";
- String id2 = o2.getId();
- if (id2.equals(CLEAR_X_CHANGE_ID))
- id2 = "ZELLE";
- return id1.compareTo(id2);
- });
+ private static final Map PAYMENT_METHOD_MAP = PAYMENT_METHODS.stream()
+ .collect(Collectors.toUnmodifiableMap(m -> m.id, m -> m));
+
+ public static List getPaymentMethods() {
+ return PAYMENT_METHODS;
}
public static PaymentMethod getDummyPaymentMethod(String id) {
@@ -371,9 +369,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable getActivePaymentMethod(String id) {
- return paymentMethods.stream()
- .filter(e -> e.getId().equals(id))
- .findFirst();
+ return Optional.ofNullable(PAYMENT_METHOD_MAP.get(id));
}
public Coin getMaxTradeLimitAsCoin(String currencyCode) {
diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java
index 5640bb8cd4..ddcb155b5c 100644
--- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java
+++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java
@@ -18,7 +18,6 @@
package bisq.core.trade.statistics;
import bisq.core.monetary.Altcoin;
-import bisq.core.monetary.AltcoinExchangeRate;
import bisq.core.monetary.Price;
import bisq.core.monetary.Volume;
import bisq.core.offer.Offer;
@@ -45,8 +44,6 @@ import bisq.common.util.Utilities;
import com.google.protobuf.ByteString;
import org.bitcoinj.core.Coin;
-import org.bitcoinj.utils.ExchangeRate;
-import org.bitcoinj.utils.Fiat;
import com.google.common.base.Charsets;
@@ -311,12 +308,10 @@ public final class TradeStatistics2 implements ProcessOncePersistableNetworkPayl
}
public Volume getTradeVolume() {
- if (getTradePrice().getMonetary() instanceof Altcoin) {
- return new Volume(new AltcoinExchangeRate((Altcoin) getTradePrice().getMonetary()).coinToAltcoin(getTradeAmount()));
- } else {
- Volume volume = new Volume(new ExchangeRate((Fiat) getTradePrice().getMonetary()).coinToFiat(getTradeAmount()));
- return VolumeUtil.getRoundedFiatVolume(volume);
- }
+ Price price = getTradePrice();
+ return price.getMonetary() instanceof Altcoin
+ ? price.getVolumeByAmount(getTradeAmount())
+ : VolumeUtil.getRoundedFiatVolume(price.getVolumeByAmount(getTradeAmount()));
}
public boolean isValid() {
diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java
index 8f88f0c525..9786d1cf50 100644
--- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java
+++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java
@@ -19,7 +19,6 @@ package bisq.core.trade.statistics;
import bisq.core.locale.CurrencyUtil;
import bisq.core.monetary.Altcoin;
-import bisq.core.monetary.AltcoinExchangeRate;
import bisq.core.monetary.Price;
import bisq.core.monetary.Volume;
import bisq.core.offer.Offer;
@@ -41,6 +40,7 @@ import bisq.common.app.Capability;
import bisq.common.crypto.Hash;
import bisq.common.proto.ProtoUtil;
import bisq.common.util.CollectionUtils;
+import bisq.common.util.ComparableExt;
import bisq.common.util.ExtraDataMapValidator;
import bisq.common.util.JsonExclude;
import bisq.common.util.Utilities;
@@ -48,8 +48,6 @@ import bisq.common.util.Utilities;
import com.google.protobuf.ByteString;
import org.bitcoinj.core.Coin;
-import org.bitcoinj.utils.ExchangeRate;
-import org.bitcoinj.utils.Fiat;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
@@ -59,15 +57,19 @@ import java.time.ZoneId;
import java.util.Arrays;
import java.util.Calendar;
+import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
@@ -78,7 +80,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
*/
@Slf4j
public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayload, PersistableNetworkPayload,
- CapabilityRequiringPayload, DateSortedTruncatablePayload {
+ CapabilityRequiringPayload, DateSortedTruncatablePayload, ComparableExt {
@JsonExclude
private transient static final ZoneId ZONE_ID = ZoneId.systemDefault();
@@ -138,7 +140,8 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
// The payment method string can be quite long and would consume 15% more space.
// When we get a new payment method we can add it to the enum at the end. Old users would add it as string if not
// recognized.
- private enum PaymentMethodMapper {
+ @VisibleForTesting
+ enum PaymentMethodMapper {
OK_PAY,
CASH_APP,
VENMO,
@@ -194,7 +197,9 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
TIKKIE,
TRANSFERWISE_USD,
ACH_TRANSFER,
- DOMESTIC_WIRE_TRANSFER
+ DOMESTIC_WIRE_TRANSFER;
+
+ private static final PaymentMethodMapper[] values = values(); // cache for perf gain
}
@Getter
@@ -298,7 +303,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
String tempPaymentMethod;
try {
tempPaymentMethod = String.valueOf(PaymentMethodMapper.valueOf(paymentMethod).ordinal());
- } catch (Throwable t) {
+ } catch (IllegalArgumentException e) {
tempPaymentMethod = paymentMethod;
}
this.paymentMethod = tempPaymentMethod;
@@ -382,8 +387,9 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
}
public LocalDateTime getLocalDateTime() {
+ LocalDateTime localDateTime = this.localDateTime;
if (localDateTime == null) {
- localDateTime = dateObj.toInstant().atZone(ZONE_ID).toLocalDateTime();
+ this.localDateTime = localDateTime = dateObj.toInstant().atZone(ZONE_ID).toLocalDateTime();
}
return localDateTime;
}
@@ -403,9 +409,12 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
}
public String getPaymentMethodId() {
+ if (paymentMethod.isEmpty() || paymentMethod.charAt(0) > '9') {
+ return paymentMethod;
+ }
try {
- return PaymentMethodMapper.values()[Integer.parseInt(paymentMethod)].name();
- } catch (Throwable ignore) {
+ return PaymentMethodMapper.values[Integer.parseInt(paymentMethod)].name();
+ } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
return paymentMethod;
}
}
@@ -413,8 +422,9 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
private transient Price priceObj;
public Price getTradePrice() {
+ Price priceObj = this.priceObj;
if (priceObj == null) {
- priceObj = Price.valueOf(currency, price);
+ this.priceObj = priceObj = Price.valueOf(currency, price);
}
return priceObj;
}
@@ -424,13 +434,12 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
}
public Volume getTradeVolume() {
+ Volume volume = this.volume;
if (volume == null) {
- if (getTradePrice().getMonetary() instanceof Altcoin) {
- volume = new Volume(new AltcoinExchangeRate((Altcoin) getTradePrice().getMonetary()).coinToAltcoin(getTradeAmount()));
- } else {
- Volume exactVolume = new Volume(new ExchangeRate((Fiat) getTradePrice().getMonetary()).coinToFiat(getTradeAmount()));
- volume = VolumeUtil.getRoundedFiatVolume(exactVolume);
- }
+ Price price = getTradePrice();
+ this.volume = volume = price.getMonetary() instanceof Altcoin
+ ? price.getVolumeByAmount(getTradeAmount())
+ : VolumeUtil.getRoundedFiatVolume(price.getVolumeByAmount(getTradeAmount()));
}
return volume;
}
@@ -472,6 +481,27 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
currencyFound;
}
+ private static > int nullsFirstCompare(T a, T b) {
+ return Comparator.nullsFirst(Comparator.naturalOrder()).compare(a, b);
+ }
+
+ @Override
+ public int compareTo(@NotNull ComparableExt o) {
+ if (this == o) {
+ return 0;
+ }
+ if (!(o instanceof TradeStatistics3)) {
+ return -o.compareTo(this);
+ }
+ TradeStatistics3 that = (TradeStatistics3) o;
+ return date != that.date ? Long.compare(date, that.date)
+ : amount != that.amount ? Long.compare(amount, that.amount)
+ : !Objects.equals(currency, that.currency) ? nullsFirstCompare(currency, that.currency)
+ : price != that.price ? Long.compare(price, that.price)
+ : !Objects.equals(paymentMethod, that.paymentMethod) ? nullsFirstCompare(paymentMethod, that.paymentMethod)
+ : Arrays.compare(hash, that.hash);
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -482,9 +512,8 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
if (price != that.price) return false;
if (amount != that.amount) return false;
if (date != that.date) return false;
- if (currency != null ? !currency.equals(that.currency) : that.currency != null) return false;
- if (paymentMethod != null ? !paymentMethod.equals(that.paymentMethod) : that.paymentMethod != null)
- return false;
+ if (!Objects.equals(currency, that.currency)) return false;
+ if (!Objects.equals(paymentMethod, that.paymentMethod)) return false;
return Arrays.equals(hash, that.hash);
}
diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java
index 9a830e5c8f..6c250f8c6e 100644
--- a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java
+++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java
@@ -47,11 +47,13 @@ import java.time.Instant;
import java.io.File;
import java.util.ArrayList;
-import java.util.Comparator;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.NavigableSet;
import java.util.Set;
+import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -68,7 +70,8 @@ public class TradeStatisticsManager {
private final TradeStatisticsConverter tradeStatisticsConverter;
private final File storageDir;
private final boolean dumpStatistics;
- private final ObservableSet observableTradeStatisticsSet = FXCollections.observableSet();
+ private final NavigableSet navigableTradeStatisticsSet = new TreeSet<>();
+ private final ObservableSet observableTradeStatisticsSet = FXCollections.observableSet(navigableTradeStatisticsSet);
private JsonFileManager jsonFileManager;
@Inject
@@ -110,39 +113,24 @@ public class TradeStatisticsManager {
}
});
- Set set = tradeStatistics3StorageService.getMapOfAllData().values().stream()
+ tradeStatistics3StorageService.getMapOfAllData().values().stream()
.filter(e -> e instanceof TradeStatistics3)
.map(e -> (TradeStatistics3) e)
.filter(TradeStatistics3::isValid)
- .collect(Collectors.toSet());
- observableTradeStatisticsSet.addAll(set);
+ .forEach(observableTradeStatisticsSet::add);
- // collate prices by ccy -- takes about 10 ms for 5000 items
- Map> allPriceByCurrencyCode = new HashMap<>();
- observableTradeStatisticsSet.forEach(e -> {
- List list;
- String currencyCode = e.getCurrency();
- if (allPriceByCurrencyCode.containsKey(currencyCode)) {
- list = allPriceByCurrencyCode.get(currencyCode);
- } else {
- list = new ArrayList<>();
- allPriceByCurrencyCode.put(currencyCode, list);
- }
- list.add(e);
- });
// get the most recent price for each ccy and notify priceFeedService
+ // (this relies on the trade statistics set being sorted by date)
Map newestPriceByCurrencyCode = new HashMap<>();
- allPriceByCurrencyCode.values().stream()
- .filter(list -> !list.isEmpty())
- .forEach(list -> {
- list.sort(Comparator.comparing(TradeStatistics3::getDate));
- TradeStatistics3 tradeStatistics = list.get(list.size() - 1);
- newestPriceByCurrencyCode.put(tradeStatistics.getCurrency(), tradeStatistics.getTradePrice());
- });
+ observableTradeStatisticsSet.forEach(e -> newestPriceByCurrencyCode.put(e.getCurrency(), e.getTradePrice()));
priceFeedService.applyInitialBisqMarketPrice(newestPriceByCurrencyCode);
maybeDumpStatistics();
}
+ public NavigableSet getNavigableTradeStatisticsSet() {
+ return Collections.unmodifiableNavigableSet(navigableTradeStatisticsSet);
+ }
+
public ObservableSet getObservableTradeStatisticsSet() {
return observableTradeStatisticsSet;
}
@@ -170,7 +158,7 @@ public class TradeStatisticsManager {
Instant yearAgo = Instant.ofEpochSecond(Instant.now().getEpochSecond() - TimeUnit.DAYS.toSeconds(365));
Set activeCurrencies = observableTradeStatisticsSet.stream()
.filter(e -> e.getDate().toInstant().isAfter(yearAgo))
- .map(p -> p.getCurrency())
+ .map(TradeStatistics3::getCurrency)
.collect(Collectors.toSet());
ArrayList activeFiatCurrencyList = fiatCurrencyList.stream()
diff --git a/core/src/main/java/bisq/core/util/AveragePriceUtil.java b/core/src/main/java/bisq/core/util/AveragePriceUtil.java
index fd1f8e65df..631719c26c 100644
--- a/core/src/main/java/bisq/core/util/AveragePriceUtil.java
+++ b/core/src/main/java/bisq/core/util/AveragePriceUtil.java
@@ -24,16 +24,21 @@ import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.common.util.MathUtils;
+import bisq.common.util.RangeUtils;
import bisq.common.util.Tuple2;
import org.bitcoinj.utils.Fiat;
+import com.google.common.collect.Range;
+import com.google.common.primitives.Doubles;
+
import java.util.ArrayList;
import java.util.Calendar;
-import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.stream.Collectors;
public class AveragePriceUtil {
@@ -58,23 +63,21 @@ public class AveragePriceUtil {
Date pastXDays,
Date date) {
double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100));
- List bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
- .filter(e -> e.getCurrency().equals("BSQ"))
- .filter(e -> e.getDate().after(pastXDays))
- .filter(e -> e.getDate().before(date))
- .collect(Collectors.toList());
- List bsqTradePastXDays = percentToTrim > 0 ?
- removeOutliers(bsqAllTradePastXDays, percentToTrim) :
- bsqAllTradePastXDays;
+ Set allTradePastXDays = RangeUtils.subSet(tradeStatisticsManager.getNavigableTradeStatisticsSet())
+ .withKey(TradeStatistics3::getDate)
+ .overRange(Range.open(pastXDays, date));
+
+ Map> bsqUsdAllTradePastXDays = allTradePastXDays.stream()
+ .filter(e -> e.getCurrency().equals("USD") || e.getCurrency().equals("BSQ"))
+ .collect(Collectors.partitioningBy(e -> e.getCurrency().equals("USD")));
+
+ List bsqTradePastXDays = percentToTrim > 0 ?
+ removeOutliers(bsqUsdAllTradePastXDays.get(false), percentToTrim) :
+ bsqUsdAllTradePastXDays.get(false);
- List usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
- .filter(e -> e.getCurrency().equals("USD"))
- .filter(e -> e.getDate().after(pastXDays))
- .filter(e -> e.getDate().before(date))
- .collect(Collectors.toList());
List usdTradePastXDays = percentToTrim > 0 ?
- removeOutliers(usdAllTradePastXDays, percentToTrim) :
- usdAllTradePastXDays;
+ removeOutliers(bsqUsdAllTradePastXDays.get(true), percentToTrim) :
+ bsqUsdAllTradePastXDays.get(true);
Price usdPrice = Price.valueOf("USD", getUSDAverage(bsqTradePastXDays, usdTradePastXDays));
Price bsqPrice = Price.valueOf("BSQ", getBTCAverage(bsqTradePastXDays));
@@ -82,17 +85,17 @@ public class AveragePriceUtil {
}
private static List removeOutliers(List list, double percentToTrim) {
- List yValues = list.stream()
+ List yValues = Doubles.asList(list.stream()
.filter(TradeStatistics3::isValid)
- .map(e -> (double) e.getPrice())
- .collect(Collectors.toList());
+ .mapToDouble(TradeStatistics3::getPrice)
+ .toArray());
Tuple2 tuple = InlierUtil.findInlierRange(yValues, percentToTrim, HOW_MANY_STD_DEVS_CONSTITUTE_OUTLIER);
double lowerBound = tuple.first;
double upperBound = tuple.second;
return list.stream()
- .filter(e -> e.getPrice() > lowerBound)
- .filter(e -> e.getPrice() < upperBound)
+ .filter(e -> (double) e.getPrice() >= lowerBound)
+ .filter(e -> (double) e.getPrice() <= upperBound)
.collect(Collectors.toList());
}
@@ -111,21 +114,23 @@ public class AveragePriceUtil {
return averagePrice;
}
- private static long getUSDAverage(List bsqList, List usdList) {
+ private static long getUSDAverage(List sortedBsqList, List sortedUsdList) {
// Use next USD/BTC print as price to calculate BSQ/USD rate
// Store each trade as amount of USD and amount of BSQ traded
- List> usdBsqList = new ArrayList<>(bsqList.size());
- usdList.sort(Comparator.comparing(TradeStatistics3::getDateAsLong));
+ List> usdBsqList = new ArrayList<>(sortedBsqList.size());
var usdBTCPrice = 10000d; // Default to 10000 USD per BTC if there is no USD feed at all
- for (TradeStatistics3 item : bsqList) {
+ int i = 0;
+ for (TradeStatistics3 item : sortedBsqList) {
// Find usd price for trade item
- usdBTCPrice = usdList.stream()
- .filter(usd -> usd.getDateAsLong() > item.getDateAsLong())
- .map(usd -> MathUtils.scaleDownByPowerOf10((double) usd.getTradePrice().getValue(),
- Fiat.SMALLEST_UNIT_EXPONENT))
- .findFirst()
- .orElse(usdBTCPrice);
+ for (; i < sortedUsdList.size(); i++) {
+ TradeStatistics3 usd = sortedUsdList.get(i);
+ if (usd.getDateAsLong() > item.getDateAsLong()) {
+ usdBTCPrice = MathUtils.scaleDownByPowerOf10((double) usd.getTradePrice().getValue(),
+ Fiat.SMALLEST_UNIT_EXPONENT);
+ break;
+ }
+ }
var bsqAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeVolume().getValue(),
Altcoin.SMALLEST_UNIT_EXPONENT);
var btcAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeAmount().getValue(),
diff --git a/core/src/main/java/bisq/core/util/InlierUtil.java b/core/src/main/java/bisq/core/util/InlierUtil.java
index 41aefac866..96bc8067b3 100644
--- a/core/src/main/java/bisq/core/util/InlierUtil.java
+++ b/core/src/main/java/bisq/core/util/InlierUtil.java
@@ -20,11 +20,15 @@ package bisq.core.util;
import bisq.common.util.DoubleSummaryStatisticsWithStdDev;
import bisq.common.util.Tuple2;
-import javafx.collections.FXCollections;
+import com.google.common.primitives.Doubles;
+import java.util.Arrays;
import java.util.DoubleSummaryStatistics;
import java.util.List;
-import java.util.stream.Collectors;
+import java.util.Spliterator;
+import java.util.function.DoublePredicate;
+import java.util.stream.DoubleStream;
+import java.util.stream.StreamSupport;
public class InlierUtil {
@@ -39,12 +43,9 @@ public class InlierUtil {
Tuple2 inlierThreshold =
computeInlierThreshold(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
- DoubleSummaryStatistics inlierStatistics =
- yValues
- .stream()
- .filter(y -> withinBounds(inlierThreshold, y))
- .mapToDouble(Double::doubleValue)
- .summaryStatistics();
+ DoubleSummaryStatistics inlierStatistics = stream(yValues)
+ .filter(withinBounds(inlierThreshold))
+ .summaryStatistics();
var inlierMin = inlierStatistics.getMin();
var inlierMax = inlierStatistics.getMax();
@@ -52,10 +53,10 @@ public class InlierUtil {
return new Tuple2<>(inlierMin, inlierMax);
}
- private static boolean withinBounds(Tuple2 bounds, double number) {
- var lowerBound = bounds.first;
- var upperBound = bounds.second;
- return (lowerBound <= number) && (number <= upperBound);
+ private static DoublePredicate withinBounds(Tuple2 bounds) {
+ double lowerBound = bounds.first;
+ double upperBound = bounds.second;
+ return number -> lowerBound <= number && number <= upperBound;
}
/* Computes the lower and upper inlier thresholds. A point lying outside
@@ -75,12 +76,10 @@ public class InlierUtil {
List trimmed = trim(percentToTrim, numbers);
- DoubleSummaryStatisticsWithStdDev summaryStatistics =
- trimmed.stream()
- .collect(
- DoubleSummaryStatisticsWithStdDev::new,
- DoubleSummaryStatisticsWithStdDev::accept,
- DoubleSummaryStatisticsWithStdDev::combine);
+ DoubleSummaryStatisticsWithStdDev summaryStatistics = stream(trimmed)
+ .collect(DoubleSummaryStatisticsWithStdDev::new,
+ DoubleSummaryStatisticsWithStdDev::accept,
+ DoubleSummaryStatisticsWithStdDev::combine);
double mean = summaryStatistics.getAverage();
double stdDev = summaryStatistics.getStandardDeviation();
@@ -111,7 +110,7 @@ public class InlierUtil {
return numbers;
}
if (totalPercentTrim == 100) {
- return FXCollections.emptyObservableList();
+ return Doubles.asList();
}
if (numbers.isEmpty()) {
@@ -124,17 +123,17 @@ public class InlierUtil {
return numbers;
}
- var sorted = numbers.stream().sorted();
+ var array = stream(numbers).toArray();
+ Arrays.sort(array);
- var oneSideTrimmed = sorted.skip(countToDropFromEachSide);
-
- // Here, having already trimmed the left-side, we are implicitly trimming
- // the right-side by specifying a limit to the stream's length.
- // An explicit right-side drop/trim/skip is not supported by the Stream API.
- var countAfterTrim = count - (countToDropFromEachSide * 2); // visada > 0? ir <= count?
- var bothSidesTrimmed = oneSideTrimmed.limit(countAfterTrim);
-
- return bothSidesTrimmed.collect(Collectors.toList());
+ var sorted = Doubles.asList(array);
+ return sorted.subList(countToDropFromEachSide, sorted.size() - countToDropFromEachSide);
}
+ private static DoubleStream stream(Iterable doubles) {
+ var spliterator = doubles.spliterator();
+ return spliterator instanceof Spliterator.OfDouble
+ ? StreamSupport.doubleStream((Spliterator.OfDouble) spliterator, false)
+ : StreamSupport.stream(spliterator, false).mapToDouble(Double::doubleValue);
+ }
}
diff --git a/core/src/main/java/bisq/core/util/VolumeUtil.java b/core/src/main/java/bisq/core/util/VolumeUtil.java
index 769e2dc9eb..9a102a05d5 100644
--- a/core/src/main/java/bisq/core/util/VolumeUtil.java
+++ b/core/src/main/java/bisq/core/util/VolumeUtil.java
@@ -19,14 +19,10 @@ package bisq.core.util;
import bisq.core.locale.Res;
import bisq.core.monetary.Altcoin;
-import bisq.core.monetary.AltcoinExchangeRate;
-import bisq.core.monetary.Price;
import bisq.core.monetary.Volume;
import bisq.core.offer.Offer;
-import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Monetary;
-import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat;
import org.bitcoinj.utils.MonetaryFormat;
@@ -66,14 +62,6 @@ public class VolumeUtil {
return Volume.parse(String.valueOf(roundedVolume), volumeByAmount.getCurrencyCode());
}
- public static Volume getVolume(Coin amount, Price price) {
- if (price.getMonetary() instanceof Altcoin) {
- return new Volume(new AltcoinExchangeRate((Altcoin) price.getMonetary()).coinToAltcoin(amount));
- } else {
- return new Volume(new ExchangeRate((Fiat) price.getMonetary()).coinToFiat(amount));
- }
- }
-
public static String formatVolume(Offer offer, Boolean decimalAligned, int maxNumberOfDigits) {
return formatVolume(offer, decimalAligned, maxNumberOfDigits, true);
diff --git a/core/src/test/java/bisq/core/trade/statistics/TradeStatistics3Test.java b/core/src/test/java/bisq/core/trade/statistics/TradeStatistics3Test.java
new file mode 100644
index 0000000000..2ed41a6196
--- /dev/null
+++ b/core/src/test/java/bisq/core/trade/statistics/TradeStatistics3Test.java
@@ -0,0 +1,47 @@
+/*
+ * 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 .
+ */
+
+package bisq.core.trade.statistics;
+
+import bisq.core.payment.payload.PaymentMethod;
+
+import com.google.common.collect.Sets;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TradeStatistics3Test {
+ @Disabled("Not fixed yet")
+ @Test
+ public void allPaymentMethodsCoveredByWrapper() {
+ Set paymentMethodCodes = PaymentMethod.getPaymentMethods().stream()
+ .map(PaymentMethod::getId)
+ .collect(Collectors.toSet());
+
+ Set wrapperCodes = Arrays.stream(TradeStatistics3.PaymentMethodMapper.values())
+ .map(Enum::name)
+ .collect(Collectors.toSet());
+
+ assertEquals(Set.of(), Sets.difference(paymentMethodCodes, wrapperCodes));
+ }
+}
diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java
index 0b3399f39e..18875498bc 100644
--- a/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java
+++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java
@@ -22,10 +22,10 @@ import bisq.desktop.common.model.ActivatableDataModel;
import java.time.Instant;
import java.time.temporal.TemporalAdjuster;
-import java.util.Comparator;
import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.function.Predicate;
+import java.util.function.ToLongFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -64,6 +64,11 @@ public abstract class ChartDataModel extends ActivatableDataModel {
return temporalAdjusterModel.toTimeInterval(instant);
}
+ // optimized for use when the input times are sequential and not too spread out
+ public ToLongFunction toCachedTimeIntervalFn() {
+ return temporalAdjusterModel.withCache()::toTimeInterval;
+ }
+
///////////////////////////////////////////////////////////////////////////////////////////
// Date filter predicate
@@ -88,7 +93,7 @@ public abstract class ChartDataModel extends ActivatableDataModel {
Map map2,
BinaryOperator mergeFunction) {
return Stream.concat(map1.entrySet().stream(),
- map2.entrySet().stream())
+ map2.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue,
mergeFunction));
diff --git a/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java b/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java
index d8cf5c3bb8..0126a1b450 100644
--- a/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java
+++ b/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java
@@ -18,6 +18,7 @@
package bisq.desktop.components.chart;
import bisq.common.util.MathUtils;
+import bisq.common.util.Tuple3;
import java.time.DayOfWeek;
import java.time.Instant;
@@ -37,6 +38,19 @@ import static java.time.temporal.ChronoField.DAY_OF_YEAR;
public class TemporalAdjusterModel {
private static final ZoneId ZONE_ID = ZoneId.systemDefault();
+ protected TemporalAdjuster temporalAdjuster = Interval.MONTH.getAdjuster();
+
+ private boolean enableCache;
+ private Tuple3 cachedDateStartEndTuple;
+ private Tuple3 cachedTimeIntervalMapping;
+
+ public TemporalAdjusterModel withCache() {
+ var model = new TemporalAdjusterModel();
+ model.temporalAdjuster = this.temporalAdjuster;
+ model.enableCache = true;
+ return model;
+ }
+
public enum Interval {
YEAR(TemporalAdjusters.firstDayOfYear()),
HALF_YEAR(temporal -> {
@@ -81,8 +95,6 @@ public class TemporalAdjusterModel {
}
}
- protected TemporalAdjuster temporalAdjuster = Interval.MONTH.getAdjuster();
-
public void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) {
this.temporalAdjuster = temporalAdjuster;
}
@@ -96,12 +108,36 @@ public class TemporalAdjusterModel {
}
public long toTimeInterval(Instant instant, TemporalAdjuster temporalAdjuster) {
- return instant
- .atZone(ZONE_ID)
- .toLocalDate()
- .with(temporalAdjuster)
- .atStartOfDay(ZONE_ID)
- .toInstant()
- .getEpochSecond();
+ LocalDate date = toLocalDate(instant);
+ Tuple3 tuple = cachedTimeIntervalMapping;
+ long timeInterval;
+ if (tuple != null && date.equals(tuple.first) && temporalAdjuster.equals(tuple.second)) {
+ timeInterval = tuple.third;
+ } else {
+ timeInterval = date
+ .with(temporalAdjuster)
+ .atStartOfDay(ZONE_ID)
+ .toEpochSecond();
+ if (enableCache) {
+ cachedTimeIntervalMapping = new Tuple3<>(date, temporalAdjuster, timeInterval);
+ }
+ }
+ return timeInterval;
+ }
+
+ private LocalDate toLocalDate(Instant instant) {
+ LocalDate date;
+ Tuple3 tuple = cachedDateStartEndTuple;
+ if (tuple != null && !instant.isBefore(tuple.second) && instant.isBefore(tuple.third)) {
+ date = tuple.first;
+ } else {
+ date = instant.atZone(ZONE_ID).toLocalDate();
+ if (enableCache) {
+ cachedDateStartEndTuple = new Tuple3<>(date,
+ date.atStartOfDay(ZONE_ID).toInstant(),
+ date.plusDays(1).atStartOfDay(ZONE_ID).toInstant());
+ }
+ }
+ return date;
}
}
diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartDataModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartDataModel.java
index 49b2a0a7b4..7341481d87 100644
--- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartDataModel.java
+++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartDataModel.java
@@ -188,9 +188,10 @@ public class PriceChartDataModel extends ChartDataModel {
private Map getPriceByInterval(Predicate collectionFilter,
Function, Double> getAveragePriceFunction) {
+ var toTimeIntervalFn = toCachedTimeIntervalFn();
return getPriceByInterval(tradeStatisticsManager.getObservableTradeStatisticsSet(),
collectionFilter,
- tradeStatistics -> toTimeInterval(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())),
+ tradeStatistics -> toTimeIntervalFn.applyAsLong(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())),
dateFilter,
getAveragePriceFunction);
}
diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartDataModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartDataModel.java
index e147ce6977..3ec89998d9 100644
--- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartDataModel.java
+++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartDataModel.java
@@ -134,8 +134,9 @@ public class VolumeChartDataModel extends ChartDataModel {
///////////////////////////////////////////////////////////////////////////////////////////
private Map getVolumeByInterval(Function, Long> getVolumeFunction) {
+ var toTimeIntervalFn = toCachedTimeIntervalFn();
return getVolumeByInterval(tradeStatisticsManager.getObservableTradeStatisticsSet(),
- tradeStatistics -> toTimeInterval(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())),
+ tradeStatistics -> toTimeIntervalFn.applyAsLong(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())),
dateFilter,
getVolumeFunction);
}
diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartDataModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartDataModel.java
index 1ce0ed17fe..31df6cea73 100644
--- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartDataModel.java
+++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartDataModel.java
@@ -45,6 +45,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
@@ -76,7 +77,6 @@ public class DaoChartDataModel extends ChartDataModel {
this.daoStateService = daoStateService;
- // TODO getBlockTime is the bottleneck. Add a lookup map to daoState to fix that in a dedicated PR.
blockTimeOfIssuanceFunction = memoize(issuance -> {
int height = daoStateService.getStartHeightOfCurrentCycle(issuance.getChainHeight()).orElse(0);
return daoStateService.getBlockTime(height);
@@ -218,7 +218,7 @@ public class DaoChartDataModel extends ChartDataModel {
return totalBurnedByInterval;
}
- totalBurnedByInterval = getBurntBsqByInterval(daoStateService.getBurntFeeTxs(), getDateFilter());
+ totalBurnedByInterval = getBurntBsqByInterval(getBurntFeeTxStream(), getDateFilter());
return totalBurnedByInterval;
}
@@ -227,7 +227,7 @@ public class DaoChartDataModel extends ChartDataModel {
return bsqTradeFeeByInterval;
}
- bsqTradeFeeByInterval = getBurntBsqByInterval(daoStateService.getTradeFeeTxs(), getDateFilter());
+ bsqTradeFeeByInterval = getBurntBsqByInterval(getTradeFeeTxStream(), getDateFilter());
return bsqTradeFeeByInterval;
}
@@ -247,7 +247,7 @@ public class DaoChartDataModel extends ChartDataModel {
return proofOfBurnByInterval;
}
- proofOfBurnByInterval = getBurntBsqByInterval(daoStateService.getProofOfBurnTxs(), getDateFilter());
+ proofOfBurnByInterval = getBurntBsqByInterval(daoStateService.getProofOfBurnTxs().stream(), getDateFilter());
return proofOfBurnByInterval;
}
@@ -278,10 +278,9 @@ public class DaoChartDataModel extends ChartDataModel {
// Tagging started Nov 2021
// opReturn data from BTC fees: 1701721206fe6b40777763de1c741f4fd2706d94775d
Set proofOfBurnTxs = daoStateService.getProofOfBurnTxs();
- Set feeTxs = proofOfBurnTxs.stream()
- .filter(tx -> "1701721206fe6b40777763de1c741f4fd2706d94775d".equals(Hex.encode(tx.getLastTxOutput().getOpReturnData())))
- .collect(Collectors.toSet());
- proofOfBurnFromBtcFeesByInterval = getBurntBsqByInterval(feeTxs, getDateFilter());
+ Stream feeTxStream = proofOfBurnTxs.stream()
+ .filter(tx -> "1701721206fe6b40777763de1c741f4fd2706d94775d".equals(Hex.encode(tx.getLastTxOutput().getOpReturnData())));
+ proofOfBurnFromBtcFeesByInterval = getBurntBsqByInterval(feeTxStream, getDateFilter());
return proofOfBurnFromBtcFeesByInterval;
}
@@ -293,11 +292,10 @@ public class DaoChartDataModel extends ChartDataModel {
// Tagging started Nov 2021
// opReturn data from delayed payout txs: 1701e47e5d8030f444c182b5e243871ebbaeadb5e82f
// opReturn data from BM trades with a trade who got reimbursed by the DAO : 1701293c488822f98e70e047012f46f5f1647f37deb7
- Set feeTxs = daoStateService.getProofOfBurnTxs().stream()
+ Stream feeTxStream = daoStateService.getProofOfBurnTxs().stream()
.filter(e -> "1701e47e5d8030f444c182b5e243871ebbaeadb5e82f".equals(Hex.encode(e.getLastTxOutput().getOpReturnData())) ||
- "1701293c488822f98e70e047012f46f5f1647f37deb7".equals(Hex.encode(e.getLastTxOutput().getOpReturnData())))
- .collect(Collectors.toSet());
- proofOfBurnFromArbitrationByInterval = getBurntBsqByInterval(feeTxs, getDateFilter());
+ "1701293c488822f98e70e047012f46f5f1647f37deb7".equals(Hex.encode(e.getLastTxOutput().getOpReturnData())));
+ proofOfBurnFromArbitrationByInterval = getBurntBsqByInterval(feeTxStream, getDateFilter());
return proofOfBurnFromArbitrationByInterval;
}
@@ -310,7 +308,7 @@ public class DaoChartDataModel extends ChartDataModel {
Collection issuanceSetForType = daoStateService.getIssuanceItems();
// get all issued and burnt BSQ, not just the filtered date range
Map tmpIssuedByInterval = getIssuedBsqByInterval(issuanceSetForType, e -> true);
- Map tmpBurnedByInterval = new TreeMap<>(getBurntBsqByInterval(daoStateService.getBurntFeeTxs(), e -> true)
+ Map tmpBurnedByInterval = new TreeMap<>(getBurntBsqByInterval(getBurntFeeTxStream(), e -> true)
.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> -e.getValue())));
Map tmpSupplyByInterval = getMergedMap(tmpIssuedByInterval, tmpBurnedByInterval, Long::sum);
@@ -367,9 +365,10 @@ public class DaoChartDataModel extends ChartDataModel {
Long::sum));
}
- private Map getBurntBsqByInterval(Collection txs, Predicate dateFilter) {
- return txs.stream()
- .collect(Collectors.groupingBy(tx -> toTimeInterval(Instant.ofEpochMilli(tx.getTime()))))
+ private Map getBurntBsqByInterval(Stream txStream, Predicate dateFilter) {
+ var toTimeIntervalFn = toCachedTimeIntervalFn();
+ return txStream
+ .collect(Collectors.groupingBy(tx -> toTimeIntervalFn.applyAsLong(Instant.ofEpochMilli(tx.getTime()))))
.entrySet()
.stream()
.filter(entry -> dateFilter.test(entry.getKey()))
@@ -384,6 +383,20 @@ public class DaoChartDataModel extends ChartDataModel {
return date -> date >= TAG_DATE.getTimeInMillis() / 1000; // we use seconds
}
+ // TODO: Consider moving these two methods to DaoStateService:
+
+ private Stream getBurntFeeTxStream() {
+ return daoStateService.getBlocks().stream()
+ .flatMap(b -> b.getTxs().stream())
+ .filter(tx -> tx.getBurntFee() > 0);
+ }
+
+ private Stream getTradeFeeTxStream() {
+ return daoStateService.getBlocks().stream()
+ .flatMap(b -> b.getTxs().stream())
+ .filter(tx -> tx.getTxType() == TxType.PAY_TRADE_FEE);
+ }
+
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java
index 3c720ee48d..ac1d6815e3 100644
--- a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java
+++ b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java
@@ -17,6 +17,7 @@
package bisq.desktop.main.market.trades;
+import bisq.desktop.main.market.trades.TradesChartsViewModel.TickUnit;
import bisq.desktop.main.market.trades.charts.CandleData;
import bisq.desktop.util.DisplayUtils;
@@ -25,37 +26,42 @@ import bisq.core.monetary.Altcoin;
import bisq.core.trade.statistics.TradeStatistics3;
import bisq.common.util.MathUtils;
+import bisq.common.util.Tuple2;
import org.bitcoinj.core.Coin;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSortedSet;
import javafx.scene.chart.XYChart;
import javafx.util.Pair;
+import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
-import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.NavigableSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import lombok.Getter;
import static bisq.desktop.main.market.trades.TradesChartsViewModel.MAX_TICKS;
public class ChartCalculations {
+ @VisibleForTesting
static final ZoneId ZONE_ID = ZoneId.systemDefault();
@@ -63,29 +69,35 @@ public class ChartCalculations {
// Async
///////////////////////////////////////////////////////////////////////////////////////////
- static CompletableFuture