Merge pull request #6697 from stejbac/speed-up-burningman-and-statistics-view-loads

Speed up burningman and statistics view loads
This commit is contained in:
Alejandro García 2023-05-16 15:53:44 +00:00 committed by GitHub
commit ac8ad24807
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 852 additions and 422 deletions

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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.
*
* <p>Implementations should define {@link Comparable#compareTo(Object)} like follows:
* <pre>{@code
* public int compareTo(@NotNull ComparableExt<Foo> o) {
* return o instanceof Foo ? this.normalCompareTo((Foo) o) : -o.compareTo(this);
* }
* }</pre>
* @param <T>
*/
public interface ComparableExt<T> extends Comparable<ComparableExt<T>> {
@SuppressWarnings("unchecked")
@Nullable
static <E extends ComparableExt<E>> E lower(NavigableSet<E> set, Predicate<? super E> filter) {
checkComparatorNullOrNatural(set.comparator(), "Set must be naturally ordered");
return (E) ((NavigableSet<ComparableExt<E>>) set).lower(Mark.of(filter));
}
@SuppressWarnings("unchecked")
@Nullable
static <E extends ComparableExt<E>> E higher(NavigableSet<E> set, Predicate<? super E> filter) {
checkComparatorNullOrNatural(set.comparator(), "Set must be naturally ordered");
return (E) ((NavigableSet<ComparableExt<E>>) set).higher(Mark.of(filter));
}
interface Mark<T> extends ComparableExt<T> {
@SuppressWarnings("unchecked")
static <T> Mark<T> of(Predicate<? super T> filter) {
return x -> x instanceof Mark ? 0 : filter.test((T) x) ? -1 : 1;
}
}
}

View file

@ -101,7 +101,7 @@ public class MathUtils {
return BigDecimal.valueOf(value1).multiply(BigDecimal.valueOf(value2)).doubleValue(); 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) { if (list.length == 0) {
return 0L; return 0L;
} }

View file

@ -1,7 +1,12 @@
package bisq.common.util; package bisq.common.util;
import com.google.common.collect.Ordering;
import java.io.File; import java.io.File;
import java.util.Comparator;
import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format; import static java.lang.String.format;
/** /**
@ -29,4 +34,10 @@ public class Preconditions {
return dir; 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);
}
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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 <E extends ComparableExt<E>> NavigableSet<E> subSet(NavigableSet<E> 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 <E extends ComparableExt<E>> SubCollection<NavigableSet<E>, E> subSet(NavigableSet<E> set) {
return new SubCollection<>() {
@Override
public <K extends Comparable<? super K>> WithKeyFunction<NavigableSet<E>, K> withKey(Function<E, K> increasingKeyFn) {
return (Range<K> range) -> {
var fromToFilter = boundFilters(increasingKeyFn, range);
return subSet(set, fromToFilter.first, fromToFilter.second);
};
}
};
}
private static <E, K extends Comparable<? super K>> Tuple2<Predicate<E>, Predicate<E>> boundFilters(Function<E, K> keyFn,
Range<K> keyRange) {
Predicate<E> 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<C, E> {
<K extends Comparable<? super K>> WithKeyFunction<C, K> withKey(Function<E, K> increasingKeyFn);
}
public interface WithKeyFunction<C, K extends Comparable<? super K>> {
C overRange(Range<K> range);
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<TestInteger> 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<TestInteger> {
int value;
TestInteger(int value) {
this.value = value;
}
@Override
public int compareTo(@NotNull ComparableExt<TestInteger> 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);
}
}
}

View file

@ -90,6 +90,7 @@ public class BurningManPresentationService implements DaoStateListener {
private int currentChainHeight; private int currentChainHeight;
private Optional<Long> burnTarget = Optional.empty(); private Optional<Long> burnTarget = Optional.empty();
private final Map<String, BurningManCandidate> burningManCandidatesByName = new HashMap<>(); private final Map<String, BurningManCandidate> burningManCandidatesByName = new HashMap<>();
private Long accumulatedDecayedBurnedAmount;
private final Set<ReimbursementModel> reimbursements = new HashSet<>(); private final Set<ReimbursementModel> reimbursements = new HashSet<>();
private Optional<Long> averageDistributionPerCycle = Optional.empty(); private Optional<Long> averageDistributionPerCycle = Optional.empty();
private Set<String> myCompensationRequestNames = null; private Set<String> myCompensationRequestNames = null;
@ -132,6 +133,7 @@ public class BurningManPresentationService implements DaoStateListener {
burningManCandidatesByName.clear(); burningManCandidatesByName.clear();
reimbursements.clear(); reimbursements.clear();
burnTarget = Optional.empty(); burnTarget = Optional.empty();
accumulatedDecayedBurnedAmount = null;
myCompensationRequestNames = null; myCompensationRequestNames = null;
averageDistributionPerCycle = Optional.empty(); averageDistributionPerCycle = Optional.empty();
legacyBurningManDPT = Optional.empty(); legacyBurningManDPT = Optional.empty();
@ -188,8 +190,7 @@ public class BurningManPresentationService implements DaoStateListener {
long lowerBaseTarget = Math.round(burnTarget * maxCompensationShare); long lowerBaseTarget = Math.round(burnTarget * maxCompensationShare);
double maxBoostedCompensationShare = burningManCandidate.getMaxBoostedCompensationShare(); double maxBoostedCompensationShare = burningManCandidate.getMaxBoostedCompensationShare();
long upperBaseTarget = Math.round(boostedBurnTarget * maxBoostedCompensationShare); long upperBaseTarget = Math.round(boostedBurnTarget * maxBoostedCompensationShare);
Collection<BurningManCandidate> burningManCandidates = getBurningManCandidatesByName().values(); long totalBurnedAmount = getAccumulatedDecayedBurnedAmount();
long totalBurnedAmount = burnTargetService.getAccumulatedDecayedBurnedAmount(burningManCandidates, currentChainHeight);
if (totalBurnedAmount == 0) { if (totalBurnedAmount == 0) {
// The first BM would reach their max burn share by 5.46 BSQ already. But we suggest the lowerBaseTarget // 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()); receiverAddressesByBurningManName.get(name).addAll(burningManCandidate.getAllAddresses());
}); });
Map<String, String> map = new HashMap<>(); Map<String, String> map = new HashMap<>();
receiverAddressesByBurningManName receiverAddressesByBurningManName
.forEach((name, addresses) -> addresses .forEach((name, addresses) -> addresses
@ -371,4 +371,12 @@ public class BurningManPresentationService implements DaoStateListener {
proofOfBurnOpReturnTxOutputByHash.putAll(burningManService.getProofOfBurnOpReturnTxOutputByHash(currentChainHeight)); proofOfBurnOpReturnTxOutputByHash.putAll(burningManService.getProofOfBurnOpReturnTxOutputByHash(currentChainHeight));
return proofOfBurnOpReturnTxOutputByHash; return proofOfBurnOpReturnTxOutputByHash;
} }
private long getAccumulatedDecayedBurnedAmount() {
if (accumulatedDecayedBurnedAmount == null) {
Collection<BurningManCandidate> burningManCandidates = getBurningManCandidatesByName().values();
accumulatedDecayedBurnedAmount = burnTargetService.getAccumulatedDecayedBurnedAmount(burningManCandidates, currentChainHeight);
}
return accumulatedDecayedBurnedAmount;
}
} }

View file

@ -32,6 +32,7 @@ import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.Block; import bisq.core.dao.state.model.blockchain.Block;
import bisq.core.monetary.Price; import bisq.core.monetary.Price;
import bisq.core.trade.statistics.TradeStatistics3;
import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences; import bisq.core.user.Preferences;
import bisq.core.util.AveragePriceUtil; import bisq.core.util.AveragePriceUtil;
@ -49,6 +50,8 @@ import javax.inject.Singleton;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.SetChangeListener;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
@ -58,6 +61,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -87,6 +91,7 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
private final Preferences preferences; private final Preferences preferences;
private final Map<Date, Price> averageBsqPriceByMonth = new HashMap<>(getHistoricalAverageBsqPriceByMonth()); private final Map<Date, Price> averageBsqPriceByMonth = new HashMap<>(getHistoricalAverageBsqPriceByMonth());
private boolean averagePricesValid;
@Getter @Getter
private final Map<String, BalanceModel> balanceModelByBurningManName = new HashMap<>(); private final Map<String, BalanceModel> balanceModelByBurningManName = new HashMap<>();
@Getter @Getter
@ -116,13 +121,13 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
@Override @Override
public void addListeners() { public void addListeners() {
tradeStatisticsManager.getObservableTradeStatisticsSet().addListener(
(SetChangeListener<TradeStatistics3>) observable -> averagePricesValid = false);
} }
@Override @Override
public void start() { public void start() {
UserThread.execute(() -> isProcessing.set(true)); 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(); updateBalanceModelByAddress();
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(() -> {
@ -180,8 +185,11 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
} }
public Map<Date, Price> getAverageBsqPriceByMonth() { public Map<Date, Price> getAverageBsqPriceByMonth() {
getAverageBsqPriceByMonth(new Date(), HIST_BSQ_PRICE_LAST_DATE_YEAR, HIST_BSQ_PRICE_LAST_DATE_MONTH) if (!averagePricesValid) {
.forEach((key, value) -> averageBsqPriceByMonth.put(new Date(key.getTime()), Price.valueOf("BSQ", value.getValue()))); // 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; return averageBsqPriceByMonth;
} }
@ -202,21 +210,7 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
Map<Date, Price> averageBsqPriceByMonth = getAverageBsqPriceByMonth(); Map<Date, Price> averageBsqPriceByMonth = getAverageBsqPriceByMonth();
return getReceivedBtcBalanceEntryListExcludingLegacyBM().stream() return getReceivedBtcBalanceEntryListExcludingLegacyBM().stream()
.filter(e -> e.getType() == BalanceEntry.Type.BTC_TRADE_FEE_TX) .filter(e -> e.getType() == BalanceEntry.Type.BTC_TRADE_FEE_TX)
.map(balanceEntry -> { .flatMapToLong(balanceEntry -> receivedBtcAsBsq(balanceEntry, averageBsqPriceByMonth).stream())
Date month = balanceEntry.getMonth();
Optional<Price> price = Optional.ofNullable(averageBsqPriceByMonth.get(month));
long receivedBtc = balanceEntry.getAmount();
Optional<Long> 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)
.sum(); .sum();
} }
@ -231,45 +225,29 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
Map<Date, Price> averageBsqPriceByMonth = getAverageBsqPriceByMonth(); Map<Date, Price> averageBsqPriceByMonth = getAverageBsqPriceByMonth();
return getReceivedBtcBalanceEntryListExcludingLegacyBM().stream() return getReceivedBtcBalanceEntryListExcludingLegacyBM().stream()
.filter(e -> e.getType() == BalanceEntry.Type.DPT_TX) .filter(e -> e.getType() == BalanceEntry.Type.DPT_TX)
.map(balanceEntry -> { .flatMapToLong(balanceEntry -> receivedBtcAsBsq(balanceEntry, averageBsqPriceByMonth).stream())
Date month = balanceEntry.getMonth();
Optional<Price> price = Optional.ofNullable(averageBsqPriceByMonth.get(month));
long receivedBtc = balanceEntry.getAmount();
Optional<Long> 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)
.sum(); .sum();
} }
public long getTotalAmountOfDistributedBsq() { public long getTotalAmountOfDistributedBsq() {
Map<Date, Price> averageBsqPriceByMonth = getAverageBsqPriceByMonth(); Map<Date, Price> averageBsqPriceByMonth = getAverageBsqPriceByMonth();
return getReceivedBtcBalanceEntryListExcludingLegacyBM().stream() return getReceivedBtcBalanceEntryListExcludingLegacyBM().stream()
.map(balanceEntry -> { .flatMapToLong(balanceEntry -> receivedBtcAsBsq(balanceEntry, averageBsqPriceByMonth).stream())
Date month = balanceEntry.getMonth();
Optional<Price> price = Optional.ofNullable(averageBsqPriceByMonth.get(month));
long receivedBtc = balanceEntry.getAmount();
Optional<Long> 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)
.sum(); .sum();
} }
private static OptionalLong receivedBtcAsBsq(ReceivedBtcBalanceEntry balanceEntry,
Map<Date, Price> 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<ReceivedBtcBalanceEntry> getReceivedBtcBalanceEntryListExcludingLegacyBM() { private List<ReceivedBtcBalanceEntry> getReceivedBtcBalanceEntryListExcludingLegacyBM() {
if (!receivedBtcBalanceEntryListExcludingLegacyBM.isEmpty()) { if (!receivedBtcBalanceEntryListExcludingLegacyBM.isEmpty()) {
return receivedBtcBalanceEntryListExcludingLegacyBM; return receivedBtcBalanceEntryListExcludingLegacyBM;
@ -329,7 +307,7 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
private void addAccountingBlockToBalanceModel(Map<String, BalanceModel> balanceModelByBurningManName, private void addAccountingBlockToBalanceModel(Map<String, BalanceModel> balanceModelByBurningManName,
AccountingBlock accountingBlock) { AccountingBlock accountingBlock) {
accountingBlock.getTxs().forEach(tx -> { accountingBlock.getTxs().forEach(tx ->
tx.getOutputs().forEach(txOutput -> { tx.getOutputs().forEach(txOutput -> {
String name = txOutput.getName(); String name = txOutput.getName();
balanceModelByBurningManName.putIfAbsent(name, new BalanceModel()); balanceModelByBurningManName.putIfAbsent(name, new BalanceModel());
@ -337,10 +315,10 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
txOutput.getValue(), txOutput.getValue(),
new Date(accountingBlock.getDate()), new Date(accountingBlock.getDate()),
toBalanceEntryType(tx.getType()))); toBalanceEntryType(tx.getType())));
}); }));
});
} }
@SuppressWarnings("SameParameterValue")
private Map<Date, Price> getAverageBsqPriceByMonth(Date from, int backToYear, int backToMonth) { private Map<Date, Price> getAverageBsqPriceByMonth(Date from, int backToYear, int backToMonth) {
Map<Date, Price> averageBsqPriceByMonth = new HashMap<>(); Map<Date, Price> averageBsqPriceByMonth = new HashMap<>();
Calendar calendar = new GregorianCalendar(); Calendar calendar = new GregorianCalendar();

View file

@ -60,34 +60,39 @@ public class AltcoinExchangeRate {
* @throws ArithmeticException if the converted altcoin amount is too high or too low. * @throws ArithmeticException if the converted altcoin amount is too high or too low.
*/ */
public Altcoin coinToAltcoin(Coin convertCoin) { public Altcoin coinToAltcoin(Coin convertCoin) {
BigInteger converted = BigInteger.valueOf(coin.value) 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)) .multiply(BigInteger.valueOf(convertCoin.value))
.divide(BigInteger.valueOf(altcoin.value)); .divide(BigInteger.valueOf(altcoin.value))
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 .longValueExact();
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) }
throw new ArithmeticException("Overflow"); return Altcoin.valueOf(altcoin.currencyCode, converted);
return Altcoin.valueOf(altcoin.currencyCode, converted.longValue());
} }
/** /**
* 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. * @throws ArithmeticException if the converted coin amount is too high or too low.
*/ */
public Coin altcoinToCoin(Altcoin convertAltcoin) { public Coin altcoinToCoin(Altcoin convertAltcoin) {
checkArgument(convertAltcoin.currencyCode.equals(altcoin.currencyCode), "Currency mismatch: %s vs %s", checkArgument(convertAltcoin.currencyCode.equals(altcoin.currencyCode), "Currency mismatch: %s vs %s",
convertAltcoin.currencyCode, altcoin.currencyCode); convertAltcoin.currencyCode, altcoin.currencyCode);
// Use BigInteger because it's much easier to maintain full precision without overflowing. long converted;
BigInteger converted = BigInteger.valueOf(altcoin.value) 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)) .multiply(BigInteger.valueOf(convertAltcoin.value))
.divide(BigInteger.valueOf(coin.value)); .divide(BigInteger.valueOf(coin.value))
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 .longValueExact();
|| 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());
} }
return Coin.valueOf(converted);
} }
} }

View file

@ -25,9 +25,6 @@ import org.bitcoinj.core.Monetary;
import org.bitcoinj.utils.ExchangeRate; import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat; import org.bitcoinj.utils.Fiat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
@ -39,12 +36,11 @@ import org.jetbrains.annotations.NotNull;
* those classes, like {@link Fiat} or {@link Altcoin}. * those classes, like {@link Fiat} or {@link Altcoin}.
*/ */
public class Price extends MonetaryWrapper implements Comparable<Price> { public class Price extends MonetaryWrapper implements Comparable<Price> {
private static final Logger log = LoggerFactory.getLogger(Price.class);
/** /**
* Create a new {@code Price} from specified {@code Monetary}. * 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) { public Price(Monetary monetary) {
super(monetary); super(monetary);
@ -82,23 +78,41 @@ public class Price extends MonetaryWrapper implements Comparable<Price> {
public Volume getVolumeByAmount(Coin amount) { public Volume getVolumeByAmount(Coin amount) {
if (monetary instanceof Fiat) 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) else if (monetary instanceof Altcoin)
return new Volume(new AltcoinExchangeRate((Altcoin) monetary).coinToAltcoin(amount)); return new Volume(new AltcoinExchangeRate((Altcoin) monetary).coinToAltcoin(amount));
else else
throw new IllegalStateException("Monetary must be either of type Fiat or Altcoin"); 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) { public Coin getAmountByVolume(Volume volume) {
Monetary monetary = volume.getMonetary(); Monetary monetary = volume.getMonetary();
if (monetary instanceof Fiat && this.monetary instanceof Fiat) 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) else if (monetary instanceof Altcoin && this.monetary instanceof Altcoin)
return new AltcoinExchangeRate((Altcoin) this.monetary).altcoinToCoin((Altcoin) monetary); return new AltcoinExchangeRate((Altcoin) this.monetary).altcoinToCoin((Altcoin) monetary);
else else
return Coin.ZERO; 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() { public String getCurrencyCode() {
return monetary instanceof Altcoin ? ((Altcoin) monetary).getCurrencyCode() : ((Fiat) monetary).getCurrencyCode(); return monetary instanceof Altcoin ? ((Altcoin) monetary).getCurrencyCode() : ((Fiat) monetary).getCurrencyCode();
} }

View file

@ -26,8 +26,6 @@ import bisq.common.util.Tuple2;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -35,6 +33,7 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -62,16 +61,11 @@ public class DisputeAgentSelection {
DisputeAgentManager<T> disputeAgentManager, DisputeAgentManager<T> disputeAgentManager,
boolean isMediator) { boolean isMediator) {
// We take last 100 entries from trade statistics // We take last 100 entries from trade statistics
List<TradeStatistics3> list = new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet()); Stream<TradeStatistics3> stream = tradeStatisticsManager.getNavigableTradeStatisticsSet().descendingSet().stream()
list.sort(Comparator.comparing(TradeStatistics3::getDateAsLong)); .limit(LOOK_BACK_RANGE);
Collections.reverse(list);
if (!list.isEmpty()) {
int max = Math.min(list.size(), LOOK_BACK_RANGE);
list = list.subList(0, max);
}
// We stored only first 4 chars of disputeAgents onion address // We stored only first 4 chars of disputeAgents onion address
List<String> lastAddressesUsedInTrades = list.stream() List<String> lastAddressesUsedInTrades = stream
.map(tradeStatistics3 -> isMediator ? tradeStatistics3.getMediator() : tradeStatistics3.getRefundAgent()) .map(tradeStatistics3 -> isMediator ? tradeStatistics3.getMediator() : tradeStatistics3.getRefundAgent())
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toList()); .collect(Collectors.toList());

View file

@ -19,9 +19,9 @@ package bisq.core.payment;
import bisq.core.dao.governance.param.Param; import bisq.core.dao.governance.param.Param;
import bisq.core.dao.governance.period.PeriodService; import bisq.core.dao.governance.period.PeriodService;
import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.common.util.MathUtils;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
@ -37,7 +37,7 @@ import javax.annotation.Nullable;
@Slf4j @Slf4j
@Singleton @Singleton
public class TradeLimits { public class TradeLimits implements DaoStateListener {
@Nullable @Nullable
@Getter @Getter
private static TradeLimits INSTANCE; private static TradeLimits INSTANCE;
@ -45,10 +45,14 @@ public class TradeLimits {
private final DaoStateService daoStateService; private final DaoStateService daoStateService;
private final PeriodService periodService; private final PeriodService periodService;
private volatile Coin cachedMaxTradeLimit;
@Inject @Inject
public TradeLimits(DaoStateService daoStateService, PeriodService periodService) { public TradeLimits(DaoStateService daoStateService, PeriodService periodService) {
this.daoStateService = daoStateService; this.daoStateService = daoStateService;
this.periodService = periodService; this.periodService = periodService;
daoStateService.addDaoStateListener(this);
INSTANCE = this; INSTANCE = this;
} }
@ -58,6 +62,10 @@ public class TradeLimits {
// guice. // guice.
} }
@Override
public void onParseBlockCompleteAfterBatchProcessing(Block block) {
cachedMaxTradeLimit = null;
}
/** /**
* The default trade limits defined as statics in PaymentMethod are only used until the DAO * 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. * @return the maximum trade limit set by the DAO.
*/ */
public Coin getMaxTradeLimit() { 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 // 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 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 to avoid more than 4 decimal places (100000000 / 32 = 3125000 or 1 BTC / 32 = 0.03125 BTC).
// We want rounding to 0.0313 BTC // We want rounding to 0.0313 BTC
double decimalForm = MathUtils.scaleDownByPowerOf10((double) smallestLimit, 8); return ((smallestLimit + 5000L) / 10000L) * 10000L;
double rounded = MathUtils.roundDouble(decimalForm, 4);
return MathUtils.roundDoubleToLong(MathUtils.scaleUpByPowerOf10(rounded, 8));
} }
} }

View file

@ -26,11 +26,13 @@ import bisq.common.proto.persistable.PersistablePayload;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import java.util.ArrayList; import java.util.Comparator;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
@ -196,8 +198,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
// The limit and duration assignment must not be changed as that could break old offers (if amount would be higher // The limit and duration assignment must not be changed as that could break old offers (if amount would be higher
// than new trade limit) and violate the maker expectation when he created the offer (duration). // than new trade limit) and violate the maker expectation when he created the offer (duration).
@Getter private static final List<PaymentMethod> PAYMENT_METHODS = Stream.of(
private final static List<PaymentMethod> paymentMethods = new ArrayList<>(Arrays.asList(
// EUR // EUR
SEPA = new PaymentMethod(SEPA_ID, 6 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK), 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), SEPA_INSTANT = new PaymentMethod(SEPA_INSTANT_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK),
@ -277,18 +278,15 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
BLOCK_CHAINS_INSTANT = new PaymentMethod(BLOCK_CHAINS_INSTANT_ID, TimeUnit.HOURS.toMillis(1), DEFAULT_TRADE_LIMIT_VERY_LOW_RISK), BLOCK_CHAINS_INSTANT = new PaymentMethod(BLOCK_CHAINS_INSTANT_ID, TimeUnit.HOURS.toMillis(1), DEFAULT_TRADE_LIMIT_VERY_LOW_RISK),
// BsqSwap // BsqSwap
BSQ_SWAP = new PaymentMethod(BSQ_SWAP_ID, 1, DEFAULT_TRADE_LIMIT_VERY_LOW_RISK) BSQ_SWAP = new PaymentMethod(BSQ_SWAP_ID, 1, DEFAULT_TRADE_LIMIT_VERY_LOW_RISK)
)); ).sorted(Comparator.comparing(
m -> m.id.equals(CLEAR_X_CHANGE_ID) ? "ZELLE" : m.id)
).collect(Collectors.toUnmodifiableList());
static { private static final Map<String, PaymentMethod> PAYMENT_METHOD_MAP = PAYMENT_METHODS.stream()
paymentMethods.sort((o1, o2) -> { .collect(Collectors.toUnmodifiableMap(m -> m.id, m -> m));
String id1 = o1.getId();
if (id1.equals(CLEAR_X_CHANGE_ID)) public static List<PaymentMethod> getPaymentMethods() {
id1 = "ZELLE"; return PAYMENT_METHODS;
String id2 = o2.getId();
if (id2.equals(CLEAR_X_CHANGE_ID))
id2 = "ZELLE";
return id1.compareTo(id2);
});
} }
public static PaymentMethod getDummyPaymentMethod(String id) { public static PaymentMethod getDummyPaymentMethod(String id) {
@ -371,9 +369,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
// We look up only our active payment methods not retired ones. // We look up only our active payment methods not retired ones.
public static Optional<PaymentMethod> getActivePaymentMethod(String id) { public static Optional<PaymentMethod> getActivePaymentMethod(String id) {
return paymentMethods.stream() return Optional.ofNullable(PAYMENT_METHOD_MAP.get(id));
.filter(e -> e.getId().equals(id))
.findFirst();
} }
public Coin getMaxTradeLimitAsCoin(String currencyCode) { public Coin getMaxTradeLimitAsCoin(String currencyCode) {

View file

@ -18,7 +18,6 @@
package bisq.core.trade.statistics; package bisq.core.trade.statistics;
import bisq.core.monetary.Altcoin; import bisq.core.monetary.Altcoin;
import bisq.core.monetary.AltcoinExchangeRate;
import bisq.core.monetary.Price; import bisq.core.monetary.Price;
import bisq.core.monetary.Volume; import bisq.core.monetary.Volume;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
@ -45,8 +44,6 @@ import bisq.common.util.Utilities;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
@ -311,12 +308,10 @@ public final class TradeStatistics2 implements ProcessOncePersistableNetworkPayl
} }
public Volume getTradeVolume() { public Volume getTradeVolume() {
if (getTradePrice().getMonetary() instanceof Altcoin) { Price price = getTradePrice();
return new Volume(new AltcoinExchangeRate((Altcoin) getTradePrice().getMonetary()).coinToAltcoin(getTradeAmount())); return price.getMonetary() instanceof Altcoin
} else { ? price.getVolumeByAmount(getTradeAmount())
Volume volume = new Volume(new ExchangeRate((Fiat) getTradePrice().getMonetary()).coinToFiat(getTradeAmount())); : VolumeUtil.getRoundedFiatVolume(price.getVolumeByAmount(getTradeAmount()));
return VolumeUtil.getRoundedFiatVolume(volume);
}
} }
public boolean isValid() { public boolean isValid() {

View file

@ -19,7 +19,6 @@ package bisq.core.trade.statistics;
import bisq.core.locale.CurrencyUtil; import bisq.core.locale.CurrencyUtil;
import bisq.core.monetary.Altcoin; import bisq.core.monetary.Altcoin;
import bisq.core.monetary.AltcoinExchangeRate;
import bisq.core.monetary.Price; import bisq.core.monetary.Price;
import bisq.core.monetary.Volume; import bisq.core.monetary.Volume;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
@ -41,6 +40,7 @@ import bisq.common.app.Capability;
import bisq.common.crypto.Hash; import bisq.common.crypto.Hash;
import bisq.common.proto.ProtoUtil; import bisq.common.proto.ProtoUtil;
import bisq.common.util.CollectionUtils; import bisq.common.util.CollectionUtils;
import bisq.common.util.ComparableExt;
import bisq.common.util.ExtraDataMapValidator; import bisq.common.util.ExtraDataMapValidator;
import bisq.common.util.JsonExclude; import bisq.common.util.JsonExclude;
import bisq.common.util.Utilities; import bisq.common.util.Utilities;
@ -48,8 +48,6 @@ import bisq.common.util.Utilities;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.bitcoinj.core.Coin; 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.annotations.VisibleForTesting;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
@ -59,15 +57,19 @@ import java.time.ZoneId;
import java.util.Arrays; import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
@ -78,7 +80,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
*/ */
@Slf4j @Slf4j
public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayload, PersistableNetworkPayload, public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayload, PersistableNetworkPayload,
CapabilityRequiringPayload, DateSortedTruncatablePayload { CapabilityRequiringPayload, DateSortedTruncatablePayload, ComparableExt<TradeStatistics3> {
@JsonExclude @JsonExclude
private transient static final ZoneId ZONE_ID = ZoneId.systemDefault(); 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. // 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 // 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. // recognized.
private enum PaymentMethodMapper { @VisibleForTesting
enum PaymentMethodMapper {
OK_PAY, OK_PAY,
CASH_APP, CASH_APP,
VENMO, VENMO,
@ -194,7 +197,9 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
TIKKIE, TIKKIE,
TRANSFERWISE_USD, TRANSFERWISE_USD,
ACH_TRANSFER, ACH_TRANSFER,
DOMESTIC_WIRE_TRANSFER DOMESTIC_WIRE_TRANSFER;
private static final PaymentMethodMapper[] values = values(); // cache for perf gain
} }
@Getter @Getter
@ -298,7 +303,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
String tempPaymentMethod; String tempPaymentMethod;
try { try {
tempPaymentMethod = String.valueOf(PaymentMethodMapper.valueOf(paymentMethod).ordinal()); tempPaymentMethod = String.valueOf(PaymentMethodMapper.valueOf(paymentMethod).ordinal());
} catch (Throwable t) { } catch (IllegalArgumentException e) {
tempPaymentMethod = paymentMethod; tempPaymentMethod = paymentMethod;
} }
this.paymentMethod = tempPaymentMethod; this.paymentMethod = tempPaymentMethod;
@ -382,8 +387,9 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
} }
public LocalDateTime getLocalDateTime() { public LocalDateTime getLocalDateTime() {
LocalDateTime localDateTime = this.localDateTime;
if (localDateTime == null) { if (localDateTime == null) {
localDateTime = dateObj.toInstant().atZone(ZONE_ID).toLocalDateTime(); this.localDateTime = localDateTime = dateObj.toInstant().atZone(ZONE_ID).toLocalDateTime();
} }
return localDateTime; return localDateTime;
} }
@ -403,9 +409,12 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
} }
public String getPaymentMethodId() { public String getPaymentMethodId() {
if (paymentMethod.isEmpty() || paymentMethod.charAt(0) > '9') {
return paymentMethod;
}
try { try {
return PaymentMethodMapper.values()[Integer.parseInt(paymentMethod)].name(); return PaymentMethodMapper.values[Integer.parseInt(paymentMethod)].name();
} catch (Throwable ignore) { } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
return paymentMethod; return paymentMethod;
} }
} }
@ -413,8 +422,9 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
private transient Price priceObj; private transient Price priceObj;
public Price getTradePrice() { public Price getTradePrice() {
Price priceObj = this.priceObj;
if (priceObj == null) { if (priceObj == null) {
priceObj = Price.valueOf(currency, price); this.priceObj = priceObj = Price.valueOf(currency, price);
} }
return priceObj; return priceObj;
} }
@ -424,13 +434,12 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
} }
public Volume getTradeVolume() { public Volume getTradeVolume() {
Volume volume = this.volume;
if (volume == null) { if (volume == null) {
if (getTradePrice().getMonetary() instanceof Altcoin) { Price price = getTradePrice();
volume = new Volume(new AltcoinExchangeRate((Altcoin) getTradePrice().getMonetary()).coinToAltcoin(getTradeAmount())); this.volume = volume = price.getMonetary() instanceof Altcoin
} else { ? price.getVolumeByAmount(getTradeAmount())
Volume exactVolume = new Volume(new ExchangeRate((Fiat) getTradePrice().getMonetary()).coinToFiat(getTradeAmount())); : VolumeUtil.getRoundedFiatVolume(price.getVolumeByAmount(getTradeAmount()));
volume = VolumeUtil.getRoundedFiatVolume(exactVolume);
}
} }
return volume; return volume;
} }
@ -472,6 +481,27 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
currencyFound; currencyFound;
} }
private static <T extends Comparable<? super T>> int nullsFirstCompare(T a, T b) {
return Comparator.nullsFirst(Comparator.<T>naturalOrder()).compare(a, b);
}
@Override
public int compareTo(@NotNull ComparableExt<TradeStatistics3> 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 @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@ -482,9 +512,8 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
if (price != that.price) return false; if (price != that.price) return false;
if (amount != that.amount) return false; if (amount != that.amount) return false;
if (date != that.date) return false; if (date != that.date) return false;
if (currency != null ? !currency.equals(that.currency) : that.currency != null) return false; if (!Objects.equals(currency, that.currency)) return false;
if (paymentMethod != null ? !paymentMethod.equals(that.paymentMethod) : that.paymentMethod != null) if (!Objects.equals(paymentMethod, that.paymentMethod)) return false;
return false;
return Arrays.equals(hash, that.hash); return Arrays.equals(hash, that.hash);
} }

View file

@ -47,11 +47,13 @@ import java.time.Instant;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.NavigableSet;
import java.util.Set; import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -68,7 +70,8 @@ public class TradeStatisticsManager {
private final TradeStatisticsConverter tradeStatisticsConverter; private final TradeStatisticsConverter tradeStatisticsConverter;
private final File storageDir; private final File storageDir;
private final boolean dumpStatistics; private final boolean dumpStatistics;
private final ObservableSet<TradeStatistics3> observableTradeStatisticsSet = FXCollections.observableSet(); private final NavigableSet<TradeStatistics3> navigableTradeStatisticsSet = new TreeSet<>();
private final ObservableSet<TradeStatistics3> observableTradeStatisticsSet = FXCollections.observableSet(navigableTradeStatisticsSet);
private JsonFileManager jsonFileManager; private JsonFileManager jsonFileManager;
@Inject @Inject
@ -110,39 +113,24 @@ public class TradeStatisticsManager {
} }
}); });
Set<TradeStatistics3> set = tradeStatistics3StorageService.getMapOfAllData().values().stream() tradeStatistics3StorageService.getMapOfAllData().values().stream()
.filter(e -> e instanceof TradeStatistics3) .filter(e -> e instanceof TradeStatistics3)
.map(e -> (TradeStatistics3) e) .map(e -> (TradeStatistics3) e)
.filter(TradeStatistics3::isValid) .filter(TradeStatistics3::isValid)
.collect(Collectors.toSet()); .forEach(observableTradeStatisticsSet::add);
observableTradeStatisticsSet.addAll(set);
// collate prices by ccy -- takes about 10 ms for 5000 items
Map<String, List<TradeStatistics3>> allPriceByCurrencyCode = new HashMap<>();
observableTradeStatisticsSet.forEach(e -> {
List<TradeStatistics3> 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 // get the most recent price for each ccy and notify priceFeedService
// (this relies on the trade statistics set being sorted by date)
Map<String, Price> newestPriceByCurrencyCode = new HashMap<>(); Map<String, Price> newestPriceByCurrencyCode = new HashMap<>();
allPriceByCurrencyCode.values().stream() observableTradeStatisticsSet.forEach(e -> newestPriceByCurrencyCode.put(e.getCurrency(), e.getTradePrice()));
.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());
});
priceFeedService.applyInitialBisqMarketPrice(newestPriceByCurrencyCode); priceFeedService.applyInitialBisqMarketPrice(newestPriceByCurrencyCode);
maybeDumpStatistics(); maybeDumpStatistics();
} }
public NavigableSet<TradeStatistics3> getNavigableTradeStatisticsSet() {
return Collections.unmodifiableNavigableSet(navigableTradeStatisticsSet);
}
public ObservableSet<TradeStatistics3> getObservableTradeStatisticsSet() { public ObservableSet<TradeStatistics3> getObservableTradeStatisticsSet() {
return observableTradeStatisticsSet; return observableTradeStatisticsSet;
} }
@ -170,7 +158,7 @@ public class TradeStatisticsManager {
Instant yearAgo = Instant.ofEpochSecond(Instant.now().getEpochSecond() - TimeUnit.DAYS.toSeconds(365)); Instant yearAgo = Instant.ofEpochSecond(Instant.now().getEpochSecond() - TimeUnit.DAYS.toSeconds(365));
Set<String> activeCurrencies = observableTradeStatisticsSet.stream() Set<String> activeCurrencies = observableTradeStatisticsSet.stream()
.filter(e -> e.getDate().toInstant().isAfter(yearAgo)) .filter(e -> e.getDate().toInstant().isAfter(yearAgo))
.map(p -> p.getCurrency()) .map(TradeStatistics3::getCurrency)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
ArrayList<CurrencyTuple> activeFiatCurrencyList = fiatCurrencyList.stream() ArrayList<CurrencyTuple> activeFiatCurrencyList = fiatCurrencyList.stream()

View file

@ -24,16 +24,21 @@ import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences; import bisq.core.user.Preferences;
import bisq.common.util.MathUtils; import bisq.common.util.MathUtils;
import bisq.common.util.RangeUtils;
import bisq.common.util.Tuple2; import bisq.common.util.Tuple2;
import org.bitcoinj.utils.Fiat; import org.bitcoinj.utils.Fiat;
import com.google.common.collect.Range;
import com.google.common.primitives.Doubles;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class AveragePriceUtil { public class AveragePriceUtil {
@ -58,23 +63,21 @@ public class AveragePriceUtil {
Date pastXDays, Date pastXDays,
Date date) { Date date) {
double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100)); double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100));
List<TradeStatistics3> bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() Set<TradeStatistics3> allTradePastXDays = RangeUtils.subSet(tradeStatisticsManager.getNavigableTradeStatisticsSet())
.filter(e -> e.getCurrency().equals("BSQ")) .withKey(TradeStatistics3::getDate)
.filter(e -> e.getDate().after(pastXDays)) .overRange(Range.open(pastXDays, date));
.filter(e -> e.getDate().before(date))
.collect(Collectors.toList()); Map<Boolean, List<TradeStatistics3>> bsqUsdAllTradePastXDays = allTradePastXDays.stream()
List<TradeStatistics3> bsqTradePastXDays = percentToTrim > 0 ? .filter(e -> e.getCurrency().equals("USD") || e.getCurrency().equals("BSQ"))
removeOutliers(bsqAllTradePastXDays, percentToTrim) : .collect(Collectors.partitioningBy(e -> e.getCurrency().equals("USD")));
bsqAllTradePastXDays;
List<TradeStatistics3> bsqTradePastXDays = percentToTrim > 0 ?
removeOutliers(bsqUsdAllTradePastXDays.get(false), percentToTrim) :
bsqUsdAllTradePastXDays.get(false);
List<TradeStatistics3> 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<TradeStatistics3> usdTradePastXDays = percentToTrim > 0 ? List<TradeStatistics3> usdTradePastXDays = percentToTrim > 0 ?
removeOutliers(usdAllTradePastXDays, percentToTrim) : removeOutliers(bsqUsdAllTradePastXDays.get(true), percentToTrim) :
usdAllTradePastXDays; bsqUsdAllTradePastXDays.get(true);
Price usdPrice = Price.valueOf("USD", getUSDAverage(bsqTradePastXDays, usdTradePastXDays)); Price usdPrice = Price.valueOf("USD", getUSDAverage(bsqTradePastXDays, usdTradePastXDays));
Price bsqPrice = Price.valueOf("BSQ", getBTCAverage(bsqTradePastXDays)); Price bsqPrice = Price.valueOf("BSQ", getBTCAverage(bsqTradePastXDays));
@ -82,17 +85,17 @@ public class AveragePriceUtil {
} }
private static List<TradeStatistics3> removeOutliers(List<TradeStatistics3> list, double percentToTrim) { private static List<TradeStatistics3> removeOutliers(List<TradeStatistics3> list, double percentToTrim) {
List<Double> yValues = list.stream() List<Double> yValues = Doubles.asList(list.stream()
.filter(TradeStatistics3::isValid) .filter(TradeStatistics3::isValid)
.map(e -> (double) e.getPrice()) .mapToDouble(TradeStatistics3::getPrice)
.collect(Collectors.toList()); .toArray());
Tuple2<Double, Double> tuple = InlierUtil.findInlierRange(yValues, percentToTrim, HOW_MANY_STD_DEVS_CONSTITUTE_OUTLIER); Tuple2<Double, Double> tuple = InlierUtil.findInlierRange(yValues, percentToTrim, HOW_MANY_STD_DEVS_CONSTITUTE_OUTLIER);
double lowerBound = tuple.first; double lowerBound = tuple.first;
double upperBound = tuple.second; double upperBound = tuple.second;
return list.stream() return list.stream()
.filter(e -> e.getPrice() > lowerBound) .filter(e -> (double) e.getPrice() >= lowerBound)
.filter(e -> e.getPrice() < upperBound) .filter(e -> (double) e.getPrice() <= upperBound)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@ -111,21 +114,23 @@ public class AveragePriceUtil {
return averagePrice; return averagePrice;
} }
private static long getUSDAverage(List<TradeStatistics3> bsqList, List<TradeStatistics3> usdList) { private static long getUSDAverage(List<TradeStatistics3> sortedBsqList, List<TradeStatistics3> sortedUsdList) {
// Use next USD/BTC print as price to calculate BSQ/USD rate // Use next USD/BTC print as price to calculate BSQ/USD rate
// Store each trade as amount of USD and amount of BSQ traded // Store each trade as amount of USD and amount of BSQ traded
List<Tuple2<Double, Double>> usdBsqList = new ArrayList<>(bsqList.size()); List<Tuple2<Double, Double>> usdBsqList = new ArrayList<>(sortedBsqList.size());
usdList.sort(Comparator.comparing(TradeStatistics3::getDateAsLong));
var usdBTCPrice = 10000d; // Default to 10000 USD per BTC if there is no USD feed at all 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 // Find usd price for trade item
usdBTCPrice = usdList.stream() for (; i < sortedUsdList.size(); i++) {
.filter(usd -> usd.getDateAsLong() > item.getDateAsLong()) TradeStatistics3 usd = sortedUsdList.get(i);
.map(usd -> MathUtils.scaleDownByPowerOf10((double) usd.getTradePrice().getValue(), if (usd.getDateAsLong() > item.getDateAsLong()) {
Fiat.SMALLEST_UNIT_EXPONENT)) usdBTCPrice = MathUtils.scaleDownByPowerOf10((double) usd.getTradePrice().getValue(),
.findFirst() Fiat.SMALLEST_UNIT_EXPONENT);
.orElse(usdBTCPrice); break;
}
}
var bsqAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeVolume().getValue(), var bsqAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeVolume().getValue(),
Altcoin.SMALLEST_UNIT_EXPONENT); Altcoin.SMALLEST_UNIT_EXPONENT);
var btcAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeAmount().getValue(), var btcAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeAmount().getValue(),

View file

@ -20,11 +20,15 @@ package bisq.core.util;
import bisq.common.util.DoubleSummaryStatisticsWithStdDev; import bisq.common.util.DoubleSummaryStatisticsWithStdDev;
import bisq.common.util.Tuple2; 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.DoubleSummaryStatistics;
import java.util.List; 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 { public class InlierUtil {
@ -39,11 +43,8 @@ public class InlierUtil {
Tuple2<Double, Double> inlierThreshold = Tuple2<Double, Double> inlierThreshold =
computeInlierThreshold(yValues, percentToTrim, howManyStdDevsConstituteOutlier); computeInlierThreshold(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
DoubleSummaryStatistics inlierStatistics = DoubleSummaryStatistics inlierStatistics = stream(yValues)
yValues .filter(withinBounds(inlierThreshold))
.stream()
.filter(y -> withinBounds(inlierThreshold, y))
.mapToDouble(Double::doubleValue)
.summaryStatistics(); .summaryStatistics();
var inlierMin = inlierStatistics.getMin(); var inlierMin = inlierStatistics.getMin();
@ -52,10 +53,10 @@ public class InlierUtil {
return new Tuple2<>(inlierMin, inlierMax); return new Tuple2<>(inlierMin, inlierMax);
} }
private static boolean withinBounds(Tuple2<Double, Double> bounds, double number) { private static DoublePredicate withinBounds(Tuple2<Double, Double> bounds) {
var lowerBound = bounds.first; double lowerBound = bounds.first;
var upperBound = bounds.second; double upperBound = bounds.second;
return (lowerBound <= number) && (number <= upperBound); return number -> lowerBound <= number && number <= upperBound;
} }
/* Computes the lower and upper inlier thresholds. A point lying outside /* Computes the lower and upper inlier thresholds. A point lying outside
@ -75,10 +76,8 @@ public class InlierUtil {
List<Double> trimmed = trim(percentToTrim, numbers); List<Double> trimmed = trim(percentToTrim, numbers);
DoubleSummaryStatisticsWithStdDev summaryStatistics = DoubleSummaryStatisticsWithStdDev summaryStatistics = stream(trimmed)
trimmed.stream() .collect(DoubleSummaryStatisticsWithStdDev::new,
.collect(
DoubleSummaryStatisticsWithStdDev::new,
DoubleSummaryStatisticsWithStdDev::accept, DoubleSummaryStatisticsWithStdDev::accept,
DoubleSummaryStatisticsWithStdDev::combine); DoubleSummaryStatisticsWithStdDev::combine);
@ -111,7 +110,7 @@ public class InlierUtil {
return numbers; return numbers;
} }
if (totalPercentTrim == 100) { if (totalPercentTrim == 100) {
return FXCollections.emptyObservableList(); return Doubles.asList();
} }
if (numbers.isEmpty()) { if (numbers.isEmpty()) {
@ -124,17 +123,17 @@ public class InlierUtil {
return numbers; return numbers;
} }
var sorted = numbers.stream().sorted(); var array = stream(numbers).toArray();
Arrays.sort(array);
var oneSideTrimmed = sorted.skip(countToDropFromEachSide); var sorted = Doubles.asList(array);
return sorted.subList(countToDropFromEachSide, sorted.size() - 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());
} }
private static DoubleStream stream(Iterable<Double> doubles) {
var spliterator = doubles.spliterator();
return spliterator instanceof Spliterator.OfDouble
? StreamSupport.doubleStream((Spliterator.OfDouble) spliterator, false)
: StreamSupport.stream(spliterator, false).mapToDouble(Double::doubleValue);
}
} }

View file

@ -19,14 +19,10 @@ package bisq.core.util;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.monetary.Altcoin; import bisq.core.monetary.Altcoin;
import bisq.core.monetary.AltcoinExchangeRate;
import bisq.core.monetary.Price;
import bisq.core.monetary.Volume; import bisq.core.monetary.Volume;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Monetary; import org.bitcoinj.core.Monetary;
import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat; import org.bitcoinj.utils.Fiat;
import org.bitcoinj.utils.MonetaryFormat; import org.bitcoinj.utils.MonetaryFormat;
@ -66,14 +62,6 @@ public class VolumeUtil {
return Volume.parse(String.valueOf(roundedVolume), volumeByAmount.getCurrencyCode()); 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) { public static String formatVolume(Offer offer, Boolean decimalAligned, int maxNumberOfDigits) {
return formatVolume(offer, decimalAligned, maxNumberOfDigits, true); return formatVolume(offer, decimalAligned, maxNumberOfDigits, true);

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> paymentMethodCodes = PaymentMethod.getPaymentMethods().stream()
.map(PaymentMethod::getId)
.collect(Collectors.toSet());
Set<String> wrapperCodes = Arrays.stream(TradeStatistics3.PaymentMethodMapper.values())
.map(Enum::name)
.collect(Collectors.toSet());
assertEquals(Set.of(), Sets.difference(paymentMethodCodes, wrapperCodes));
}
}

View file

@ -22,10 +22,10 @@ import bisq.desktop.common.model.ActivatableDataModel;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.TemporalAdjuster; import java.time.temporal.TemporalAdjuster;
import java.util.Comparator;
import java.util.Map; import java.util.Map;
import java.util.function.BinaryOperator; import java.util.function.BinaryOperator;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.ToLongFunction;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -64,6 +64,11 @@ public abstract class ChartDataModel extends ActivatableDataModel {
return temporalAdjusterModel.toTimeInterval(instant); return temporalAdjusterModel.toTimeInterval(instant);
} }
// optimized for use when the input times are sequential and not too spread out
public ToLongFunction<Instant> toCachedTimeIntervalFn() {
return temporalAdjusterModel.withCache()::toTimeInterval;
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Date filter predicate // Date filter predicate

View file

@ -18,6 +18,7 @@
package bisq.desktop.components.chart; package bisq.desktop.components.chart;
import bisq.common.util.MathUtils; import bisq.common.util.MathUtils;
import bisq.common.util.Tuple3;
import java.time.DayOfWeek; import java.time.DayOfWeek;
import java.time.Instant; import java.time.Instant;
@ -37,6 +38,19 @@ import static java.time.temporal.ChronoField.DAY_OF_YEAR;
public class TemporalAdjusterModel { public class TemporalAdjusterModel {
private static final ZoneId ZONE_ID = ZoneId.systemDefault(); private static final ZoneId ZONE_ID = ZoneId.systemDefault();
protected TemporalAdjuster temporalAdjuster = Interval.MONTH.getAdjuster();
private boolean enableCache;
private Tuple3<LocalDate, Instant, Instant> cachedDateStartEndTuple;
private Tuple3<LocalDate, TemporalAdjuster, Long> cachedTimeIntervalMapping;
public TemporalAdjusterModel withCache() {
var model = new TemporalAdjusterModel();
model.temporalAdjuster = this.temporalAdjuster;
model.enableCache = true;
return model;
}
public enum Interval { public enum Interval {
YEAR(TemporalAdjusters.firstDayOfYear()), YEAR(TemporalAdjusters.firstDayOfYear()),
HALF_YEAR(temporal -> { HALF_YEAR(temporal -> {
@ -81,8 +95,6 @@ public class TemporalAdjusterModel {
} }
} }
protected TemporalAdjuster temporalAdjuster = Interval.MONTH.getAdjuster();
public void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) { public void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) {
this.temporalAdjuster = temporalAdjuster; this.temporalAdjuster = temporalAdjuster;
} }
@ -96,12 +108,36 @@ public class TemporalAdjusterModel {
} }
public long toTimeInterval(Instant instant, TemporalAdjuster temporalAdjuster) { public long toTimeInterval(Instant instant, TemporalAdjuster temporalAdjuster) {
return instant LocalDate date = toLocalDate(instant);
.atZone(ZONE_ID) Tuple3<LocalDate, TemporalAdjuster, Long> tuple = cachedTimeIntervalMapping;
.toLocalDate() long timeInterval;
if (tuple != null && date.equals(tuple.first) && temporalAdjuster.equals(tuple.second)) {
timeInterval = tuple.third;
} else {
timeInterval = date
.with(temporalAdjuster) .with(temporalAdjuster)
.atStartOfDay(ZONE_ID) .atStartOfDay(ZONE_ID)
.toInstant() .toEpochSecond();
.getEpochSecond(); if (enableCache) {
cachedTimeIntervalMapping = new Tuple3<>(date, temporalAdjuster, timeInterval);
}
}
return timeInterval;
}
private LocalDate toLocalDate(Instant instant) {
LocalDate date;
Tuple3<LocalDate, Instant, Instant> 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;
} }
} }

View file

@ -188,9 +188,10 @@ public class PriceChartDataModel extends ChartDataModel {
private Map<Long, Double> getPriceByInterval(Predicate<TradeStatistics3> collectionFilter, private Map<Long, Double> getPriceByInterval(Predicate<TradeStatistics3> collectionFilter,
Function<List<TradeStatistics3>, Double> getAveragePriceFunction) { Function<List<TradeStatistics3>, Double> getAveragePriceFunction) {
var toTimeIntervalFn = toCachedTimeIntervalFn();
return getPriceByInterval(tradeStatisticsManager.getObservableTradeStatisticsSet(), return getPriceByInterval(tradeStatisticsManager.getObservableTradeStatisticsSet(),
collectionFilter, collectionFilter,
tradeStatistics -> toTimeInterval(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())), tradeStatistics -> toTimeIntervalFn.applyAsLong(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())),
dateFilter, dateFilter,
getAveragePriceFunction); getAveragePriceFunction);
} }

View file

@ -134,8 +134,9 @@ public class VolumeChartDataModel extends ChartDataModel {
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private Map<Long, Long> getVolumeByInterval(Function<List<TradeStatistics3>, Long> getVolumeFunction) { private Map<Long, Long> getVolumeByInterval(Function<List<TradeStatistics3>, Long> getVolumeFunction) {
var toTimeIntervalFn = toCachedTimeIntervalFn();
return getVolumeByInterval(tradeStatisticsManager.getObservableTradeStatisticsSet(), return getVolumeByInterval(tradeStatisticsManager.getObservableTradeStatisticsSet(),
tradeStatistics -> toTimeInterval(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())), tradeStatistics -> toTimeIntervalFn.applyAsLong(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())),
dateFilter, dateFilter,
getVolumeFunction); getVolumeFunction);
} }

View file

@ -45,6 +45,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -76,7 +77,6 @@ public class DaoChartDataModel extends ChartDataModel {
this.daoStateService = daoStateService; this.daoStateService = daoStateService;
// TODO getBlockTime is the bottleneck. Add a lookup map to daoState to fix that in a dedicated PR.
blockTimeOfIssuanceFunction = memoize(issuance -> { blockTimeOfIssuanceFunction = memoize(issuance -> {
int height = daoStateService.getStartHeightOfCurrentCycle(issuance.getChainHeight()).orElse(0); int height = daoStateService.getStartHeightOfCurrentCycle(issuance.getChainHeight()).orElse(0);
return daoStateService.getBlockTime(height); return daoStateService.getBlockTime(height);
@ -218,7 +218,7 @@ public class DaoChartDataModel extends ChartDataModel {
return totalBurnedByInterval; return totalBurnedByInterval;
} }
totalBurnedByInterval = getBurntBsqByInterval(daoStateService.getBurntFeeTxs(), getDateFilter()); totalBurnedByInterval = getBurntBsqByInterval(getBurntFeeTxStream(), getDateFilter());
return totalBurnedByInterval; return totalBurnedByInterval;
} }
@ -227,7 +227,7 @@ public class DaoChartDataModel extends ChartDataModel {
return bsqTradeFeeByInterval; return bsqTradeFeeByInterval;
} }
bsqTradeFeeByInterval = getBurntBsqByInterval(daoStateService.getTradeFeeTxs(), getDateFilter()); bsqTradeFeeByInterval = getBurntBsqByInterval(getTradeFeeTxStream(), getDateFilter());
return bsqTradeFeeByInterval; return bsqTradeFeeByInterval;
} }
@ -247,7 +247,7 @@ public class DaoChartDataModel extends ChartDataModel {
return proofOfBurnByInterval; return proofOfBurnByInterval;
} }
proofOfBurnByInterval = getBurntBsqByInterval(daoStateService.getProofOfBurnTxs(), getDateFilter()); proofOfBurnByInterval = getBurntBsqByInterval(daoStateService.getProofOfBurnTxs().stream(), getDateFilter());
return proofOfBurnByInterval; return proofOfBurnByInterval;
} }
@ -278,10 +278,9 @@ public class DaoChartDataModel extends ChartDataModel {
// Tagging started Nov 2021 // Tagging started Nov 2021
// opReturn data from BTC fees: 1701721206fe6b40777763de1c741f4fd2706d94775d // opReturn data from BTC fees: 1701721206fe6b40777763de1c741f4fd2706d94775d
Set<Tx> proofOfBurnTxs = daoStateService.getProofOfBurnTxs(); Set<Tx> proofOfBurnTxs = daoStateService.getProofOfBurnTxs();
Set<Tx> feeTxs = proofOfBurnTxs.stream() Stream<Tx> feeTxStream = proofOfBurnTxs.stream()
.filter(tx -> "1701721206fe6b40777763de1c741f4fd2706d94775d".equals(Hex.encode(tx.getLastTxOutput().getOpReturnData()))) .filter(tx -> "1701721206fe6b40777763de1c741f4fd2706d94775d".equals(Hex.encode(tx.getLastTxOutput().getOpReturnData())));
.collect(Collectors.toSet()); proofOfBurnFromBtcFeesByInterval = getBurntBsqByInterval(feeTxStream, getDateFilter());
proofOfBurnFromBtcFeesByInterval = getBurntBsqByInterval(feeTxs, getDateFilter());
return proofOfBurnFromBtcFeesByInterval; return proofOfBurnFromBtcFeesByInterval;
} }
@ -293,11 +292,10 @@ public class DaoChartDataModel extends ChartDataModel {
// Tagging started Nov 2021 // Tagging started Nov 2021
// opReturn data from delayed payout txs: 1701e47e5d8030f444c182b5e243871ebbaeadb5e82f // opReturn data from delayed payout txs: 1701e47e5d8030f444c182b5e243871ebbaeadb5e82f
// opReturn data from BM trades with a trade who got reimbursed by the DAO : 1701293c488822f98e70e047012f46f5f1647f37deb7 // opReturn data from BM trades with a trade who got reimbursed by the DAO : 1701293c488822f98e70e047012f46f5f1647f37deb7
Set<Tx> feeTxs = daoStateService.getProofOfBurnTxs().stream() Stream<Tx> feeTxStream = daoStateService.getProofOfBurnTxs().stream()
.filter(e -> "1701e47e5d8030f444c182b5e243871ebbaeadb5e82f".equals(Hex.encode(e.getLastTxOutput().getOpReturnData())) || .filter(e -> "1701e47e5d8030f444c182b5e243871ebbaeadb5e82f".equals(Hex.encode(e.getLastTxOutput().getOpReturnData())) ||
"1701293c488822f98e70e047012f46f5f1647f37deb7".equals(Hex.encode(e.getLastTxOutput().getOpReturnData()))) "1701293c488822f98e70e047012f46f5f1647f37deb7".equals(Hex.encode(e.getLastTxOutput().getOpReturnData())));
.collect(Collectors.toSet()); proofOfBurnFromArbitrationByInterval = getBurntBsqByInterval(feeTxStream, getDateFilter());
proofOfBurnFromArbitrationByInterval = getBurntBsqByInterval(feeTxs, getDateFilter());
return proofOfBurnFromArbitrationByInterval; return proofOfBurnFromArbitrationByInterval;
} }
@ -310,7 +308,7 @@ public class DaoChartDataModel extends ChartDataModel {
Collection<Issuance> issuanceSetForType = daoStateService.getIssuanceItems(); Collection<Issuance> issuanceSetForType = daoStateService.getIssuanceItems();
// get all issued and burnt BSQ, not just the filtered date range // get all issued and burnt BSQ, not just the filtered date range
Map<Long, Long> tmpIssuedByInterval = getIssuedBsqByInterval(issuanceSetForType, e -> true); Map<Long, Long> tmpIssuedByInterval = getIssuedBsqByInterval(issuanceSetForType, e -> true);
Map<Long, Long> tmpBurnedByInterval = new TreeMap<>(getBurntBsqByInterval(daoStateService.getBurntFeeTxs(), e -> true) Map<Long, Long> tmpBurnedByInterval = new TreeMap<>(getBurntBsqByInterval(getBurntFeeTxStream(), e -> true)
.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> -e.getValue()))); .entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> -e.getValue())));
Map<Long, Long> tmpSupplyByInterval = getMergedMap(tmpIssuedByInterval, tmpBurnedByInterval, Long::sum); Map<Long, Long> tmpSupplyByInterval = getMergedMap(tmpIssuedByInterval, tmpBurnedByInterval, Long::sum);
@ -367,9 +365,10 @@ public class DaoChartDataModel extends ChartDataModel {
Long::sum)); Long::sum));
} }
private Map<Long, Long> getBurntBsqByInterval(Collection<Tx> txs, Predicate<Long> dateFilter) { private Map<Long, Long> getBurntBsqByInterval(Stream<Tx> txStream, Predicate<Long> dateFilter) {
return txs.stream() var toTimeIntervalFn = toCachedTimeIntervalFn();
.collect(Collectors.groupingBy(tx -> toTimeInterval(Instant.ofEpochMilli(tx.getTime())))) return txStream
.collect(Collectors.groupingBy(tx -> toTimeIntervalFn.applyAsLong(Instant.ofEpochMilli(tx.getTime()))))
.entrySet() .entrySet()
.stream() .stream()
.filter(entry -> dateFilter.test(entry.getKey())) .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 return date -> date >= TAG_DATE.getTimeInMillis() / 1000; // we use seconds
} }
// TODO: Consider moving these two methods to DaoStateService:
private Stream<Tx> getBurntFeeTxStream() {
return daoStateService.getBlocks().stream()
.flatMap(b -> b.getTxs().stream())
.filter(tx -> tx.getBurntFee() > 0);
}
private Stream<Tx> getTradeFeeTxStream() {
return daoStateService.getBlocks().stream()
.flatMap(b -> b.getTxs().stream())
.filter(tx -> tx.getTxType() == TxType.PAY_TRADE_FEE);
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Utils // Utils

View file

@ -17,6 +17,7 @@
package bisq.desktop.main.market.trades; 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.main.market.trades.charts.CandleData;
import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.DisplayUtils;
@ -25,37 +26,42 @@ import bisq.core.monetary.Altcoin;
import bisq.core.trade.statistics.TradeStatistics3; import bisq.core.trade.statistics.TradeStatistics3;
import bisq.common.util.MathUtils; import bisq.common.util.MathUtils;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSortedSet;
import javafx.scene.chart.XYChart; import javafx.scene.chart.XYChart;
import javafx.util.Pair; import javafx.util.Pair;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.NavigableSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.Getter; import lombok.Getter;
import static bisq.desktop.main.market.trades.TradesChartsViewModel.MAX_TICKS; import static bisq.desktop.main.market.trades.TradesChartsViewModel.MAX_TICKS;
public class ChartCalculations { public class ChartCalculations {
@VisibleForTesting
static final ZoneId ZONE_ID = ZoneId.systemDefault(); static final ZoneId ZONE_ID = ZoneId.systemDefault();
@ -63,29 +69,35 @@ public class ChartCalculations {
// Async // Async
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
static CompletableFuture<Map<TradesChartsViewModel.TickUnit, Map<Long, Long>>> getUsdAveragePriceMapsPerTickUnit(Set<TradeStatistics3> tradeStatisticsSet) { static CompletableFuture<Map<TickUnit, Map<Long, Long>>> getUsdAveragePriceMapsPerTickUnit(NavigableSet<TradeStatistics3> sortedTradeStatisticsSet) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
Map<TradesChartsViewModel.TickUnit, Map<Long, Long>> usdAveragePriceMapsPerTickUnit = new HashMap<>(); Map<TickUnit, Map<Long, PriceAccumulator>> priceAccumulatorMapsPerTickUnit = new HashMap<>();
Map<TradesChartsViewModel.TickUnit, Map<Long, List<TradeStatistics3>>> dateMapsPerTickUnit = new HashMap<>(); for (TickUnit tick : TickUnit.values()) {
for (TradesChartsViewModel.TickUnit tick : TradesChartsViewModel.TickUnit.values()) { priceAccumulatorMapsPerTickUnit.put(tick, new HashMap<>());
dateMapsPerTickUnit.put(tick, new HashMap<>());
} }
tradeStatisticsSet.stream() // Stream the trade statistics in reverse chronological order
TickUnit[] tickUnits = TickUnit.values();
sortedTradeStatisticsSet.descendingSet().stream()
.filter(e -> e.getCurrency().equals("USD")) .filter(e -> e.getCurrency().equals("USD"))
.forEach(tradeStatistics -> { .forEach(tradeStatistics -> {
for (TradesChartsViewModel.TickUnit tick : TradesChartsViewModel.TickUnit.values()) { for (TickUnit tickUnit : tickUnits) {
long time = roundToTick(tradeStatistics.getLocalDateTime(), tick).getTime(); Map<Long, PriceAccumulator> map = priceAccumulatorMapsPerTickUnit.get(tickUnit);
Map<Long, List<TradeStatistics3>> map = dateMapsPerTickUnit.get(tick); if (map.size() > MAX_TICKS) {
map.putIfAbsent(time, new ArrayList<>()); // No more prices are needed once more than MAX_TICKS candles have been spanned
map.get(time).add(tradeStatistics); // (and tick size is decreasing so we may break out of the whole loop)
break;
}
long time = roundToTick(tradeStatistics.getLocalDateTime(), tickUnit).getTime();
map.computeIfAbsent(time, t -> new PriceAccumulator()).add(tradeStatistics);
} }
}); });
dateMapsPerTickUnit.forEach((tick, map) -> { Map<TickUnit, Map<Long, Long>> usdAveragePriceMapsPerTickUnit = new HashMap<>();
priceAccumulatorMapsPerTickUnit.forEach((tickUnit, map) -> {
HashMap<Long, Long> priceMap = new HashMap<>(); HashMap<Long, Long> priceMap = new HashMap<>();
map.forEach((date, tradeStatisticsList) -> priceMap.put(date, getAveragePrice(tradeStatisticsList))); map.forEach((date, accumulator) -> priceMap.put(date, accumulator.getAveragePrice()));
usdAveragePriceMapsPerTickUnit.put(tick, priceMap); usdAveragePriceMapsPerTickUnit.put(tickUnit, priceMap);
}); });
return usdAveragePriceMapsPerTickUnit; return usdAveragePriceMapsPerTickUnit;
}); });
@ -94,34 +106,33 @@ public class ChartCalculations {
static CompletableFuture<List<TradeStatistics3>> getTradeStatisticsForCurrency(Set<TradeStatistics3> tradeStatisticsSet, static CompletableFuture<List<TradeStatistics3>> getTradeStatisticsForCurrency(Set<TradeStatistics3> tradeStatisticsSet,
String currencyCode, String currencyCode,
boolean showAllTradeCurrencies) { boolean showAllTradeCurrencies) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> tradeStatisticsSet.stream()
return tradeStatisticsSet.stream()
.filter(e -> showAllTradeCurrencies || e.getCurrency().equals(currencyCode)) .filter(e -> showAllTradeCurrencies || e.getCurrency().equals(currencyCode))
.collect(Collectors.toList()); .collect(Collectors.toList()));
});
} }
static CompletableFuture<UpdateChartResult> getUpdateChartResult(List<TradeStatistics3> tradeStatisticsByCurrency, static CompletableFuture<UpdateChartResult> getUpdateChartResult(List<TradeStatistics3> tradeStatisticsByCurrency,
TradesChartsViewModel.TickUnit tickUnit, TickUnit tickUnit,
Map<TradesChartsViewModel.TickUnit, Map<Long, Long>> usdAveragePriceMapsPerTickUnit, Map<TickUnit, Map<Long, Long>> usdAveragePriceMapsPerTickUnit,
String currencyCode) { String currencyCode) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
// Generate date range and create sets for all ticks // Generate date range and create sets for all ticks
Map<Long, Pair<Date, Set<TradeStatistics3>>> itemsPerInterval = getItemsPerInterval(tradeStatisticsByCurrency, tickUnit); List<Pair<Date, Set<TradeStatistics3>>> itemsPerInterval = getItemsPerInterval(tradeStatisticsByCurrency, tickUnit);
Map<Long, Long> usdAveragePriceMap = usdAveragePriceMapsPerTickUnit.get(tickUnit); Map<Long, Long> usdAveragePriceMap = usdAveragePriceMapsPerTickUnit.get(tickUnit);
AtomicLong averageUsdPrice = new AtomicLong(0); AtomicLong averageUsdPrice = new AtomicLong(0);
// create CandleData for defined time interval // create CandleData for defined time interval
List<CandleData> candleDataList = itemsPerInterval.entrySet().stream() List<CandleData> candleDataList = IntStream.range(0, itemsPerInterval.size())
.filter(entry -> entry.getKey() >= 0 && !entry.getValue().getValue().isEmpty()) .filter(i -> !itemsPerInterval.get(i).getValue().isEmpty())
.map(entry -> { .mapToObj(i -> {
long tickStartDate = entry.getValue().getKey().getTime(); Pair<Date, Set<TradeStatistics3>> pair = itemsPerInterval.get(i);
long tickStartDate = pair.getKey().getTime();
// If we don't have a price we take the previous one // If we don't have a price we take the previous one
if (usdAveragePriceMap.containsKey(tickStartDate)) { if (usdAveragePriceMap.containsKey(tickStartDate)) {
averageUsdPrice.set(usdAveragePriceMap.get(tickStartDate)); averageUsdPrice.set(usdAveragePriceMap.get(tickStartDate));
} }
return getCandleData(entry.getKey(), entry.getValue().getValue(), averageUsdPrice.get(), tickUnit, currencyCode, itemsPerInterval); return getCandleData(i, pair.getValue(), averageUsdPrice.get(), tickUnit, currencyCode, itemsPerInterval);
}) })
.sorted(Comparator.comparingLong(o -> o.tick)) .sorted(Comparator.comparingLong(o -> o.tick))
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -144,12 +155,12 @@ public class ChartCalculations {
@Getter @Getter
static class UpdateChartResult { static class UpdateChartResult {
private final Map<Long, Pair<Date, Set<TradeStatistics3>>> itemsPerInterval; private final List<Pair<Date, Set<TradeStatistics3>>> itemsPerInterval;
private final List<XYChart.Data<Number, Number>> priceItems; private final List<XYChart.Data<Number, Number>> priceItems;
private final List<XYChart.Data<Number, Number>> volumeItems; private final List<XYChart.Data<Number, Number>> volumeItems;
private final List<XYChart.Data<Number, Number>> volumeInUsdItems; private final List<XYChart.Data<Number, Number>> volumeInUsdItems;
public UpdateChartResult(Map<Long, Pair<Date, Set<TradeStatistics3>>> itemsPerInterval, public UpdateChartResult(List<Pair<Date, Set<TradeStatistics3>>> itemsPerInterval,
List<XYChart.Data<Number, Number>> priceItems, List<XYChart.Data<Number, Number>> priceItems,
List<XYChart.Data<Number, Number>> volumeItems, List<XYChart.Data<Number, Number>> volumeItems,
List<XYChart.Data<Number, Number>> volumeInUsdItems) { List<XYChart.Data<Number, Number>> volumeInUsdItems) {
@ -166,76 +177,92 @@ public class ChartCalculations {
// Private // Private
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
static Map<Long, Pair<Date, Set<TradeStatistics3>>> getItemsPerInterval(List<TradeStatistics3> tradeStatisticsByCurrency, static List<Pair<Date, Set<TradeStatistics3>>> getItemsPerInterval(List<TradeStatistics3> tradeStatisticsByCurrency,
TradesChartsViewModel.TickUnit tickUnit) { TickUnit tickUnit) {
// Generate date range and create sets for all ticks // Generate date range and create lists for all ticks
Map<Long, Pair<Date, Set<TradeStatistics3>>> itemsPerInterval = new HashMap<>(); List<Pair<Date, List<TradeStatistics3>>> itemsPerInterval = new ArrayList<>(Collections.nCopies(MAX_TICKS + 2, null));
Date time = new Date(); Date time = new Date();
for (long i = MAX_TICKS + 1; i >= 0; --i) { for (int i = MAX_TICKS + 1; i >= 0; --i) {
Pair<Date, Set<TradeStatistics3>> pair = new Pair<>((Date) time.clone(), new HashSet<>()); Pair<Date, List<TradeStatistics3>> pair = new Pair<>((Date) time.clone(), new ArrayList<>());
itemsPerInterval.put(i, pair); itemsPerInterval.set(i, pair);
// We adjust the time for the next iteration // We adjust the time for the next iteration
time.setTime(time.getTime() - 1); time.setTime(time.getTime() - 1);
time = roundToTick(time, tickUnit); time = roundToTick(time, tickUnit);
} }
// Get all entries for the defined time interval // Get all entries for the defined time interval
tradeStatisticsByCurrency.forEach(tradeStatistics -> { int i = MAX_TICKS;
for (long i = MAX_TICKS; i > 0; --i) { for (TradeStatistics3 tradeStatistics : tradeStatisticsByCurrency) {
Pair<Date, Set<TradeStatistics3>> pair = itemsPerInterval.get(i); // Start from the last used tick index - move forwards if necessary
for (; i < MAX_TICKS; i++) {
Pair<Date, List<TradeStatistics3>> pair = itemsPerInterval.get(i + 1);
if (!tradeStatistics.getDate().after(pair.getKey())) {
break;
}
}
// Scan backwards until the correct tick is reached
for (; i > 0; --i) {
Pair<Date, List<TradeStatistics3>> pair = itemsPerInterval.get(i);
if (tradeStatistics.getDate().after(pair.getKey())) { if (tradeStatistics.getDate().after(pair.getKey())) {
pair.getValue().add(tradeStatistics); pair.getValue().add(tradeStatistics);
break; break;
} }
} }
}); }
return itemsPerInterval; // Convert the lists into sorted sets
return itemsPerInterval.stream()
.map(pair -> new Pair<>(pair.getKey(), (Set<TradeStatistics3>) ImmutableSortedSet.copyOf(pair.getValue())))
.collect(Collectors.toList());
} }
static Date roundToTick(LocalDateTime localDate, TradesChartsViewModel.TickUnit tickUnit) { private static LocalDateTime roundToTickAsLocalDateTime(LocalDateTime localDateTime,
TickUnit tickUnit) {
switch (tickUnit) { switch (tickUnit) {
case YEAR: case YEAR:
return Date.from(localDate.withMonth(1).withDayOfYear(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); return localDateTime.withMonth(1).withDayOfYear(1).toLocalDate().atStartOfDay();
case MONTH: case MONTH:
return Date.from(localDate.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); return localDateTime.withDayOfMonth(1).toLocalDate().atStartOfDay();
case WEEK: case WEEK:
int dayOfWeek = localDate.getDayOfWeek().getValue(); int dayOfWeek = localDateTime.getDayOfWeek().getValue();
LocalDateTime firstDayOfWeek = ChronoUnit.DAYS.addTo(localDate, 1 - dayOfWeek); LocalDate firstDayOfWeek = localDateTime.toLocalDate().minusDays(dayOfWeek - 1);
return Date.from(firstDayOfWeek.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); return firstDayOfWeek.atStartOfDay();
case DAY: case DAY:
return Date.from(localDate.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); return localDateTime.toLocalDate().atStartOfDay();
case HOUR: case HOUR:
return Date.from(localDate.withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); return localDateTime.withMinute(0).withSecond(0).withNano(0);
case MINUTE_10: case MINUTE_10:
return Date.from(localDate.withMinute(localDate.getMinute() - localDate.getMinute() % 10).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); return localDateTime.withMinute(localDateTime.getMinute() - localDateTime.getMinute() % 10).withSecond(0).withNano(0);
default: default:
return Date.from(localDate.atZone(ZONE_ID).toInstant()); return localDateTime;
} }
} }
static Date roundToTick(Date time, TradesChartsViewModel.TickUnit tickUnit) { // Use an array rather than an EnumMap here, since the latter is not thread safe - this gives benign races only:
return roundToTick(time.toInstant().atZone(ChartCalculations.ZONE_ID).toLocalDateTime(), tickUnit); private static final Tuple2<?, ?>[] cachedLocalDateTimeToDateMappings = new Tuple2<?, ?>[TickUnit.values().length];
}
private static long getAveragePrice(List<TradeStatistics3> tradeStatisticsList) { private static Date roundToTick(LocalDateTime localDateTime, TickUnit tickUnit) {
long accumulatedAmount = 0; LocalDateTime rounded = roundToTickAsLocalDateTime(localDateTime, tickUnit);
long accumulatedVolume = 0; // Benefits from caching last result (per tick unit) since trade statistics are pre-sorted by date
for (TradeStatistics3 tradeStatistics : tradeStatisticsList) { int i = tickUnit.ordinal();
accumulatedAmount += tradeStatistics.getAmount(); var tuple = cachedLocalDateTimeToDateMappings[i];
accumulatedVolume += tradeStatistics.getTradeVolume().getValue(); if (tuple == null || !rounded.equals(tuple.first)) {
cachedLocalDateTimeToDateMappings[i] = tuple = new Tuple2<>(rounded, Date.from(rounded.atZone(ZONE_ID).toInstant()));
} }
return (Date) tuple.second;
double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT);
return MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount);
} }
@VisibleForTesting @VisibleForTesting
static CandleData getCandleData(long tick, Set<TradeStatistics3> set, static Date roundToTick(Date time, TickUnit tickUnit) {
return roundToTick(time.toInstant().atZone(ChartCalculations.ZONE_ID).toLocalDateTime(), tickUnit);
}
@VisibleForTesting
static CandleData getCandleData(int tickIndex, Set<TradeStatistics3> set,
long averageUsdPrice, long averageUsdPrice,
TradesChartsViewModel.TickUnit tickUnit, TickUnit tickUnit,
String currencyCode, String currencyCode,
Map<Long, Pair<Date, Set<TradeStatistics3>>> itemsPerInterval) { List<Pair<Date, Set<TradeStatistics3>>> itemsPerInterval) {
long open = 0; long open = 0;
long close = 0; long close = 0;
long high = 0; long high = 0;
@ -243,7 +270,9 @@ public class ChartCalculations {
long accumulatedVolume = 0; long accumulatedVolume = 0;
long accumulatedAmount = 0; long accumulatedAmount = 0;
long numTrades = set.size(); long numTrades = set.size();
List<Long> tradePrices = new ArrayList<>();
int arrayIndex = 0;
long[] tradePrices = new long[set.size()];
for (TradeStatistics3 item : set) { for (TradeStatistics3 item : set) {
long tradePriceAsLong = item.getTradePrice().getValue(); long tradePriceAsLong = item.getTradePrice().getValue();
// Previously a check was done which inverted the low and high for cryptocurrencies. // Previously a check was done which inverted the low and high for cryptocurrencies.
@ -252,21 +281,18 @@ public class ChartCalculations {
accumulatedVolume += item.getTradeVolume().getValue(); accumulatedVolume += item.getTradeVolume().getValue();
accumulatedAmount += item.getTradeAmount().getValue(); accumulatedAmount += item.getTradeAmount().getValue();
tradePrices.add(tradePriceAsLong); tradePrices[arrayIndex++] = tradePriceAsLong;
} }
Collections.sort(tradePrices); Arrays.sort(tradePrices);
List<TradeStatistics3> list = new ArrayList<>(set); if (!set.isEmpty()) {
list.sort(Comparator.comparingLong(TradeStatistics3::getDateAsLong)); NavigableSet<TradeStatistics3> sortedSet = ImmutableSortedSet.copyOf(set);
if (list.size() > 0) { open = sortedSet.first().getTradePrice().getValue();
open = list.get(0).getTradePrice().getValue(); close = sortedSet.last().getTradePrice().getValue();
close = list.get(list.size() - 1).getTradePrice().getValue();
} }
long averagePrice; long averagePrice;
Long[] prices = new Long[tradePrices.size()]; long medianPrice = MathUtils.getMedian(tradePrices);
tradePrices.toArray(prices);
long medianPrice = MathUtils.getMedian(prices);
boolean isBullish; boolean isBullish;
if (CurrencyUtil.isCryptoCurrency(currencyCode)) { if (CurrencyUtil.isCryptoCurrency(currencyCode)) {
isBullish = close < open; isBullish = close < open;
@ -278,9 +304,9 @@ public class ChartCalculations {
averagePrice = MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount); averagePrice = MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount);
} }
Date dateFrom = new Date(getTimeFromTickIndex(tick, itemsPerInterval)); Date dateFrom = new Date(getTimeFromTickIndex(tickIndex, itemsPerInterval));
Date dateTo = new Date(getTimeFromTickIndex(tick + 1, itemsPerInterval)); Date dateTo = new Date(getTimeFromTickIndex(tickIndex + 1, itemsPerInterval));
String dateString = tickUnit.ordinal() > TradesChartsViewModel.TickUnit.DAY.ordinal() ? String dateString = tickUnit.ordinal() > TickUnit.DAY.ordinal() ?
DisplayUtils.formatDateTimeSpan(dateFrom, dateTo) : DisplayUtils.formatDateTimeSpan(dateFrom, dateTo) :
DisplayUtils.formatDate(dateFrom) + " - " + DisplayUtils.formatDate(dateTo); DisplayUtils.formatDate(dateFrom) + " - " + DisplayUtils.formatDate(dateTo);
@ -289,15 +315,29 @@ public class ChartCalculations {
long volumeInUsd = averageUsdPrice * (long) MathUtils.scaleDownByPowerOf10((double) accumulatedAmount, 4); long volumeInUsd = averageUsdPrice * (long) MathUtils.scaleDownByPowerOf10((double) accumulatedAmount, 4);
// We store USD value without decimals as its only total volume, no precision is needed. // We store USD value without decimals as its only total volume, no precision is needed.
volumeInUsd = (long) MathUtils.scaleDownByPowerOf10((double) volumeInUsd, 4); volumeInUsd = (long) MathUtils.scaleDownByPowerOf10((double) volumeInUsd, 4);
return new CandleData(tick, open, close, high, low, averagePrice, medianPrice, accumulatedAmount, accumulatedVolume, return new CandleData(tickIndex, open, close, high, low, averagePrice, medianPrice, accumulatedAmount, accumulatedVolume,
numTrades, isBullish, dateString, volumeInUsd); numTrades, isBullish, dateString, volumeInUsd);
} }
static long getTimeFromTickIndex(long tick, Map<Long, Pair<Date, Set<TradeStatistics3>>> itemsPerInterval) { static long getTimeFromTickIndex(int tickIndex, List<Pair<Date, Set<TradeStatistics3>>> itemsPerInterval) {
if (tick > MAX_TICKS + 1 || if (tickIndex < 0 || tickIndex >= itemsPerInterval.size()) {
itemsPerInterval.get(tick) == null) {
return 0; return 0;
} }
return itemsPerInterval.get(tick).getKey().getTime(); return itemsPerInterval.get(tickIndex).getKey().getTime();
}
private static class PriceAccumulator {
private long accumulatedAmount;
private long accumulatedVolume;
void add(TradeStatistics3 tradeStatistics) {
accumulatedAmount += tradeStatistics.getAmount();
accumulatedVolume += tradeStatistics.getTradeVolume().getValue();
}
long getAveragePrice() {
double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT);
return MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount);
}
} }
} }

View file

@ -26,6 +26,9 @@ import bisq.core.util.FormattingUtils;
import bisq.core.util.VolumeUtil; import bisq.core.util.VolumeUtil;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ObservableValue;
import lombok.experimental.Delegate; import lombok.experimental.Delegate;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -35,6 +38,7 @@ public class TradeStatistics3ListItem {
private final TradeStatistics3 tradeStatistics3; private final TradeStatistics3 tradeStatistics3;
private final CoinFormatter coinFormatter; private final CoinFormatter coinFormatter;
private final boolean showAllTradeCurrencies; private final boolean showAllTradeCurrencies;
private final ObservableValue<TradeStatistics3ListItem> observableWrapper;
private String dateString; private String dateString;
private String market; private String market;
private String priceString; private String priceString;
@ -48,6 +52,11 @@ public class TradeStatistics3ListItem {
this.tradeStatistics3 = tradeStatistics3; this.tradeStatistics3 = tradeStatistics3;
this.coinFormatter = coinFormatter; this.coinFormatter = coinFormatter;
this.showAllTradeCurrencies = showAllTradeCurrencies; this.showAllTradeCurrencies = showAllTradeCurrencies;
observableWrapper = new ReadOnlyObjectWrapper<>(this).getReadOnlyProperty();
}
public ObservableValue<TradeStatistics3ListItem> asObservableValue() {
return observableWrapper;
} }
public String getDateString() { public String getDateString() {

View file

@ -53,6 +53,8 @@ import com.googlecode.jcsv.writer.CSVEntryConverter;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import com.google.common.collect.Lists;
import com.jfoenix.controls.JFXTabPane; import com.jfoenix.controls.JFXTabPane;
import javafx.stage.Stage; import javafx.stage.Stage;
@ -85,7 +87,6 @@ import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription; import org.fxmisc.easybind.Subscription;
import org.fxmisc.easybind.monadic.MonadicBinding; import org.fxmisc.easybind.monadic.MonadicBinding;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
@ -389,13 +390,15 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
private void fillList() { private void fillList() {
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
CompletableFuture.supplyAsync(() -> { boolean showAllTradeCurrencies = model.showAllTradeCurrenciesProperty.get();
return model.tradeStatisticsByCurrency.stream() // Collect the list items in reverse chronological order, as this is the likely
// order 'sortedList' will place them in - this skips most of its (slow) sorting.
CompletableFuture.supplyAsync(() -> Lists.reverse(model.tradeStatisticsByCurrency).stream()
.map(tradeStatistics -> new TradeStatistics3ListItem(tradeStatistics, .map(tradeStatistics -> new TradeStatistics3ListItem(tradeStatistics,
coinFormatter, coinFormatter,
model.showAllTradeCurrenciesProperty.get())) showAllTradeCurrencies))
.collect(Collectors.toCollection(FXCollections::observableArrayList)); .collect(Collectors.toCollection(FXCollections::observableArrayList))
}).whenComplete((listItems, throwable) -> { ).whenComplete((listItems, throwable) -> {
log.debug("Creating listItems took {} ms", System.currentTimeMillis() - ts); log.debug("Creating listItems took {} ms", System.currentTimeMillis() - ts);
long ts2 = System.currentTimeMillis(); long ts2 = System.currentTimeMillis();
@ -640,7 +643,7 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
return new StringConverter<>() { return new StringConverter<>() {
@Override @Override
public String toString(Number object) { public String toString(Number object) {
long index = MathUtils.doubleToLong((double) object); int index = object.intValue();
// The last tick is on the chart edge, it is not well spaced with // The last tick is on the chart edge, it is not well spaced with
// the previous tick and interferes with its label. // the previous tick and interferes with its label.
if (MAX_TICKS + 1 == index) return ""; if (MAX_TICKS + 1 == index) return "";
@ -755,7 +758,7 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
} }
}; };
dateColumn.getStyleClass().addAll("number-column", "first-column"); dateColumn.getStyleClass().addAll("number-column", "first-column");
dateColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); dateColumn.setCellValueFactory(tradeStatistics -> tradeStatistics.getValue().asObservableValue());
dateColumn.setCellFactory( dateColumn.setCellFactory(
new Callback<>() { new Callback<>() {
@Override @Override
@ -784,7 +787,7 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
} }
}; };
marketColumn.getStyleClass().add("number-column"); marketColumn.getStyleClass().add("number-column");
marketColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); marketColumn.setCellValueFactory(tradeStatistics -> tradeStatistics.getValue().asObservableValue());
marketColumn.setCellFactory( marketColumn.setCellFactory(
new Callback<>() { new Callback<>() {
@Override @Override
@ -808,7 +811,7 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
// price // price
priceColumn = new TableColumn<>(); priceColumn = new TableColumn<>();
priceColumn.getStyleClass().add("number-column"); priceColumn.getStyleClass().add("number-column");
priceColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); priceColumn.setCellValueFactory(tradeStatistics -> tradeStatistics.getValue().asObservableValue());
priceColumn.setCellFactory( priceColumn.setCellFactory(
new Callback<>() { new Callback<>() {
@Override @Override
@ -832,7 +835,7 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
// amount // amount
TableColumn<TradeStatistics3ListItem, TradeStatistics3ListItem> amountColumn = new AutoTooltipTableColumn<>(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode())); TableColumn<TradeStatistics3ListItem, TradeStatistics3ListItem> amountColumn = new AutoTooltipTableColumn<>(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode()));
amountColumn.getStyleClass().add("number-column"); amountColumn.getStyleClass().add("number-column");
amountColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); amountColumn.setCellValueFactory(tradeStatistics -> tradeStatistics.getValue().asObservableValue());
amountColumn.setCellFactory( amountColumn.setCellFactory(
new Callback<>() { new Callback<>() {
@Override @Override
@ -857,7 +860,7 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
// volume // volume
volumeColumn = new TableColumn<>(); volumeColumn = new TableColumn<>();
volumeColumn.getStyleClass().add("number-column"); volumeColumn.getStyleClass().add("number-column");
volumeColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); volumeColumn.setCellValueFactory(tradeStatistics -> tradeStatistics.getValue().asObservableValue());
volumeColumn.setCellFactory( volumeColumn.setCellFactory(
new Callback<>() { new Callback<>() {
@Override @Override
@ -881,7 +884,7 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
// paymentMethod // paymentMethod
TableColumn<TradeStatistics3ListItem, TradeStatistics3ListItem> paymentMethodColumn = new AutoTooltipTableColumn<>(Res.get("shared.paymentMethod")); TableColumn<TradeStatistics3ListItem, TradeStatistics3ListItem> paymentMethodColumn = new AutoTooltipTableColumn<>(Res.get("shared.paymentMethod"));
paymentMethodColumn.getStyleClass().add("number-column"); paymentMethodColumn.getStyleClass().add("number-column");
paymentMethodColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); paymentMethodColumn.setCellValueFactory(tradeStatistics -> tradeStatistics.getValue().asObservableValue());
paymentMethodColumn.setCellFactory( paymentMethodColumn.setCellFactory(
new Callback<>() { new Callback<>() {
@Override @Override

View file

@ -73,6 +73,7 @@ class TradesChartsViewModel extends ActivatableViewModel {
// Enum // Enum
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// NOTE: For the code to work correctly, order must be from biggest to smallest duration:
public enum TickUnit { public enum TickUnit {
YEAR, YEAR,
MONTH, MONTH,
@ -96,7 +97,7 @@ class TradesChartsViewModel extends ActivatableViewModel {
final ObservableList<XYChart.Data<Number, Number>> priceItems = FXCollections.observableArrayList(); final ObservableList<XYChart.Data<Number, Number>> priceItems = FXCollections.observableArrayList();
final ObservableList<XYChart.Data<Number, Number>> volumeItems = FXCollections.observableArrayList(); final ObservableList<XYChart.Data<Number, Number>> volumeItems = FXCollections.observableArrayList();
final ObservableList<XYChart.Data<Number, Number>> volumeInUsdItems = FXCollections.observableArrayList(); final ObservableList<XYChart.Data<Number, Number>> volumeInUsdItems = FXCollections.observableArrayList();
private final Map<Long, Pair<Date, Set<TradeStatistics3>>> itemsPerInterval = new HashMap<>(); private final List<Pair<Date, Set<TradeStatistics3>>> itemsPerInterval = new ArrayList<>();
TickUnit tickUnit; TickUnit tickUnit;
private int selectedTabIndex; private int selectedTabIndex;
@ -208,7 +209,7 @@ class TradesChartsViewModel extends ActivatableViewModel {
private void applyAsyncUsdAveragePriceMapsPerTickUnit(CompletableFuture<Boolean> completeFuture) { private void applyAsyncUsdAveragePriceMapsPerTickUnit(CompletableFuture<Boolean> completeFuture) {
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
ChartCalculations.getUsdAveragePriceMapsPerTickUnit(tradeStatisticsManager.getObservableTradeStatisticsSet()) ChartCalculations.getUsdAveragePriceMapsPerTickUnit(tradeStatisticsManager.getNavigableTradeStatisticsSet())
.whenComplete((usdAveragePriceMapsPerTickUnit, throwable) -> { .whenComplete((usdAveragePriceMapsPerTickUnit, throwable) -> {
if (deactivateCalled) { if (deactivateCalled) {
return; return;
@ -278,7 +279,7 @@ class TradesChartsViewModel extends ActivatableViewModel {
} }
UserThread.execute(() -> { UserThread.execute(() -> {
itemsPerInterval.clear(); itemsPerInterval.clear();
itemsPerInterval.putAll(updateChartResult.getItemsPerInterval()); itemsPerInterval.addAll(updateChartResult.getItemsPerInterval());
priceItems.setAll(updateChartResult.getPriceItems()); priceItems.setAll(updateChartResult.getPriceItems());
volumeItems.setAll(updateChartResult.getVolumeItems()); volumeItems.setAll(updateChartResult.getVolumeItems());
@ -356,8 +357,8 @@ class TradesChartsViewModel extends ActivatableViewModel {
return currencyListItems.getObservableList().stream().filter(e -> e.tradeCurrency.equals(selectedTradeCurrencyProperty.get())).findAny(); return currencyListItems.getObservableList().stream().filter(e -> e.tradeCurrency.equals(selectedTradeCurrencyProperty.get())).findAny();
} }
long getTimeFromTickIndex(long tick) { long getTimeFromTickIndex(int tickIndex) {
return ChartCalculations.getTimeFromTickIndex(tick, itemsPerInterval); return ChartCalculations.getTimeFromTickIndex(tickIndex, itemsPerInterval);
} }
@ -367,7 +368,7 @@ class TradesChartsViewModel extends ActivatableViewModel {
private void fillTradeCurrencies() { private void fillTradeCurrencies() {
// Don't use a set as we need all entries // Don't use a set as we need all entries
List<TradeCurrency> tradeCurrencyList = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() List<TradeCurrency> tradeCurrencyList = tradeStatisticsManager.getNavigableTradeStatisticsSet().parallelStream()
.flatMap(e -> CurrencyUtil.getTradeCurrency(e.getCurrency()).stream()) .flatMap(e -> CurrencyUtil.getTradeCurrency(e.getCurrency()).stream())
.collect(Collectors.toList()); .collect(Collectors.toList());
currencyListItems.updateWithCurrencies(tradeCurrencyList, showAllCurrencyListItem); currencyListItems.updateWithCurrencies(tradeCurrencyList, showAllCurrencyListItem);

View file

@ -54,6 +54,7 @@ import bisq.core.util.coin.CoinUtil;
import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PService;
import bisq.common.util.MathUtils; import bisq.common.util.MathUtils;
import bisq.common.util.RangeUtils;
import bisq.common.util.Tuple2; import bisq.common.util.Tuple2;
import bisq.common.util.Utilities; import bisq.common.util.Utilities;
@ -62,6 +63,8 @@ import org.bitcoinj.core.Transaction;
import javax.inject.Named; import javax.inject.Named;
import com.google.common.collect.Range;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty; import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
@ -79,7 +82,6 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.collections.SetChangeListener; import javafx.collections.SetChangeListener;
import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -362,14 +364,15 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
// Get average historic prices over for the prior trade period equaling the lock time // Get average historic prices over for the prior trade period equaling the lock time
var blocksRange = Restrictions.getLockTime(paymentAccount.getPaymentMethod().isBlockchain()); var blocksRange = Restrictions.getLockTime(paymentAccount.getPaymentMethod().isBlockchain());
var startDate = new Date(System.currentTimeMillis() - blocksRange * 10L * 60000); var startDate = new Date(System.currentTimeMillis() - blocksRange * 10L * 60000);
var sortedRangeData = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() var sortedRangeData = RangeUtils.subSet(tradeStatisticsManager.getNavigableTradeStatisticsSet())
.withKey(TradeStatistics3::getDate)
.overRange(Range.atLeast(startDate));
var sortedFilteredRangeData = sortedRangeData.stream()
.filter(e -> e.getCurrency().equals(getTradeCurrency().getCode())) .filter(e -> e.getCurrency().equals(getTradeCurrency().getCode()))
.filter(e -> e.getDate().compareTo(startDate) >= 0)
.sorted(Comparator.comparing(TradeStatistics3::getDate))
.collect(Collectors.toList()); .collect(Collectors.toList());
var movingAverage = new MathUtils.MovingAverage(10, 0.2); var movingAverage = new MathUtils.MovingAverage(10, 0.2);
double[] extremes = {Double.MAX_VALUE, Double.MIN_VALUE}; double[] extremes = {Double.MAX_VALUE, Double.MIN_VALUE};
sortedRangeData.forEach(e -> { sortedFilteredRangeData.forEach(e -> {
var price = e.getTradePrice().getValue(); var price = e.getTradePrice().getValue();
movingAverage.next(price).ifPresent(val -> { movingAverage.next(price).ifPresent(val -> {
if (val < extremes[0]) extremes[0] = val; if (val < extremes[0]) extremes[0] = val;

View file

@ -33,7 +33,6 @@ import bisq.core.trade.bsq_swap.BsqSwapTradeManager;
import bisq.core.trade.model.Tradable; import bisq.core.trade.model.Tradable;
import bisq.core.user.Preferences; import bisq.core.user.Preferences;
import bisq.core.util.PriceUtil; import bisq.core.util.PriceUtil;
import bisq.core.util.VolumeUtil;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
@ -120,7 +119,7 @@ class ClosedTradesDataModel extends ActivatableDataModel {
} }
Price price = PriceUtil.marketPriceToPrice(marketPrice); Price price = PriceUtil.marketPriceToPrice(marketPrice);
return Optional.of(VolumeUtil.getVolume(amount, price)); return Optional.of(price.getVolumeByAmount(amount));
} }
Volume getBsqVolumeInUsdWithAveragePrice(Coin amount) { Volume getBsqVolumeInUsdWithAveragePrice(Coin amount) {

View file

@ -21,6 +21,7 @@ import bisq.desktop.Navigation;
import bisq.desktop.main.market.trades.charts.CandleData; import bisq.desktop.main.market.trades.charts.CandleData;
import bisq.core.locale.FiatCurrency; import bisq.core.locale.FiatCurrency;
import bisq.core.locale.GlobalSettings;
import bisq.core.monetary.Price; import bisq.core.monetary.Price;
import bisq.core.offer.bisq_v1.OfferPayload; import bisq.core.offer.bisq_v1.OfferPayload;
import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.PaymentMethod;
@ -37,17 +38,19 @@ import javafx.collections.ObservableSet;
import javafx.util.Pair; import javafx.util.Pair;
import java.time.LocalDateTime;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.List;
import java.util.Locale;
import java.util.Set; import java.util.Set;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -62,7 +65,6 @@ public class TradesChartsViewModelTest {
TradeStatisticsManager tradeStatisticsManager; TradeStatisticsManager tradeStatisticsManager;
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
private File dir;
OfferPayload offer = new OfferPayload(null, OfferPayload offer = new OfferPayload(null,
0, 0,
null, null,
@ -105,14 +107,10 @@ public class TradesChartsViewModelTest {
@BeforeEach @BeforeEach
public void setup() throws IOException { public void setup() throws IOException {
GlobalSettings.setLocale(Locale.US);
tradeStatisticsManager = mock(TradeStatisticsManager.class); tradeStatisticsManager = mock(TradeStatisticsManager.class);
model = new TradesChartsViewModel(tradeStatisticsManager, mock(Preferences.class), mock(PriceFeedService.class), model = new TradesChartsViewModel(tradeStatisticsManager, mock(Preferences.class), mock(PriceFeedService.class),
mock(Navigation.class)); mock(Navigation.class));
dir = File.createTempFile("temp_tests1", "");
//noinspection ResultOfMethodCallIgnored
dir.delete();
//noinspection ResultOfMethodCallIgnored
dir.mkdir();
} }
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
@ -130,9 +128,14 @@ public class TradesChartsViewModelTest {
long amount = Coin.parseCoin("4").value; long amount = Coin.parseCoin("4").value;
long volume = Fiat.parseFiat("EUR", "2200").value; long volume = Fiat.parseFiat("EUR", "2200").value;
boolean isBullish = true; boolean isBullish = true;
// NOTE: This is the LAST candle date (today) on the chart, which is presently formatted with
// both days the same, whereas earlier candle dates span two consecutive days (for TickUnit.DAY):
// TODO: Is this a bug?
String date = "May 7, 2023 - May 7, 2023";
Set<TradeStatistics3> set = new HashSet<>(); Set<TradeStatistics3> set = new HashSet<>();
final Date now = new Date(); Date now = Date.from(LocalDateTime.of(2023, 5, 7, 12, 34)
.atZone(ChartCalculations.ZONE_ID).toInstant());
set.add(new TradeStatistics3(offer.getCurrencyCode(), set.add(new TradeStatistics3(offer.getCurrencyCode(),
Price.parse("EUR", "520").getValue(), Price.parse("EUR", "520").getValue(),
@ -171,9 +174,11 @@ public class TradesChartsViewModelTest {
null, null,
null)); null));
Map<Long, Pair<Date, Set<TradeStatistics3>>> itemsPerInterval = null; Date tickStart = ChartCalculations.roundToTick(now, TradesChartsViewModel.TickUnit.DAY);
long tick = ChartCalculations.roundToTick(now, TradesChartsViewModel.TickUnit.DAY).getTime(); List<Pair<Date, Set<TradeStatistics3>>> itemsPerInterval = List.of(
CandleData candleData = ChartCalculations.getCandleData(tick, new Pair<>(tickStart, set), new Pair<>(now, Set.of())
);
CandleData candleData = ChartCalculations.getCandleData(0,
set, set,
0, 0,
TradesChartsViewModel.TickUnit.DAY, currencyCode, TradesChartsViewModel.TickUnit.DAY, currencyCode,
@ -187,12 +192,13 @@ public class TradesChartsViewModelTest {
assertEquals(amount, candleData.accumulatedAmount); assertEquals(amount, candleData.accumulatedAmount);
assertEquals(volume, candleData.accumulatedVolume); assertEquals(volume, candleData.accumulatedVolume);
assertEquals(isBullish, candleData.isBullish); assertEquals(isBullish, candleData.isBullish);
assertEquals(date, candleData.date);
} }
// TODO JMOCKIT // TODO JMOCKIT
@Disabled @Disabled
@Test @Test
public void testItemLists() throws ParseException { public void testItemLists() {
// Helper class to add historic trades // Helper class to add historic trades
class Trade { class Trade {
Trade(String date, String size, String price, String cc) { Trade(String date, String size, String price, String cc) {
@ -207,9 +213,9 @@ public class TradesChartsViewModelTest {
} }
Date date; Date date;
String size; final String size;
String price; final String price;
String cc; final String cc;
} }
// Trade EUR // Trade EUR

View file

@ -60,9 +60,7 @@ import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.natpryce.makeiteasy.Maker; import com.natpryce.makeiteasy.Maker;
@ -86,7 +84,6 @@ import static org.mockito.Mockito.when;
public class OfferBookViewModelTest { public class OfferBookViewModelTest {
private final CoinFormatter coinFormatter = new ImmutableCoinFormatter(Config.baseCurrencyNetworkParameters().getMonetaryFormat()); private final CoinFormatter coinFormatter = new ImmutableCoinFormatter(Config.baseCurrencyNetworkParameters().getMonetaryFormat());
private static final Logger log = LoggerFactory.getLogger(OfferBookViewModelTest.class);
private User user; private User user;
@BeforeEach @BeforeEach
@ -101,7 +98,7 @@ public class OfferBookViewModelTest {
private PriceUtil getPriceUtil() { private PriceUtil getPriceUtil() {
PriceFeedService priceFeedService = mock(PriceFeedService.class); PriceFeedService priceFeedService = mock(PriceFeedService.class);
TradeStatisticsManager tradeStatisticsManager = mock(TradeStatisticsManager.class); TradeStatisticsManager tradeStatisticsManager = mock(TradeStatisticsManager.class);
when(tradeStatisticsManager.getObservableTradeStatisticsSet()).thenReturn(FXCollections.observableSet()); when(tradeStatisticsManager.getNavigableTradeStatisticsSet()).thenReturn(Collections.emptyNavigableSet());
return new PriceUtil(priceFeedService, tradeStatisticsManager, empty); return new PriceUtil(priceFeedService, tradeStatisticsManager, empty);
} }
@ -639,4 +636,3 @@ public class OfferBookViewModelTest {
1)); 1));
} }
} }