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();
}
public static long getMedian(Long[] list) {
public static long getMedian(long[] list) {
if (list.length == 0) {
return 0L;
}

View file

@ -1,7 +1,12 @@
package bisq.common.util;
import com.google.common.collect.Ordering;
import java.io.File;
import java.util.Comparator;
import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
/**
@ -29,4 +34,10 @@ public class Preconditions {
return dir;
}
// needed since Guava makes it impossible to create an ImmutableSorted[Set|Map] with a null comparator:
public static void checkComparatorNullOrNatural(Comparator<?> comparator, Object errorMessage) {
checkArgument(comparator == null || comparator.equals(Ordering.natural()) ||
comparator.equals(Comparator.naturalOrder()), errorMessage);
}
}

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 Optional<Long> burnTarget = Optional.empty();
private final Map<String, BurningManCandidate> burningManCandidatesByName = new HashMap<>();
private Long accumulatedDecayedBurnedAmount;
private final Set<ReimbursementModel> reimbursements = new HashSet<>();
private Optional<Long> averageDistributionPerCycle = Optional.empty();
private Set<String> myCompensationRequestNames = null;
@ -132,6 +133,7 @@ public class BurningManPresentationService implements DaoStateListener {
burningManCandidatesByName.clear();
reimbursements.clear();
burnTarget = Optional.empty();
accumulatedDecayedBurnedAmount = null;
myCompensationRequestNames = null;
averageDistributionPerCycle = Optional.empty();
legacyBurningManDPT = Optional.empty();
@ -188,8 +190,7 @@ public class BurningManPresentationService implements DaoStateListener {
long lowerBaseTarget = Math.round(burnTarget * maxCompensationShare);
double maxBoostedCompensationShare = burningManCandidate.getMaxBoostedCompensationShare();
long upperBaseTarget = Math.round(boostedBurnTarget * maxBoostedCompensationShare);
Collection<BurningManCandidate> burningManCandidates = getBurningManCandidatesByName().values();
long totalBurnedAmount = burnTargetService.getAccumulatedDecayedBurnedAmount(burningManCandidates, currentChainHeight);
long totalBurnedAmount = getAccumulatedDecayedBurnedAmount();
if (totalBurnedAmount == 0) {
// The first BM would reach their max burn share by 5.46 BSQ already. But we suggest the lowerBaseTarget
@ -344,7 +345,6 @@ public class BurningManPresentationService implements DaoStateListener {
receiverAddressesByBurningManName.get(name).addAll(burningManCandidate.getAllAddresses());
});
Map<String, String> map = new HashMap<>();
receiverAddressesByBurningManName
.forEach((name, addresses) -> addresses
@ -371,4 +371,12 @@ public class BurningManPresentationService implements DaoStateListener {
proofOfBurnOpReturnTxOutputByHash.putAll(burningManService.getProofOfBurnOpReturnTxOutputByHash(currentChainHeight));
return proofOfBurnOpReturnTxOutputByHash;
}
private long getAccumulatedDecayedBurnedAmount() {
if (accumulatedDecayedBurnedAmount == null) {
Collection<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.model.blockchain.Block;
import bisq.core.monetary.Price;
import bisq.core.trade.statistics.TradeStatistics3;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.core.util.AveragePriceUtil;
@ -49,6 +50,8 @@ import javax.inject.Singleton;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.SetChangeListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@ -58,6 +61,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -87,6 +91,7 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
private final Preferences preferences;
private final Map<Date, Price> averageBsqPriceByMonth = new HashMap<>(getHistoricalAverageBsqPriceByMonth());
private boolean averagePricesValid;
@Getter
private final Map<String, BalanceModel> balanceModelByBurningManName = new HashMap<>();
@Getter
@ -116,13 +121,13 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
@Override
public void addListeners() {
tradeStatisticsManager.getObservableTradeStatisticsSet().addListener(
(SetChangeListener<TradeStatistics3>) observable -> averagePricesValid = false);
}
@Override
public void start() {
UserThread.execute(() -> isProcessing.set(true));
// Create the map from now back to the last entry of the historical data (April 2019-Nov. 2022).
averageBsqPriceByMonth.putAll(getAverageBsqPriceByMonth(new Date(), 2022, 10));
updateBalanceModelByAddress();
CompletableFuture.runAsync(() -> {
@ -180,8 +185,11 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
}
public Map<Date, Price> getAverageBsqPriceByMonth() {
getAverageBsqPriceByMonth(new Date(), HIST_BSQ_PRICE_LAST_DATE_YEAR, HIST_BSQ_PRICE_LAST_DATE_MONTH)
.forEach((key, value) -> averageBsqPriceByMonth.put(new Date(key.getTime()), Price.valueOf("BSQ", value.getValue())));
if (!averagePricesValid) {
// Fill the map from now back to the last entry of the historical data (April 2019-Nov. 2022).
averageBsqPriceByMonth.putAll(getAverageBsqPriceByMonth(new Date(), HIST_BSQ_PRICE_LAST_DATE_YEAR, HIST_BSQ_PRICE_LAST_DATE_MONTH));
averagePricesValid = true;
}
return averageBsqPriceByMonth;
}
@ -202,21 +210,7 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
Map<Date, Price> averageBsqPriceByMonth = getAverageBsqPriceByMonth();
return getReceivedBtcBalanceEntryListExcludingLegacyBM().stream()
.filter(e -> e.getType() == BalanceEntry.Type.BTC_TRADE_FEE_TX)
.map(balanceEntry -> {
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)
.flatMapToLong(balanceEntry -> receivedBtcAsBsq(balanceEntry, averageBsqPriceByMonth).stream())
.sum();
}
@ -231,45 +225,29 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
Map<Date, Price> averageBsqPriceByMonth = getAverageBsqPriceByMonth();
return getReceivedBtcBalanceEntryListExcludingLegacyBM().stream()
.filter(e -> e.getType() == BalanceEntry.Type.DPT_TX)
.map(balanceEntry -> {
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)
.flatMapToLong(balanceEntry -> receivedBtcAsBsq(balanceEntry, averageBsqPriceByMonth).stream())
.sum();
}
public long getTotalAmountOfDistributedBsq() {
Map<Date, Price> averageBsqPriceByMonth = getAverageBsqPriceByMonth();
return getReceivedBtcBalanceEntryListExcludingLegacyBM().stream()
.map(balanceEntry -> {
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)
.flatMapToLong(balanceEntry -> receivedBtcAsBsq(balanceEntry, averageBsqPriceByMonth).stream())
.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() {
if (!receivedBtcBalanceEntryListExcludingLegacyBM.isEmpty()) {
return receivedBtcBalanceEntryListExcludingLegacyBM;
@ -329,7 +307,7 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
private void addAccountingBlockToBalanceModel(Map<String, BalanceModel> balanceModelByBurningManName,
AccountingBlock accountingBlock) {
accountingBlock.getTxs().forEach(tx -> {
accountingBlock.getTxs().forEach(tx ->
tx.getOutputs().forEach(txOutput -> {
String name = txOutput.getName();
balanceModelByBurningManName.putIfAbsent(name, new BalanceModel());
@ -337,10 +315,10 @@ public class BurningManAccountingService implements DaoSetupService, DaoStateLis
txOutput.getValue(),
new Date(accountingBlock.getDate()),
toBalanceEntryType(tx.getType())));
});
});
}));
}
@SuppressWarnings("SameParameterValue")
private Map<Date, Price> getAverageBsqPriceByMonth(Date from, int backToYear, int backToMonth) {
Map<Date, Price> averageBsqPriceByMonth = new HashMap<>();
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.
*/
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))
.divide(BigInteger.valueOf(altcoin.value));
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
throw new ArithmeticException("Overflow");
return Altcoin.valueOf(altcoin.currencyCode, converted.longValue());
.divide(BigInteger.valueOf(altcoin.value))
.longValueExact();
}
return Altcoin.valueOf(altcoin.currencyCode, converted);
}
/**
* Convert a altcoin amount to a coin amount using this exchange rate.
* Convert an altcoin amount to a coin amount using this exchange rate.
*
* @throws ArithmeticException if the converted coin amount is too high or too low.
*/
public Coin altcoinToCoin(Altcoin convertAltcoin) {
checkArgument(convertAltcoin.currencyCode.equals(altcoin.currencyCode), "Currency mismatch: %s vs %s",
convertAltcoin.currencyCode, altcoin.currencyCode);
// Use BigInteger because it's much easier to maintain full precision without overflowing.
BigInteger converted = BigInteger.valueOf(altcoin.value)
long converted;
if ((int) altcoin.value == altcoin.value && (int) convertAltcoin.value == convertAltcoin.value) {
// Short circuit in a common case where long arithmetic won't overflow.
converted = altcoin.value * convertAltcoin.value / coin.value;
} else {
// Otherwise use BigInteger to maintain full precision without overflowing.
converted = BigInteger.valueOf(altcoin.value)
.multiply(BigInteger.valueOf(convertAltcoin.value))
.divide(BigInteger.valueOf(coin.value));
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
throw new ArithmeticException("Overflow");
try {
return Coin.valueOf(converted.longValue());
} catch (IllegalArgumentException x) {
throw new ArithmeticException("Overflow: " + x.getMessage());
.divide(BigInteger.valueOf(coin.value))
.longValueExact();
}
return Coin.valueOf(converted);
}
}

View file

@ -25,9 +25,6 @@ import org.bitcoinj.core.Monetary;
import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jetbrains.annotations.NotNull;
/**
@ -39,12 +36,11 @@ import org.jetbrains.annotations.NotNull;
* those classes, like {@link Fiat} or {@link Altcoin}.
*/
public class Price extends MonetaryWrapper implements Comparable<Price> {
private static final Logger log = LoggerFactory.getLogger(Price.class);
/**
* Create a new {@code Price} from specified {@code Monetary}.
*
* @param monetary
* @param monetary The monetary (either {@link Altcoin} or {@link Fiat}) to wrap
*/
public Price(Monetary monetary) {
super(monetary);
@ -82,23 +78,41 @@ public class Price extends MonetaryWrapper implements Comparable<Price> {
public Volume getVolumeByAmount(Coin amount) {
if (monetary instanceof Fiat)
return new Volume(new ExchangeRate((Fiat) monetary).coinToFiat(amount));
return new Volume(coinToFiat(new ExchangeRate((Fiat) monetary), amount));
else if (monetary instanceof Altcoin)
return new Volume(new AltcoinExchangeRate((Altcoin) monetary).coinToAltcoin(amount));
else
throw new IllegalStateException("Monetary must be either of type Fiat or Altcoin");
}
// Short circuit BigInteger logic in ExchangeRate.coinToFiat in a common case where long arithmetic won't overflow.
private static Fiat coinToFiat(ExchangeRate rate, Coin convertCoin) {
if ((int) convertCoin.value == convertCoin.value && (int) rate.fiat.value == rate.fiat.value) {
long converted = convertCoin.value * rate.fiat.value / rate.coin.value;
return Fiat.valueOf(rate.fiat.currencyCode, converted);
}
return rate.coinToFiat(convertCoin);
}
public Coin getAmountByVolume(Volume volume) {
Monetary monetary = volume.getMonetary();
if (monetary instanceof Fiat && this.monetary instanceof Fiat)
return new ExchangeRate((Fiat) this.monetary).fiatToCoin((Fiat) monetary);
return fiatToCoin(new ExchangeRate((Fiat) this.monetary), (Fiat) monetary);
else if (monetary instanceof Altcoin && this.monetary instanceof Altcoin)
return new AltcoinExchangeRate((Altcoin) this.monetary).altcoinToCoin((Altcoin) monetary);
else
return Coin.ZERO;
}
// Short circuit BigInteger logic in ExchangeRate.fiatToCoin in a common case where long arithmetic won't overflow.
private static Coin fiatToCoin(ExchangeRate rate, Fiat convertFiat) {
if ((int) convertFiat.value == convertFiat.value && (int) rate.coin.value == rate.coin.value) {
long converted = convertFiat.value * rate.coin.value / rate.fiat.value;
return Coin.valueOf(converted);
}
return rate.fiatToCoin(convertFiat);
}
public String getCurrencyCode() {
return monetary instanceof Altcoin ? ((Altcoin) monetary).getCurrencyCode() : ((Fiat) monetary).getCurrencyCode();
}

View file

@ -26,8 +26,6 @@ import bisq.common.util.Tuple2;
import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
@ -35,6 +33,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
@ -62,16 +61,11 @@ public class DisputeAgentSelection {
DisputeAgentManager<T> disputeAgentManager,
boolean isMediator) {
// We take last 100 entries from trade statistics
List<TradeStatistics3> list = new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet());
list.sort(Comparator.comparing(TradeStatistics3::getDateAsLong));
Collections.reverse(list);
if (!list.isEmpty()) {
int max = Math.min(list.size(), LOOK_BACK_RANGE);
list = list.subList(0, max);
}
Stream<TradeStatistics3> stream = tradeStatisticsManager.getNavigableTradeStatisticsSet().descendingSet().stream()
.limit(LOOK_BACK_RANGE);
// 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())
.filter(Objects::nonNull)
.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.period.PeriodService;
import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService;
import bisq.common.util.MathUtils;
import bisq.core.dao.state.model.blockchain.Block;
import org.bitcoinj.core.Coin;
@ -37,7 +37,7 @@ import javax.annotation.Nullable;
@Slf4j
@Singleton
public class TradeLimits {
public class TradeLimits implements DaoStateListener {
@Nullable
@Getter
private static TradeLimits INSTANCE;
@ -45,10 +45,14 @@ public class TradeLimits {
private final DaoStateService daoStateService;
private final PeriodService periodService;
private volatile Coin cachedMaxTradeLimit;
@Inject
public TradeLimits(DaoStateService daoStateService, PeriodService periodService) {
this.daoStateService = daoStateService;
this.periodService = periodService;
daoStateService.addDaoStateListener(this);
INSTANCE = this;
}
@ -58,6 +62,10 @@ public class TradeLimits {
// guice.
}
@Override
public void onParseBlockCompleteAfterBatchProcessing(Block block) {
cachedMaxTradeLimit = null;
}
/**
* The default trade limits defined as statics in PaymentMethod are only used until the DAO
@ -67,7 +75,11 @@ public class TradeLimits {
* @return the maximum trade limit set by the DAO.
*/
public Coin getMaxTradeLimit() {
return daoStateService.getParamValueAsCoin(Param.MAX_TRADE_LIMIT, periodService.getChainHeight());
Coin limit = cachedMaxTradeLimit;
if (limit == null) {
cachedMaxTradeLimit = limit = daoStateService.getParamValueAsCoin(Param.MAX_TRADE_LIMIT, periodService.getChainHeight());
}
return limit;
}
// We possibly rounded value for the first month gets multiplied by 4 to get the trade limit after the account
@ -98,8 +110,6 @@ public class TradeLimits {
long smallestLimit = maxLimit / (4 * riskFactor); // e.g. 100000000 / 32 = 3125000
// We want to avoid more than 4 decimal places (100000000 / 32 = 3125000 or 1 BTC / 32 = 0.03125 BTC).
// We want rounding to 0.0313 BTC
double decimalForm = MathUtils.scaleDownByPowerOf10((double) smallestLimit, 8);
double rounded = MathUtils.roundDouble(decimalForm, 4);
return MathUtils.roundDoubleToLong(MathUtils.scaleUpByPowerOf10(rounded, 8));
return ((smallestLimit + 5000L) / 10000L) * 10000L;
}
}

View file

@ -26,11 +26,13 @@ import bisq.common.proto.persistable.PersistablePayload;
import org.bitcoinj.core.Coin;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@ -196,8 +198,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
// 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).
@Getter
private final static List<PaymentMethod> paymentMethods = new ArrayList<>(Arrays.asList(
private static final List<PaymentMethod> PAYMENT_METHODS = Stream.of(
// EUR
SEPA = new PaymentMethod(SEPA_ID, 6 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK),
SEPA_INSTANT = new PaymentMethod(SEPA_INSTANT_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK),
@ -277,18 +278,15 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
BLOCK_CHAINS_INSTANT = new PaymentMethod(BLOCK_CHAINS_INSTANT_ID, TimeUnit.HOURS.toMillis(1), DEFAULT_TRADE_LIMIT_VERY_LOW_RISK),
// BsqSwap
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 {
paymentMethods.sort((o1, o2) -> {
String id1 = o1.getId();
if (id1.equals(CLEAR_X_CHANGE_ID))
id1 = "ZELLE";
String id2 = o2.getId();
if (id2.equals(CLEAR_X_CHANGE_ID))
id2 = "ZELLE";
return id1.compareTo(id2);
});
private static final Map<String, PaymentMethod> PAYMENT_METHOD_MAP = PAYMENT_METHODS.stream()
.collect(Collectors.toUnmodifiableMap(m -> m.id, m -> m));
public static List<PaymentMethod> getPaymentMethods() {
return PAYMENT_METHODS;
}
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.
public static Optional<PaymentMethod> getActivePaymentMethod(String id) {
return paymentMethods.stream()
.filter(e -> e.getId().equals(id))
.findFirst();
return Optional.ofNullable(PAYMENT_METHOD_MAP.get(id));
}
public Coin getMaxTradeLimitAsCoin(String currencyCode) {

View file

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

View file

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

View file

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

View file

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

View file

@ -20,11 +20,15 @@ package bisq.core.util;
import bisq.common.util.DoubleSummaryStatisticsWithStdDev;
import bisq.common.util.Tuple2;
import javafx.collections.FXCollections;
import com.google.common.primitives.Doubles;
import java.util.Arrays;
import java.util.DoubleSummaryStatistics;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Spliterator;
import java.util.function.DoublePredicate;
import java.util.stream.DoubleStream;
import java.util.stream.StreamSupport;
public class InlierUtil {
@ -39,11 +43,8 @@ public class InlierUtil {
Tuple2<Double, Double> inlierThreshold =
computeInlierThreshold(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
DoubleSummaryStatistics inlierStatistics =
yValues
.stream()
.filter(y -> withinBounds(inlierThreshold, y))
.mapToDouble(Double::doubleValue)
DoubleSummaryStatistics inlierStatistics = stream(yValues)
.filter(withinBounds(inlierThreshold))
.summaryStatistics();
var inlierMin = inlierStatistics.getMin();
@ -52,10 +53,10 @@ public class InlierUtil {
return new Tuple2<>(inlierMin, inlierMax);
}
private static boolean withinBounds(Tuple2<Double, Double> bounds, double number) {
var lowerBound = bounds.first;
var upperBound = bounds.second;
return (lowerBound <= number) && (number <= upperBound);
private static DoublePredicate withinBounds(Tuple2<Double, Double> bounds) {
double lowerBound = bounds.first;
double upperBound = bounds.second;
return number -> lowerBound <= number && number <= upperBound;
}
/* Computes the lower and upper inlier thresholds. A point lying outside
@ -75,10 +76,8 @@ public class InlierUtil {
List<Double> trimmed = trim(percentToTrim, numbers);
DoubleSummaryStatisticsWithStdDev summaryStatistics =
trimmed.stream()
.collect(
DoubleSummaryStatisticsWithStdDev::new,
DoubleSummaryStatisticsWithStdDev summaryStatistics = stream(trimmed)
.collect(DoubleSummaryStatisticsWithStdDev::new,
DoubleSummaryStatisticsWithStdDev::accept,
DoubleSummaryStatisticsWithStdDev::combine);
@ -111,7 +110,7 @@ public class InlierUtil {
return numbers;
}
if (totalPercentTrim == 100) {
return FXCollections.emptyObservableList();
return Doubles.asList();
}
if (numbers.isEmpty()) {
@ -124,17 +123,17 @@ public class InlierUtil {
return numbers;
}
var sorted = numbers.stream().sorted();
var array = stream(numbers).toArray();
Arrays.sort(array);
var oneSideTrimmed = sorted.skip(countToDropFromEachSide);
// Here, having already trimmed the left-side, we are implicitly trimming
// the right-side by specifying a limit to the stream's length.
// An explicit right-side drop/trim/skip is not supported by the Stream API.
var countAfterTrim = count - (countToDropFromEachSide * 2); // visada > 0? ir <= count?
var bothSidesTrimmed = oneSideTrimmed.limit(countAfterTrim);
return bothSidesTrimmed.collect(Collectors.toList());
var sorted = Doubles.asList(array);
return sorted.subList(countToDropFromEachSide, sorted.size() - countToDropFromEachSide);
}
private static DoubleStream stream(Iterable<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.monetary.Altcoin;
import bisq.core.monetary.AltcoinExchangeRate;
import bisq.core.monetary.Price;
import bisq.core.monetary.Volume;
import bisq.core.offer.Offer;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Monetary;
import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat;
import org.bitcoinj.utils.MonetaryFormat;
@ -66,14 +62,6 @@ public class VolumeUtil {
return Volume.parse(String.valueOf(roundedVolume), volumeByAmount.getCurrencyCode());
}
public static Volume getVolume(Coin amount, Price price) {
if (price.getMonetary() instanceof Altcoin) {
return new Volume(new AltcoinExchangeRate((Altcoin) price.getMonetary()).coinToAltcoin(amount));
} else {
return new Volume(new ExchangeRate((Fiat) price.getMonetary()).coinToFiat(amount));
}
}
public static String formatVolume(Offer offer, Boolean decimalAligned, int maxNumberOfDigits) {
return formatVolume(offer, decimalAligned, maxNumberOfDigits, true);

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

View file

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

View file

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

View file

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

View file

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

View file

@ -53,6 +53,8 @@ import com.googlecode.jcsv.writer.CSVEntryConverter;
import javax.inject.Inject;
import javax.inject.Named;
import com.google.common.collect.Lists;
import com.jfoenix.controls.JFXTabPane;
import javafx.stage.Stage;
@ -85,7 +87,6 @@ import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import org.fxmisc.easybind.monadic.MonadicBinding;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
@ -389,13 +390,15 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
private void fillList() {
long ts = System.currentTimeMillis();
CompletableFuture.supplyAsync(() -> {
return model.tradeStatisticsByCurrency.stream()
boolean showAllTradeCurrencies = model.showAllTradeCurrenciesProperty.get();
// 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,
coinFormatter,
model.showAllTradeCurrenciesProperty.get()))
.collect(Collectors.toCollection(FXCollections::observableArrayList));
}).whenComplete((listItems, throwable) -> {
showAllTradeCurrencies))
.collect(Collectors.toCollection(FXCollections::observableArrayList))
).whenComplete((listItems, throwable) -> {
log.debug("Creating listItems took {} ms", System.currentTimeMillis() - ts);
long ts2 = System.currentTimeMillis();
@ -640,7 +643,7 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
return new StringConverter<>() {
@Override
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 previous tick and interferes with its label.
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.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue()));
dateColumn.setCellValueFactory(tradeStatistics -> tradeStatistics.getValue().asObservableValue());
dateColumn.setCellFactory(
new Callback<>() {
@Override
@ -784,7 +787,7 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
}
};
marketColumn.getStyleClass().add("number-column");
marketColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue()));
marketColumn.setCellValueFactory(tradeStatistics -> tradeStatistics.getValue().asObservableValue());
marketColumn.setCellFactory(
new Callback<>() {
@Override
@ -808,7 +811,7 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
// price
priceColumn = new TableColumn<>();
priceColumn.getStyleClass().add("number-column");
priceColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue()));
priceColumn.setCellValueFactory(tradeStatistics -> tradeStatistics.getValue().asObservableValue());
priceColumn.setCellFactory(
new Callback<>() {
@Override
@ -832,7 +835,7 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
// amount
TableColumn<TradeStatistics3ListItem, TradeStatistics3ListItem> amountColumn = new AutoTooltipTableColumn<>(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode()));
amountColumn.getStyleClass().add("number-column");
amountColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue()));
amountColumn.setCellValueFactory(tradeStatistics -> tradeStatistics.getValue().asObservableValue());
amountColumn.setCellFactory(
new Callback<>() {
@Override
@ -857,7 +860,7 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
// volume
volumeColumn = new TableColumn<>();
volumeColumn.getStyleClass().add("number-column");
volumeColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue()));
volumeColumn.setCellValueFactory(tradeStatistics -> tradeStatistics.getValue().asObservableValue());
volumeColumn.setCellFactory(
new Callback<>() {
@Override
@ -881,7 +884,7 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
// paymentMethod
TableColumn<TradeStatistics3ListItem, TradeStatistics3ListItem> paymentMethodColumn = new AutoTooltipTableColumn<>(Res.get("shared.paymentMethod"));
paymentMethodColumn.getStyleClass().add("number-column");
paymentMethodColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue()));
paymentMethodColumn.setCellValueFactory(tradeStatistics -> tradeStatistics.getValue().asObservableValue());
paymentMethodColumn.setCellFactory(
new Callback<>() {
@Override

View file

@ -73,6 +73,7 @@ class TradesChartsViewModel extends ActivatableViewModel {
// Enum
///////////////////////////////////////////////////////////////////////////////////////////
// NOTE: For the code to work correctly, order must be from biggest to smallest duration:
public enum TickUnit {
YEAR,
MONTH,
@ -96,7 +97,7 @@ class TradesChartsViewModel extends ActivatableViewModel {
final ObservableList<XYChart.Data<Number, Number>> priceItems = FXCollections.observableArrayList();
final ObservableList<XYChart.Data<Number, Number>> volumeItems = 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;
private int selectedTabIndex;
@ -208,7 +209,7 @@ class TradesChartsViewModel extends ActivatableViewModel {
private void applyAsyncUsdAveragePriceMapsPerTickUnit(CompletableFuture<Boolean> completeFuture) {
long ts = System.currentTimeMillis();
ChartCalculations.getUsdAveragePriceMapsPerTickUnit(tradeStatisticsManager.getObservableTradeStatisticsSet())
ChartCalculations.getUsdAveragePriceMapsPerTickUnit(tradeStatisticsManager.getNavigableTradeStatisticsSet())
.whenComplete((usdAveragePriceMapsPerTickUnit, throwable) -> {
if (deactivateCalled) {
return;
@ -278,7 +279,7 @@ class TradesChartsViewModel extends ActivatableViewModel {
}
UserThread.execute(() -> {
itemsPerInterval.clear();
itemsPerInterval.putAll(updateChartResult.getItemsPerInterval());
itemsPerInterval.addAll(updateChartResult.getItemsPerInterval());
priceItems.setAll(updateChartResult.getPriceItems());
volumeItems.setAll(updateChartResult.getVolumeItems());
@ -356,8 +357,8 @@ class TradesChartsViewModel extends ActivatableViewModel {
return currencyListItems.getObservableList().stream().filter(e -> e.tradeCurrency.equals(selectedTradeCurrencyProperty.get())).findAny();
}
long getTimeFromTickIndex(long tick) {
return ChartCalculations.getTimeFromTickIndex(tick, itemsPerInterval);
long getTimeFromTickIndex(int tickIndex) {
return ChartCalculations.getTimeFromTickIndex(tickIndex, itemsPerInterval);
}
@ -367,7 +368,7 @@ class TradesChartsViewModel extends ActivatableViewModel {
private void fillTradeCurrencies() {
// 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())
.collect(Collectors.toList());
currencyListItems.updateWithCurrencies(tradeCurrencyList, showAllCurrencyListItem);

View file

@ -54,6 +54,7 @@ import bisq.core.util.coin.CoinUtil;
import bisq.network.p2p.P2PService;
import bisq.common.util.MathUtils;
import bisq.common.util.RangeUtils;
import bisq.common.util.Tuple2;
import bisq.common.util.Utilities;
@ -62,6 +63,8 @@ import org.bitcoinj.core.Transaction;
import javax.inject.Named;
import com.google.common.collect.Range;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
@ -79,7 +82,6 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.SetChangeListener;
import java.util.Comparator;
import java.util.Date;
import java.util.Objects;
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
var blocksRange = Restrictions.getLockTime(paymentAccount.getPaymentMethod().isBlockchain());
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.getDate().compareTo(startDate) >= 0)
.sorted(Comparator.comparing(TradeStatistics3::getDate))
.collect(Collectors.toList());
var movingAverage = new MathUtils.MovingAverage(10, 0.2);
double[] extremes = {Double.MAX_VALUE, Double.MIN_VALUE};
sortedRangeData.forEach(e -> {
sortedFilteredRangeData.forEach(e -> {
var price = e.getTradePrice().getValue();
movingAverage.next(price).ifPresent(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.user.Preferences;
import bisq.core.util.PriceUtil;
import bisq.core.util.VolumeUtil;
import org.bitcoinj.core.Coin;
@ -120,7 +119,7 @@ class ClosedTradesDataModel extends ActivatableDataModel {
}
Price price = PriceUtil.marketPriceToPrice(marketPrice);
return Optional.of(VolumeUtil.getVolume(amount, price));
return Optional.of(price.getVolumeByAmount(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.core.locale.FiatCurrency;
import bisq.core.locale.GlobalSettings;
import bisq.core.monetary.Price;
import bisq.core.offer.bisq_v1.OfferPayload;
import bisq.core.payment.payload.PaymentMethod;
@ -37,17 +38,19 @@ import javafx.collections.ObservableSet;
import javafx.util.Pair;
import java.time.LocalDateTime;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
@ -62,7 +65,6 @@ public class TradesChartsViewModelTest {
TradeStatisticsManager tradeStatisticsManager;
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
private File dir;
OfferPayload offer = new OfferPayload(null,
0,
null,
@ -105,14 +107,10 @@ public class TradesChartsViewModelTest {
@BeforeEach
public void setup() throws IOException {
GlobalSettings.setLocale(Locale.US);
tradeStatisticsManager = mock(TradeStatisticsManager.class);
model = new TradesChartsViewModel(tradeStatisticsManager, mock(Preferences.class), mock(PriceFeedService.class),
mock(Navigation.class));
dir = File.createTempFile("temp_tests1", "");
//noinspection ResultOfMethodCallIgnored
dir.delete();
//noinspection ResultOfMethodCallIgnored
dir.mkdir();
}
@SuppressWarnings("ConstantConditions")
@ -130,9 +128,14 @@ public class TradesChartsViewModelTest {
long amount = Coin.parseCoin("4").value;
long volume = Fiat.parseFiat("EUR", "2200").value;
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<>();
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(),
Price.parse("EUR", "520").getValue(),
@ -171,9 +174,11 @@ public class TradesChartsViewModelTest {
null,
null));
Map<Long, Pair<Date, Set<TradeStatistics3>>> itemsPerInterval = null;
long tick = ChartCalculations.roundToTick(now, TradesChartsViewModel.TickUnit.DAY).getTime();
CandleData candleData = ChartCalculations.getCandleData(tick,
Date tickStart = ChartCalculations.roundToTick(now, TradesChartsViewModel.TickUnit.DAY);
List<Pair<Date, Set<TradeStatistics3>>> itemsPerInterval = List.of(
new Pair<>(tickStart, set), new Pair<>(now, Set.of())
);
CandleData candleData = ChartCalculations.getCandleData(0,
set,
0,
TradesChartsViewModel.TickUnit.DAY, currencyCode,
@ -187,12 +192,13 @@ public class TradesChartsViewModelTest {
assertEquals(amount, candleData.accumulatedAmount);
assertEquals(volume, candleData.accumulatedVolume);
assertEquals(isBullish, candleData.isBullish);
assertEquals(date, candleData.date);
}
// TODO JMOCKIT
@Disabled
@Test
public void testItemLists() throws ParseException {
public void testItemLists() {
// Helper class to add historic trades
class Trade {
Trade(String date, String size, String price, String cc) {
@ -207,9 +213,9 @@ public class TradesChartsViewModelTest {
}
Date date;
String size;
String price;
String cc;
final String size;
final String price;
final String cc;
}
// Trade EUR

View file

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