Skip to content

Commit

Permalink
Merge pull request #4347 from sqrrm/deposit-improvements
Browse files Browse the repository at this point in the history
Deposit improvements
  • Loading branch information
ripcurlx authored Jul 3, 2020
2 parents 45a9803 + 17fbc8d commit cfc3252
Show file tree
Hide file tree
Showing 19 changed files with 299 additions and 34 deletions.
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 @@ -182,7 +182,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 txFeeToUse = txFee.isPositive() ? txFee : txFeeFromFeeService;
Coin makerFeeAsCoin = getMakerFee(amount);
Expand Down Expand Up @@ -287,8 +287,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 @@ -120,6 +120,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 @@ -294,7 +303,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 @@ -317,14 +326,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 @@ -591,9 +635,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 @@ -650,7 +697,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

0 comments on commit cfc3252

Please sign in to comment.