diff --git a/common/src/main/java/bisq/common/util/MathUtils.java b/common/src/main/java/bisq/common/util/MathUtils.java index 56685137629..78b787843a5 100644 --- a/common/src/main/java/bisq/common/util/MathUtils.java +++ b/common/src/main/java/bisq/common/util/MathUtils.java @@ -92,4 +92,15 @@ public static double scaleDownByPowerOf10(long value, int exponent) { public static double exactMultiply(double value1, double value2) { return BigDecimal.valueOf(value1).multiply(BigDecimal.valueOf(value2)).doubleValue(); } + + public static Long getMedian(Long[] list) { + int middle = list.length / 2; + long median; + if (list.length % 2 == 1) { + median = list[middle]; + } else { + median = MathUtils.roundDoubleToLong((list[middle - 1] + list[middle]) / 2.0); + } + return median; + } } diff --git a/core/src/main/java/bisq/core/offer/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java index 7d26b6cad39..1e5f9b491f0 100644 --- a/core/src/main/java/bisq/core/offer/OfferUtil.java +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -278,10 +278,21 @@ static Coin getAdjustedAmount(Coin amount, Price price, long maxTradeLimit, int public static Optional getFeeInUserFiatCurrency(Coin makerFee, boolean isCurrencyForMakerFeeBtc, Preferences preferences, PriceFeedService priceFeedService, BsqFormatter bsqFormatter) { - // We use the users currency derived from his selected country. - // We don't use the preferredTradeCurrency from preferences as that can be also set to an altcoin. String countryCode = preferences.getUserCountry().code; String userCurrencyCode = CurrencyUtil.getCurrencyByCountryCode(countryCode).getCode(); + return getFeeInUserFiatCurrency(makerFee, + isCurrencyForMakerFeeBtc, + userCurrencyCode, + priceFeedService, + bsqFormatter); + } + + public static Optional getFeeInUserFiatCurrency(Coin makerFee, boolean isCurrencyForMakerFeeBtc, + String userCurrencyCode, PriceFeedService priceFeedService, + BsqFormatter bsqFormatter) { + // We use the users currency derived from his selected country. + // We don't use the preferredTradeCurrency from preferences as that can be also set to an altcoin. + MarketPrice marketPrice = priceFeedService.getMarketPrice(userCurrencyCode); if (marketPrice != null && makerFee != null) { long marketPriceAsLong = MathUtils.roundDoubleToLong(MathUtils.scaleUpByPowerOf10(marketPrice.getPrice(), Fiat.SMALLEST_UNIT_EXPONENT)); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index c534f009849..1d134691339 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2055,6 +2055,8 @@ dao.factsAndFigures.menuItem.transactions=BSQ Transactions dao.factsAndFigures.dashboard.marketPrice=Market data dao.factsAndFigures.dashboard.price=Latest BSQ/BTC trade price (in Bisq) +dao.factsAndFigures.dashboard.avgPrice90=90 days average BSQ/BTC trade price +dao.factsAndFigures.dashboard.medianPrice90=90 days median BSQ/BTC trade price dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on trade price) dao.factsAndFigures.dashboard.availableAmount=Total available BSQ diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java index 0de2e7e6252..34b21b7cffd 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java @@ -20,21 +20,23 @@ import bisq.desktop.common.view.ActivatableView; import bisq.desktop.common.view.FxmlView; import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; import bisq.core.dao.DaoFacade; import bisq.core.dao.state.DaoStateListener; -import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.Block; import bisq.core.dao.state.model.governance.IssuanceType; import bisq.core.locale.Res; +import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.statistics.TradeStatistics2; import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.user.Preferences; -import bisq.core.util.BSFormatter; import bisq.core.util.BsqFormatter; +import bisq.common.util.MathUtils; +import bisq.common.util.Tuple2; import bisq.common.util.Tuple3; import org.bitcoinj.core.Coin; @@ -67,7 +69,12 @@ import java.time.temporal.TemporalAdjuster; import java.time.temporal.TemporalAdjusters; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; import java.util.Comparator; +import java.util.Date; +import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -78,9 +85,6 @@ import static bisq.desktop.util.FormBuilder.addTopLabelReadOnlyTextField; - -import java.sql.Date; - @FxmlView public class BsqDashboardView extends ActivatableView implements DaoStateListener { @@ -90,18 +94,15 @@ public class BsqDashboardView extends ActivatableView implements private final DaoFacade daoFacade; private final TradeStatisticsManager tradeStatisticsManager; private final PriceFeedService priceFeedService; - private final DaoStateService daoStateService; private final Preferences preferences; private final BsqFormatter bsqFormatter; - private final BSFormatter btcFormatter; private ChangeListener priceChangeListener; private AreaChart bsqPriceChart; - private XYChart.Series seriesBSQAdded, seriesBSQBurnt; private XYChart.Series seriesBSQPrice; - private TextField marketCapTextField, availableAmountTextField; + private TextField avgPrice90TextField, medianPrice90TextField, marketCapTextField, availableAmountTextField; private Label marketPriceLabel; private Coin availableAmount; @@ -116,17 +117,13 @@ public class BsqDashboardView extends ActivatableView implements private BsqDashboardView(DaoFacade daoFacade, TradeStatisticsManager tradeStatisticsManager, PriceFeedService priceFeedService, - DaoStateService daoStateService, Preferences preferences, - BsqFormatter bsqFormatter, - BSFormatter btcFormatter) { + BsqFormatter bsqFormatter) { this.daoFacade = daoFacade; this.tradeStatisticsManager = tradeStatisticsManager; this.priceFeedService = priceFeedService; - this.daoStateService = daoStateService; this.preferences = preferences; this.bsqFormatter = bsqFormatter; - this.btcFormatter = btcFormatter; } @Override @@ -137,7 +134,10 @@ public void initialize() { createKPIs(); createChart(); - priceChangeListener = (observable, oldValue, newValue) -> updatePrice(); + priceChangeListener = (observable, oldValue, newValue) -> { + updatePrice(); + updateAverageAndMedianPrice(); + }; } private void createKPIs() { @@ -148,6 +148,12 @@ private void createKPIs() { marketPriceBox.second.getStyleClass().add("dao-kpi-subtext"); + avgPrice90TextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.dashboard.avgPrice90")).second; + + medianPrice90TextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + Res.get("dao.factsAndFigures.dashboard.medianPrice90")).second; + marketCapTextField = addTopLabelReadOnlyTextField(root, ++gridRow, Res.get("dao.factsAndFigures.dashboard.marketCap")).second; @@ -165,6 +171,7 @@ protected void activate() { updateWithBsqBlockChainData(); updatePrice(); updateChartData(); + updateAverageAndMedianPrice(); } @@ -236,8 +243,8 @@ public Number fromString(String string) { bsqPriceChart.setLegendVisible(false); bsqPriceChart.setAnimated(false); bsqPriceChart.setId("charts-dao"); - bsqPriceChart.setMinHeight(385); - bsqPriceChart.setPrefHeight(385); + bsqPriceChart.setMinHeight(335); + bsqPriceChart.setPrefHeight(bsqPriceChart.getMinHeight()); bsqPriceChart.setCreateSymbols(true); bsqPriceChart.setPadding(new Insets(0)); bsqPriceChart.getData().addAll(seriesBSQPrice); @@ -260,16 +267,16 @@ public Number fromString(String string) { } private void updateChartData() { - updateBSQPriceData(); + updateBsqPriceData(); } - private void updateBSQPriceData() { + private void updateBsqPriceData() { seriesBSQPrice.getData().clear(); Map> bsqPriceByDate = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() .filter(e -> e.getCurrencyCode().equals("BSQ")) .sorted(Comparator.comparing(TradeStatistics2::getTradeDate)) - .collect(Collectors.groupingBy(item -> new Date(item.getTradeDate().getTime()).toLocalDate() + .collect(Collectors.groupingBy(item -> new java.sql.Date(item.getTradeDate().getTime()).toLocalDate() .with(ADJUSTERS.get(DAY)))); List> updatedBSQPrice = bsqPriceByDate.keySet().stream() @@ -321,5 +328,56 @@ private void updatePrice() { marketCapTextField.setText(Res.get("shared.na")); } } + + private void updateAverageAndMedianPrice() { + Date past90 = getPastDate(90); + List bsqTradePast90Days = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + .filter(e -> e.getCurrencyCode().equals("BSQ")) + .filter(e -> e.getTradeDate().after(past90)) + .collect(Collectors.toList()); + Tuple2 averageAndMedian = getAverageAndMedian(bsqTradePast90Days); + Coin oneBsq = Coin.valueOf(100); + + Price avgPrice = Price.valueOf("BSQ", averageAndMedian.first); + String avg = bsqFormatter.formatPrice(avgPrice); + String bsqInUsdAvg = GUIUtil.getBsqInUsd(avgPrice, oneBsq, priceFeedService, bsqFormatter); + avgPrice90TextField.setText(avg + " BSQ/BTC (" + "1 BSQ = " + bsqInUsdAvg + ")"); + + Price medianPrice = Price.valueOf("BSQ", averageAndMedian.second); + String median = bsqFormatter.formatPrice(medianPrice); + String bsqInUsdMedian = GUIUtil.getBsqInUsd(medianPrice, oneBsq, priceFeedService, bsqFormatter); + medianPrice90TextField.setText(median + " BSQ/BTC (" + "1 BSQ = " + bsqInUsdMedian + ")"); + } + + private Tuple2 getAverageAndMedian(List list) { + long accumulatedVolume = 0; + long accumulatedAmount = 0; + List tradePrices = new ArrayList<>(list.size()); + + for (TradeStatistics2 item : list) { + item.getTradeVolume(); + accumulatedVolume += item.getTradeVolume().getValue(); + accumulatedAmount += item.getTradeAmount().getValue(); + tradePrices.add(item.getTradePrice().getValue()); + } + Collections.sort(tradePrices); + list.sort(Comparator.comparingLong(o -> o.getTradeDate().getTime())); + + long averagePrice; + Long[] prices = new Long[tradePrices.size()]; + tradePrices.toArray(prices); + long medianPrice = MathUtils.getMedian(prices); + double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, Altcoin.SMALLEST_UNIT_EXPONENT); + averagePrice = MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / (double) accumulatedVolume); + + return new Tuple2<>(averagePrice, medianPrice); + } + + private Date getPastDate(int days) { + Calendar cal = new GregorianCalendar(); + cal.setTime(new Date()); + cal.add(Calendar.DAY_OF_MONTH, -1 * days); + return cal.getTime(); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index c9da02597ec..3dfe37b9683 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -330,7 +330,7 @@ CandleData getCandleData(long tick, Set set) { long averagePrice; Long[] prices = new Long[tradePrices.size()]; tradePrices.toArray(prices); - long medianPrice = findMedian(prices); + long medianPrice = MathUtils.getMedian(prices); boolean isBullish; if (CurrencyUtil.isCryptoCurrency(getCurrencyCode())) { isBullish = close < open; @@ -350,17 +350,6 @@ CandleData getCandleData(long tick, Set set) { return new CandleData(tick, open, close, high, low, averagePrice, medianPrice, accumulatedAmount, accumulatedVolume, numTrades, isBullish, dateString); } - - Long findMedian(Long[] prices) { - int middle = prices.length / 2; - long median; - if (prices.length % 2 == 1) { - median = prices[middle]; - } else { - median = MathUtils.roundDoubleToLong((prices[middle - 1] + prices[middle]) / 2.0); - } - return median; - } Date roundToTick(Date time, TickUnit tickUnit) { ZonedDateTime zdt = time.toInstant().atZone(ZoneId.systemDefault()); diff --git a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java index aa772f2c531..1afbc534333 100644 --- a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java @@ -31,10 +31,14 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccountList; 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.user.DontShowAgainLookup; import bisq.core.user.Preferences; import bisq.core.user.User; @@ -51,6 +55,7 @@ import bisq.common.storage.CorruptedDatabaseFilesHandler; import bisq.common.storage.FileUtil; import bisq.common.storage.Storage; +import bisq.common.util.MathUtils; import bisq.common.util.Tuple2; import bisq.common.util.Tuple3; import bisq.common.util.Utilities; @@ -59,6 +64,7 @@ import org.bitcoinj.core.Coin; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.uri.BitcoinURI; +import org.bitcoinj.utils.Fiat; import org.bitcoinj.wallet.DeterministicSeed; import com.googlecode.jcsv.CSVStrategy; @@ -1022,4 +1028,22 @@ public static void openTxInBsqBlockExplorer(String txId, Preferences preferences if (txId != null) GUIUtil.openWebPage(preferences.getBsqBlockChainExplorer().txUrl + txId, false); } + + public static String getBsqInUsd(Price bsqPrice, + Coin bsqAmount, + PriceFeedService priceFeedService, + BsqFormatter bsqFormatter) { + MarketPrice usdMarketPrice = priceFeedService.getMarketPrice("USD"); + if (usdMarketPrice == null) { + return Res.get("shared.na"); + } + long usdMarketPriceAsLong = MathUtils.roundDoubleToLong(MathUtils.scaleUpByPowerOf10(usdMarketPrice.getPrice(), + Fiat.SMALLEST_UNIT_EXPONENT)); + Price usdPrice = Price.valueOf("USD", usdMarketPriceAsLong); + String bsqAmountAsString = bsqFormatter.formatCoin(bsqAmount); + Volume bsqAmountAsVolume = Volume.parse(bsqAmountAsString, "BSQ"); + Coin requiredBtc = bsqPrice.getAmountByVolume(bsqAmountAsVolume); + Volume volumeByAmount = usdPrice.getVolumeByAmount(requiredBtc); + return bsqFormatter.formatVolumeWithCode(volumeByAmount); + } }