Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deposit improvements #4347

Merged
merged 11 commits into from
Jul 3, 2020
60 changes: 60 additions & 0 deletions common/src/main/java/bisq/common/util/MathUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
import java.math.BigDecimal;
import java.math.RoundingMode;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -107,4 +111,60 @@ public static long getMedian(Long[] list) {
}
return median;
}

public static class MovingAverage {
Deque<Long> window;
private int size;
private long sum;
private double outlier;

// Outlier as ratio
public MovingAverage(int size, double outlier) {
this.size = size;
window = new ArrayDeque<>(size);
this.outlier = outlier;
sum = 0;
}

public Optional<Double> next(long val) {
var fullAtStart = isFull();
if (fullAtStart) {
if (outlier > 0) {
// Return early if it's an outlier
var avg = (double) sum / size;
if (Math.abs(avg - val) / avg > outlier) {
return Optional.empty();
}
}
sum -= window.remove();
}
window.add(val);
sum += val;
if (!fullAtStart && isFull() && outlier != 0) {
removeInitialOutlier();
}
// When discarding outliers, the first n non discarded elements return Optional.empty()
return outlier > 0 && !isFull() ? Optional.empty() : current();
}

boolean isFull() {
return window.size() == size;
}

private void removeInitialOutlier() {
var element = window.iterator();
while (element.hasNext()) {
var val = element.next();
var avgExVal = (double) (sum - val) / (size - 1);
if (Math.abs(avgExVal - val) / avgExVal > outlier) {
element.remove();
break;
}
}
}

public Optional<Double> current() {
return window.size() == 0 ? Optional.empty() : Optional.of((double) sum / window.size());
}
}
}
57 changes: 57 additions & 0 deletions common/src/test/java/bisq/common/util/MathUtilsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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 org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;

public class MathUtilsTest {


@SuppressWarnings("OptionalGetWithoutIsPresent")
@Test
public void testMovingAverageWithoutOutlierExclusion() {
var values = new int[]{4, 5, 3, 1, 2, 4};
// Moving average = 4, 4.5, 4, 3, 2, 7/3
var movingAverage = new MathUtils.MovingAverage(3, 0);
int i = 0;
assertEquals(4, movingAverage.next(values[i++]).get(),0.001);
assertEquals(4.5, movingAverage.next(values[i++]).get(),0.001);
assertEquals(4, movingAverage.next(values[i++]).get(),0.001);
assertEquals(3, movingAverage.next(values[i++]).get(),0.001);
assertEquals(2, movingAverage.next(values[i++]).get(),0.001);
assertEquals((double) 7 / 3, movingAverage.next(values[i]).get(),0.001);
}

@SuppressWarnings("OptionalGetWithoutIsPresent")
@Test
public void testMovingAverageWithOutlierExclusion() {
var values = new int[]{100, 102, 95, 101, 120, 115};
// Moving average = N/A, N/A, 99, 99.333..., N/A, 103.666...
var movingAverage = new MathUtils.MovingAverage(3, 0.2);
int i = 0;
assertFalse(movingAverage.next(values[i++]).isPresent());
assertFalse(movingAverage.next(values[i++]).isPresent());
assertEquals(99, movingAverage.next(values[i++]).get(),0.001);
assertEquals(99.333, movingAverage.next(values[i++]).get(),0.001);
assertFalse(movingAverage.next(values[i++]).isPresent());
assertEquals(103.666, movingAverage.next(values[i]).get(),0.001);
}
}
5 changes: 5 additions & 0 deletions core/src/main/java/bisq/core/btc/wallet/Restrictions.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,9 @@ public static Coin getMinRefundAtMediatedDispute() {
MIN_REFUND_AT_MEDIATED_DISPUTE = Coin.parseCoin("0.003"); // 0.003 BTC about 21 USD @ 7000 USD/BTC
return MIN_REFUND_AT_MEDIATED_DISPUTE;
}

public static int getLockTime(boolean isAsset) {
// 10 days for altcoins, 20 days for other payment methods
return isAsset ? 144 * 10 : 144 * 20;
}
}
7 changes: 4 additions & 3 deletions core/src/main/java/bisq/core/offer/CreateOfferService.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ public Offer createAndGetOffer(String offerId,
List<String> acceptedCountryCodes = PaymentAccountUtil.getAcceptedCountryCodes(paymentAccount);
String bankId = PaymentAccountUtil.getBankId(paymentAccount);
List<String> acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount);
double sellerSecurityDeposit = getSellerSecurityDepositAsDouble();
double sellerSecurityDeposit = getSellerSecurityDepositAsDouble(buyerSecurityDepositAsDouble);
Coin txFeeFromFeeService = getEstimatedFeeAndTxSize(amount, direction, buyerSecurityDepositAsDouble, sellerSecurityDeposit).first;
Coin makerFeeAsCoin = getMakerFee(amount);
boolean isCurrencyForMakerFeeBtc = OfferUtil.isCurrencyForMakerFeeBtc(preferences, bsqWalletService, amount);
Expand Down Expand Up @@ -285,8 +285,9 @@ public Coin getSecurityDeposit(OfferPayload.Direction direction,
getSellerSecurityDeposit(amount, sellerSecurityDeposit);
}

public double getSellerSecurityDepositAsDouble() {
return Restrictions.getSellerSecurityDepositAsPercent();
public double getSellerSecurityDepositAsDouble(double buyerSecurityDeposit) {
return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? buyerSecurityDeposit :
Restrictions.getSellerSecurityDepositAsPercent();
}

public Coin getMakerFee(Coin amount) {
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/java/bisq/core/offer/OpenOfferManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.dao.DaoFacade;
import bisq.core.exceptions.TradePriceOutOfToleranceException;
import bisq.core.locale.Res;
import bisq.core.filter.FilterManager;
import bisq.core.locale.Res;
import bisq.core.offer.availability.DisputeAgentSelection;
import bisq.core.offer.messages.OfferAvailabilityRequest;
import bisq.core.offer.messages.OfferAvailabilityResponse;
Expand Down Expand Up @@ -353,7 +353,7 @@ public void placeOffer(Offer offer,
Coin reservedFundsForOffer = createOfferService.getReservedFundsForOffer(offer.getDirection(),
offer.getAmount(),
buyerSecurityDeposit,
createOfferService.getSellerSecurityDepositAsDouble());
createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDeposit));

PlaceOfferModel model = new PlaceOfferModel(offer,
reservedFundsForOffer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package bisq.core.trade.protocol.tasks.maker;

import bisq.core.btc.wallet.Restrictions;
import bisq.core.trade.Trade;
import bisq.core.trade.protocol.tasks.TradeTask;

Expand All @@ -38,7 +39,7 @@ protected void run() {
runInterceptHook();

// 10 days for altcoins, 20 days for other payment methods
int delay = processModel.getOffer().getPaymentMethod().isAsset() ? 144 * 10 : 144 * 20;
int delay = Restrictions.getLockTime(processModel.getOffer().getPaymentMethod().isAsset());
if (Config.baseCurrencyNetwork().isRegtest()) {
delay = 5;
}
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/java/bisq/core/user/Preferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
new BlockChainExplorer("bsq.bisq.cc (@m52go)", "https://bsq.bisq.cc/tx.html?tx=", "https://bsq.bisq.cc/Address.html?addr=")
));

public static final boolean USE_SYMMETRIC_SECURITY_DEPOSIT = true;


// payload is initialized so the default values are available for Property initialization.
@Setter
@Delegate(excludes = ExcludesDelegateMethods.class)
Expand Down
9 changes: 9 additions & 0 deletions core/src/main/java/bisq/core/util/FormattingUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ public static String formatToPercentWithSymbol(double value) {
return formatToPercent(value) + "%";
}

public static String formatToRoundedPercentWithSymbol(double value) {
return formatToPercent(value, new DecimalFormat("#")) + "%";
}

public static String formatPercentagePrice(double value) {
return formatToPercentWithSymbol(value);
}
Expand All @@ -219,6 +223,11 @@ public static String formatToPercent(double value) {
DecimalFormat decimalFormat = new DecimalFormat("#.##");
decimalFormat.setMinimumFractionDigits(2);
decimalFormat.setMaximumFractionDigits(2);

return formatToPercent(value, decimalFormat);
}

public static String formatToPercent(double value, DecimalFormat decimalFormat) {
return decimalFormat.format(MathUtils.roundDouble(value * 100.0, 2)).replace(",", ".");
}

Expand Down
3 changes: 3 additions & 0 deletions core/src/main/resources/i18n/displayStrings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,8 @@ shared.notSigned.noNeed=This account type doesn't use signing

offerbook.nrOffers=No. of offers: {0}
offerbook.volume={0} (min - max)
offerbook.deposit=Deposit BTC (%)
offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed.

offerbook.createOfferToBuy=Create new offer to buy {0}
offerbook.createOfferToSell=Create new offer to sell {0}
Expand Down Expand Up @@ -480,6 +482,7 @@ createOffer.tac=With publishing this offer I agree to trade with any trader who
createOffer.currencyForFee=Trade fee
createOffer.setDeposit=Set buyer's security deposit (%)
createOffer.setDepositAsBuyer=Set my security deposit as buyer (%)
createOffer.setDepositForBothTraders=Set both traders' security deposit (%)
createOffer.securityDepositInfo=Your buyer''s security deposit will be {0}
createOffer.securityDepositInfoAsBuyer=Your security deposit as buyer will be {0}
createOffer.minSecurityDepositUsed=Min. buyer security deposit is used
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.handlers.TransactionResultHandler;
import bisq.core.trade.statistics.TradeStatistics2;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.core.util.FormattingUtils;
Expand Down Expand Up @@ -79,8 +81,11 @@
import javafx.collections.ObservableList;
import javafx.collections.SetChangeListener;

import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Optional;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

Expand Down Expand Up @@ -126,6 +131,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
private boolean marketPriceAvailable;
private int feeTxSize = TxFeeEstimationService.TYPICAL_TX_WITH_1_INPUT_SIZE;
protected boolean allowAmountUpdate = true;
private final TradeStatisticsManager tradeStatisticsManager;


///////////////////////////////////////////////////////////////////////////////////////////
Expand All @@ -145,6 +151,7 @@ public MutableOfferDataModel(CreateOfferService createOfferService,
FeeService feeService,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
MakerFeeProvider makerFeeProvider,
TradeStatisticsManager tradeStatisticsManager,
Navigation navigation) {
super(btcWalletService);

Expand All @@ -160,6 +167,7 @@ public MutableOfferDataModel(CreateOfferService createOfferService,
this.btcFormatter = btcFormatter;
this.makerFeeProvider = makerFeeProvider;
this.navigation = navigation;
this.tradeStatisticsManager = tradeStatisticsManager;

offerId = createOfferService.getRandomOfferId();
shortOfferId = Utilities.getShortId(offerId);
Expand Down Expand Up @@ -257,6 +265,7 @@ public boolean initWithData(OfferPayload.Direction direction, TradeCurrency trad
calculateVolume();
calculateTotalToPay();
updateBalance();
setSuggestedSecurityDeposit(getPaymentAccount());

return true;
}
Expand Down Expand Up @@ -293,7 +302,7 @@ public void updateEstimatedFeeAndTxSize() {
Tuple2<Coin, Integer> estimatedFeeAndTxSize = createOfferService.getEstimatedFeeAndTxSize(amount.get(),
direction,
buyerSecurityDeposit.get(),
createOfferService.getSellerSecurityDepositAsDouble());
createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDeposit.get()));
txFeeFromFeeService = estimatedFeeAndTxSize.first;
feeTxSize = estimatedFeeAndTxSize.second;
}
Expand All @@ -316,14 +325,49 @@ void onPaymentAccountSelected(PaymentAccount paymentAccount) {
this.paymentAccount = paymentAccount;

setTradeCurrencyFromPaymentAccount(paymentAccount);

buyerSecurityDeposit.set(preferences.getBuyerSecurityDepositAsPercent(getPaymentAccount()));
setSuggestedSecurityDeposit(getPaymentAccount());

if (amount.get() != null)
this.amount.set(Coin.valueOf(Math.min(amount.get().value, getMaxTradeLimit())));
}
}

private void setSuggestedSecurityDeposit(PaymentAccount paymentAccount) {
var minSecurityDeposit = preferences.getBuyerSecurityDepositAsPercent(getPaymentAccount());
if (getTradeCurrency() == null) {
setBuyerSecurityDeposit(minSecurityDeposit, false);
return;
}
// Get average historic prices over for the prior trade period equaling the lock time
var blocksRange = Restrictions.getLockTime(paymentAccount.getPaymentMethod().isAsset());
var startDate = new Date(System.currentTimeMillis() - blocksRange * 10 * 60000);
var sortedRangeData = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
.filter(e -> e.getCurrencyCode().equals(getTradeCurrency().getCode()))
.filter(e -> e.getTradeDate().compareTo(startDate) >= 0)
.sorted(Comparator.comparing(TradeStatistics2::getTradeDate))
.collect(Collectors.toList());
var movingAverage = new MathUtils.MovingAverage(10, 0.2);
double[] extremes = {Double.MAX_VALUE, Double.MIN_VALUE};
sortedRangeData.forEach(e -> {
var price = e.getTradePrice().getValue();
movingAverage.next(price).ifPresent(val -> {
if (val < extremes[0]) extremes[0] = val;
if (val > extremes[1]) extremes[1] = val;
});
});
var min = extremes[0];
var max = extremes[1];
if (min == 0d || max == 0d) {
setBuyerSecurityDeposit(minSecurityDeposit, false);
return;
}
// Suggested deposit is double the trade range over the previous lock time period, bounded by min/max deposit
var suggestedSecurityDeposit =
Math.min(2 * (max - min) / max, Restrictions.getMaxBuyerSecurityDepositAsPercent());
buyerSecurityDeposit.set(Math.max(suggestedSecurityDeposit, minSecurityDeposit));
}


private void setTradeCurrencyFromPaymentAccount(PaymentAccount paymentAccount) {
if (!paymentAccount.getTradeCurrencies().contains(tradeCurrency)) {
if (paymentAccount.getSelectedTradeCurrency() != null)
Expand Down Expand Up @@ -590,9 +634,12 @@ protected void setVolume(Volume volume) {
this.volume.set(volume);
}

void setBuyerSecurityDeposit(double value) {
void setBuyerSecurityDeposit(double value, boolean persist) {
this.buyerSecurityDeposit.set(value);
preferences.setBuyerSecurityDepositAsPercent(value, getPaymentAccount());
if (persist) {
// Only expected to persist for manually changed deposit values
preferences.setBuyerSecurityDepositAsPercent(value, getPaymentAccount());
}
}

protected boolean isUseMarketBasedPriceValue() {
Expand Down Expand Up @@ -649,7 +696,8 @@ private Coin getSellerSecurityDepositAsCoin() {
if (amountAsCoin == null)
amountAsCoin = Coin.ZERO;

Coin percentOfAmountAsCoin = CoinUtil.getPercentOfAmountAsCoin(createOfferService.getSellerSecurityDepositAsDouble(), amountAsCoin);
Coin percentOfAmountAsCoin = CoinUtil.getPercentOfAmountAsCoin(
createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDeposit.get()), amountAsCoin);
return getBoundedSellerSecurityDepositAsCoin(percentOfAmountAsCoin);
}

Expand Down
Loading