From 80c23883c81aaea42122e897c82561abd211a7a0 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 15 Mar 2021 16:30:11 -0500 Subject: [PATCH] Add popup for summary of trade history --- .../main/java/bisq/core/util/VolumeUtil.java | 15 ++ .../resources/i18n/displayStrings.properties | 12 ++ .../java/bisq/desktop/main/PriceUtil.java | 11 ++ .../windows/ClosedTradesSummaryWindow.java | 89 +++++++++++ .../closedtrades/ClosedTradesDataModel.java | 142 +++++++++++++++++- .../closedtrades/ClosedTradesView.fxml | 1 + .../closedtrades/ClosedTradesView.java | 7 +- .../closedtrades/ClosedTradesViewModel.java | 74 ++++++++- 8 files changed, 343 insertions(+), 8 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/main/overlays/windows/ClosedTradesSummaryWindow.java diff --git a/core/src/main/java/bisq/core/util/VolumeUtil.java b/core/src/main/java/bisq/core/util/VolumeUtil.java index 71712bd365..4150aaa875 100644 --- a/core/src/main/java/bisq/core/util/VolumeUtil.java +++ b/core/src/main/java/bisq/core/util/VolumeUtil.java @@ -17,8 +17,15 @@ package bisq.core.util; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.AltcoinExchangeRate; +import bisq.core.monetary.Price; import bisq.core.monetary.Volume; +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.ExchangeRate; +import org.bitcoinj.utils.Fiat; + public class VolumeUtil { public static Volume getRoundedFiatVolume(Volume volumeByAmount) { @@ -47,4 +54,12 @@ public class VolumeUtil { roundedVolume = Math.max(factor, roundedVolume); return Volume.parse(String.valueOf(roundedVolume), volumeByAmount.getCurrencyCode()); } + + public static Volume getVolume(Coin amount, Price price) { + if (price.getMonetary() instanceof Altcoin) { + return new Volume(new AltcoinExchangeRate((Altcoin) price.getMonetary()).coinToAltcoin(amount)); + } else { + return new Volume(new ExchangeRate((Fiat) price.getMonetary()).coinToFiat(amount)); + } + } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index ad211c2e1a..8596c3bbcc 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -119,6 +119,7 @@ shared.sendingConfirmation=Sending confirmation... shared.sendingConfirmationAgain=Please send confirmation again shared.exportCSV=Export to CSV shared.exportJSON=Export to JSON +shared.summary=Show summary shared.noDateAvailable=No date available shared.noDetailsAvailable=No details available shared.notUsedYet=Not used yet @@ -2722,6 +2723,17 @@ txDetailsWindow.bsq.note=You have sent BSQ funds. \ txDetailsWindow.sentTo=Sent to txDetailsWindow.txId=TxId +closedTradesSummaryWindow.headline=Trade history summary +closedTradesSummaryWindow.totalAmount.title=Total trade amount +closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) +closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} +closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Sum of all trade fees paid in BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Sum of all trade fees paid in BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} of total trade amount) + walletPasswordWindow.headline=Enter password to unlock torNetworkSettingWindow.header=Tor networks settings diff --git a/desktop/src/main/java/bisq/desktop/main/PriceUtil.java b/desktop/src/main/java/bisq/desktop/main/PriceUtil.java index a63e0086b1..7901c4689c 100644 --- a/desktop/src/main/java/bisq/desktop/main/PriceUtil.java +++ b/desktop/src/main/java/bisq/desktop/main/PriceUtil.java @@ -106,6 +106,17 @@ public class PriceUtil { } } + public static Price marketPriceToPrice(MarketPrice marketPrice) { + String currencyCode = marketPrice.getCurrencyCode(); + double priceAsDouble = marketPrice.getPrice(); + int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + double scaled = MathUtils.scaleUpByPowerOf10(priceAsDouble, precision); + long roundedToLong = MathUtils.roundDoubleToLong(scaled); + return Price.valueOf(currencyCode, roundedToLong); + } + public void recalculateBsq30DayAveragePrice() { bsq30DayAveragePrice = null; bsq30DayAveragePrice = getBsq30DayAveragePrice(); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ClosedTradesSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ClosedTradesSummaryWindow.java new file mode 100644 index 0000000000..97099f3ad3 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ClosedTradesSummaryWindow.java @@ -0,0 +1,89 @@ +/* + * 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 . + */ + +package bisq.desktop.main.overlays.windows; + +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.main.portfolio.closedtrades.ClosedTradesViewModel; +import bisq.desktop.util.Layout; + +import bisq.core.locale.Res; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import javafx.geometry.Insets; + +import java.util.Map; + +import static bisq.desktop.util.FormBuilder.addConfirmationLabelLabel; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; + +public class ClosedTradesSummaryWindow extends Overlay { + private final ClosedTradesViewModel model; + + @Inject + public ClosedTradesSummaryWindow(ClosedTradesViewModel model) { + this.model = model; + type = Type.Information; + } + + public void show() { + rowIndex = 0; + width = 900; + createGridPane(); + addContent(); + addButtons(); + display(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void createGridPane() { + super.createGridPane(); + gridPane.setPadding(new Insets(35, 40, 30, 40)); + gridPane.getStyleClass().add("grid-pane"); + } + + private void addContent() { + Map totalVolumeByCurrency = model.getTotalVolumeByCurrency(); + int rowSpan = totalVolumeByCurrency.size() + 4; + addTitledGroupBg(gridPane, rowIndex, rowSpan, Res.get("closedTradesSummaryWindow.headline")); + Coin totalTradeAmount = model.getTotalTradeAmount(); + addConfirmationLabelLabel(gridPane, rowIndex, + Res.get("closedTradesSummaryWindow.totalAmount.title"), + model.getTotalAmountWithVolume(totalTradeAmount), Layout.TWICE_FIRST_ROW_DISTANCE); + totalVolumeByCurrency.entrySet().forEach(entry -> { + addConfirmationLabelLabel(gridPane, ++rowIndex, + Res.get("closedTradesSummaryWindow.totalVolume.title", entry.getKey()), entry.getValue()); + }); + addConfirmationLabelLabel(gridPane, ++rowIndex, + Res.get("closedTradesSummaryWindow.totalMinerFee.title"), + model.getTotalTxFee(totalTradeAmount)); + addConfirmationLabelLabel(gridPane, ++rowIndex, + Res.get("closedTradesSummaryWindow.totalTradeFeeInBtc.title"), + model.getTotalTradeFeeInBtc(totalTradeAmount)); + addConfirmationLabelLabel(gridPane, ++rowIndex, + Res.get("closedTradesSummaryWindow.totalTradeFeeInBsq.title") + " ", // lets give some extra space + model.getTotalTradeFeeInBsq(totalTradeAmount)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesDataModel.java index 47eaf7e018..2d24efe97c 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesDataModel.java @@ -18,11 +18,27 @@ package bisq.desktop.main.portfolio.closedtrades; import bisq.desktop.common.model.ActivatableDataModel; +import bisq.desktop.main.PriceUtil; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; +import bisq.core.provider.price.MarketPrice; +import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.Tradable; +import bisq.core.trade.Trade; import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.Preferences; +import bisq.core.util.AveragePriceUtil; +import bisq.core.util.VolumeUtil; + +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.Fiat; import com.google.inject.Inject; @@ -30,17 +46,33 @@ import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; class ClosedTradesDataModel extends ActivatableDataModel { final ClosedTradableManager closedTradableManager; + private final BsqWalletService bsqWalletService; + private final Preferences preferences; + private final TradeStatisticsManager tradeStatisticsManager; + private final PriceFeedService priceFeedService; private final ObservableList list = FXCollections.observableArrayList(); private final ListChangeListener tradesListChangeListener; @Inject - public ClosedTradesDataModel(ClosedTradableManager closedTradableManager) { + public ClosedTradesDataModel(ClosedTradableManager closedTradableManager, + BsqWalletService bsqWalletService, + Preferences preferences, + TradeStatisticsManager tradeStatisticsManager, + PriceFeedService priceFeedService) { this.closedTradableManager = closedTradableManager; + this.bsqWalletService = bsqWalletService; + this.preferences = preferences; + this.tradeStatisticsManager = tradeStatisticsManager; + this.priceFeedService = priceFeedService; tradesListChangeListener = change -> applyList(); } @@ -73,4 +105,112 @@ class ClosedTradesDataModel extends ActivatableDataModel { list.sort((o1, o2) -> o2.getTradable().getDate().compareTo(o1.getTradable().getDate())); } + boolean wasMyOffer(Tradable tradable) { + return closedTradableManager.wasMyOffer(tradable.getOffer()); + } + + Coin getTotalAmount() { + return Coin.valueOf(getList().stream() + .map(ClosedTradableListItem::getTradable) + .filter(e -> e instanceof Trade) + .map(e -> (Trade) e) + .mapToLong(Trade::getTradeAmountAsLong) + .sum()); + } + + Map getTotalVolumeByCurrency() { + Map map = new HashMap<>(); + getList().stream() + .map(ClosedTradableListItem::getTradable) + .filter(e -> e instanceof Trade) + .map(e -> (Trade) e) + .map(Trade::getTradeVolume) + .filter(Objects::nonNull) + .forEach(volume -> { + String currencyCode = volume.getCurrencyCode(); + map.putIfAbsent(currencyCode, 0L); + map.put(currencyCode, volume.getValue() + map.get(currencyCode)); + }); + return map; + } + + public Optional getVolumeInUserFiatCurrency(Coin amount) { + return getVolume(amount, preferences.getPreferredTradeCurrency().getCode()); + } + + public Optional getVolume(Coin amount, String currencyCode) { + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + if (marketPrice == null) { + return Optional.empty(); + } + + Price price = PriceUtil.marketPriceToPrice(marketPrice); + return Optional.of(VolumeUtil.getVolume(amount, price)); + } + + public Volume getBsqVolumeInUsdWithAveragePrice(Coin amount) { + Tuple2 tuple = AveragePriceUtil.getAveragePriceTuple(preferences, tradeStatisticsManager, 30); + Price usdPrice = tuple.first; + long value = Math.round(amount.value * usdPrice.getValue() / 100d); + return new Volume(Fiat.valueOf("USD", value)); + } + + public Coin getTotalTxFee() { + return Coin.valueOf(getList().stream() + .map(ClosedTradableListItem::getTradable) + .mapToLong(tradable -> { + if (wasMyOffer(tradable)) { + return tradable.getOffer().getTxFee().value; + } else { + // taker pays for 3 transactions + return ((Trade) tradable).getTxFee().multiply(3).value; + } + }) + .sum()); + } + + public Coin getTotalTradeFee(boolean expectBtcFee) { + return Coin.valueOf(getList().stream() + .map(ClosedTradableListItem::getTradable) + .mapToLong(tradable -> getTradeFee(tradable, expectBtcFee)) + .sum()); + } + + protected long getTradeFee(Tradable tradable, boolean expectBtcFee) { + Offer offer = tradable.getOffer(); + if (wasMyOffer(tradable)) { + String makerFeeTxId = offer.getOfferFeePaymentTxId(); + boolean notInBsqWallet = bsqWalletService.getTransaction(makerFeeTxId) == null; + if (expectBtcFee) { + if (notInBsqWallet) { + return offer.getMakerFee().value; + } else { + return 0; + } + } else { + if (notInBsqWallet) { + return 0; + } else { + return offer.getMakerFee().value; + } + } + } else { + Trade trade = (Trade) tradable; + String takerFeeTxId = trade.getTakerFeeTxId(); + boolean notInBsqWallet = bsqWalletService.getTransaction(takerFeeTxId) == null; + if (expectBtcFee) { + if (notInBsqWallet) { + return trade.getTakerFee().value; + } else { + return 0; + } + } else { + if (notInBsqWallet) { + return 0; + } else { + return trade.getTakerFee().value; + } + } + } + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml index d94c682373..5e0f1cd0f1 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml @@ -62,6 +62,7 @@ diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java index 2ec1fc97a8..50841a3afd 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java @@ -25,6 +25,7 @@ import bisq.desktop.components.AutoTooltipTableColumn; import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.components.InputTextField; import bisq.desktop.components.PeerInfoIcon; +import bisq.desktop.main.overlays.windows.ClosedTradesSummaryWindow; import bisq.desktop.main.overlays.windows.OfferDetailsWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; import bisq.desktop.util.GUIUtil; @@ -124,7 +125,7 @@ public class ClosedTradesView extends ActivatableViewAndModel new ClosedTradesSummaryWindow(model).show()); + filterTextField.textProperty().addListener(filterTextFieldListener); applyFilteredListPredicate(filterTextField.getText()); root.widthProperty().addListener(widthListener); @@ -301,6 +305,7 @@ public class ClosedTradesView extends ActivatableViewAndModel implements ViewModel { - private final BtcWalletService btcWalletService; +public class ClosedTradesViewModel extends ActivatableWithDataModel implements ViewModel { private final BsqWalletService bsqWalletService; private final BsqFormatter bsqFormatter; private final CoinFormatter btcFormatter; @@ -55,13 +59,11 @@ class ClosedTradesViewModel extends ActivatableWithDataModel { + return Res.get("closedTradesSummaryWindow.totalAmount.value", + btcFormatter.formatCoin(totalTradeAmount, true), + DisplayUtils.formatVolumeWithCode(volume)); + }) + .orElse(""); + } + + public Map getTotalVolumeByCurrency() { + return dataModel.getTotalVolumeByCurrency().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, + entry -> { + String currencyCode = entry.getKey(); + Monetary monetary; + if (CurrencyUtil.isCryptoCurrency(currencyCode)) { + monetary = Altcoin.valueOf(currencyCode, entry.getValue()); + } else { + monetary = Fiat.valueOf(currencyCode, entry.getValue()); + } + return DisplayUtils.formatVolumeWithCode(new Volume(monetary)); + } + )); + } + + public String getTotalTxFee(Coin totalTradeAmount) { + Coin totalTxFee = dataModel.getTotalTxFee(); + double percentage = ((double) totalTxFee.value) / totalTradeAmount.value; + return Res.get("closedTradesSummaryWindow.totalMinerFee.value", + btcFormatter.formatCoin(totalTxFee, true), + FormattingUtils.formatToPercentWithSymbol(percentage)); + } + + public String getTotalTradeFeeInBtc(Coin totalTradeAmount) { + Coin totalTradeFee = dataModel.getTotalTradeFee(true); + double percentage = ((double) totalTradeFee.value) / totalTradeAmount.value; + return Res.get("closedTradesSummaryWindow.totalTradeFeeInBtc.value", + btcFormatter.formatCoin(totalTradeFee, true), + FormattingUtils.formatToPercentWithSymbol(percentage)); + } + + public String getTotalTradeFeeInBsq(Coin totalTradeAmount) { + return dataModel.getVolume(totalTradeAmount, "USD") + .filter(v -> v.getValue() > 0) + .map(tradeAmountVolume -> { + Coin totalTradeFee = dataModel.getTotalTradeFee(false); + Volume bsqVolumeInUsd = dataModel.getBsqVolumeInUsdWithAveragePrice(totalTradeFee); // with 4 decimal + double percentage = ((double) bsqVolumeInUsd.getValue()) / tradeAmountVolume.getValue(); + return Res.get("closedTradesSummaryWindow.totalTradeFeeInBsq.value", + bsqFormatter.formatCoin(totalTradeFee, true), + FormattingUtils.formatToPercentWithSymbol(percentage)); + }) + .orElse(""); } }