From 8bfe9b82c8315aa428b39c23084bf5257795f2b7 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sun, 25 Oct 2020 18:34:22 -0500 Subject: [PATCH] Remove outliers when calculating BSQ rate Add percentage for upper and lower threshold for outlier detection. --- .../main/java/bisq/core/user/Preferences.java | 7 +++ .../bisq/core/user/PreferencesPayload.java | 7 ++- .../resources/i18n/displayStrings.properties | 1 + .../economy/dashboard/BsqDashboardView.java | 46 ++++++++++++---- .../settings/preferences/PreferencesView.java | 53 +++++++++++++++---- .../bisq/desktop/util/AxisInlierUtils.java | 2 +- proto/src/main/proto/pb.proto | 1 + 7 files changed, 95 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index 656f056522a..c827d51680a 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -416,6 +416,11 @@ public void setTacAcceptedV120(boolean tacAccepted) { requestPersistence(); } + public void setBsqAverageTrimThreshold(double bsqAverageTrimThreshold) { + prefPayload.setBsqAverageTrimThreshold(bsqAverageTrimThreshold); + requestPersistence(); + } + public Optional findAutoConfirmSettings(String currencyCode) { return prefPayload.getAutoConfirmSettingsList().stream() .filter(e -> e.getCurrencyCode().equals(currencyCode)) @@ -1034,6 +1039,8 @@ private interface ExcludesDelegateMethods { void setTacAcceptedV120(boolean tacAccepted); + void setBsqAverageTrimThreshold(double bsqAverageTrimThreshold); + void setAutoConfirmSettings(AutoConfirmSettings autoConfirmSettings); } } diff --git a/core/src/main/java/bisq/core/user/PreferencesPayload.java b/core/src/main/java/bisq/core/user/PreferencesPayload.java index d84862e9132..9c919185df7 100644 --- a/core/src/main/java/bisq/core/user/PreferencesPayload.java +++ b/core/src/main/java/bisq/core/user/PreferencesPayload.java @@ -124,6 +124,7 @@ public final class PreferencesPayload implements PersistableEnvelope { private double buyerSecurityDepositAsPercentForCrypto = getDefaultBuyerSecurityDepositAsPercent(); private int blockNotifyPort; private boolean tacAcceptedV120; + private double bsqAverageTrimThreshold = 0.05; // Added at 1.3.8 private List autoConfirmSettingsList = new ArrayList<>(); @@ -188,9 +189,10 @@ public Message toProtoMessage() { .setBuyerSecurityDepositAsPercentForCrypto(buyerSecurityDepositAsPercentForCrypto) .setBlockNotifyPort(blockNotifyPort) .setTacAcceptedV120(tacAcceptedV120) + .setBsqAverageTrimThreshold(bsqAverageTrimThreshold) .addAllAutoConfirmSettings(autoConfirmSettingsList.stream() - .map(autoConfirmSettings -> ((protobuf.AutoConfirmSettings) autoConfirmSettings.toProtoMessage())) - .collect(Collectors.toList())); + .map(autoConfirmSettings -> ((protobuf.AutoConfirmSettings) autoConfirmSettings.toProtoMessage())) + .collect(Collectors.toList())); Optional.ofNullable(backupDirectory).ifPresent(builder::setBackupDirectory); Optional.ofNullable(preferredTradeCurrency).ifPresent(e -> builder.setPreferredTradeCurrency((protobuf.TradeCurrency) e.toProtoMessage())); @@ -280,6 +282,7 @@ public static PreferencesPayload fromProto(protobuf.PreferencesPayload proto, Co proto.getBuyerSecurityDepositAsPercentForCrypto(), proto.getBlockNotifyPort(), proto.getTacAcceptedV120(), + proto.getBsqAverageTrimThreshold(), proto.getAutoConfirmSettingsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAutoConfirmSettingsList().stream() .map(AutoConfirmSettings::fromProto) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 9c3e4fc792c..78aaccff44f 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1184,6 +1184,7 @@ setting.preferences.general=General preferences setting.preferences.explorer=Bitcoin Explorer setting.preferences.explorer.bsq=Bisq Explorer setting.preferences.deviation=Max. deviation from market price +setting.preferences.bsqAverageTrimThreshold=Outlier threshold for BSQ rate setting.preferences.avoidStandbyMode=Avoid standby mode setting.preferences.autoConfirmXMR=XMR auto-confirm setting.preferences.autoConfirmEnabled=Enabled 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 4e5bea571ca..1838688b839 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,6 +20,7 @@ import bisq.desktop.common.view.ActivatableView; import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.TextFieldWithIcon; +import bisq.desktop.util.AxisInlierUtils; import bisq.core.dao.DaoFacade; import bisq.core.dao.state.DaoStateListener; @@ -112,8 +113,10 @@ public class BsqDashboardView extends ActivatableView implements private Label marketPriceLabel; private Coin availableAmount; - private int gridRow = 0; + double percentToTrim = 5; + double howManyStdDevsConstituteOutlier = 10; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -121,10 +124,10 @@ public class BsqDashboardView extends ActivatableView implements @Inject public BsqDashboardView(DaoFacade daoFacade, - TradeStatisticsManager tradeStatisticsManager, - PriceFeedService priceFeedService, - Preferences preferences, - BsqFormatter bsqFormatter) { + TradeStatisticsManager tradeStatisticsManager, + PriceFeedService priceFeedService, + Preferences preferences, + BsqFormatter bsqFormatter) { this.daoFacade = daoFacade; this.tradeStatisticsManager = tradeStatisticsManager; this.priceFeedService = priceFeedService; @@ -134,7 +137,6 @@ public BsqDashboardView(DaoFacade daoFacade, @Override public void initialize() { - ADJUSTERS.put(DAY, TemporalAdjusters.ofDateAdjuster(d -> d)); createKPIs(); @@ -368,15 +370,24 @@ private void updateAveragePriceFields(TextField field90, TextFieldWithIcon field } private long updateAveragePriceField(TextField textField, int days, boolean isUSDField) { + double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100)); Date pastXDays = getPastDate(days); - List bsqTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + List bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() .filter(e -> e.getCurrency().equals("BSQ")) .filter(e -> e.getDate().after(pastXDays)) .collect(Collectors.toList()); - List usdTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + List bsqTradePastXDays = percentToTrim > 0 ? + removeOutliers(bsqAllTradePastXDays, percentToTrim) : + bsqAllTradePastXDays; + + List usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() .filter(e -> e.getCurrency().equals("USD")) .filter(e -> e.getDate().after(pastXDays)) .collect(Collectors.toList()); + List usdTradePastXDays = percentToTrim > 0 ? + removeOutliers(usdAllTradePastXDays, percentToTrim) : + usdAllTradePastXDays; + long average = isUSDField ? getUSDAverage(bsqTradePastXDays, usdTradePastXDays) : getBTCAverage(bsqTradePastXDays); Price avgPrice = isUSDField ? Price.valueOf("USD", average) : @@ -390,11 +401,26 @@ private long updateAveragePriceField(TextField textField, int days, boolean isUS return average; } - private long getBTCAverage(List bsqList) { + private List removeOutliers(List list, double percentToTrim) { + List yValues = list.stream() + .filter(TradeStatistics3::isValid) + .map(e -> (double) e.getPrice()) + .collect(Collectors.toList()); + + Tuple2 tuple = AxisInlierUtils.findInlierRange(yValues, percentToTrim, howManyStdDevsConstituteOutlier); + double lowerBound = tuple.first; + double upperBound = tuple.second; + return list.stream() + .filter(e -> e.getPrice() > lowerBound) + .filter(e -> e.getPrice() < upperBound) + .collect(Collectors.toList()); + } + + private long getBTCAverage(List list) { long accumulatedVolume = 0; long accumulatedAmount = 0; - for (TradeStatistics3 item : bsqList) { + for (TradeStatistics3 item : list) { accumulatedVolume += item.getTradeVolume().getValue(); accumulatedAmount += item.getTradeAmount().getValue(); // Amount of BTC traded } diff --git a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java index ef8c6951096..59e0d7f23aa 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java @@ -148,11 +148,11 @@ public class PreferencesView extends ActivatableViewAndModel cryptoCurrencies; private ObservableList allCryptoCurrencies; private ObservableList tradeCurrencies; - private InputTextField deviationInputTextField; - private ChangeListener deviationListener, ignoreTradersListListener, ignoreDustThresholdListener, + private InputTextField deviationInputTextField, bsqAverageTrimThresholdTextField; + private ChangeListener deviationListener, bsqAverageTrimThresholdListener, ignoreTradersListListener, ignoreDustThresholdListener, rpcUserListener, rpcPwListener, blockNotifyPortListener, autoConfTradeLimitListener, autoConfServiceAddressListener; - private ChangeListener deviationFocusedListener; + private ChangeListener deviationFocusedListener, bsqAverageTrimThresholdFocusedListener; private ChangeListener useCustomFeeCheckboxListener; private ChangeListener transactionFeeChangeListener; private final boolean daoOptionsSet; @@ -318,7 +318,6 @@ private void initializeGeneralOptions() { // deviation deviationInputTextField = addInputTextField(root, ++gridRow, Res.get("setting.preferences.deviation")); - deviationListener = (observable, oldValue, newValue) -> { try { double value = ParsingUtils.parsePercentStringToDouble(newValue); @@ -327,16 +326,16 @@ private void initializeGeneralOptions() { preferences.setMaxPriceDistanceInPercent(value); } else { new Popup().warning(Res.get("setting.preferences.deviationToLarge", maxDeviation * 100)).show(); - UserThread.runAfter(() -> deviationInputTextField.setText(FormattingUtils.formatPercentagePrice(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); + UserThread.runAfter(() -> deviationInputTextField.setText(FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); } } catch (NumberFormatException t) { log.error("Exception at parseDouble deviation: " + t.toString()); - UserThread.runAfter(() -> deviationInputTextField.setText(FormattingUtils.formatPercentagePrice(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); + UserThread.runAfter(() -> deviationInputTextField.setText(FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); } }; deviationFocusedListener = (observable1, oldValue1, newValue1) -> { if (oldValue1 && !newValue1) - UserThread.runAfter(() -> deviationInputTextField.setText(FormattingUtils.formatPercentagePrice(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); + UserThread.runAfter(() -> deviationInputTextField.setText(FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); }; // ignoreTraders @@ -617,7 +616,7 @@ private void initializeDisplayOptions() { } private void initializeDaoOptions() { - daoOptionsTitledGroupBg = addTitledGroupBg(root, ++gridRow, 3, Res.get("setting.preferences.daoOptions"), Layout.GROUP_DISTANCE); + daoOptionsTitledGroupBg = addTitledGroupBg(root, ++gridRow, 4, Res.get("setting.preferences.daoOptions"), Layout.GROUP_DISTANCE); resyncDaoFromResourcesButton = addButton(root, gridRow, Res.get("setting.preferences.dao.resyncFromResources.label"), Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); resyncDaoFromResourcesButton.setMaxWidth(Double.MAX_VALUE); GridPane.setHgrow(resyncDaoFromResourcesButton, Priority.ALWAYS); @@ -626,6 +625,36 @@ private void initializeDaoOptions() { resyncDaoFromGenesisButton.setMaxWidth(Double.MAX_VALUE); GridPane.setHgrow(resyncDaoFromGenesisButton, Priority.ALWAYS); + bsqAverageTrimThresholdTextField = addInputTextField(root, ++gridRow, + Res.get("setting.preferences.bsqAverageTrimThreshold")); + bsqAverageTrimThresholdTextField.setText(FormattingUtils.formatToPercentWithSymbol(preferences.getBsqAverageTrimThreshold())); + + bsqAverageTrimThresholdListener = (observable, oldValue, newValue) -> { + try { + double value = ParsingUtils.parsePercentStringToDouble(newValue); + double maxValue = 0.49; + checkArgument(value >= 0, "Input must be positive"); + if (value <= maxValue) { + preferences.setBsqAverageTrimThreshold(value); + } else { + new Popup().warning(Res.get("setting.preferences.deviationToLarge", + maxValue * 100)).show(); + UserThread.runAfter(() -> bsqAverageTrimThresholdTextField.setText(FormattingUtils.formatToPercentWithSymbol( + preferences.getBsqAverageTrimThreshold())), 100, TimeUnit.MILLISECONDS); + } + } catch (NumberFormatException t) { + log.error("Exception: " + t.toString()); + UserThread.runAfter(() -> bsqAverageTrimThresholdTextField.setText(FormattingUtils.formatToPercentWithSymbol( + preferences.getBsqAverageTrimThreshold())), 100, TimeUnit.MILLISECONDS); + } + }; + bsqAverageTrimThresholdFocusedListener = (observable1, oldValue1, newValue1) -> { + if (oldValue1 && !newValue1) + UserThread.runAfter(() -> bsqAverageTrimThresholdTextField.setText(FormattingUtils.formatToPercentWithSymbol( + preferences.getBsqAverageTrimThreshold())), 100, TimeUnit.MILLISECONDS); + }; + + isDaoFullNodeToggleButton = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.dao.isDaoFullNode")); rpcUserTextField = addInputTextField(root, ++gridRow, Res.get("setting.preferences.dao.rpcUser")); rpcUserTextField.setVisible(false); @@ -861,7 +890,7 @@ public BlockChainExplorer fromString(String string) { }); bsqBlockChainExplorerComboBox.setOnAction(e -> preferences.setBsqBlockChainExplorer(bsqBlockChainExplorerComboBox.getSelectionModel().getSelectedItem())); - deviationInputTextField.setText(FormattingUtils.formatPercentagePrice(preferences.getMaxPriceDistanceInPercent())); + deviationInputTextField.setText(FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent())); deviationInputTextField.textProperty().addListener(deviationListener); deviationInputTextField.focusedProperty().addListener(deviationFocusedListener); @@ -950,6 +979,10 @@ private void activateDisplayPreferences() { private void activateDaoPreferences() { boolean daoFullNode = preferences.isDaoFullNode(); isDaoFullNodeToggleButton.setSelected(daoFullNode); + + bsqAverageTrimThresholdTextField.textProperty().addListener(bsqAverageTrimThresholdListener); + bsqAverageTrimThresholdTextField.focusedProperty().addListener(bsqAverageTrimThresholdFocusedListener); + String rpcUser = preferences.getRpcUser(); String rpcPw = preferences.getRpcPw(); int blockNotifyPort = preferences.getBlockNotifyPort(); @@ -1091,6 +1124,8 @@ private void deactivateDisplayPreferences() { private void deactivateDaoPreferences() { resyncDaoFromResourcesButton.setOnAction(null); resyncDaoFromGenesisButton.setOnAction(null); + bsqAverageTrimThresholdTextField.textProperty().removeListener(bsqAverageTrimThresholdListener); + bsqAverageTrimThresholdTextField.focusedProperty().removeListener(bsqAverageTrimThresholdFocusedListener); isDaoFullNodeToggleButton.setOnAction(null); rpcUserTextField.textProperty().removeListener(rpcUserListener); rpcPwTextField.textProperty().removeListener(rpcPwListener); diff --git a/desktop/src/main/java/bisq/desktop/util/AxisInlierUtils.java b/desktop/src/main/java/bisq/desktop/util/AxisInlierUtils.java index 9551ae5fd4e..d80d79a009b 100644 --- a/desktop/src/main/java/bisq/desktop/util/AxisInlierUtils.java +++ b/desktop/src/main/java/bisq/desktop/util/AxisInlierUtils.java @@ -91,7 +91,7 @@ private static List extractYValues(ObservableList findInlierRange( + public static Tuple2 findInlierRange( List yValues, double percentToTrim, double howManyStdDevsConstituteOutlier diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 199918b8f47..ed1b326c7da 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1576,6 +1576,7 @@ message PreferencesPayload { int32 css_theme = 54; bool tac_accepted_v120 = 55; repeated AutoConfirmSettings auto_confirm_settings = 56; + double bsq_average_trim_threshold = 57; } message AutoConfirmSettings {