mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 09:52:23 +01:00
Merge branch 'master' into 03-add-txFeeRate-param
This commit is contained in:
commit
2842070afd
@ -110,7 +110,11 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
// For Priority.HIGH data we want to write to disk in any case to be on the safe side if we might have missed
|
||||
// a requestPersistence call after an important state update. Those are usually rather small data stores.
|
||||
// Otherwise we only persist if requestPersistence was called since the last persist call.
|
||||
if (persistenceManager.source.flushAtShutDown || persistenceManager.persistenceRequested) {
|
||||
// We also check if we have called read already to avoid a very early write attempt before we have ever
|
||||
// read the data, which would lead to a write of empty data
|
||||
// (fixes https://github.com/bisq-network/bisq/issues/4844).
|
||||
if (persistenceManager.readCalled.get() &&
|
||||
(persistenceManager.source.flushAtShutDown || persistenceManager.persistenceRequested)) {
|
||||
// We always get our completeHandler called even if exceptions happen. In case a file write fails
|
||||
// we still call our shutdown and count down routine as the completeHandler is triggered in any case.
|
||||
|
||||
@ -184,6 +188,7 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
private Timer timer;
|
||||
private ExecutorService writeToDiskExecutor;
|
||||
public final AtomicBoolean initCalled = new AtomicBoolean(false);
|
||||
public final AtomicBoolean readCalled = new AtomicBoolean(false);
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -303,6 +308,8 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
return null;
|
||||
}
|
||||
|
||||
readCalled.set(true);
|
||||
|
||||
File storageFile = new File(dir, fileName);
|
||||
if (!storageFile.exists()) {
|
||||
return null;
|
||||
|
@ -533,11 +533,19 @@ public class AccountAgeWitnessService {
|
||||
Coin tradeAmount,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
checkNotNull(offer);
|
||||
|
||||
// In case we don't find the witness we check if the trade amount is above the
|
||||
// TOLERATED_SMALL_TRADE_AMOUNT (0.01 BTC) and only in that case return false.
|
||||
return findWitness(offer)
|
||||
.map(witness -> verifyPeersTradeLimit(offer, tradeAmount, witness, new Date(), errorMessageHandler))
|
||||
.orElse(false);
|
||||
.orElse(isToleratedSmalleAmount(tradeAmount));
|
||||
}
|
||||
|
||||
private boolean isToleratedSmalleAmount(Coin tradeAmount) {
|
||||
return tradeAmount.value <= OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Package scope verification subroutines
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -45,6 +45,8 @@ import java.util.function.Consumer;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Provides high level interface to functionality of core Bisq features.
|
||||
* E.g. useful for different APIs to access data of different domains of Bisq.
|
||||
@ -208,8 +210,8 @@ public class CoreApi {
|
||||
coreTradesService.keepFunds(tradeId);
|
||||
}
|
||||
|
||||
public void withdrawFunds(String tradeId, String address) {
|
||||
coreTradesService.withdrawFunds(tradeId, address);
|
||||
public void withdrawFunds(String tradeId, String address, @Nullable String memo) {
|
||||
coreTradesService.withdrawFunds(tradeId, address, memo);
|
||||
}
|
||||
|
||||
public Trade getTrade(String tradeId) {
|
||||
|
@ -41,6 +41,8 @@ import java.util.function.Consumer;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static bisq.core.btc.model.AddressEntry.Context.TRADE_PAYOUT;
|
||||
import static java.lang.String.format;
|
||||
|
||||
@ -154,7 +156,7 @@ class CoreTradesService {
|
||||
tradeManager.onTradeCompleted(trade);
|
||||
}
|
||||
|
||||
void withdrawFunds(String tradeId, String toAddress) {
|
||||
void withdrawFunds(String tradeId, String toAddress, @Nullable String memo) {
|
||||
// An encrypted wallet must be unlocked for this operation.
|
||||
verifyTradeIsNotClosed(tradeId);
|
||||
var trade = getOpenTrade(tradeId).orElseThrow(() ->
|
||||
@ -184,6 +186,7 @@ class CoreTradesService {
|
||||
fee,
|
||||
coreWalletsService.getKey(),
|
||||
trade,
|
||||
memo,
|
||||
() -> {
|
||||
},
|
||||
(errorMessage, throwable) -> {
|
||||
|
@ -254,8 +254,8 @@ public class BtcWalletService extends WalletService {
|
||||
sendRequest.signInputs = false;
|
||||
|
||||
sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
|
||||
sigSizePerInput * numLegacyInputs +
|
||||
sigSizePerInput * numSegwitInputs / 4);
|
||||
sigSizePerInput * numLegacyInputs +
|
||||
sigSizePerInput * numSegwitInputs / 4);
|
||||
|
||||
sendRequest.feePerKb = Coin.ZERO;
|
||||
sendRequest.ensureMinRequiredFee = false;
|
||||
@ -274,8 +274,8 @@ public class BtcWalletService extends WalletService {
|
||||
numSegwitInputs = numInputs.second;
|
||||
txVsizeWithUnsignedInputs = resultTx.getVsize();
|
||||
long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
|
||||
sigSizePerInput * numLegacyInputs +
|
||||
sigSizePerInput * numSegwitInputs / 4).value;
|
||||
sigSizePerInput * numLegacyInputs +
|
||||
sigSizePerInput * numSegwitInputs / 4).value;
|
||||
|
||||
// calculated fee must be inside of a tolerance range with tx fee
|
||||
isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000;
|
||||
@ -374,8 +374,8 @@ public class BtcWalletService extends WalletService {
|
||||
sendRequest.signInputs = false;
|
||||
|
||||
sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
|
||||
sigSizePerInput * numLegacyInputs +
|
||||
sigSizePerInput * numSegwitInputs / 4);
|
||||
sigSizePerInput * numLegacyInputs +
|
||||
sigSizePerInput * numSegwitInputs / 4);
|
||||
sendRequest.feePerKb = Coin.ZERO;
|
||||
sendRequest.ensureMinRequiredFee = false;
|
||||
|
||||
@ -393,8 +393,8 @@ public class BtcWalletService extends WalletService {
|
||||
numSegwitInputs = numInputs.second;
|
||||
txVsizeWithUnsignedInputs = resultTx.getVsize();
|
||||
final long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
|
||||
sigSizePerInput * numLegacyInputs +
|
||||
sigSizePerInput * numSegwitInputs / 4).value;
|
||||
sigSizePerInput * numLegacyInputs +
|
||||
sigSizePerInput * numSegwitInputs / 4).value;
|
||||
// calculated fee must be inside of a tolerance range with tx fee
|
||||
isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000;
|
||||
}
|
||||
@ -593,7 +593,7 @@ public class BtcWalletService extends WalletService {
|
||||
for (TransactionInput input : tx.getInputs()) {
|
||||
TransactionOutput connectedOutput = input.getConnectedOutput();
|
||||
if (connectedOutput == null || ScriptPattern.isP2PKH(connectedOutput.getScriptPubKey()) ||
|
||||
ScriptPattern.isP2PK(connectedOutput.getScriptPubKey())) {
|
||||
ScriptPattern.isP2PK(connectedOutput.getScriptPubKey())) {
|
||||
// If connectedOutput is null, we don't know here the input type. To avoid underpaying fees,
|
||||
// we treat it as a legacy input which will result in a higher fee estimation.
|
||||
numLegacyInputs++;
|
||||
@ -1110,12 +1110,15 @@ public class BtcWalletService extends WalletService {
|
||||
Coin fee,
|
||||
@Nullable KeyParameter aesKey,
|
||||
@SuppressWarnings("SameParameterValue") AddressEntry.Context context,
|
||||
@Nullable String memo,
|
||||
FutureCallback<Transaction> callback) throws AddressFormatException,
|
||||
AddressEntryException, InsufficientMoneyException {
|
||||
SendRequest sendRequest = getSendRequest(fromAddress, toAddress, receiverAmount, fee, aesKey, context);
|
||||
Wallet.SendResult sendResult = wallet.sendCoins(sendRequest);
|
||||
Futures.addCallback(sendResult.broadcastComplete, callback, MoreExecutors.directExecutor());
|
||||
|
||||
if (memo != null) {
|
||||
sendResult.tx.setMemo(memo);
|
||||
}
|
||||
printTx("sendFunds", sendResult.tx);
|
||||
return sendResult.tx.getTxId().toString();
|
||||
}
|
||||
@ -1126,13 +1129,16 @@ public class BtcWalletService extends WalletService {
|
||||
Coin fee,
|
||||
@Nullable String changeAddress,
|
||||
@Nullable KeyParameter aesKey,
|
||||
@Nullable String memo,
|
||||
FutureCallback<Transaction> callback) throws AddressFormatException,
|
||||
AddressEntryException, InsufficientMoneyException {
|
||||
|
||||
SendRequest request = getSendRequestForMultipleAddresses(fromAddresses, toAddress, receiverAmount, fee, changeAddress, aesKey);
|
||||
Wallet.SendResult sendResult = wallet.sendCoins(request);
|
||||
Futures.addCallback(sendResult.broadcastComplete, callback, MoreExecutors.directExecutor());
|
||||
|
||||
if (memo != null) {
|
||||
sendResult.tx.setMemo(memo);
|
||||
}
|
||||
printTx("sendFunds", sendResult.tx);
|
||||
return sendResult.tx;
|
||||
}
|
||||
|
@ -30,8 +30,10 @@ import bisq.core.provider.fee.FeeService;
|
||||
import bisq.core.provider.price.MarketPrice;
|
||||
import bisq.core.provider.price.PriceFeedService;
|
||||
import bisq.core.trade.statistics.ReferralIdService;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
import bisq.core.user.AutoConfirmSettings;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.AveragePriceUtil;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
import bisq.core.util.coin.CoinUtil;
|
||||
|
||||
@ -39,6 +41,7 @@ import bisq.network.p2p.P2PService;
|
||||
|
||||
import bisq.common.app.Capabilities;
|
||||
import bisq.common.util.MathUtils;
|
||||
import bisq.common.util.Tuple2;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.utils.Fiat;
|
||||
@ -80,6 +83,7 @@ public class OfferUtil {
|
||||
private final PriceFeedService priceFeedService;
|
||||
private final P2PService p2PService;
|
||||
private final ReferralIdService referralIdService;
|
||||
private final TradeStatisticsManager tradeStatisticsManager;
|
||||
|
||||
private final Predicate<String> isValidFeePaymentCurrencyCode = (c) ->
|
||||
c.equalsIgnoreCase("BSQ") || c.equalsIgnoreCase("BTC");
|
||||
@ -91,7 +95,8 @@ public class OfferUtil {
|
||||
Preferences preferences,
|
||||
PriceFeedService priceFeedService,
|
||||
P2PService p2PService,
|
||||
ReferralIdService referralIdService) {
|
||||
ReferralIdService referralIdService,
|
||||
TradeStatisticsManager tradeStatisticsManager) {
|
||||
this.accountAgeWitnessService = accountAgeWitnessService;
|
||||
this.bsqWalletService = bsqWalletService;
|
||||
this.filterManager = filterManager;
|
||||
@ -99,6 +104,7 @@ public class OfferUtil {
|
||||
this.priceFeedService = priceFeedService;
|
||||
this.p2PService = p2PService;
|
||||
this.referralIdService = referralIdService;
|
||||
this.tradeStatisticsManager = tradeStatisticsManager;
|
||||
}
|
||||
|
||||
public void maybeSetFeePaymentCurrencyPreference(String feeCurrencyCode) {
|
||||
@ -286,8 +292,14 @@ public class OfferUtil {
|
||||
public Optional<Volume> getFeeInUserFiatCurrency(Coin makerFee,
|
||||
boolean isCurrencyForMakerFeeBtc,
|
||||
CoinFormatter bsqFormatter) {
|
||||
String countryCode = preferences.getUserCountry().code;
|
||||
String userCurrencyCode = CurrencyUtil.getCurrencyByCountryCode(countryCode).getCode();
|
||||
String userCurrencyCode = preferences.getPreferredTradeCurrency().getCode();
|
||||
if (CurrencyUtil.isCryptoCurrency(userCurrencyCode)) {
|
||||
// In case the user has selected a altcoin as preferredTradeCurrency
|
||||
// we derive the fiat currency from the user country
|
||||
String countryCode = preferences.getUserCountry().code;
|
||||
userCurrencyCode = CurrencyUtil.getCurrencyByCountryCode(countryCode).getCode();
|
||||
}
|
||||
|
||||
return getFeeInUserFiatCurrency(makerFee,
|
||||
isCurrencyForMakerFeeBtc,
|
||||
userCurrencyCode,
|
||||
@ -347,8 +359,6 @@ public class OfferUtil {
|
||||
boolean isCurrencyForMakerFeeBtc,
|
||||
String userCurrencyCode,
|
||||
CoinFormatter bsqFormatter) {
|
||||
// We use the users currency derived from his selected country. We don't use the
|
||||
// preferredTradeCurrency from preferences as that can be also set to an altcoin.
|
||||
MarketPrice marketPrice = priceFeedService.getMarketPrice(userCurrencyCode);
|
||||
if (marketPrice != null && makerFee != null) {
|
||||
long marketPriceAsLong = roundDoubleToLong(
|
||||
@ -358,16 +368,16 @@ public class OfferUtil {
|
||||
if (isCurrencyForMakerFeeBtc) {
|
||||
return Optional.of(userCurrencyPrice.getVolumeByAmount(makerFee));
|
||||
} else {
|
||||
Optional<Price> optionalBsqPrice = priceFeedService.getBsqPrice();
|
||||
if (optionalBsqPrice.isPresent()) {
|
||||
Price bsqPrice = optionalBsqPrice.get();
|
||||
String inputValue = bsqFormatter.formatCoin(makerFee);
|
||||
Volume makerFeeAsVolume = Volume.parse(inputValue, "BSQ");
|
||||
Coin requiredBtc = bsqPrice.getAmountByVolume(makerFeeAsVolume);
|
||||
return Optional.of(userCurrencyPrice.getVolumeByAmount(requiredBtc));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
// We use the current market price for the fiat currency and the 30 day average BSQ price
|
||||
Tuple2<Price, Price> tuple = AveragePriceUtil.getAveragePriceTuple(preferences,
|
||||
tradeStatisticsManager,
|
||||
30);
|
||||
Price bsqPrice = tuple.second;
|
||||
String inputValue = bsqFormatter.formatCoin(makerFee);
|
||||
Volume makerFeeAsVolume = Volume.parse(inputValue, "BSQ");
|
||||
Coin requiredBtc = bsqPrice.getAmountByVolume(makerFeeAsVolume);
|
||||
Volume volumeByAmount = userCurrencyPrice.getVolumeByAmount(requiredBtc);
|
||||
return Optional.of(volumeByAmount);
|
||||
}
|
||||
} else {
|
||||
return Optional.empty();
|
||||
|
@ -462,8 +462,14 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
// Complete trade
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void onWithdrawRequest(String toAddress, Coin amount, Coin fee, KeyParameter aesKey,
|
||||
Trade trade, ResultHandler resultHandler, FaultHandler faultHandler) {
|
||||
public void onWithdrawRequest(String toAddress,
|
||||
Coin amount,
|
||||
Coin fee,
|
||||
KeyParameter aesKey,
|
||||
Trade trade,
|
||||
@Nullable String memo,
|
||||
ResultHandler resultHandler,
|
||||
FaultHandler faultHandler) {
|
||||
String fromAddress = btcWalletService.getOrCreateAddressEntry(trade.getId(),
|
||||
AddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||
FutureCallback<Transaction> callback = new FutureCallback<>() {
|
||||
@ -487,7 +493,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
}
|
||||
};
|
||||
try {
|
||||
btcWalletService.sendFunds(fromAddress, toAddress, amount, fee, aesKey, AddressEntry.Context.TRADE_PAYOUT, callback);
|
||||
btcWalletService.sendFunds(fromAddress, toAddress, amount, fee, aesKey,
|
||||
AddressEntry.Context.TRADE_PAYOUT, memo, callback);
|
||||
} catch (AddressFormatException | InsufficientMoneyException | AddressEntryException e) {
|
||||
e.printStackTrace();
|
||||
log.error(e.getMessage());
|
||||
|
139
core/src/main/java/bisq/core/util/AveragePriceUtil.java
Normal file
139
core/src/main/java/bisq/core/util/AveragePriceUtil.java
Normal file
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import bisq.core.monetary.Altcoin;
|
||||
import bisq.core.monetary.Price;
|
||||
import bisq.core.trade.statistics.TradeStatistics3;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
import bisq.core.user.Preferences;
|
||||
|
||||
import bisq.common.util.MathUtils;
|
||||
import bisq.common.util.Tuple2;
|
||||
|
||||
import org.bitcoinj.utils.Fiat;
|
||||
|
||||
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.stream.Collectors;
|
||||
|
||||
public class AveragePriceUtil {
|
||||
private static final double HOW_MANY_STD_DEVS_CONSTITUTE_OUTLIER = 10;
|
||||
|
||||
public static Tuple2<Price, Price> getAveragePriceTuple(Preferences preferences,
|
||||
TradeStatisticsManager tradeStatisticsManager,
|
||||
int days) {
|
||||
double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100));
|
||||
Date pastXDays = getPastDate(days);
|
||||
List<TradeStatistics3> bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
|
||||
.filter(e -> e.getCurrency().equals("BSQ"))
|
||||
.filter(e -> e.getDate().after(pastXDays))
|
||||
.collect(Collectors.toList());
|
||||
List<TradeStatistics3> bsqTradePastXDays = percentToTrim > 0 ?
|
||||
removeOutliers(bsqAllTradePastXDays, percentToTrim) :
|
||||
bsqAllTradePastXDays;
|
||||
|
||||
List<TradeStatistics3> usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
|
||||
.filter(e -> e.getCurrency().equals("USD"))
|
||||
.filter(e -> e.getDate().after(pastXDays))
|
||||
.collect(Collectors.toList());
|
||||
List<TradeStatistics3> usdTradePastXDays = percentToTrim > 0 ?
|
||||
removeOutliers(usdAllTradePastXDays, percentToTrim) :
|
||||
usdAllTradePastXDays;
|
||||
|
||||
Price usdPrice = Price.valueOf("USD", getUSDAverage(bsqTradePastXDays, usdTradePastXDays));
|
||||
Price bsqPrice = Price.valueOf("BSQ", getBTCAverage(bsqTradePastXDays));
|
||||
return new Tuple2<>(usdPrice, bsqPrice);
|
||||
}
|
||||
|
||||
private static List<TradeStatistics3> removeOutliers(List<TradeStatistics3> list, double percentToTrim) {
|
||||
List<Double> yValues = list.stream()
|
||||
.filter(TradeStatistics3::isValid)
|
||||
.map(e -> (double) e.getPrice())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
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)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static long getBTCAverage(List<TradeStatistics3> list) {
|
||||
long accumulatedVolume = 0;
|
||||
long accumulatedAmount = 0;
|
||||
|
||||
for (TradeStatistics3 item : list) {
|
||||
accumulatedVolume += item.getTradeVolume().getValue();
|
||||
accumulatedAmount += item.getTradeAmount().getValue(); // Amount of BTC traded
|
||||
}
|
||||
long averagePrice;
|
||||
double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, Altcoin.SMALLEST_UNIT_EXPONENT);
|
||||
averagePrice = accumulatedVolume > 0 ? MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / (double) accumulatedVolume) : 0;
|
||||
|
||||
return averagePrice;
|
||||
}
|
||||
|
||||
private static long getUSDAverage(List<TradeStatistics3> bsqList, List<TradeStatistics3> usdList) {
|
||||
// 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));
|
||||
var usdBTCPrice = 10000d; // Default to 10000 USD per BTC if there is no USD feed at all
|
||||
|
||||
for (TradeStatistics3 item : bsqList) {
|
||||
// Find usdprice 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);
|
||||
var bsqAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeVolume().getValue(),
|
||||
Altcoin.SMALLEST_UNIT_EXPONENT);
|
||||
var btcAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeAmount().getValue(),
|
||||
Altcoin.SMALLEST_UNIT_EXPONENT);
|
||||
usdBsqList.add(new Tuple2<>(usdBTCPrice * btcAmount, bsqAmount));
|
||||
}
|
||||
long averagePrice;
|
||||
var usdTraded = usdBsqList.stream()
|
||||
.mapToDouble(item -> item.first)
|
||||
.sum();
|
||||
var bsqTraded = usdBsqList.stream()
|
||||
.mapToDouble(item -> item.second)
|
||||
.sum();
|
||||
var averageAsDouble = bsqTraded > 0 ? usdTraded / bsqTraded : 0d;
|
||||
var averageScaledUp = MathUtils.scaleUpByPowerOf10(averageAsDouble, Fiat.SMALLEST_UNIT_EXPONENT);
|
||||
averagePrice = bsqTraded > 0 ? MathUtils.roundDoubleToLong(averageScaledUp) : 0;
|
||||
|
||||
return averagePrice;
|
||||
}
|
||||
|
||||
private static Date getPastDate(int days) {
|
||||
Calendar cal = new GregorianCalendar();
|
||||
cal.setTime(new Date());
|
||||
cal.add(Calendar.DAY_OF_MONTH, -1 * days);
|
||||
return cal.getTime();
|
||||
}
|
||||
}
|
140
core/src/main/java/bisq/core/util/InlierUtil.java
Normal file
140
core/src/main/java/bisq/core/util/InlierUtil.java
Normal file
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import bisq.common.util.DoubleSummaryStatisticsWithStdDev;
|
||||
import bisq.common.util.Tuple2;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
|
||||
import java.util.DoubleSummaryStatistics;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class InlierUtil {
|
||||
|
||||
/* Finds the minimum and maximum inlier values. The returned values may be NaN.
|
||||
* See `computeInlierThreshold` for the definition of inlier.
|
||||
*/
|
||||
public static Tuple2<Double, Double> findInlierRange(
|
||||
List<Double> yValues,
|
||||
double percentToTrim,
|
||||
double howManyStdDevsConstituteOutlier
|
||||
) {
|
||||
Tuple2<Double, Double> inlierThreshold =
|
||||
computeInlierThreshold(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
|
||||
|
||||
DoubleSummaryStatistics inlierStatistics =
|
||||
yValues
|
||||
.stream()
|
||||
.filter(y -> withinBounds(inlierThreshold, y))
|
||||
.mapToDouble(Double::doubleValue)
|
||||
.summaryStatistics();
|
||||
|
||||
var inlierMin = inlierStatistics.getMin();
|
||||
var inlierMax = inlierStatistics.getMax();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/* Computes the lower and upper inlier thresholds. A point lying outside
|
||||
* these thresholds is considered an outlier, and a point lying within
|
||||
* is considered an inlier.
|
||||
* The thresholds are found by trimming the dataset (see method `trim`),
|
||||
* then adding or subtracting a multiple of its (trimmed) standard
|
||||
* deviation from its (trimmed) mean.
|
||||
*/
|
||||
private static Tuple2<Double, Double> computeInlierThreshold(
|
||||
List<Double> numbers, double percentToTrim, double howManyStdDevsConstituteOutlier
|
||||
) {
|
||||
if (howManyStdDevsConstituteOutlier <= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"howManyStdDevsConstituteOutlier should be a positive number");
|
||||
}
|
||||
|
||||
List<Double> trimmed = trim(percentToTrim, numbers);
|
||||
|
||||
DoubleSummaryStatisticsWithStdDev summaryStatistics =
|
||||
trimmed.stream()
|
||||
.collect(
|
||||
DoubleSummaryStatisticsWithStdDev::new,
|
||||
DoubleSummaryStatisticsWithStdDev::accept,
|
||||
DoubleSummaryStatisticsWithStdDev::combine);
|
||||
|
||||
double mean = summaryStatistics.getAverage();
|
||||
double stdDev = summaryStatistics.getStandardDeviation();
|
||||
|
||||
var inlierLowerThreshold = mean - (stdDev * howManyStdDevsConstituteOutlier);
|
||||
var inlierUpperThreshold = mean + (stdDev * howManyStdDevsConstituteOutlier);
|
||||
|
||||
return new Tuple2<>(inlierLowerThreshold, inlierUpperThreshold);
|
||||
}
|
||||
|
||||
/* Sorts the data and discards given percentage from the left and right sides each.
|
||||
* E.g. 5% trim implies a total of 10% (2x 5%) of elements discarded.
|
||||
* Used in calculating trimmed mean (and in turn trimmed standard deviation),
|
||||
* which is more robust to outliers than a simple mean.
|
||||
*/
|
||||
private static List<Double> trim(double percentToTrim, List<Double> numbers) {
|
||||
var minPercentToTrim = 0;
|
||||
var maxPercentToTrim = 50;
|
||||
if (minPercentToTrim > percentToTrim || percentToTrim > maxPercentToTrim) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format(
|
||||
"The percentage of data points to trim must be in the range [%d,%d].",
|
||||
minPercentToTrim, maxPercentToTrim));
|
||||
}
|
||||
|
||||
var totalPercentTrim = percentToTrim * 2;
|
||||
if (totalPercentTrim == 0) {
|
||||
return numbers;
|
||||
}
|
||||
if (totalPercentTrim == 100) {
|
||||
return FXCollections.emptyObservableList();
|
||||
}
|
||||
|
||||
if (numbers.isEmpty()) {
|
||||
return numbers;
|
||||
}
|
||||
|
||||
var count = numbers.size();
|
||||
int countToDropFromEachSide = (int) Math.round((count / 100d) * percentToTrim); // visada >= 0?
|
||||
if (countToDropFromEachSide == 0) {
|
||||
return numbers;
|
||||
}
|
||||
|
||||
var sorted = numbers.stream().sorted();
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
@ -144,7 +144,8 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
|
||||
public void withdrawFunds(WithdrawFundsRequest req,
|
||||
StreamObserver<WithdrawFundsReply> responseObserver) {
|
||||
try {
|
||||
coreApi.withdrawFunds(req.getTradeId(), req.getAddress());
|
||||
//TODO @ghubstan Feel free to add a memo param for withdrawal requests (was just added in UI)
|
||||
coreApi.withdrawFunds(req.getTradeId(), req.getAddress(), null);
|
||||
var reply = WithdrawFundsReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
|
@ -20,28 +20,25 @@ package bisq.desktop.main.dao.economy.dashboard;
|
||||
import bisq.desktop.common.view.ActivatableView;
|
||||
import bisq.desktop.common.view.FxmlView;
|
||||
import bisq.desktop.components.TextFieldWithIcon;
|
||||
import bisq.desktop.util.AxisInlierUtils;
|
||||
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.dao.state.DaoStateListener;
|
||||
import bisq.core.dao.state.model.blockchain.Block;
|
||||
import bisq.core.dao.state.model.governance.IssuanceType;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.monetary.Altcoin;
|
||||
import bisq.core.monetary.Price;
|
||||
import bisq.core.provider.price.PriceFeedService;
|
||||
import bisq.core.trade.statistics.TradeStatistics3;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.AveragePriceUtil;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
import bisq.core.util.coin.BsqFormatter;
|
||||
|
||||
import bisq.common.util.MathUtils;
|
||||
import bisq.common.util.Tuple2;
|
||||
import bisq.common.util.Tuple3;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.utils.Fiat;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@ -75,11 +72,7 @@ import java.time.format.FormatStyle;
|
||||
import java.time.temporal.TemporalAdjuster;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -114,7 +107,6 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
|
||||
|
||||
private Coin availableAmount;
|
||||
private int gridRow = 0;
|
||||
double howManyStdDevsConstituteOutlier = 10;
|
||||
private Price avg30DayUSDPrice;
|
||||
|
||||
|
||||
@ -374,110 +366,21 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
|
||||
}
|
||||
|
||||
private long updateAveragePriceField(TextField textField, int days, boolean isUSDField) {
|
||||
double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100));
|
||||
Date pastXDays = getPastDate(days);
|
||||
List<TradeStatistics3> bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
|
||||
.filter(e -> e.getCurrency().equals("BSQ"))
|
||||
.filter(e -> e.getDate().after(pastXDays))
|
||||
.collect(Collectors.toList());
|
||||
List<TradeStatistics3> bsqTradePastXDays = percentToTrim > 0 ?
|
||||
removeOutliers(bsqAllTradePastXDays, percentToTrim) :
|
||||
bsqAllTradePastXDays;
|
||||
Tuple2<Price, Price> tuple = AveragePriceUtil.getAveragePriceTuple(preferences, tradeStatisticsManager, days);
|
||||
Price usdPrice = tuple.first;
|
||||
Price bsqPrice = tuple.second;
|
||||
|
||||
List<TradeStatistics3> usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
|
||||
.filter(e -> e.getCurrency().equals("USD"))
|
||||
.filter(e -> e.getDate().after(pastXDays))
|
||||
.collect(Collectors.toList());
|
||||
List<TradeStatistics3> usdTradePastXDays = percentToTrim > 0 ?
|
||||
removeOutliers(usdAllTradePastXDays, percentToTrim) :
|
||||
usdAllTradePastXDays;
|
||||
|
||||
long average = isUSDField ? getUSDAverage(bsqTradePastXDays, usdTradePastXDays) :
|
||||
getBTCAverage(bsqTradePastXDays);
|
||||
Price avgPrice = isUSDField ? Price.valueOf("USD", average) :
|
||||
Price.valueOf("BSQ", average);
|
||||
String avg = FormattingUtils.formatPrice(avgPrice);
|
||||
if (isUSDField) {
|
||||
textField.setText(avg + " USD/BSQ");
|
||||
textField.setText(usdPrice + " USD/BSQ");
|
||||
if (days == 30) {
|
||||
avg30DayUSDPrice = avgPrice;
|
||||
avg30DayUSDPrice = usdPrice;
|
||||
}
|
||||
} else {
|
||||
textField.setText(avg + " BSQ/BTC");
|
||||
textField.setText(bsqPrice + " BSQ/BTC");
|
||||
}
|
||||
return average;
|
||||
}
|
||||
|
||||
private List<TradeStatistics3> removeOutliers(List<TradeStatistics3> list, double percentToTrim) {
|
||||
List<Double> yValues = list.stream()
|
||||
.filter(TradeStatistics3::isValid)
|
||||
.map(e -> (double) e.getPrice())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Tuple2<Double, Double> tuple = AxisInlierUtils.findInlierRange(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
|
||||
double lowerBound = tuple.first;
|
||||
double upperBound = tuple.second;
|
||||
return list.stream()
|
||||
.filter(e -> e.getPrice() > lowerBound)
|
||||
.filter(e -> e.getPrice() < upperBound)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private long getBTCAverage(List<TradeStatistics3> list) {
|
||||
long accumulatedVolume = 0;
|
||||
long accumulatedAmount = 0;
|
||||
|
||||
for (TradeStatistics3 item : list) {
|
||||
accumulatedVolume += item.getTradeVolume().getValue();
|
||||
accumulatedAmount += item.getTradeAmount().getValue(); // Amount of BTC traded
|
||||
}
|
||||
long averagePrice;
|
||||
double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, Altcoin.SMALLEST_UNIT_EXPONENT);
|
||||
averagePrice = accumulatedVolume > 0 ? MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / (double) accumulatedVolume) : 0;
|
||||
|
||||
return averagePrice;
|
||||
}
|
||||
|
||||
private long getUSDAverage(List<TradeStatistics3> bsqList, List<TradeStatistics3> usdList) {
|
||||
// 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));
|
||||
var usdBTCPrice = 10000d; // Default to 10000 USD per BTC if there is no USD feed at all
|
||||
|
||||
for (TradeStatistics3 item : bsqList) {
|
||||
// Find usdprice 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);
|
||||
var bsqAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeVolume().getValue(),
|
||||
Altcoin.SMALLEST_UNIT_EXPONENT);
|
||||
var btcAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeAmount().getValue(),
|
||||
Altcoin.SMALLEST_UNIT_EXPONENT);
|
||||
usdBsqList.add(new Tuple2<>(usdBTCPrice * btcAmount, bsqAmount));
|
||||
}
|
||||
long averagePrice;
|
||||
var usdTraded = usdBsqList.stream()
|
||||
.mapToDouble(item -> item.first)
|
||||
.sum();
|
||||
var bsqTraded = usdBsqList.stream()
|
||||
.mapToDouble(item -> item.second)
|
||||
.sum();
|
||||
var averageAsDouble = bsqTraded > 0 ? usdTraded / bsqTraded : 0d;
|
||||
var averageScaledUp = MathUtils.scaleUpByPowerOf10(averageAsDouble, Fiat.SMALLEST_UNIT_EXPONENT);
|
||||
averagePrice = bsqTraded > 0 ? MathUtils.roundDoubleToLong(averageScaledUp) : 0;
|
||||
|
||||
return averagePrice;
|
||||
}
|
||||
|
||||
private Date getPastDate(int days) {
|
||||
Calendar cal = new GregorianCalendar();
|
||||
cal.setTime(new Date());
|
||||
cal.add(Calendar.DAY_OF_MONTH, -1 * days);
|
||||
return cal.getTime();
|
||||
Price average = isUSDField ? usdPrice : bsqPrice;
|
||||
return average.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -494,14 +494,19 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
|
||||
|
||||
private void sendFunds(Coin amount, Coin fee, KeyParameter aesKey, FutureCallback<Transaction> callback) {
|
||||
try {
|
||||
String memo = withdrawMemoTextField.getText();
|
||||
if (memo.isEmpty()) {
|
||||
memo = null;
|
||||
}
|
||||
Transaction transaction = btcWalletService.sendFundsForMultipleAddresses(fromAddresses,
|
||||
withdrawToTextField.getText(),
|
||||
amount,
|
||||
fee,
|
||||
null,
|
||||
aesKey,
|
||||
memo,
|
||||
callback);
|
||||
transaction.setMemo(withdrawMemoTextField.getText());
|
||||
|
||||
reset();
|
||||
updateList();
|
||||
} catch (AddressFormatException e) {
|
||||
|
@ -209,6 +209,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||
Coin amount,
|
||||
Coin fee,
|
||||
KeyParameter aesKey,
|
||||
@Nullable String memo,
|
||||
ResultHandler resultHandler,
|
||||
FaultHandler faultHandler) {
|
||||
checkNotNull(getTrade(), "trade must not be null");
|
||||
@ -220,6 +221,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||
fee,
|
||||
aesKey,
|
||||
getTrade(),
|
||||
memo,
|
||||
() -> {
|
||||
resultHandler.handleResult();
|
||||
selectBestItem();
|
||||
|
@ -74,7 +74,7 @@ import static bisq.desktop.util.FormBuilder.addTitledGroupBg;
|
||||
public class BuyerStep4View extends TradeStepView {
|
||||
// private final ChangeListener<Boolean> focusedPropertyListener;
|
||||
|
||||
private InputTextField withdrawAddressTextField;
|
||||
private InputTextField withdrawAddressTextField, withdrawMemoTextField;
|
||||
private Button withdrawToExternalWalletButton, useSavingsWalletButton;
|
||||
private TitledGroupBg withdrawTitledGroupBg;
|
||||
|
||||
@ -131,10 +131,17 @@ public class BuyerStep4View extends TradeStepView {
|
||||
withdrawTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 1, Res.get("portfolio.pending.step5_buyer.withdrawBTC"), Layout.COMPACT_GROUP_DISTANCE);
|
||||
withdrawTitledGroupBg.getStyleClass().add("last");
|
||||
addCompactTopLabelTextField(gridPane, gridRow, Res.get("portfolio.pending.step5_buyer.amount"), model.getPayoutAmount(), Layout.FIRST_ROW_AND_GROUP_DISTANCE);
|
||||
|
||||
withdrawAddressTextField = addInputTextField(gridPane, ++gridRow, Res.get("portfolio.pending.step5_buyer.withdrawToAddress"));
|
||||
withdrawAddressTextField.setManaged(false);
|
||||
withdrawAddressTextField.setVisible(false);
|
||||
|
||||
withdrawMemoTextField = addInputTextField(gridPane, ++gridRow,
|
||||
Res.get("funds.withdrawal.memoLabel", Res.getBaseCurrencyCode()));
|
||||
withdrawMemoTextField.setPromptText(Res.get("funds.withdrawal.memo"));
|
||||
withdrawMemoTextField.setManaged(false);
|
||||
withdrawMemoTextField.setVisible(false);
|
||||
|
||||
HBox hBox = new HBox();
|
||||
hBox.setSpacing(10);
|
||||
useSavingsWalletButton = new AutoTooltipButton(Res.get("portfolio.pending.step5_buyer.moveToBisqWallet"));
|
||||
@ -170,7 +177,9 @@ public class BuyerStep4View extends TradeStepView {
|
||||
private void onWithdrawal() {
|
||||
withdrawAddressTextField.setManaged(true);
|
||||
withdrawAddressTextField.setVisible(true);
|
||||
GridPane.setRowSpan(withdrawTitledGroupBg, 2);
|
||||
withdrawMemoTextField.setManaged(true);
|
||||
withdrawMemoTextField.setVisible(true);
|
||||
GridPane.setRowSpan(withdrawTitledGroupBg, 3);
|
||||
withdrawToExternalWalletButton.setDefaultButton(true);
|
||||
useSavingsWalletButton.setDefaultButton(false);
|
||||
withdrawToExternalWalletButton.getStyleClass().add("action-button");
|
||||
@ -271,10 +280,15 @@ public class BuyerStep4View extends TradeStepView {
|
||||
FaultHandler faultHandler) {
|
||||
useSavingsWalletButton.setDisable(true);
|
||||
withdrawToExternalWalletButton.setDisable(true);
|
||||
String memo = withdrawMemoTextField.getText();
|
||||
if (memo.isEmpty()) {
|
||||
memo = null;
|
||||
}
|
||||
model.dataModel.onWithdrawRequest(toAddress,
|
||||
amount,
|
||||
fee,
|
||||
aesKey,
|
||||
memo,
|
||||
resultHandler,
|
||||
faultHandler);
|
||||
}
|
||||
|
@ -17,7 +17,8 @@
|
||||
|
||||
package bisq.desktop.util;
|
||||
|
||||
import bisq.common.util.DoubleSummaryStatisticsWithStdDev;
|
||||
import bisq.core.util.InlierUtil;
|
||||
|
||||
import bisq.common.util.Tuple2;
|
||||
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
@ -25,9 +26,7 @@ import javafx.scene.chart.XYChart;
|
||||
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.FXCollections;
|
||||
|
||||
import java.util.DoubleSummaryStatistics;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -76,7 +75,7 @@ public class AxisInlierUtils {
|
||||
}
|
||||
|
||||
Tuple2<Double, Double> inlierRange =
|
||||
findInlierRange(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
|
||||
InlierUtil.findInlierRange(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
|
||||
|
||||
applyRange(yAxis, maxNumberOfTicks, inlierRange);
|
||||
}
|
||||
@ -88,115 +87,6 @@ public class AxisInlierUtils {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/* Finds the minimum and maximum inlier values. The returned values may be NaN.
|
||||
* See `computeInlierThreshold` for the definition of inlier.
|
||||
*/
|
||||
public static Tuple2<Double, Double> findInlierRange(
|
||||
List<Double> yValues,
|
||||
double percentToTrim,
|
||||
double howManyStdDevsConstituteOutlier
|
||||
) {
|
||||
Tuple2<Double, Double> inlierThreshold =
|
||||
computeInlierThreshold(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
|
||||
|
||||
DoubleSummaryStatistics inlierStatistics =
|
||||
yValues
|
||||
.stream()
|
||||
.filter(y -> withinBounds(inlierThreshold, y))
|
||||
.mapToDouble(Double::doubleValue)
|
||||
.summaryStatistics();
|
||||
|
||||
var inlierMin = inlierStatistics.getMin();
|
||||
var inlierMax = inlierStatistics.getMax();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/* Computes the lower and upper inlier thresholds. A point lying outside
|
||||
* these thresholds is considered an outlier, and a point lying within
|
||||
* is considered an inlier.
|
||||
* The thresholds are found by trimming the dataset (see method `trim`),
|
||||
* then adding or subtracting a multiple of its (trimmed) standard
|
||||
* deviation from its (trimmed) mean.
|
||||
*/
|
||||
private static Tuple2<Double, Double> computeInlierThreshold(
|
||||
List<Double> numbers, double percentToTrim, double howManyStdDevsConstituteOutlier
|
||||
) {
|
||||
if (howManyStdDevsConstituteOutlier <= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"howManyStdDevsConstituteOutlier should be a positive number");
|
||||
}
|
||||
|
||||
List<Double> trimmed = trim(percentToTrim, numbers);
|
||||
|
||||
DoubleSummaryStatisticsWithStdDev summaryStatistics =
|
||||
trimmed.stream()
|
||||
.collect(
|
||||
DoubleSummaryStatisticsWithStdDev::new,
|
||||
DoubleSummaryStatisticsWithStdDev::accept,
|
||||
DoubleSummaryStatisticsWithStdDev::combine);
|
||||
|
||||
double mean = summaryStatistics.getAverage();
|
||||
double stdDev = summaryStatistics.getStandardDeviation();
|
||||
|
||||
var inlierLowerThreshold = mean - (stdDev * howManyStdDevsConstituteOutlier);
|
||||
var inlierUpperThreshold = mean + (stdDev * howManyStdDevsConstituteOutlier);
|
||||
|
||||
return new Tuple2<>(inlierLowerThreshold, inlierUpperThreshold);
|
||||
}
|
||||
|
||||
/* Sorts the data and discards given percentage from the left and right sides each.
|
||||
* E.g. 5% trim implies a total of 10% (2x 5%) of elements discarded.
|
||||
* Used in calculating trimmed mean (and in turn trimmed standard deviation),
|
||||
* which is more robust to outliers than a simple mean.
|
||||
*/
|
||||
private static List<Double> trim(double percentToTrim, List<Double> numbers) {
|
||||
var minPercentToTrim = 0;
|
||||
var maxPercentToTrim = 50;
|
||||
if (minPercentToTrim > percentToTrim || percentToTrim > maxPercentToTrim) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format(
|
||||
"The percentage of data points to trim must be in the range [%d,%d].",
|
||||
minPercentToTrim, maxPercentToTrim));
|
||||
}
|
||||
|
||||
var totalPercentTrim = percentToTrim * 2;
|
||||
if (totalPercentTrim == 0) {
|
||||
return numbers;
|
||||
}
|
||||
if (totalPercentTrim == 100) {
|
||||
return FXCollections.emptyObservableList();
|
||||
}
|
||||
|
||||
if (numbers.isEmpty()) {
|
||||
return numbers;
|
||||
}
|
||||
|
||||
var count = numbers.size();
|
||||
int countToDropFromEachSide = (int) Math.round((count / 100d) * percentToTrim); // visada >= 0?
|
||||
if (countToDropFromEachSide == 0) {
|
||||
return numbers;
|
||||
}
|
||||
|
||||
var sorted = numbers.stream().sorted();
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
/* On the given axis, sets the provided lower and upper bounds, and
|
||||
* computes an appropriate major tick unit (distance between major ticks in data-space).
|
||||
* External computation of tick unit is necessary, because JavaFX doesn't support automatic
|
||||
|
@ -30,6 +30,8 @@ import bisq.common.UserThread;
|
||||
import bisq.common.app.AppModule;
|
||||
import bisq.common.app.Capabilities;
|
||||
import bisq.common.app.Capability;
|
||||
import bisq.common.config.BaseCurrencyNetwork;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -147,6 +149,12 @@ public class SeedNodeMain extends ExecutableForAppWithP2p {
|
||||
}
|
||||
|
||||
private void setupConnectionLossCheck() {
|
||||
// For dev testing (usually on BTC_REGTEST) we don't want to get the seed shut
|
||||
// down as it is normal that the seed is the only actively running node.
|
||||
if (Config.baseCurrencyNetwork() == BaseCurrencyNetwork.BTC_REGTEST) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkConnectionLossTime != null) {
|
||||
return;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user