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 all commits
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
2 changes: 2 additions & 0 deletions core/src/main/java/bisq/core/api/CoreOffersService.java
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,11 @@ private void placeOffer(Offer offer,
double buyerSecurityDeposit,
boolean useSavingsWallet,
Consumer<Transaction> resultHandler) {
// TODO add support for triggerPrice parameter. If value is 0 it is interpreted as not used. Its an optional value
openOfferManager.placeOffer(offer,
buyerSecurityDeposit,
useSavingsWallet,
0,
resultHandler::accept,
log::error);

Expand Down
7 changes: 6 additions & 1 deletion core/src/main/java/bisq/core/app/DomainInitialisation.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import bisq.core.notifications.alerts.market.MarketAlerts;
import bisq.core.notifications.alerts.price.PriceAlert;
import bisq.core.offer.OpenOfferManager;
import bisq.core.offer.TriggerPriceService;
import bisq.core.payment.RevolutAccount;
import bisq.core.payment.TradeLimits;
import bisq.core.provider.fee.FeeService;
Expand Down Expand Up @@ -106,6 +107,7 @@ public class DomainInitialisation {
private final MarketAlerts marketAlerts;
private final User user;
private final DaoStateSnapshotService daoStateSnapshotService;
private final TriggerPriceService triggerPriceService;

@Inject
public DomainInitialisation(ClockWatcher clockWatcher,
Expand Down Expand Up @@ -141,7 +143,8 @@ public DomainInitialisation(ClockWatcher clockWatcher,
PriceAlert priceAlert,
MarketAlerts marketAlerts,
User user,
DaoStateSnapshotService daoStateSnapshotService) {
DaoStateSnapshotService daoStateSnapshotService,
TriggerPriceService triggerPriceService) {
this.clockWatcher = clockWatcher;
this.tradeLimits = tradeLimits;
this.arbitrationManager = arbitrationManager;
Expand Down Expand Up @@ -176,6 +179,7 @@ public DomainInitialisation(ClockWatcher clockWatcher,
this.marketAlerts = marketAlerts;
this.user = user;
this.daoStateSnapshotService = daoStateSnapshotService;
this.triggerPriceService = triggerPriceService;
}

public void initDomainServices(Consumer<String> rejectedTxErrorMessageHandler,
Expand Down Expand Up @@ -254,6 +258,7 @@ public void initDomainServices(Consumer<String> rejectedTxErrorMessageHandler,
disputeMsgEvents.onAllServicesInitialized();
priceAlert.onAllServicesInitialized();
marketAlerts.onAllServicesInitialized();
triggerPriceService.onAllServicesInitialized();

if (revolutAccountsUpdateHandler != null) {
revolutAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream()
Expand Down
18 changes: 16 additions & 2 deletions core/src/main/java/bisq/core/offer/OpenOffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,18 @@ public enum State {
@Nullable
private NodeAddress refundAgentNodeAddress;

// Added in v1.5.3.
// If market price reaches that trigger price the offer gets deactivated
@Getter
private final long triggerPrice;

public OpenOffer(Offer offer) {
this(offer, 0);
}

public OpenOffer(Offer offer, long triggerPrice) {
this.offer = offer;
this.triggerPrice = triggerPrice;
state = State.AVAILABLE;
}

Expand All @@ -83,12 +92,14 @@ private OpenOffer(Offer offer,
State state,
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable NodeAddress mediatorNodeAddress,
@Nullable NodeAddress refundAgentNodeAddress) {
@Nullable NodeAddress refundAgentNodeAddress,
long triggerPrice) {
this.offer = offer;
this.state = state;
this.arbitratorNodeAddress = arbitratorNodeAddress;
this.mediatorNodeAddress = mediatorNodeAddress;
this.refundAgentNodeAddress = refundAgentNodeAddress;
this.triggerPrice = triggerPrice;

if (this.state == State.RESERVED)
setState(State.AVAILABLE);
Expand All @@ -98,6 +109,7 @@ private OpenOffer(Offer offer,
public protobuf.Tradable toProtoMessage() {
protobuf.OpenOffer.Builder builder = protobuf.OpenOffer.newBuilder()
.setOffer(offer.toProtoMessage())
.setTriggerPrice(triggerPrice)
.setState(protobuf.OpenOffer.State.valueOf(state.name()));

Optional.ofNullable(arbitratorNodeAddress).ifPresent(nodeAddress -> builder.setArbitratorNodeAddress(nodeAddress.toProtoMessage()));
Expand All @@ -112,7 +124,8 @@ public static Tradable fromProto(protobuf.OpenOffer proto) {
ProtoUtil.enumFromProto(OpenOffer.State.class, proto.getState().name()),
proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null,
proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null,
proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null);
proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null,
proto.getTriggerPrice());
}


Expand Down Expand Up @@ -178,6 +191,7 @@ public String toString() {
",\n arbitratorNodeAddress=" + arbitratorNodeAddress +
",\n mediatorNodeAddress=" + mediatorNodeAddress +
",\n refundAgentNodeAddress=" + refundAgentNodeAddress +
",\n triggerPrice=" + triggerPrice +
"\n}";
}
}
Expand Down
8 changes: 5 additions & 3 deletions core/src/main/java/bisq/core/offer/OpenOfferManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ public void onAwakeFromStandby() {
public void placeOffer(Offer offer,
double buyerSecurityDeposit,
boolean useSavingsWallet,
long triggerPrice,
TransactionResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
checkNotNull(offer.getMakerFee(), "makerFee must not be null");
Expand All @@ -382,7 +383,7 @@ public void placeOffer(Offer offer,
PlaceOfferProtocol placeOfferProtocol = new PlaceOfferProtocol(
model,
transaction -> {
OpenOffer openOffer = new OpenOffer(offer);
OpenOffer openOffer = new OpenOffer(offer, triggerPrice);
openOffers.add(openOffer);
requestPersistence();
resultHandler.handleResult(transaction);
Expand Down Expand Up @@ -486,6 +487,7 @@ public void editOpenOfferStart(OpenOffer openOffer,
}

public void editOpenOfferPublish(Offer editedOffer,
long triggerPrice,
OpenOffer.State originalState,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
Expand All @@ -498,7 +500,7 @@ public void editOpenOfferPublish(Offer editedOffer,
openOffer.setState(OpenOffer.State.CANCELED);
openOffers.remove(openOffer);

OpenOffer editedOpenOffer = new OpenOffer(editedOffer);
OpenOffer editedOpenOffer = new OpenOffer(editedOffer, triggerPrice);
editedOpenOffer.setState(originalState);

openOffers.add(editedOpenOffer);
Expand Down Expand Up @@ -855,7 +857,7 @@ private void maybeUpdatePersistedOffers() {
updatedOffer.setPriceFeedService(priceFeedService);
updatedOffer.setState(originalOfferState);

OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer);
OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, originalOpenOffer.getTriggerPrice());
updatedOpenOffer.setState(originalOpenOfferState);
openOffers.add(updatedOpenOffer);
requestPersistence();
Expand Down
163 changes: 163 additions & 0 deletions core/src/main/java/bisq/core/offer/TriggerPriceService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* 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.core.offer;

import bisq.core.locale.CurrencyUtil;
import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price;
import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService;

import bisq.common.util.MathUtils;

import org.bitcoinj.utils.Fiat;

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

import javafx.collections.ListChangeListener;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import lombok.extern.slf4j.Slf4j;

import static bisq.common.util.MathUtils.roundDoubleToLong;
import static bisq.common.util.MathUtils.scaleUpByPowerOf10;

@Slf4j
@Singleton
public class TriggerPriceService {
private final OpenOfferManager openOfferManager;
private final PriceFeedService priceFeedService;
private final Map<String, Set<OpenOffer>> openOffersByCurrency = new HashMap<>();

@Inject
public TriggerPriceService(OpenOfferManager openOfferManager, PriceFeedService priceFeedService) {
this.openOfferManager = openOfferManager;
this.priceFeedService = priceFeedService;
}

public void onAllServicesInitialized() {
openOfferManager.getObservableList().addListener((ListChangeListener<OpenOffer>) c -> {
c.next();
if (c.wasAdded()) {
onAddedOpenOffers(c.getAddedSubList());
}
if (c.wasRemoved()) {
onRemovedOpenOffers(c.getRemoved());
}
});
onAddedOpenOffers(openOfferManager.getObservableList());

priceFeedService.updateCounterProperty().addListener((observable, oldValue, newValue) -> onPriceFeedChanged());
onPriceFeedChanged();
}

private void onPriceFeedChanged() {
openOffersByCurrency.keySet().stream()
.map(priceFeedService::getMarketPrice)
.filter(Objects::nonNull)
.filter(marketPrice -> openOffersByCurrency.containsKey(marketPrice.getCurrencyCode()))
.forEach(marketPrice -> {
openOffersByCurrency.get(marketPrice.getCurrencyCode()).stream()
.filter(openOffer -> !openOffer.isDeactivated())
.forEach(openOffer -> checkPriceThreshold(marketPrice, openOffer));
});
}

public static boolean wasTriggered(MarketPrice marketPrice, OpenOffer openOffer) {
Price price = openOffer.getOffer().getPrice();
if (price == null) {
return false;
}

String currencyCode = openOffer.getOffer().getCurrencyCode();
boolean cryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode);
int smallestUnitExponent = cryptoCurrency ?
Altcoin.SMALLEST_UNIT_EXPONENT :
Fiat.SMALLEST_UNIT_EXPONENT;
long marketPriceAsLong = roundDoubleToLong(
scaleUpByPowerOf10(marketPrice.getPrice(), smallestUnitExponent));
long triggerPrice = openOffer.getTriggerPrice();
if (triggerPrice <= 0) {
return false;
}

OfferPayload.Direction direction = openOffer.getOffer().getDirection();
boolean isSellOffer = direction == OfferPayload.Direction.SELL;
boolean condition = isSellOffer && !cryptoCurrency || !isSellOffer && cryptoCurrency;
return condition ?
marketPriceAsLong < triggerPrice :
marketPriceAsLong > triggerPrice;
}

private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) {
if (wasTriggered(marketPrice, openOffer)) {
String currencyCode = openOffer.getOffer().getCurrencyCode();
int smallestUnitExponent = CurrencyUtil.isCryptoCurrency(currencyCode) ?
Altcoin.SMALLEST_UNIT_EXPONENT :
Fiat.SMALLEST_UNIT_EXPONENT;
long triggerPrice = openOffer.getTriggerPrice();

log.info("Market price exceeded the trigger price of the open offer.\n" +
"We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" +
"Market price: {};\nTrigger price: {}",
openOffer.getOffer().getShortId(),
currencyCode,
openOffer.getOffer().getDirection(),
marketPrice.getPrice(),
MathUtils.scaleDownByPowerOf10(triggerPrice, smallestUnitExponent)
);

openOfferManager.deactivateOpenOffer(openOffer, () -> {
}, errorMessage -> {
});
}
}

private void onAddedOpenOffers(List<? extends OpenOffer> openOffers) {
openOffers.forEach(openOffer -> {
String currencyCode = openOffer.getOffer().getCurrencyCode();
openOffersByCurrency.putIfAbsent(currencyCode, new HashSet<>());
openOffersByCurrency.get(currencyCode).add(openOffer);

MarketPrice marketPrice = priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode());
if (marketPrice != null) {
checkPriceThreshold(marketPrice, openOffer);
}
});
}

private void onRemovedOpenOffers(List<? extends OpenOffer> openOffers) {
openOffers.forEach(openOffer -> {
String currencyCode = openOffer.getOffer().getCurrencyCode();
if (openOffersByCurrency.containsKey(currencyCode)) {
Set<OpenOffer> set = openOffersByCurrency.get(currencyCode);
set.remove(openOffer);
if (set.isEmpty()) {
openOffersByCurrency.remove(currencyCode);
}
}
});
}
}
2 changes: 1 addition & 1 deletion core/src/main/java/bisq/core/util/FormattingUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ public static String formatMarketPrice(double price, String currencyCode) {
return formatMarketPrice(price, 8);
}

private static String formatMarketPrice(double price, int precision) {
public static String formatMarketPrice(double price, int precision) {
return formatRoundedDoubleWithPrecision(price, precision);
}

Expand Down
13 changes: 12 additions & 1 deletion core/src/main/resources/i18n/displayStrings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ shared.selectTradingAccount=Select trading account
shared.fundFromSavingsWalletButton=Transfer funds from Bisq wallet
shared.fundFromExternalWalletButton=Open your external wallet for funding
shared.openDefaultWalletFailed=Failed to open a Bitcoin wallet application. Are you sure you have one installed?
shared.distanceInPercent=Distance in % from market price
shared.belowInPercent=Below % from market price
shared.aboveInPercent=Above % from market price
shared.enterPercentageValue=Enter % value
Expand Down Expand Up @@ -455,6 +454,13 @@ createOffer.warning.buyAboveMarketPrice=You will always pay {0}% more than the c
createOffer.tradeFee.descriptionBTCOnly=Trade fee
createOffer.tradeFee.descriptionBSQEnabled=Select trade fee currency

createOffer.triggerPrice.prompt=Set optional trigger price
createOffer.triggerPrice.label=Deactivate offer if market price is {0}
createOffer.triggerPrice.tooltip=As protecting against drastic price movements you can set a trigger price which \
deactivates the offer if the market price reaches that value.
createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0}
createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0}

# new entries
createOffer.placeOfferButton=Review: Place offer to {0} bitcoin
createOffer.createOfferFundWalletInfo.headline=Fund your offer
Expand Down Expand Up @@ -551,6 +557,11 @@ takeOffer.tac=With taking this offer I agree to the trade conditions as defined
# Offerbook / Edit offer
####################################################################

openOffer.header.triggerPrice=Trigger price
openOffer.triggerPrice=Trigger price {0}
openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\n\
Please edit the offer to define a new trigger price

editOffer.setPrice=Set price
editOffer.confirmEdit=Confirm: Edit offer
editOffer.publishOffer=Publishing your offer.
Expand Down
Loading