mirror of
https://github.com/bisq-network/bisq.git
synced 2025-03-13 11:09:10 +01:00
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:
commit
ac8ad24807
32 changed files with 852 additions and 422 deletions
65
common/src/main/java/bisq/common/util/ComparableExt.java
Normal file
65
common/src/main/java/bisq/common/util/ComparableExt.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
82
common/src/main/java/bisq/common/util/RangeUtils.java
Normal file
82
common/src/main/java/bisq/common/util/RangeUtils.java
Normal 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);
|
||||
}
|
||||
}
|
103
common/src/test/java/bisq/common/util/RangeUtilsTest.java
Normal file
103
common/src/test/java/bisq/common/util/RangeUtilsTest.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue