Merge pull request #4347 from sqrrm/deposit-improvements

Deposit improvements
This commit is contained in:
Christoph Atteneder 2020-07-03 21:20:27 +02:00 committed by GitHub
commit cfc3252f7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 299 additions and 34 deletions

View file

@ -22,6 +22,10 @@ import com.google.common.math.DoubleMath;
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;
@ -107,4 +111,60 @@ public class MathUtils {
}
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());
}
}
}

View file

@ -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);
}
}

View file

@ -90,4 +90,9 @@ public class Restrictions {
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;
}
}

View file

@ -182,7 +182,7 @@ public class CreateOfferService {
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);
@ -287,8 +287,9 @@ public class CreateOfferService {
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) {

View file

@ -22,8 +22,8 @@ import bisq.core.btc.wallet.BtcWalletService;
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;
@ -353,7 +353,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
Coin reservedFundsForOffer = createOfferService.getReservedFundsForOffer(offer.getDirection(),
offer.getAmount(),
buyerSecurityDeposit,
createOfferService.getSellerSecurityDepositAsDouble());
createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDeposit));
PlaceOfferModel model = new PlaceOfferModel(offer,
reservedFundsForOffer,

View file

@ -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;
@ -38,7 +39,7 @@ public class MakerSetsLockTime extends TradeTask {
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;
}

View file

@ -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)

View file

@ -211,6 +211,10 @@ public class FormattingUtils {
return formatToPercent(value) + "%";
}
public static String formatToRoundedPercentWithSymbol(double value) {
return formatToPercent(value, new DecimalFormat("#")) + "%";
}
public static String formatPercentagePrice(double value) {
return formatToPercentWithSymbol(value);
}
@ -219,6 +223,11 @@ public class FormattingUtils {
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(",", ".");
}

View file

@ -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}
@ -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

View file

@ -43,6 +43,8 @@ import bisq.core.payment.PaymentAccount;
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;
@ -79,8 +81,11 @@ import javafx.collections.FXCollections;
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;
@ -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;
///////////////////////////////////////////////////////////////////////////////////////////
@ -145,6 +151,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
FeeService feeService,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
MakerFeeProvider makerFeeProvider,
TradeStatisticsManager tradeStatisticsManager,
Navigation navigation) {
super(btcWalletService);
@ -160,6 +167,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
this.btcFormatter = btcFormatter;
this.makerFeeProvider = makerFeeProvider;
this.navigation = navigation;
this.tradeStatisticsManager = tradeStatisticsManager;
offerId = createOfferService.getRandomOfferId();
shortOfferId = Utilities.getShortId(offerId);
@ -257,6 +265,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
calculateVolume();
calculateTotalToPay();
updateBalance();
setSuggestedSecurityDeposit(getPaymentAccount());
return true;
}
@ -294,7 +303,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
Tuple2<Coin, Integer> estimatedFeeAndTxSize = createOfferService.getEstimatedFeeAndTxSize(amount.get(),
direction,
buyerSecurityDeposit.get(),
createOfferService.getSellerSecurityDepositAsDouble());
createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDeposit.get()));
txFeeFromFeeService = estimatedFeeAndTxSize.first;
feeTxSize = estimatedFeeAndTxSize.second;
}
@ -317,14 +326,49 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
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)
@ -591,10 +635,13 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
this.volume.set(volume);
}
void setBuyerSecurityDeposit(double value) {
void setBuyerSecurityDeposit(double value, boolean persist) {
this.buyerSecurityDeposit.set(value);
if (persist) {
// Only expected to persist for manually changed deposit values
preferences.setBuyerSecurityDepositAsPercent(value, getPaymentAccount());
}
}
protected boolean isUseMarketBasedPriceValue() {
return marketPriceAvailable && useMarketBasedPrice.get() && !isHalCashAccount();
@ -650,7 +697,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
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);
}

View file

@ -421,7 +421,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
securityDepositStringListener = (ov, oldValue, newValue) -> {
if (!ignoreSecurityDepositStringListener) {
if (securityDepositValidator.validate(newValue).isValid) {
setBuyerSecurityDepositToModel();
setBuyerSecurityDepositToModel(false);
dataModel.calculateTotalToPay();
}
updateButtonDisableState();
@ -898,7 +898,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
.width(800)
.actionButtonText(Res.get("createOffer.resetToDefault"))
.onAction(() -> {
dataModel.setBuyerSecurityDeposit(defaultSecurityDeposit);
dataModel.setBuyerSecurityDeposit(defaultSecurityDeposit, false);
ignoreSecurityDepositStringListener = true;
buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDeposit().get()));
ignoreSecurityDepositStringListener = false;
@ -915,7 +915,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
private void applyBuyerSecurityDepositOnFocusOut() {
setBuyerSecurityDepositToModel();
setBuyerSecurityDepositToModel(true);
ignoreSecurityDepositStringListener = true;
buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDeposit().get()));
ignoreSecurityDepositStringListener = false;
@ -966,7 +966,8 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
public String getSecurityDepositLabel() {
return dataModel.isBuyOffer() ? Res.get("createOffer.setDepositAsBuyer") : Res.get("createOffer.setDeposit");
return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? Res.get("createOffer.setDepositForBothTraders") :
dataModel.isBuyOffer() ? Res.get("createOffer.setDepositAsBuyer") : Res.get("createOffer.setDeposit");
}
public String getSecurityDepositPopOverLabel(String depositInBTC) {
@ -1145,11 +1146,13 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
}
private void setBuyerSecurityDepositToModel() {
private void setBuyerSecurityDepositToModel(boolean persistPreference) {
if (buyerSecurityDeposit.get() != null && !buyerSecurityDeposit.get().isEmpty()) {
dataModel.setBuyerSecurityDeposit(ParsingUtils.parsePercentStringToDouble(buyerSecurityDeposit.get()));
dataModel.setBuyerSecurityDeposit(ParsingUtils.parsePercentStringToDouble(buyerSecurityDeposit.get()),
persistPreference);
} else {
dataModel.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent());
dataModel.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent(),
persistPreference);
}
}
@ -1157,7 +1160,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
// If the security deposit in the model is not valid percent
String value = FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDeposit().get());
if (!securityDepositValidator.validate(value).isValid) {
dataModel.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent());
dataModel.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent(), false);
}
}

View file

@ -32,6 +32,7 @@ import bisq.core.offer.CreateOfferService;
import bisq.core.offer.OpenOfferManager;
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.core.util.FormattingUtils;
@ -63,6 +64,7 @@ class CreateOfferDataModel extends MutableOfferDataModel {
FeeService feeService,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
MakerFeeProvider makerFeeProvider,
TradeStatisticsManager tradeStatisticsManager,
Navigation navigation) {
super(createOfferService,
openOfferManager,
@ -76,6 +78,7 @@ class CreateOfferDataModel extends MutableOfferDataModel {
feeService,
btcFormatter,
makerFeeProvider,
tradeStatisticsManager,
navigation);
}
}

View file

@ -38,7 +38,6 @@ import bisq.desktop.main.offer.OfferView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.OfferDetailsWindow;
import bisq.desktop.util.CssTheme;
import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.FormBuilder;
import bisq.desktop.util.GUIUtil;
import bisq.desktop.util.Layout;
@ -130,7 +129,7 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
private AutocompleteComboBox<PaymentMethod> paymentMethodComboBox;
private AutoTooltipButton createOfferButton;
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> amountColumn, volumeColumn, marketColumn,
priceColumn, paymentMethodColumn, signingStateColumn, avatarColumn;
priceColumn, paymentMethodColumn, depositColumn, signingStateColumn, avatarColumn;
private TableView<OfferBookListItem> tableView;
private OfferView.OfferActionHandler offerActionHandler;
@ -224,6 +223,8 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
tableView.getColumns().add(volumeColumn);
paymentMethodColumn = getPaymentMethodColumn();
tableView.getColumns().add(paymentMethodColumn);
depositColumn = getDepositColumn();
tableView.getColumns().add(depositColumn);
signingStateColumn = getSigningStateColumn();
tableView.getColumns().add(signingStateColumn);
avatarColumn = getAvatarColumn();
@ -245,6 +246,14 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
volumeColumn.setComparator(Comparator.comparing(o -> o.getOffer().getMinVolume(), Comparator.nullsFirst(Comparator.naturalOrder())));
paymentMethodColumn.setComparator(Comparator.comparing(o -> o.getOffer().getPaymentMethod()));
avatarColumn.setComparator(Comparator.comparing(o -> o.getOffer().getOwnerNodeAddress().getFullAddress()));
depositColumn.setComparator(Comparator.comparing(o -> {
var isSellOffer = o.getOffer().getDirection() == OfferPayload.Direction.SELL;
var deposit = isSellOffer ? o.getOffer().getBuyerSecurityDeposit() :
o.getOffer().getSellerSecurityDeposit();
return (deposit == null) ? 0.0 : deposit.getValue() / (double) o.getOffer().getAmount().getValue();
}, Comparator.nullsFirst(Comparator.naturalOrder())));
nrOfOffersLabel = new AutoTooltipLabel("");
nrOfOffersLabel.setId("num-offers");
@ -927,10 +936,56 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
return column;
}
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> getDepositColumn() {
AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> column = new AutoTooltipTableColumn<>(
Res.get("offerbook.deposit"),
Res.get("offerbook.deposit.help")) {
{
setMinWidth(70);
setSortable(true);
}
};
column.getStyleClass().add("number-column");
column.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<OfferBookListItem, OfferBookListItem> call(
TableColumn<OfferBookListItem, OfferBookListItem> column) {
return new TableCell<>() {
@Override
public void updateItem(final OfferBookListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
var isSellOffer = item.getOffer().getDirection() == OfferPayload.Direction.SELL;
var deposit = isSellOffer ? item.getOffer().getBuyerSecurityDeposit() :
item.getOffer().getSellerSecurityDeposit();
if (deposit == null) {
setText(Res.get("shared.na"));
setGraphic(null);
} else {
setText("");
setGraphic(new ColoredDecimalPlacesWithZerosText(model.formatDepositString(
deposit, item.getOffer().getAmount().getValue()),
GUIUtil.AMOUNT_DECIMALS_WITH_ZEROS));
}
} else {
setText("");
setGraphic(null);
}
}
};
}
});
return column;
}
private TableColumn<OfferBookListItem, OfferBookListItem> getActionColumn() {
TableColumn<OfferBookListItem, OfferBookListItem> column = new AutoTooltipTableColumn<>(Res.get("shared.actions")) {
{
setMinWidth(200);
setMinWidth(180);
setSortable(false);
}
};
@ -1147,8 +1202,8 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> getAvatarColumn() {
AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> column = new AutoTooltipTableColumn<>(Res.get("offerbook.trader")) {
{
setMinWidth(80);
setMaxWidth(80);
setMinWidth(60);
setMaxWidth(60);
setSortable(true);
}
};

View file

@ -47,8 +47,8 @@ import bisq.core.trade.Trade;
import bisq.core.trade.closed.ClosedTradableManager;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.core.util.coin.BsqFormatter;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.BsqFormatter;
import bisq.core.util.coin.CoinFormatter;
import bisq.network.p2p.NodeAddress;
@ -168,9 +168,7 @@ class OfferBookViewModel extends ActivatableViewModel {
this.filteredItems = new FilteredList<>(offerBook.getOfferBookListItems());
this.sortedItems = new SortedList<>(filteredItems);
tradeCurrencyListChangeListener = c -> {
fillAllTradeCurrencies();
};
tradeCurrencyListChangeListener = c -> fillAllTradeCurrencies();
filterItemsListener = c -> {
final Optional<OfferBookListItem> highestAmountOffer = filteredItems.stream()
@ -645,4 +643,9 @@ class OfferBookViewModel extends ActivatableViewModel {
else
return (direction == OfferPayload.Direction.SELL) ? Res.get("shared.buyingCurrency", currencyCode) : Res.get("shared.sellingCurrency", currencyCode);
}
public String formatDepositString(Coin deposit, long amount) {
var percentage = FormattingUtils.formatToRoundedPercentWithSymbol(deposit.getValue() / (double) amount);
return btcFormatter.formatCoin(deposit) + " (" + percentage + ")";
}
}

View file

@ -37,6 +37,7 @@ import bisq.core.payment.PaymentAccount;
import bisq.core.proto.persistable.CorePersistenceProtoResolver;
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.core.util.FormattingUtils;
@ -74,6 +75,7 @@ class EditOfferDataModel extends MutableOfferDataModel {
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
CorePersistenceProtoResolver corePersistenceProtoResolver,
MakerFeeProvider makerFeeProvider,
TradeStatisticsManager tradeStatisticsManager,
Navigation navigation) {
super(createOfferService,
openOfferManager,
@ -87,6 +89,7 @@ class EditOfferDataModel extends MutableOfferDataModel {
feeService,
btcFormatter,
makerFeeProvider,
tradeStatisticsManager,
navigation);
this.corePersistenceProtoResolver = corePersistenceProtoResolver;
}

View file

@ -15,11 +15,14 @@ import bisq.core.payment.PaymentAccount;
import bisq.core.payment.RevolutAccount;
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import org.bitcoinj.core.Coin;
import javafx.collections.FXCollections;
import java.util.HashSet;
import java.util.UUID;
@ -52,17 +55,19 @@ public class CreateOfferDataModelTest {
CreateOfferService createOfferService = mock(CreateOfferService.class);
preferences = mock(Preferences.class);
user = mock(User.class);
var tradeStats = mock(TradeStatisticsManager.class);
when(btcWalletService.getOrCreateAddressEntry(anyString(), any())).thenReturn(addressEntry);
when(preferences.isUsePercentageBasedPrice()).thenReturn(true);
when(preferences.getBuyerSecurityDepositAsPercent(null)).thenReturn(0.01);
when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString());
when(tradeStats.getObservableTradeStatisticsSet()).thenReturn(FXCollections.observableSet());
makerFeeProvider = mock(MakerFeeProvider.class);
model = new CreateOfferDataModel(createOfferService, null, btcWalletService,
null, preferences, user, null,
priceFeedService, null,
feeService, null, makerFeeProvider, null);
feeService, null, makerFeeProvider, tradeStats, null);
}
@Test

View file

@ -34,14 +34,16 @@ import bisq.core.locale.Res;
import bisq.core.offer.CreateOfferService;
import bisq.core.offer.OfferPayload;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.core.util.coin.ImmutableCoinFormatter;
import bisq.core.util.coin.BsqFormatter;
import bisq.core.util.coin.CoinFormatter;
import bisq.core.util.coin.ImmutableCoinFormatter;
import bisq.core.util.validation.InputValidator;
import bisq.common.config.Config;
@ -95,6 +97,7 @@ public class CreateOfferViewModelTest {
SecurityDepositValidator securityDepositValidator = mock(SecurityDepositValidator.class);
AccountAgeWitnessService accountAgeWitnessService = mock(AccountAgeWitnessService.class);
CreateOfferService createOfferService = mock(CreateOfferService.class);
var tradeStats = mock(TradeStatisticsManager.class);
when(btcWalletService.getOrCreateAddressEntry(anyString(), any())).thenReturn(addressEntry);
when(btcWalletService.getBalanceForAddress(any())).thenReturn(Coin.valueOf(1000L));
@ -102,6 +105,7 @@ public class CreateOfferViewModelTest {
when(priceFeedService.getMarketPrice(anyString())).thenReturn(new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true));
when(feeService.getTxFee(anyInt())).thenReturn(Coin.valueOf(1000L));
when(user.findFirstPaymentAccountWithCurrency(any())).thenReturn(paymentAccount);
when(paymentAccount.getPaymentMethod()).thenReturn(mock(PaymentMethod.class));
when(user.getPaymentAccountsAsObservable()).thenReturn(FXCollections.observableSet());
when(securityDepositValidator.validate(any())).thenReturn(new InputValidator.ValidationResult(false));
when(accountAgeWitnessService.getMyTradeLimit(any(), any(), any())).thenReturn(100000000L);
@ -109,11 +113,12 @@ public class CreateOfferViewModelTest {
when(bsqFormatter.formatCoin(any())).thenReturn("0");
when(bsqWalletService.getAvailableConfirmedBalance()).thenReturn(Coin.ZERO);
when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString());
when(tradeStats.getObservableTradeStatisticsSet()).thenReturn(FXCollections.observableSet());
CreateOfferDataModel dataModel = new CreateOfferDataModel(createOfferService, null, btcWalletService,
bsqWalletService, empty, user, null, priceFeedService,
accountAgeWitnessService, feeService,
coinFormatter, mock(MakerFeeProvider.class), null);
coinFormatter, mock(MakerFeeProvider.class), tradeStats, null);
dataModel.initWithData(OfferPayload.Direction.BUY, new CryptoCurrency("BTC", "bitcoin"));
dataModel.activate();

View file

@ -19,6 +19,7 @@ import bisq.core.payment.PaymentAccount;
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.core.util.coin.BsqFormatter;
@ -95,7 +96,7 @@ public class EditOfferDataModelTest {
btcWalletService, bsqWalletService, empty, user,
null, priceFeedService,
accountAgeWitnessService, feeService, null, null,
mock(MakerFeeProvider.class), null);
mock(MakerFeeProvider.class), mock(TradeStatisticsManager.class), null);
}
@Test