Skip to content

Commit

Permalink
Merge pull request #5001 from chimp1984/deactivate-open-offer-if-trig…
Browse files Browse the repository at this point in the history
…ger-price-is-reached

Deactivate open offer if trigger price is reached
  • Loading branch information
ripcurlx authored Dec 28, 2020
2 parents 78d4176 + 681fed6 commit 976caeb
Show file tree
Hide file tree
Showing 34 changed files with 970 additions and 337 deletions.
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

0 comments on commit 976caeb

Please sign in to comment.