Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deactivate open offer if trigger price is reached #5001

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions desktop/src/main/java/bisq/desktop/main/PriceUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I instinctively need to ask if PriceUtil could be moved to core. I see dependencies on ChatView.log and ui validators, but much of the util logic could be made available to the api.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I was thinking the same... was just to lazy to separate pure domain code with formatting stuff which is using desktop scope classes... But yes we should refactor that.

* 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;

import bisq.core.locale.CurrencyUtil;
import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price;
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.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.core.util.AveragePriceUtil;

import bisq.common.util.MathUtils;

import org.bitcoinj.utils.Fiat;

import javax.inject.Inject;
import javax.inject.Singleton;

import java.util.Optional;

import javax.annotation.Nullable;

import static bisq.desktop.main.shared.ChatView.log;
import static com.google.common.base.Preconditions.checkNotNull;

@Singleton
public class PriceUtil {
private final PriceFeedService priceFeedService;
private final TradeStatisticsManager tradeStatisticsManager;
private final Preferences preferences;
@Nullable
private Price bsq30DayAveragePrice;

@Inject
public PriceUtil(PriceFeedService priceFeedService,
TradeStatisticsManager tradeStatisticsManager,
Preferences preferences) {
this.priceFeedService = priceFeedService;
this.tradeStatisticsManager = tradeStatisticsManager;
this.preferences = preferences;
}

public void recalculateBsq30DayAveragePrice() {
bsq30DayAveragePrice = null;
bsq30DayAveragePrice = getBsq30DayAveragePrice();
}

public Price getBsq30DayAveragePrice() {
if (bsq30DayAveragePrice == null) {
bsq30DayAveragePrice = AveragePriceUtil.getAveragePriceTuple(preferences,
tradeStatisticsManager, 30).second;
}
return bsq30DayAveragePrice;
}

public boolean hasMarketPrice(Offer offer) {
String currencyCode = offer.getCurrencyCode();
checkNotNull(priceFeedService, "priceFeed must not be null");
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
Price price = offer.getPrice();
return price != null && marketPrice != null && marketPrice.isRecentExternalPriceAvailable();
}

public Optional<Double> getMarketBasedPrice(Offer offer,
OfferPayload.Direction direction) {
if (offer.isUseMarketBasedPrice()) {
return Optional.of(offer.getMarketPriceMargin());
}

if (!hasMarketPrice(offer)) {
if (offer.getCurrencyCode().equals("BSQ")) {
Price bsq30DayAveragePrice = getBsq30DayAveragePrice();
if (bsq30DayAveragePrice.isPositive()) {
double scaled = MathUtils.scaleDownByPowerOf10(bsq30DayAveragePrice.getValue(), 8);
return calculatePercentage(offer, scaled, direction);
} else {
return Optional.empty();
}
} else {
log.trace("We don't have a market price. " +
"That case could only happen if you don't have a price feed.");
return Optional.empty();
}
}

String currencyCode = offer.getCurrencyCode();
checkNotNull(priceFeedService, "priceFeed must not be null");
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
double marketPriceAsDouble = checkNotNull(marketPrice).getPrice();
return calculatePercentage(offer, marketPriceAsDouble, direction);
}

public Optional<Double> calculatePercentage(Offer offer,
double marketPrice,
OfferPayload.Direction direction) {
// If the offer did not use % price we calculate % from current market price
String currencyCode = offer.getCurrencyCode();
Price price = offer.getPrice();
int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ?
Altcoin.SMALLEST_UNIT_EXPONENT :
Fiat.SMALLEST_UNIT_EXPONENT;
long priceAsLong = checkNotNull(price).getValue();
double scaled = MathUtils.scaleDownByPowerOf10(priceAsLong, precision);
double value;
if (direction == OfferPayload.Direction.SELL) {
if (CurrencyUtil.isFiatCurrency(currencyCode)) {
if (marketPrice == 0) {
return Optional.empty();
}
value = 1 - scaled / marketPrice;
} else {
if (marketPrice == 1) {
return Optional.empty();
}
value = scaled / marketPrice - 1;
}
} else {
if (CurrencyUtil.isFiatCurrency(currencyCode)) {
if (marketPrice == 1) {
return Optional.empty();
}
value = scaled / marketPrice - 1;
} else {
if (marketPrice == 0) {
return Optional.empty();
}
value = 1 - scaled / marketPrice;
}
}
return Optional.of(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import bisq.desktop.Navigation;
import bisq.desktop.common.model.ActivatableViewModel;
import bisq.desktop.main.MainView;
import bisq.desktop.main.PriceUtil;
import bisq.desktop.main.settings.SettingsView;
import bisq.desktop.main.settings.preferences.PreferencesView;
import bisq.desktop.util.DisplayUtils;
Expand All @@ -35,7 +36,6 @@
import bisq.core.locale.GlobalSettings;
import bisq.core.locale.Res;
import bisq.core.locale.TradeCurrency;
import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price;
import bisq.core.monetary.Volume;
import bisq.core.offer.Offer;
Expand All @@ -44,14 +44,11 @@
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.PaymentAccountUtil;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService;
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.user.User;
import bisq.core.util.AveragePriceUtil;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.BsqFormatter;
import bisq.core.util.coin.CoinFormatter;
Expand All @@ -62,10 +59,8 @@
import bisq.common.app.Version;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import bisq.common.util.MathUtils;

import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.Fiat;

import com.google.inject.Inject;

Expand Down Expand Up @@ -96,10 +91,6 @@

import lombok.extern.slf4j.Slf4j;

import javax.annotation.Nullable;

import static com.google.common.base.Preconditions.checkNotNull;

@Slf4j
class OfferBookViewModel extends ActivatableViewModel {
private final OpenOfferManager openOfferManager;
Expand All @@ -113,7 +104,7 @@ class OfferBookViewModel extends ActivatableViewModel {
private final FilterManager filterManager;
final AccountAgeWitnessService accountAgeWitnessService;
private final Navigation navigation;
private final TradeStatisticsManager tradeStatisticsManager;
private final PriceUtil priceUtil;
private final CoinFormatter btcFormatter;
private final BsqFormatter bsqFormatter;

Expand All @@ -139,8 +130,6 @@ class OfferBookViewModel extends ActivatableViewModel {
final IntegerProperty maxPlacesForPrice = new SimpleIntegerProperty();
final IntegerProperty maxPlacesForMarketPriceMargin = new SimpleIntegerProperty();
boolean showAllPaymentMethods = true;
@Nullable
private Price bsq30DayAveragePrice;


///////////////////////////////////////////////////////////////////////////////////////////
Expand All @@ -159,7 +148,7 @@ public OfferBookViewModel(User user,
FilterManager filterManager,
AccountAgeWitnessService accountAgeWitnessService,
Navigation navigation,
TradeStatisticsManager tradeStatisticsManager,
PriceUtil priceUtil,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
BsqFormatter bsqFormatter) {
super();
Expand All @@ -175,7 +164,7 @@ public OfferBookViewModel(User user,
this.filterManager = filterManager;
this.accountAgeWitnessService = accountAgeWitnessService;
this.navigation = navigation;
this.tradeStatisticsManager = tradeStatisticsManager;
this.priceUtil = priceUtil;
this.btcFormatter = btcFormatter;
this.bsqFormatter = bsqFormatter;

Expand Down Expand Up @@ -239,12 +228,7 @@ protected void activate() {
applyFilterPredicate();
setMarketPriceFeedCurrency();

// Null check needed for tests passing null for tradeStatisticsManager
if (tradeStatisticsManager != null) {
bsq30DayAveragePrice = AveragePriceUtil.getAveragePriceTuple(preferences,
tradeStatisticsManager,
30).second;
}
priceUtil.recalculateBsq30DayAveragePrice();
}

@Override
Expand Down Expand Up @@ -390,77 +374,7 @@ String getPriceAsPercentage(OfferBookListItem item) {
}

public Optional<Double> getMarketBasedPrice(Offer offer) {
if (offer.isUseMarketBasedPrice()) {
return Optional.of(offer.getMarketPriceMargin());
}

if (!hasMarketPrice(offer)) {
if (offer.getCurrencyCode().equals("BSQ")) {
if (bsq30DayAveragePrice != null && bsq30DayAveragePrice.isPositive()) {
double scaled = MathUtils.scaleDownByPowerOf10(bsq30DayAveragePrice.getValue(), 8);
return calculatePercentage(offer, scaled);
} else {
return Optional.empty();
}
} else {
log.trace("We don't have a market price. " +
"That case could only happen if you don't have a price feed.");
return Optional.empty();
}
}

String currencyCode = offer.getCurrencyCode();
checkNotNull(priceFeedService, "priceFeed must not be null");
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
double marketPriceAsDouble = checkNotNull(marketPrice).getPrice();
return calculatePercentage(offer, marketPriceAsDouble);
}

protected Optional<Double> calculatePercentage(Offer offer, double marketPrice) {
// If the offer did not use % price we calculate % from current market price
String currencyCode = offer.getCurrencyCode();
Price price = offer.getPrice();
int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ?
Altcoin.SMALLEST_UNIT_EXPONENT :
Fiat.SMALLEST_UNIT_EXPONENT;
long priceAsLong = checkNotNull(price).getValue();
double scaled = MathUtils.scaleDownByPowerOf10(priceAsLong, precision);

double value;
if (direction == OfferPayload.Direction.SELL) {
if (CurrencyUtil.isFiatCurrency(currencyCode)) {
if (marketPrice == 0) {
return Optional.empty();
}
value = 1 - scaled / marketPrice;
} else {
if (marketPrice == 1) {
return Optional.empty();
}
value = scaled / marketPrice - 1;
}
} else {
if (CurrencyUtil.isFiatCurrency(currencyCode)) {
if (marketPrice == 1) {
return Optional.empty();
}
value = scaled / marketPrice - 1;
} else {
if (marketPrice == 0) {
return Optional.empty();
}
value = 1 - scaled / marketPrice;
}
}
return Optional.of(value);
}

public boolean hasMarketPrice(Offer offer) {
String currencyCode = offer.getCurrencyCode();
checkNotNull(priceFeedService, "priceFeed must not be null");
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
Price price = offer.getPrice();
return price != null && marketPrice != null && marketPrice.isRecentExternalPriceAvailable();
return priceUtil.getMarketBasedPrice(offer, direction);
}

String formatMarketPriceMargin(Offer offer, boolean decimalAligned) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,7 @@ public void initialize() {
marketColumn.setComparator(Comparator.comparing(model::getMarketLabel));
amountColumn.setComparator(Comparator.comparing(o -> o.getOffer().getAmount()));
priceColumn.setComparator(Comparator.comparing(o -> o.getOffer().getPrice(), Comparator.nullsFirst(Comparator.naturalOrder())));
deviationColumn.setComparator(Comparator.comparing(o ->
o.getOffer().isUseMarketBasedPrice() ? o.getOffer().getMarketPriceMargin() : 1,
Comparator.nullsFirst(Comparator.naturalOrder())));
deviationColumn.setComparator(Comparator.comparing(model::getPriceDeviationAsDouble, Comparator.nullsFirst(Comparator.naturalOrder())));
volumeColumn.setComparator(Comparator.comparing(o -> o.getOffer().getVolume(), Comparator.nullsFirst(Comparator.naturalOrder())));
dateColumn.setComparator(Comparator.comparing(o -> o.getOffer().getDate()));
paymentMethodColumn.setComparator(Comparator.comparing(o -> Res.get(o.getOffer().getPaymentMethod().getId())));
Expand Down Expand Up @@ -504,7 +502,9 @@ public void updateItem(final OpenOfferListItem item, boolean empty) {

if (item != null) {
if (model.isDeactivated(item)) getStyleClass().add("offer-disabled");
setGraphic(new AutoTooltipLabel(model.getPriceDeviation(item)));
AutoTooltipLabel autoTooltipLabel = new AutoTooltipLabel(model.getPriceDeviation(item));
autoTooltipLabel.setOpacity(item.getOffer().isUseMarketBasedPrice() ? 1 : 0.4);
setGraphic(autoTooltipLabel);
} else {
setGraphic(null);
}
Expand Down
Loading