diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/RequestInfo.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/RequestInfo.java index b86e9e5e458..c55a9e04eef 100644 --- a/core/src/main/java/bisq/core/network/p2p/inventory/model/RequestInfo.java +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/RequestInfo.java @@ -29,7 +29,7 @@ @Getter public class RequestInfo { // Carries latest commit hash of feature changes (not latest commit as that is then the commit for editing that field) - public static final String COMMIT_HASH = "1c50cb6c"; + public static final String COMMIT_HASH = "c07d47a8"; private final long requestStartTime; @Setter diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java index 71cb5e86023..0c9a05ec7f5 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java @@ -50,6 +50,9 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; +import java.time.LocalDateTime; +import java.time.ZoneId; + import java.util.Arrays; import java.util.Date; import java.util.HashMap; @@ -71,6 +74,8 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayload, PersistableNetworkPayload, CapabilityRequiringPayload, DateSortedTruncatablePayload { + @JsonExclude + private transient static final ZoneId ZONE_ID = ZoneId.systemDefault(); public static TradeStatistics3 from(Trade trade, @Nullable String referralId, @@ -186,6 +191,11 @@ private enum PaymentMethodMapper { @JsonExclude private transient final Date dateObj; + @JsonExclude + private transient Volume volume = null; + @JsonExclude + private transient LocalDateTime localDateTime; + public TradeStatistics3(String currency, long price, long amount, @@ -328,6 +338,13 @@ public Date getDate() { return dateObj; } + public LocalDateTime getLocalDateTime() { + if (localDateTime == null) { + localDateTime = dateObj.toInstant().atZone(ZONE_ID).toLocalDateTime(); + } + return localDateTime; + } + public long getDateAsLong() { return date; } @@ -350,8 +367,13 @@ public String getPaymentMethod() { } } + private transient Price priceObj; + public Price getTradePrice() { - return Price.valueOf(currency, price); + if (priceObj == null) { + priceObj = Price.valueOf(currency, price); + } + return priceObj; } public Coin getTradeAmount() { @@ -359,12 +381,15 @@ public Coin getTradeAmount() { } public Volume getTradeVolume() { - if (getTradePrice().getMonetary() instanceof Altcoin) { - return new Volume(new AltcoinExchangeRate((Altcoin) getTradePrice().getMonetary()).coinToAltcoin(getTradeAmount())); - } else { - Volume volume = new Volume(new ExchangeRate((Fiat) getTradePrice().getMonetary()).coinToFiat(getTradeAmount())); - return VolumeUtil.getRoundedFiatVolume(volume); + if (volume == null) { + if (getTradePrice().getMonetary() instanceof Altcoin) { + volume = new Volume(new AltcoinExchangeRate((Altcoin) getTradePrice().getMonetary()).coinToAltcoin(getTradeAmount())); + } else { + Volume exactVolume = new Volume(new ExchangeRate((Fiat) getTradePrice().getMonetary()).coinToFiat(getTradeAmount())); + volume = VolumeUtil.getRoundedFiatVolume(exactVolume); + } } + return volume; } public boolean isValid() { diff --git a/core/src/main/java/bisq/core/user/Cookie.java b/core/src/main/java/bisq/core/user/Cookie.java index bc73d6bbd03..d5cb0c013f7 100644 --- a/core/src/main/java/bisq/core/user/Cookie.java +++ b/core/src/main/java/bisq/core/user/Cookie.java @@ -45,6 +45,16 @@ public Optional getAsOptionalDouble(CookieKey key) { } } + public void putAsBoolean(CookieKey key, boolean value) { + put(key, value ? "1" : "0"); + } + + public Optional getAsOptionalBoolean(CookieKey key) { + return containsKey(key) ? + Optional.of(get(key).equals("1")) : + Optional.empty(); + } + public Map toProtoMessage() { Map protoMap = new HashMap<>(); this.forEach((key, value) -> protoMap.put(key.name(), value)); diff --git a/core/src/main/java/bisq/core/user/CookieKey.java b/core/src/main/java/bisq/core/user/CookieKey.java index c8653e54bb2..2dc3c8a0433 100644 --- a/core/src/main/java/bisq/core/user/CookieKey.java +++ b/core/src/main/java/bisq/core/user/CookieKey.java @@ -22,5 +22,7 @@ public enum CookieKey { STAGE_X, STAGE_Y, STAGE_W, - STAGE_H + STAGE_H, + TRADE_STAT_CHART_USE_USD, + CLEAN_TOR_DIR_AT_RESTART } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index fa728340308..69b020b98d6 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -316,7 +316,7 @@ market.spread.expanded=Expanded view # TradesChartsView market.trades.nrOfTrades=Trades: {0} -market.trades.tooltip.volumeBar=Volume: {0}\nNo. of trades: {1}\nDate: {2} +market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} market.trades.tooltip.candle.open=Open: market.trades.tooltip.candle.close=Close: market.trades.tooltip.candle.high=High: @@ -324,6 +324,7 @@ market.trades.tooltip.candle.low=Low: market.trades.tooltip.candle.average=Average: market.trades.tooltip.candle.median=Median: market.trades.tooltip.candle.date=Date: +market.trades.showVolumeInUSD=Show volume in USD #################################################################### # OfferView diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradeStatistics3ListItem.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradeStatistics3ListItem.java index 80fdcb922fc..234342d0727 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradeStatistics3ListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradeStatistics3ListItem.java @@ -34,6 +34,12 @@ public class TradeStatistics3ListItem { private final TradeStatistics3 tradeStatistics3; private final CoinFormatter coinFormatter; private final boolean showAllTradeCurrencies; + private String dateString; + private String market; + private String priceString; + private String volumeString; + private String paymentMethodString; + private String amountString; public TradeStatistics3ListItem(@Nullable TradeStatistics3 tradeStatistics3, CoinFormatter coinFormatter, @@ -44,31 +50,47 @@ public TradeStatistics3ListItem(@Nullable TradeStatistics3 tradeStatistics3, } public String getDateString() { - return tradeStatistics3 != null ? DisplayUtils.formatDateTime(tradeStatistics3.getDate()) : ""; + if (dateString == null) { + dateString = tradeStatistics3 != null ? DisplayUtils.formatDateTime(tradeStatistics3.getDate()) : ""; + } + return dateString; } public String getMarket() { - return tradeStatistics3 != null ? CurrencyUtil.getCurrencyPair(tradeStatistics3.getCurrency()) : ""; + if (market == null) { + market = tradeStatistics3 != null ? CurrencyUtil.getCurrencyPair(tradeStatistics3.getCurrency()) : ""; + } + return market; } public String getPriceString() { - return tradeStatistics3 != null ? FormattingUtils.formatPrice(tradeStatistics3.getTradePrice()) : ""; + if (priceString == null) { + priceString = tradeStatistics3 != null ? FormattingUtils.formatPrice(tradeStatistics3.getTradePrice()) : ""; + } + return priceString; } public String getVolumeString() { - if (tradeStatistics3 == null) { - return ""; + if (volumeString == null) { + volumeString = tradeStatistics3 != null ? showAllTradeCurrencies ? + DisplayUtils.formatVolumeWithCode(tradeStatistics3.getTradeVolume()) : + DisplayUtils.formatVolume(tradeStatistics3.getTradeVolume()) + : ""; } - return showAllTradeCurrencies ? - DisplayUtils.formatVolumeWithCode(tradeStatistics3.getTradeVolume()) : - DisplayUtils.formatVolume(tradeStatistics3.getTradeVolume()); + return volumeString; } public String getPaymentMethodString() { - return tradeStatistics3 != null ? Res.get(tradeStatistics3.getPaymentMethod()) : ""; + if (paymentMethodString == null) { + paymentMethodString = tradeStatistics3 != null ? Res.get(tradeStatistics3.getPaymentMethod()) : ""; + } + return paymentMethodString; } public String getAmountString() { - return tradeStatistics3 != null ? coinFormatter.formatCoin(tradeStatistics3.getTradeAmount(), 4) : ""; + if (amountString == null) { + amountString = tradeStatistics3 != null ? coinFormatter.formatCoin(tradeStatistics3.getTradeAmount(), 4) : ""; + } + return amountString; } } diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java index 4320599881f..b42e2c3a435 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java @@ -20,6 +20,7 @@ import bisq.desktop.common.view.ActivatableViewAndModel; import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.AutoTooltipSlideToggleButton; import bisq.desktop.components.AutoTooltipTableColumn; import bisq.desktop.components.AutoTooltipToggleButton; import bisq.desktop.components.AutocompleteComboBox; @@ -34,6 +35,8 @@ import bisq.core.locale.Res; import bisq.core.monetary.Price; import bisq.core.trade.statistics.TradeStatistics3; +import bisq.core.user.CookieKey; +import bisq.core.user.User; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; @@ -139,9 +142,10 @@ private CurrencyListItem specialShowAllItem() { } } + private final User user; private final CoinFormatter coinFormatter; - private VolumeChart volumeChart; + private VolumeChart volumeChart, volumeInUsdChart; private CandleStickChart priceChart; private AutocompleteComboBox currencyComboBox; private TableView tableView; @@ -150,6 +154,7 @@ private CurrencyListItem specialShowAllItem() { private Pane rootParent; private AnchorPane priceChartPane, volumeChartPane; private HBox footer; + private AutoTooltipSlideToggleButton showVolumeAsUsdToggleButton; private Label nrOfTradeStatisticsLabel; private ToggleGroup toggleGroup; private SingleSelectionModel tabPaneSelectionModel; @@ -172,9 +177,10 @@ private CurrencyListItem specialShowAllItem() { private Subscription currencySelectionSubscriber; private final StringProperty priceColumnLabel = new SimpleStringProperty(); - private NumberAxis priceAxisX, priceAxisY, volumeAxisY, volumeAxisX; + private NumberAxis priceAxisX, priceAxisY, volumeAxisY, volumeAxisX, volumeInUsdAxisX; private XYChart.Series priceSeries; - private XYChart.Series volumeSeries; + private final XYChart.Series volumeSeries = new XYChart.Series<>(); + private final XYChart.Series volumeInUsdSeries = new XYChart.Series<>(); private double priceAxisYWidth; private double volumeAxisYWidth; @@ -186,8 +192,10 @@ private CurrencyListItem specialShowAllItem() { @SuppressWarnings("WeakerAccess") @Inject public TradesChartsView(TradesChartsViewModel model, + User user, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter coinFormatter) { super(model); + this.user = user; this.coinFormatter = coinFormatter; } @@ -221,6 +229,7 @@ public void initialize() { model.setTickUnit((TradesChartsViewModel.TickUnit) newValue.getUserData()); priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); + volumeInUsdAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); } }; priceAxisYWidthListener = (observable, oldValue, newValue) -> { @@ -232,7 +241,7 @@ public void initialize() { layoutChart(); }; tradeStatisticsByCurrencyListener = c -> nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", - model.tradeStatisticsByCurrency.size())); + model.selectedTradeStatistics.size())); parentHeightListener = (observable, oldValue, newValue) -> layout(); priceColumnLabelListener = (o, oldVal, newVal) -> priceColumn.setGraphic(new AutoTooltipLabel(newVal)); @@ -254,8 +263,10 @@ public void initialize() { tableView.getColumns().add(1, marketColumn); volumeChart.setPrefHeight(volumeChart.getMaxHeight()); + volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMaxHeight()); } else { volumeChart.setPrefHeight(volumeChart.getMinHeight()); + volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMinHeight()); priceSeries.setName(selectedTradeCurrency.getName()); String code = selectedTradeCurrency.getCode(); volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", code))); @@ -264,9 +275,11 @@ public void initialize() { tableView.getColumns().remove(marketColumn); } + layout(); return null; }); + } @Override @@ -305,27 +318,26 @@ else if (model.getSelectedCurrencyListItem().isPresent()) toggleGroup.selectedToggleProperty().addListener(timeUnitChangeListener); priceAxisY.widthProperty().addListener(priceAxisYWidthListener); volumeAxisY.widthProperty().addListener(volumeAxisYWidthListener); - model.tradeStatisticsByCurrency.addListener(tradeStatisticsByCurrencyListener); + model.selectedTradeStatistics.addListener(tradeStatisticsByCurrencyListener); priceAxisY.labelProperty().bind(priceColumnLabel); priceColumnLabel.addListener(priceColumnLabelListener); - currencySelectionSubscriber = currencySelectionBinding.subscribe((observable, oldValue, newValue) -> { }); - sortedList.comparatorProperty().bind(tableView.comparatorProperty()); - tableView.setItems(sortedList); - priceChart.setAnimated(model.preferences.isUseAnimations()); - volumeChart.setAnimated(model.preferences.isUseAnimations()); + boolean useAnimations = model.preferences.isUseAnimations(); + priceChart.setAnimated(useAnimations); + volumeChart.setAnimated(useAnimations); + volumeInUsdChart.setAnimated(useAnimations); priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); + volumeInUsdAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); - nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.tradeStatisticsByCurrency.size())); + nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.selectedTradeStatistics.size())); exportLink.setOnAction(e -> exportToCsv()); - UserThread.runAfter(this::updateChartData, 100, TimeUnit.MILLISECONDS); if (root.getParent() instanceof Pane) { @@ -333,7 +345,19 @@ else if (model.getSelectedCurrencyListItem().isPresent()) rootParent.heightProperty().addListener(parentHeightListener); } + user.getCookie().getAsOptionalBoolean(CookieKey.TRADE_STAT_CHART_USE_USD).ifPresent(showUsd -> { + showVolumeAsUsdToggleButton.setSelected(showUsd); + showVolumeAsUsd(showUsd); + }); + showVolumeAsUsdToggleButton.setOnAction(e -> { + boolean selected = showVolumeAsUsdToggleButton.isSelected(); + showVolumeAsUsd(selected); + user.getCookie().putAsBoolean(CookieKey.TRADE_STAT_CHART_USE_USD, selected); + user.requestPersistence(); + }); + fillList(); + tableView.setItems(sortedList); layout(); } @@ -344,10 +368,10 @@ protected void deactivate() { toggleGroup.selectedToggleProperty().removeListener(timeUnitChangeListener); priceAxisY.widthProperty().removeListener(priceAxisYWidthListener); volumeAxisY.widthProperty().removeListener(volumeAxisYWidthListener); - model.tradeStatisticsByCurrency.removeListener(tradeStatisticsByCurrencyListener); + model.selectedTradeStatistics.removeListener(tradeStatisticsByCurrencyListener); priceAxisY.labelProperty().unbind(); - priceColumn.textProperty().removeListener(priceColumnLabelListener); + priceColumnLabel.removeListener(priceColumnLabelListener); currencySelectionSubscriber.unsubscribe(); @@ -357,18 +381,27 @@ protected void deactivate() { priceChart.getData().clear(); exportLink.setOnAction(null); + showVolumeAsUsdToggleButton.setOnAction(null); if (rootParent != null) { rootParent.heightProperty().removeListener(parentHeightListener); } } + private void showVolumeAsUsd(Boolean showUsd) { + volumeChart.setVisible(!showUsd); + volumeChart.setManaged(!showUsd); + volumeInUsdChart.setVisible(showUsd); + volumeInUsdChart.setManaged(showUsd); + } + private void fillList() { - ObservableList tradeStatistics3ListItems = FXCollections.observableList(model.tradeStatisticsByCurrency.stream() - .map(tradeStatistics3 -> new TradeStatistics3ListItem(tradeStatistics3, - coinFormatter, - model.showAllTradeCurrenciesProperty.get())) - .collect(Collectors.toList())); + ObservableList tradeStatistics3ListItems = FXCollections.observableList( + model.selectedTradeStatistics.stream() + .map(tradeStatistics -> new TradeStatistics3ListItem(tradeStatistics, + coinFormatter, + model.showAllTradeCurrenciesProperty.get())) + .collect(Collectors.toList())); listItems.clear(); listItems.addAll(tradeStatistics3ListItems); } @@ -493,24 +526,52 @@ public Number fromString(String string) { priceChartPane.getChildren().add(priceChart); - volumeSeries = new XYChart.Series<>(); - volumeAxisX = new NumberAxis(0, model.maxTicks + 1, 1); - volumeAxisX.setTickUnit(4); - volumeAxisX.setMinorTickCount(4); - volumeAxisX.setMinorTickVisible(true); - volumeAxisX.setForceZeroInRange(false); - volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); - addTickMarkLabelCssClass(volumeAxisX, "axis-tick-mark-text-node"); - volumeAxisY = new NumberAxis(); - volumeAxisY.setForceZeroInRange(true); - volumeAxisY.setAutoRanging(true); - volumeAxisY.setLabel(Res.get("shared.volumeWithCur", Res.getBaseCurrencyCode())); - volumeAxisY.setTickLabelFormatter(new StringConverter<>() { + volumeChart = getVolumeChart(volumeAxisX, volumeAxisY, volumeSeries, "BTC"); + + volumeInUsdAxisX = new NumberAxis(0, model.maxTicks + 1, 1); + NumberAxis volumeInUsdAxisY = new NumberAxis(); + volumeInUsdChart = getVolumeChart(volumeInUsdAxisX, volumeInUsdAxisY, volumeInUsdSeries, "USD"); + volumeInUsdChart.setVisible(false); + volumeInUsdChart.setManaged(false); + + showVolumeAsUsdToggleButton = new AutoTooltipSlideToggleButton(); + showVolumeAsUsdToggleButton.setText(Res.get("market.trades.showVolumeInUSD")); + showVolumeAsUsdToggleButton.setPadding(new Insets(-15, 0, 0, 10)); + + VBox vBox = new VBox(); + AnchorPane.setTopAnchor(vBox, 15d); + AnchorPane.setBottomAnchor(vBox, 10d); + AnchorPane.setLeftAnchor(vBox, 0d); + AnchorPane.setRightAnchor(vBox, 10d); + vBox.getChildren().addAll(showVolumeAsUsdToggleButton, volumeChart, volumeInUsdChart); + + volumeChartPane = new AnchorPane(); + volumeChartPane.getStyleClass().add("chart-pane"); + volumeChartPane.getChildren().add(vBox); + } + + private VolumeChart getVolumeChart(NumberAxis axisX, + NumberAxis axisY, + XYChart.Series series, + String currency) { + axisX.setTickUnit(4); + axisX.setMinorTickCount(4); + axisX.setMinorTickVisible(true); + axisX.setForceZeroInRange(false); + axisX.setTickLabelFormatter(getTimeAxisStringConverter()); + addTickMarkLabelCssClass(axisX, "axis-tick-mark-text-node"); + + axisY.setForceZeroInRange(true); + axisY.setAutoRanging(true); + axisY.setLabel(Res.get("shared.volumeWithCur", currency)); + axisY.setTickLabelFormatter(new StringConverter<>() { @Override - public String toString(Number object) { - return coinFormatter.formatCoin(Coin.valueOf(MathUtils.doubleToLong((double) object))); + public String toString(Number volume) { + return currency.equals("BTC") ? + coinFormatter.formatCoin(Coin.valueOf(MathUtils.doubleToLong((double) volume))) : + DisplayUtils.formatLargeFiatWithUnitPostFix((double) volume, "USD"); } @Override @@ -519,38 +580,31 @@ public Number fromString(String string) { } }); - volumeChart = new VolumeChart(volumeAxisX, volumeAxisY, new StringConverter<>() { + StringConverter btcStringConverter = new StringConverter<>() { @Override - public String toString(Number object) { - return coinFormatter.formatCoinWithCode(Coin.valueOf((long) object)); + public String toString(Number volume) { + return coinFormatter.formatCoinWithCode(Coin.valueOf((long) volume)); } @Override public Number fromString(String string) { return null; } - }); + }; + VolumeChart volumeChart = new VolumeChart(axisX, axisY, btcStringConverter); volumeChart.setId("volume-chart"); - volumeChart.setData(FXCollections.observableArrayList(List.of(volumeSeries))); + volumeChart.setData(FXCollections.observableArrayList(List.of(series))); volumeChart.setMinHeight(138); volumeChart.setPrefHeight(138); volumeChart.setMaxHeight(200); volumeChart.setLegendVisible(false); volumeChart.setPadding(new Insets(0)); - - volumeChartPane = new AnchorPane(); - volumeChartPane.getStyleClass().add("chart-pane"); - - AnchorPane.setTopAnchor(volumeChart, 15d); - AnchorPane.setBottomAnchor(volumeChart, 10d); - AnchorPane.setLeftAnchor(volumeChart, 0d); - AnchorPane.setRightAnchor(volumeChart, 10d); - - volumeChartPane.getChildren().add(volumeChart); + return volumeChart; } private void updateChartData() { volumeSeries.getData().setAll(model.volumeItems); + volumeInUsdSeries.getData().setAll(model.volumeInUsdItems); // At price chart we need to set the priceSeries new otherwise the lines are not rendered correctly // TODO should be fixed in candle chart @@ -566,9 +620,11 @@ private void layoutChart() { if (volumeAxisYWidth > priceAxisYWidth) { priceChart.setPadding(new Insets(0, 0, 0, volumeAxisYWidth - priceAxisYWidth)); volumeChart.setPadding(new Insets(0, 0, 0, 0)); + volumeInUsdChart.setPadding(new Insets(0, 0, 0, 0)); } else if (volumeAxisYWidth < priceAxisYWidth) { priceChart.setPadding(new Insets(0, 0, 0, 0)); volumeChart.setPadding(new Insets(0, 0, 0, priceAxisYWidth - volumeAxisYWidth)); + volumeInUsdChart.setPadding(new Insets(0, 0, 0, priceAxisYWidth - volumeAxisYWidth)); } }); } @@ -586,21 +642,21 @@ public String toString(Number object) { long time = model.getTimeFromTickIndex(index); String fmt = ""; switch (model.tickUnit) { - case YEAR: - fmt = "yyyy"; - break; - case MONTH: - fmt = "MMMyy"; - break; - case WEEK: - case DAY: - fmt = "dd/MMM\nyyyy"; - break; - case HOUR : - case MINUTE_10: - fmt = "HH:mm\ndd/MMM"; - break; - default: // nothing here + case YEAR: + fmt = "yyyy"; + break; + case MONTH: + fmt = "MMMyy"; + break; + case WEEK: + case DAY: + fmt = "dd/MMM\nyyyy"; + break; + case HOUR: + case MINUTE_10: + fmt = "HH:mm\ndd/MMM"; + break; + default: // nothing here } return DisplayUtils.formatDateAxis(new Date(time), fmt); @@ -616,16 +672,16 @@ public Number fromString(String string) { private void addTickMarkLabelCssClass(NumberAxis axis, String cssClass) { // grab the axis tick mark label (text object) and add a CSS class. axis.getChildrenUnmodifiable().addListener((ListChangeListener) c -> { - while (c.next()) { - if (c.wasAdded()) { - for (Node mark : c.getAddedSubList()) { - if (mark instanceof Text) { - mark.getStyleClass().add(cssClass); - } + while (c.next()) { + if (c.wasAdded()) { + for (Node mark : c.getAddedSubList()) { + if (mark instanceof Text) { + mark.getStyleClass().add(cssClass); } } } - }); + } + }); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -849,19 +905,36 @@ public void updateItem(final TradeStatistics3ListItem item, boolean empty) { } private void layout() { - UserThread.runAfter(() -> { - double available; - if (root.getParent() instanceof Pane) { - available = ((Pane) root.getParent()).getHeight(); - } else { - available = root.getHeight(); - } + double available; + if (root.getParent() instanceof Pane) { + available = ((Pane) root.getParent()).getHeight(); + } else { + available = root.getHeight(); + } + if (available == 0) { + UserThread.execute(this::layout); + return; + } + + available = available - volumeChartPane.getHeight() - toolBox.getHeight() - footer.getHeight() - 60; - available = available - volumeChartPane.getHeight() - toolBox.getHeight() - footer.getHeight() - 60; - if (priceChart.isManaged()) { - available = available - priceChartPane.getHeight(); + if (!model.showAllTradeCurrenciesProperty.get()) { + double priceChartPaneHeight = priceChartPane.getHeight(); + if (priceChartPaneHeight == 0) { + UserThread.execute(this::layout); + return; } - tableView.setPrefHeight(available); - }, 100, TimeUnit.MILLISECONDS); + available -= priceChartPaneHeight; + } else { + // If rendering is not done we get the height which is smaller than the volumeChart max Height so we + // delay to next render frame. + // Using runAfter does not work well as filling the table list and creating the chart can be a bit slow and + // its hard to estimate correct delay. + if (volumeChartPane.getHeight() < volumeChart.getMaxHeight()) { + UserThread.execute(this::layout); + return; + } + } + tableView.setPrefHeight(available); } } 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 0e67d26b025..5227a4d00d2 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 @@ -61,10 +61,10 @@ import java.time.LocalDateTime; import java.time.ZoneId; -import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; @@ -73,6 +73,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -80,6 +81,8 @@ class TradesChartsViewModel extends ActivatableViewModel { private static final int TAB_INDEX = 2; + private static final ZoneId ZONE_ID = ZoneId.systemDefault(); + /////////////////////////////////////////////////////////////////////////////////////////// // Enum @@ -104,14 +107,17 @@ public enum TickUnit { final BooleanProperty showAllTradeCurrenciesProperty = new SimpleBooleanProperty(false); private final CurrencyList currencyListItems; private final CurrencyListItem showAllCurrencyListItem = new CurrencyListItem(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, ""), -1); - final ObservableList tradeStatisticsByCurrency = FXCollections.observableArrayList(); + final ObservableList selectedTradeStatistics = FXCollections.observableArrayList(); final ObservableList> priceItems = FXCollections.observableArrayList(); final ObservableList> volumeItems = FXCollections.observableArrayList(); + final ObservableList> volumeInUsdItems = FXCollections.observableArrayList(); private Map>> itemsPerInterval; TickUnit tickUnit; final int maxTicks = 90; private int selectedTabIndex; + final Map> usdPriceMapsPerTickUnit = new HashMap<>(); + private boolean fillTradeCurrenciesOnActiavetCalled; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -126,6 +132,7 @@ public enum TickUnit { this.navigation = navigation; setChangeListener = change -> { + updateSelectedTradeStatistics(getCurrencyCode()); updateChartData(); fillTradeCurrencies(); }; @@ -142,19 +149,15 @@ public enum TickUnit { currencyListItems = new CurrencyList(this.preferences); } - private void fillTradeCurrencies() { - // Don't use a set as we need all entries - List tradeCurrencyList = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() - .flatMap(e -> CurrencyUtil.getTradeCurrency(e.getCurrency()).stream()) - .collect(Collectors.toList()); - - currencyListItems.updateWithCurrencies(tradeCurrencyList, showAllCurrencyListItem); - } - @Override protected void activate() { tradeStatisticsManager.getObservableTradeStatisticsSet().addListener(setChangeListener); - fillTradeCurrencies(); + if (!fillTradeCurrenciesOnActiavetCalled) { + fillTradeCurrencies(); + fillTradeCurrenciesOnActiavetCalled = true; + } + buildUsdPricesPerDay(); + updateSelectedTradeStatistics(getCurrencyCode()); updateChartData(); syncPriceFeedCurrency(); setMarketPriceFeedCurrency(); @@ -171,25 +174,26 @@ protected void deactivate() { void onSetTradeCurrency(TradeCurrency tradeCurrency) { if (tradeCurrency != null) { - final String code = tradeCurrency.getCode(); + String code = tradeCurrency.getCode(); if (isEditEntry(code)) { navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class); - } else { - boolean showAllEntry = isShowAllEntry(code); - showAllTradeCurrenciesProperty.set(showAllEntry); - if (!showAllEntry) { - selectedTradeCurrencyProperty.set(tradeCurrency); - } - preferences.setTradeChartsScreenCurrencyCode(code); + return; + } - updateChartData(); + boolean showAllEntry = isShowAllEntry(code); + showAllTradeCurrenciesProperty.set(showAllEntry); + if (showAllEntry) { + priceFeedService.setCurrencyCode(GlobalSettings.getDefaultTradeCurrency().getCode()); + } else { + selectedTradeCurrencyProperty.set(tradeCurrency); + priceFeedService.setCurrencyCode(code); - if (showAllEntry) - priceFeedService.setCurrencyCode(GlobalSettings.getDefaultTradeCurrency().getCode()); - else - priceFeedService.setCurrencyCode(code); } + preferences.setTradeChartsScreenCurrencyCode(code); + + updateSelectedTradeStatistics(getCurrencyCode()); + updateChartData(); } } @@ -225,6 +229,15 @@ public Optional getSelectedCurrencyListItem() { // Private /////////////////////////////////////////////////////////////////////////////////////////// + private void fillTradeCurrencies() { + // Don't use a set as we need all entries + List tradeCurrencyList = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + .flatMap(e -> CurrencyUtil.getTradeCurrency(e.getCurrency()).stream()) + .collect(Collectors.toList()); + + currencyListItems.updateWithCurrencies(tradeCurrencyList, showAllCurrencyListItem); + } + private void setMarketPriceFeedCurrency() { if (selectedTabIndex == TAB_INDEX) { if (showAllTradeCurrenciesProperty.get()) @@ -239,37 +252,81 @@ private void syncPriceFeedCurrency() { priceFeedService.setCurrencyCode(selectedTradeCurrencyProperty.get().getCode()); } - private void updateChartData() { - tradeStatisticsByCurrency.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() - .filter(e -> showAllTradeCurrenciesProperty.get() || e.getCurrency().equals(getCurrencyCode())) - .collect(Collectors.toList())); + private void buildUsdPricesPerDay() { + if (usdPriceMapsPerTickUnit.isEmpty()) { + Map>> dateMapsPerTickUnit = new HashMap<>(); + for (TickUnit tick : TickUnit.values()) { + dateMapsPerTickUnit.put(tick, new HashMap<>()); + } + + tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + .filter(e -> e.getCurrency().equals("USD")) + .forEach(tradeStatistics -> { + for (TickUnit tick : TickUnit.values()) { + long time = roundToTick(tradeStatistics.getLocalDateTime(), tick).getTime(); + Map> map = dateMapsPerTickUnit.get(tick); + map.putIfAbsent(time, new ArrayList<>()); + map.get(time).add(tradeStatistics); + } + }); + + dateMapsPerTickUnit.forEach((tick, map) -> { + HashMap priceMap = new HashMap<>(); + map.forEach((date, tradeStatisticsList) -> priceMap.put(date, getAveragePrice(tradeStatisticsList))); + usdPriceMapsPerTickUnit.put(tick, priceMap); + }); + } + } + + private long getAveragePrice(List tradeStatisticsList) { + long accumulatedAmount = 0; + long accumulatedVolume = 0; + for (TradeStatistics3 tradeStatistics : tradeStatisticsList) { + accumulatedAmount += tradeStatistics.getAmount(); + accumulatedVolume += tradeStatistics.getTradeVolume().getValue(); + } + double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT); + return MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount); + } + + private void updateChartData() { // Generate date range and create sets for all ticks itemsPerInterval = new HashMap<>(); Date time = new Date(); for (long i = maxTicks + 1; i >= 0; --i) { - Set set = new HashSet<>(); - Pair> pair = new Pair<>((Date) time.clone(), set); + Pair> pair = new Pair<>((Date) time.clone(), new HashSet<>()); itemsPerInterval.put(i, pair); + // We adjust the time for the next iteration time.setTime(time.getTime() - 1); time = roundToTick(time, tickUnit); } // Get all entries for the defined time interval - tradeStatisticsByCurrency.forEach(e -> { + selectedTradeStatistics.forEach(tradeStatistics -> { for (long i = maxTicks; i > 0; --i) { - Pair> p = itemsPerInterval.get(i); - if (e.getDate().after(p.getKey())) { - p.getValue().add(e); + Pair> pair = itemsPerInterval.get(i); + if (tradeStatistics.getDate().after(pair.getKey())) { + pair.getValue().add(tradeStatistics); break; } } }); + Map map = usdPriceMapsPerTickUnit.get(tickUnit); + AtomicLong averageUsdPrice = new AtomicLong(0); + // create CandleData for defined time interval List candleDataList = itemsPerInterval.entrySet().stream() .filter(entry -> entry.getKey() >= 0 && !entry.getValue().getValue().isEmpty()) - .map(entry -> getCandleData(entry.getKey(), entry.getValue().getValue())) + .map(entry -> { + long tickStartDate = entry.getValue().getKey().getTime(); + // If we don't have a price we take the previous one + if (map.containsKey(tickStartDate)) { + averageUsdPrice.set(map.get(tickStartDate)); + } + return getCandleData(entry.getKey(), entry.getValue().getValue(), averageUsdPrice.get()); + }) .sorted(Comparator.comparingLong(o -> o.tick)) .collect(Collectors.toList()); @@ -278,12 +335,22 @@ private void updateChartData() { .collect(Collectors.toList())); volumeItems.setAll(candleDataList.stream() - .map(e -> new XYChart.Data(e.tick, e.accumulatedAmount, e)) + .map(candleData -> new XYChart.Data(candleData.tick, candleData.accumulatedAmount, candleData)) + .collect(Collectors.toList())); + + volumeInUsdItems.setAll(candleDataList.stream() + .map(candleData -> new XYChart.Data(candleData.tick, candleData.volumeInUsd, candleData)) + .collect(Collectors.toList())); + } + + private void updateSelectedTradeStatistics(String currencyCode) { + selectedTradeStatistics.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + .filter(e -> showAllTradeCurrenciesProperty.get() || e.getCurrency().equals(currencyCode)) .collect(Collectors.toList())); } @VisibleForTesting - CandleData getCandleData(long tick, Set set) { + CandleData getCandleData(long tick, Set set, long averageUsdPrice) { long open = 0; long close = 0; long high = 0; @@ -291,8 +358,7 @@ CandleData getCandleData(long tick, Set set) { long accumulatedVolume = 0; long accumulatedAmount = 0; long numTrades = set.size(); - ObservableList tradePrices = FXCollections.observableArrayList(); - + List tradePrices = new ArrayList<>(); for (TradeStatistics3 item : set) { long tradePriceAsLong = item.getTradePrice().getValue(); // Previously a check was done which inverted the low and high for cryptocurrencies. @@ -301,16 +367,15 @@ CandleData getCandleData(long tick, Set set) { accumulatedVolume += item.getTradeVolume().getValue(); accumulatedAmount += item.getTradeAmount().getValue(); - tradePrices.add(item.getTradePrice().getValue()); + tradePrices.add(tradePriceAsLong); } - FXCollections.sort(tradePrices); + Collections.sort(tradePrices); List list = new ArrayList<>(set); - ObservableList obsList = FXCollections.observableArrayList(list); - obsList.sort(Comparator.comparingLong(TradeStatistics3::getDateAsLong)); - if (obsList.size() > 0) { - open = obsList.get(0).getTradePrice().getValue(); - close = obsList.get(obsList.size() - 1).getTradePrice().getValue(); + list.sort(Comparator.comparingLong(TradeStatistics3::getDateAsLong)); + if (list.size() > 0) { + open = list.get(0).getTradePrice().getValue(); + close = list.get(list.size() - 1).getTradePrice().getValue(); } long averagePrice; @@ -328,36 +393,43 @@ CandleData getCandleData(long tick, Set set) { averagePrice = MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount); } - final Date dateFrom = new Date(getTimeFromTickIndex(tick)); - final Date dateTo = new Date(getTimeFromTickIndex(tick + 1)); + Date dateFrom = new Date(getTimeFromTickIndex(tick)); + Date dateTo = new Date(getTimeFromTickIndex(tick + 1)); String dateString = tickUnit.ordinal() > TickUnit.DAY.ordinal() ? DisplayUtils.formatDateTimeSpan(dateFrom, dateTo) : DisplayUtils.formatDate(dateFrom) + " - " + DisplayUtils.formatDate(dateTo); + + // We do not need precision, so we scale down before multiplication otherwise we could get an overflow. + averageUsdPrice = (long) MathUtils.scaleDownByPowerOf10((double) averageUsdPrice, 4); + long volumeInUsd = averageUsdPrice * (long) MathUtils.scaleDownByPowerOf10((double) accumulatedAmount, 4); + // We store USD value without decimals as its only total volume, no precision is needed. + volumeInUsd = (long) MathUtils.scaleDownByPowerOf10((double) volumeInUsd, 4); return new CandleData(tick, open, close, high, low, averagePrice, medianPrice, accumulatedAmount, accumulatedVolume, - numTrades, isBullish, dateString); + numTrades, isBullish, dateString, volumeInUsd); } Date roundToTick(Date time, TickUnit tickUnit) { - ZonedDateTime zdt = time.toInstant().atZone(ZoneId.systemDefault()); - LocalDateTime tradeLocal = zdt.toLocalDateTime(); + return roundToTick(time.toInstant().atZone(ZONE_ID).toLocalDateTime(), tickUnit); + } + Date roundToTick(LocalDateTime localDate, TickUnit tickUnit) { switch (tickUnit) { case YEAR: - return Date.from(tradeLocal.withMonth(1).withDayOfYear(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZoneId.systemDefault()).toInstant()); + return Date.from(localDate.withMonth(1).withDayOfYear(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); case MONTH: - return Date.from(tradeLocal.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZoneId.systemDefault()).toInstant()); + return Date.from(localDate.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); case WEEK: - int dayOfWeek = tradeLocal.getDayOfWeek().getValue(); - LocalDateTime firstDayOfWeek = ChronoUnit.DAYS.addTo(tradeLocal, 1 - dayOfWeek); - return Date.from(firstDayOfWeek.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZoneId.systemDefault()).toInstant()); + int dayOfWeek = localDate.getDayOfWeek().getValue(); + LocalDateTime firstDayOfWeek = ChronoUnit.DAYS.addTo(localDate, 1 - dayOfWeek); + return Date.from(firstDayOfWeek.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); case DAY: - return Date.from(tradeLocal.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZoneId.systemDefault()).toInstant()); + return Date.from(localDate.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); case HOUR: - return Date.from(tradeLocal.withMinute(0).withSecond(0).withNano(0).atZone(ZoneId.systemDefault()).toInstant()); + return Date.from(localDate.withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); case MINUTE_10: - return Date.from(tradeLocal.withMinute(tradeLocal.getMinute() - tradeLocal.getMinute() % 10).withSecond(0).withNano(0).atZone(ZoneId.systemDefault()).toInstant()); + return Date.from(localDate.withMinute(localDate.getMinute() - localDate.getMinute() % 10).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); default: - return Date.from(tradeLocal.atZone(ZoneId.systemDefault()).toInstant()); + return Date.from(localDate.atZone(ZONE_ID).toInstant()); } } diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/CandleData.java b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/CandleData.java index 328d8a51397..59fc321b19d 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/CandleData.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/CandleData.java @@ -30,10 +30,11 @@ public class CandleData { public final long numTrades; public final boolean isBullish; public final String date; + public final long volumeInUsd; public CandleData(long tick, long open, long close, long high, long low, long average, long median, long accumulatedAmount, long accumulatedVolume, long numTrades, - boolean isBullish, String date) { + boolean isBullish, String date, long volumeInUsd) { this.tick = tick; this.open = open; this.close = close; @@ -46,5 +47,6 @@ public CandleData(long tick, long open, long close, long high, long low, long av this.numTrades = numTrades; this.isBullish = isBullish; this.date = date; + this.volumeInUsd = volumeInUsd; } } diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/Candle.java b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/Candle.java index 394a5a4d23f..bad1ed721fb 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/Candle.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/Candle.java @@ -58,15 +58,10 @@ import javafx.util.StringConverter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * Candle node used for drawing a candle */ public class Candle extends Group { - private static final Logger log = LoggerFactory.getLogger(Candle.class); - private String seriesStyleClass; private String dataStyleClass; private final CandleTooltip candleTooltip; diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/CandleStickChart.java b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/CandleStickChart.java index 2403ac4fc7f..993bb93992b 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/CandleStickChart.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/CandleStickChart.java @@ -108,14 +108,14 @@ protected void layoutPlotChildren() { // update candle positions for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) { XYChart.Series series = getData().get(seriesIndex); - Iterator> iter = getDisplayedDataIterator(series); + Iterator> iterator = getDisplayedDataIterator(series); Path seriesPath = null; if (series.getNode() instanceof Path) { seriesPath = (Path) series.getNode(); seriesPath.getElements().clear(); } - while (iter.hasNext()) { - XYChart.Data item = iter.next(); + while (iterator.hasNext()) { + XYChart.Data item = iterator.next(); double x = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item)); double y = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item)); Node itemNode = item.getNode(); diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java index 41d45dab767..b6637cce7c8 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java @@ -18,6 +18,7 @@ package bisq.desktop.main.market.trades.charts.volume; import bisq.desktop.main.market.trades.charts.CandleData; +import bisq.desktop.util.DisplayUtils; import bisq.core.locale.Res; @@ -27,12 +28,7 @@ import javafx.util.StringConverter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public class VolumeBar extends Group { - private static final Logger log = LoggerFactory.getLogger(VolumeBar.class); - private String seriesStyleClass; private String dataStyleClass; private final StringConverter volumeStringConverter; @@ -60,8 +56,9 @@ public void setSeriesAndDataStyleClasses(String seriesStyleClass, String dataSty public void update(double height, double candleWidth, CandleData candleData) { bar.resizeRelocate(-candleWidth / 2, 0, candleWidth, height); - String vol = volumeStringConverter.toString(candleData.accumulatedAmount); - tooltip.setText(Res.get("market.trades.tooltip.volumeBar", vol, candleData.numTrades, candleData.date)); + String volumeInBtc = volumeStringConverter.toString(candleData.accumulatedAmount); + String volumeInUsd = DisplayUtils.formatLargeFiat(candleData.volumeInUsd, "USD"); + tooltip.setText(Res.get("market.trades.tooltip.volumeBar", volumeInBtc, volumeInUsd, candleData.numTrades, candleData.date)); } private void updateStyleClasses() { diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeChart.java b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeChart.java index d5a0f456da4..581fbadf0f1 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeChart.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeChart.java @@ -53,9 +53,9 @@ protected void layoutPlotChildren() { } for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) { XYChart.Series series = getData().get(seriesIndex); - Iterator> iter = getDisplayedDataIterator(series); - while (iter.hasNext()) { - XYChart.Data item = iter.next(); + Iterator> iterator = getDisplayedDataIterator(series); + while (iterator.hasNext()) { + XYChart.Data item = iterator.next(); double x = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item)); double y = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item)); Node itemNode = item.getNode(); diff --git a/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java b/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java index f8d144e6fa9..21ff4f1f8ba 100644 --- a/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java +++ b/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java @@ -21,12 +21,15 @@ import org.apache.commons.lang3.time.DurationFormatUtils; import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Date; +import java.util.Locale; import java.util.Optional; import lombok.extern.slf4j.Slf4j; @@ -106,6 +109,24 @@ public static String formatVolume(Offer offer, Boolean decimalAligned, int maxNu return formattedVolume; } + public static String formatLargeFiat(double value, String currency) { + if (value <= 0) { + return "0"; + } + NumberFormat numberFormat = DecimalFormat.getInstance(Locale.US); + numberFormat.setGroupingUsed(true); + return numberFormat.format(value) + " " + currency; + } + + public static String formatLargeFiatWithUnitPostFix(double value, String currency) { + if (value <= 0) { + return "0"; + } + String[] units = new String[]{"", "K", "M", "B"}; + int digitGroups = (int) (Math.log10(value) / Math.log10(1000)); + return new DecimalFormat("#,##0.###").format(value / Math.pow(1000, digitGroups)) + units[digitGroups] + " " + currency; + } + public static String formatVolume(Volume volume) { return formatVolume(volume, FIAT_VOLUME_FORMAT, false); } diff --git a/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java index 8d3a7d25660..470c86b853b 100644 --- a/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java @@ -167,7 +167,7 @@ public void testGetCandleData() { null, null)); - CandleData candleData = model.getCandleData(model.roundToTick(now, TradesChartsViewModel.TickUnit.DAY).getTime(), set); + CandleData candleData = model.getCandleData(model.roundToTick(now, TradesChartsViewModel.TickUnit.DAY).getTime(), set, 0); assertEquals(open, candleData.open); assertEquals(close, candleData.close); assertEquals(high, candleData.high); diff --git a/seednode/src/main/java/bisq/seednode/SeedNodeMain.java b/seednode/src/main/java/bisq/seednode/SeedNodeMain.java index 25f9a334f93..99422c11e4e 100644 --- a/seednode/src/main/java/bisq/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/bisq/seednode/SeedNodeMain.java @@ -21,6 +21,9 @@ import bisq.core.app.misc.ExecutableForAppWithP2p; import bisq.core.app.misc.ModuleForAppWithP2p; import bisq.core.dao.state.DaoStateSnapshotService; +import bisq.core.user.Cookie; +import bisq.core.user.CookieKey; +import bisq.core.user.User; import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PServiceListener; @@ -112,6 +115,16 @@ protected void applyInjector() { @Override protected void startApplication() { + Cookie cookie = injector.getInstance(User.class).getCookie(); + cookie.getAsOptionalBoolean(CookieKey.CLEAN_TOR_DIR_AT_RESTART).ifPresent(wasCleanTorDirSet -> { + if (wasCleanTorDirSet) { + injector.getInstance(TorSetup.class).cleanupTorFiles(() -> { + log.info("Tor directory reset"); + cookie.remove(CookieKey.CLEAN_TOR_DIR_AT_RESTART); + }, log::error); + } + }); + seedNode.startApplication(); injector.getInstance(P2PService.class).addP2PServiceListener(new P2PServiceListener() { @@ -175,11 +188,10 @@ private void setupConnectionLossCheck() { checkConnectionLossTime = UserThread.runPeriodically(() -> { if (injector.getInstance(PeerManager.class).getNumAllConnectionsLostEvents() > 1) { - // Removing cache files help in case the node got flagged from Tor's dos protection - injector.getInstance(TorSetup.class).cleanupTorFiles(() -> { - log.info("Tor directory reset"); - shutDown(this); - }, log::error); + // We set a flag to clear tor cache files at re-start. We cannot clear it now as Tor is used and + // that can cause problems. + injector.getInstance(User.class).getCookie().putAsBoolean(CookieKey.CLEAN_TOR_DIR_AT_RESTART, true); + shutDown(this); } }, CHECK_CONNECTION_LOSS_SEC);