Add popup for summary of trade history

This commit is contained in:
chimp1984 2021-03-15 16:30:11 -05:00
parent 1e593205d7
commit 80c23883c8
No known key found for this signature in database
GPG key ID: 9801B4EC591F90E3
8 changed files with 343 additions and 8 deletions

View file

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

View file

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

View file

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

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ClosedTradesSummaryWindow> {
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<String, String> 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));
}
}

View file

@ -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<ClosedTradableListItem> list = FXCollections.observableArrayList();
private final ListChangeListener<Tradable> 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<String, Long> getTotalVolumeByCurrency() {
Map<String, Long> 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<Volume> getVolumeInUserFiatCurrency(Coin amount) {
return getVolume(amount, preferences.getPreferredTradeCurrency().getCode());
}
public Optional<Volume> 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<Price, Price> 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;
}
}
}
}
}

View file

@ -62,6 +62,7 @@
<HBox spacing="10">
<Label fx:id="numItems"/>
<Region fx:id="footerSpacer"/>
<AutoTooltipButton fx:id="summaryButton"/>
<AutoTooltipButton fx:id="exportButton"/>
</HBox>
</VBox>

View file

@ -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<VBox, ClosedTrades
@FXML
Pane searchBoxSpacer;
@FXML
AutoTooltipButton exportButton;
AutoTooltipButton exportButton, summaryButton;
@FXML
Label numItems;
@FXML
@ -242,6 +243,7 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
HBox.setHgrow(footerSpacer, Priority.ALWAYS);
HBox.setMargin(exportButton, new Insets(0, 10, 0, 0));
exportButton.updateText(Res.get("shared.exportCSV"));
summaryButton.updateText(Res.get("shared.summary"));
}
@Override
@ -291,6 +293,8 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
new ClosedTradableListItem(null), sortedList, (Stage) root.getScene().getWindow());
});
summaryButton.setOnAction(event -> new ClosedTradesSummaryWindow(model).show());
filterTextField.textProperty().addListener(filterTextFieldListener);
applyFilteredListPredicate(filterTextField.getText());
root.widthProperty().addListener(widthListener);
@ -301,6 +305,7 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
protected void deactivate() {
sortedList.comparatorProperty().unbind();
exportButton.setOnAction(null);
summaryButton.setOnAction(null);
filterTextField.textProperty().removeListener(filterTextFieldListener);
root.widthProperty().removeListener(widthListener);

View file

@ -23,9 +23,9 @@ import bisq.desktop.util.DisplayUtils;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Volume;
import bisq.core.offer.Offer;
import bisq.core.offer.OpenOffer;
@ -37,16 +37,20 @@ import bisq.core.util.coin.CoinFormatter;
import bisq.network.p2p.NodeAddress;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Monetary;
import org.bitcoinj.utils.Fiat;
import com.google.inject.Inject;
import javax.inject.Named;
import javafx.collections.ObservableList;
import java.util.Map;
import java.util.stream.Collectors;
class ClosedTradesViewModel extends ActivatableWithDataModel<ClosedTradesDataModel> implements ViewModel {
private final BtcWalletService btcWalletService;
public class ClosedTradesViewModel extends ActivatableWithDataModel<ClosedTradesDataModel> implements ViewModel {
private final BsqWalletService bsqWalletService;
private final BsqFormatter bsqFormatter;
private final CoinFormatter btcFormatter;
@ -55,13 +59,11 @@ class ClosedTradesViewModel extends ActivatableWithDataModel<ClosedTradesDataMod
@Inject
public ClosedTradesViewModel(ClosedTradesDataModel dataModel,
AccountAgeWitnessService accountAgeWitnessService,
BtcWalletService btcWalletService,
BsqWalletService bsqWalletService,
BsqFormatter bsqFormatter,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) {
super(dataModel);
this.accountAgeWitnessService = accountAgeWitnessService;
this.btcWalletService = btcWalletService;
this.bsqWalletService = bsqWalletService;
this.bsqFormatter = bsqFormatter;
this.btcFormatter = btcFormatter;
@ -275,6 +277,66 @@ class ClosedTradesViewModel extends ActivatableWithDataModel<ClosedTradesDataMod
}
boolean wasMyOffer(Tradable tradable) {
return dataModel.closedTradableManager.wasMyOffer(tradable.getOffer());
return dataModel.wasMyOffer(tradable);
}
public Coin getTotalTradeAmount() {
return dataModel.getTotalAmount();
}
public String getTotalAmountWithVolume(Coin totalTradeAmount) {
return dataModel.getVolumeInUserFiatCurrency(totalTradeAmount)
.map(volume -> {
return Res.get("closedTradesSummaryWindow.totalAmount.value",
btcFormatter.formatCoin(totalTradeAmount, true),
DisplayUtils.formatVolumeWithCode(volume));
})
.orElse("");
}
public Map<String, String> 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("");
}
}