diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 7a4b073e..3e53abc2 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -19,6 +19,7 @@ package bisq.core.api; import bisq.core.api.model.AddressBalanceInfo; import bisq.core.api.model.BalancesInfo; +import bisq.core.api.model.MarketDepthInfo; import bisq.core.api.model.MarketPriceInfo; import bisq.core.api.model.TxFeeRateInfo; import bisq.core.app.AppStartupState; @@ -31,7 +32,6 @@ import bisq.core.payment.payload.PaymentMethod; import bisq.core.trade.Trade; import bisq.core.trade.statistics.TradeStatistics3; import bisq.core.trade.statistics.TradeStatisticsManager; - import bisq.common.app.Version; import bisq.common.config.Config; import bisq.common.crypto.IncorrectPasswordException; @@ -461,6 +461,10 @@ public class CoreApi { return corePriceService.getMarketPrices(); } + public MarketDepthInfo getMarketDepth(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException { + return corePriceService.getMarketDepth(currencyCode); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Trades /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CorePriceService.java b/core/src/main/java/bisq/core/api/CorePriceService.java index 228ab5ba..261f2644 100644 --- a/core/src/main/java/bisq/core/api/CorePriceService.java +++ b/core/src/main/java/bisq/core/api/CorePriceService.java @@ -17,29 +17,41 @@ package bisq.core.api; +import bisq.core.api.model.MarketDepthInfo; import bisq.core.api.model.MarketPriceInfo; import bisq.core.locale.CurrencyUtil; +import bisq.core.monetary.Price; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferBookService; +import bisq.core.offer.OfferPayload.Direction; import bisq.core.provider.price.PriceFeedService; import javax.inject.Inject; import javax.inject.Singleton; +import com.google.common.math.LongMath; + import java.util.List; +import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; + @Singleton @Slf4j class CorePriceService { private final PriceFeedService priceFeedService; + private final OfferBookService offerBookService; @Inject - public CorePriceService(PriceFeedService priceFeedService) { + public CorePriceService(PriceFeedService priceFeedService, OfferBookService offerBookService) { this.priceFeedService = priceFeedService; + this.offerBookService = offerBookService; } /** @@ -65,6 +77,71 @@ class CorePriceService { .collect(Collectors.toList()); } + /** + * @return Data for market depth chart + */ + public MarketDepthInfo getMarketDepth(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException { + if (priceFeedService.requestAllPrices().get(currencyCode.toUpperCase()) == null) throw new IllegalArgumentException("Currency not found: " + currencyCode) ; + + // Offer price can be null (if price feed unavailable), thus a null-tolerant comparator is used. + Comparator offerPriceComparator = Comparator.comparing(Offer::getPrice, Comparator.nullsLast(Comparator.naturalOrder())); + + // Trading btc-fiat is considered as buying/selling BTC, but trading btc-altcoin is + // considered as buying/selling Altcoin. Because of this, when viewing a btc-altcoin pair, + // the buy column is actually the sell column and vice versa. To maintain the expected + // ordering, we have to reverse the price comparator. + boolean isCrypto = CurrencyUtil.isCryptoCurrency(currencyCode); + if (isCrypto) offerPriceComparator = offerPriceComparator.reversed(); + + // Offer amounts are used for the secondary sort. They are sorted from high to low. + Comparator offerAmountComparator = Comparator.comparing(Offer::getAmount).reversed(); + + var buyOfferSortComparator = + offerPriceComparator.reversed() // Buy offers, as opposed to sell offers, are primarily sorted from high price to low. + .thenComparing(offerAmountComparator); + var sellOfferSortComparator = + offerPriceComparator + .thenComparing(offerAmountComparator); + List buyOffers = offerBookService.getOffersByCurrency(Direction.BUY.name(), currencyCode).stream().sorted(buyOfferSortComparator).collect(Collectors.toList()); + List sellOffers = offerBookService.getOffersByCurrency(Direction.SELL.name(), currencyCode).stream().sorted(sellOfferSortComparator).collect(Collectors.toList()); + + // Create buyer hashmap {key:price, value:count}, uses LinkedHashMap to maintain insertion order + double accumulatedAmount = 0; + LinkedHashMap buyTM = new LinkedHashMap(); + for(Offer offer: buyOffers) { + Price price = offer.getPrice(); + if (price != null) { + double amount = (double) offer.getAmount().value / LongMath.pow(10, offer.getAmount().smallestUnitExponent()); + accumulatedAmount += amount; + double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent()); + buyTM.put(mapPriceFeedServicePrice(priceAsDouble, currencyCode), accumulatedAmount); + } + }; + + // Create buyer hashmap {key:price, value:count}, uses TreeMap to sort by key (asc) + accumulatedAmount = 0; + LinkedHashMap sellTM = new LinkedHashMap(); + for(Offer offer: sellOffers){ + Price price = offer.getPrice(); + if (price != null) { + double amount = (double) offer.getAmount().value / LongMath.pow(10, offer.getAmount().smallestUnitExponent()); + accumulatedAmount += amount; + double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent()); + sellTM.put(mapPriceFeedServicePrice(priceAsDouble, currencyCode), accumulatedAmount); + } + }; + + // Make array of buyPrices and buyDepth + Double[] buyDepth = buyTM.values().toArray(new Double[buyTM.size()]); + Double[] buyPrices = buyTM.keySet().toArray(new Double[buyTM.size()]); + + // Make array of sellPrices and sellDepth + Double[] sellDepth = sellTM.values().toArray(new Double[sellTM.size()]); + Double[] sellPrices = sellTM.keySet().toArray(new Double[sellTM.size()]); + + return new MarketDepthInfo(currencyCode, buyPrices, buyDepth, sellPrices, sellDepth); + } + /** * PriceProvider returns different values for crypto and fiat, * e.g. 1 XMR = X USD @@ -80,3 +157,4 @@ class CorePriceService { // TODO PriceProvider.getAll() could provide these values directly when the original values are not needed for the 'desktop' UI anymore } } + diff --git a/core/src/main/java/bisq/core/api/model/MarketDepthInfo.java b/core/src/main/java/bisq/core/api/model/MarketDepthInfo.java new file mode 100644 index 00000000..c986c8b7 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/MarketDepthInfo.java @@ -0,0 +1,49 @@ +/* + * 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 . + */ + +package bisq.core.api.model; + +import java.util.Arrays; + +import lombok.AllArgsConstructor; + +import lombok.ToString; + +@ToString +@AllArgsConstructor +public class MarketDepthInfo { + public final String currencyCode; + public final Double[] buyPrices; + public final Double[] buyDepth; + public final Double[] sellPrices; + public final Double[] sellDepth; + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + // @Override + public bisq.proto.grpc.MarketDepthInfo toProtoMessage() { + return bisq.proto.grpc.MarketDepthInfo.newBuilder() + .setCurrencyCode(currencyCode) + .addAllBuyPrices(Arrays.asList(buyPrices)) + .addAllBuyDepth(Arrays.asList((buyDepth))) + .addAllSellPrices(Arrays.asList(sellPrices)) + .addAllSellDepth(Arrays.asList(sellDepth)) + .build(); + } +} diff --git a/core/src/main/java/bisq/core/monetary/Price.java b/core/src/main/java/bisq/core/monetary/Price.java index 5563185b..edc72df9 100644 --- a/core/src/main/java/bisq/core/monetary/Price.java +++ b/core/src/main/java/bisq/core/monetary/Price.java @@ -20,6 +20,8 @@ package bisq.core.monetary; import bisq.core.locale.CurrencyUtil; import bisq.core.util.ParsingUtils; +import java.math.BigDecimal; + import org.bitcoinj.core.Coin; import org.bitcoinj.core.Monetary; import org.bitcoinj.utils.ExchangeRate; @@ -103,10 +105,18 @@ public class Price extends MonetaryWrapper implements Comparable { return monetary instanceof Altcoin ? ((Altcoin) monetary).getCurrencyCode() : ((Fiat) monetary).getCurrencyCode(); } + @Override public long getValue() { return monetary.getValue(); } + /** + * Get the amount of whole coins or fiat units as double. + */ + public double getDoubleValue() { + return BigDecimal.valueOf(monetary.getValue()).movePointLeft(monetary.smallestUnitExponent()).doubleValue(); + } + @Override public int compareTo(@NotNull Price other) { if (!this.getCurrencyCode().equals(other.getCurrencyCode())) diff --git a/core/src/main/java/bisq/core/offer/OfferBookService.java b/core/src/main/java/bisq/core/offer/OfferBookService.java index 7df1c69e..20e7f424 100644 --- a/core/src/main/java/bisq/core/offer/OfferBookService.java +++ b/core/src/main/java/bisq/core/offer/OfferBookService.java @@ -202,6 +202,12 @@ public class OfferBookService { .collect(Collectors.toList()); } + public List getOffersByCurrency(String direction, String currencyCode) { + return getOffers().stream() + .filter(o -> o.getOfferPayload().getBaseCurrencyCode().equalsIgnoreCase(currencyCode) && o.getDirection().name() == direction) + .collect(Collectors.toList()); + } + public void removeOfferAtShutDown(OfferPayload offerPayload) { removeOffer(offerPayload, null, null); } diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java index 3234e41c..f6f5a7c2 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java @@ -195,7 +195,7 @@ class GrpcOffersService extends OffersImplBase { put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); - put(getCreateOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getCreateOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); }} ))); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java index 09b421ce..ca901ea7 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java @@ -174,7 +174,7 @@ class GrpcPaymentAccountsService extends PaymentAccountsImplBase { .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getCreatePaymentAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); - put(getCreateCryptoCurrencyPaymentAccountMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getCreateCryptoCurrencyPaymentAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getGetPaymentAccountsMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getGetPaymentMethodsMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getGetPaymentAccountFormMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java index 07135b1b..4439ab1f 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java @@ -18,8 +18,10 @@ package bisq.daemon.grpc; import bisq.core.api.CoreApi; +import bisq.core.api.model.MarketDepthInfo; import bisq.core.api.model.MarketPriceInfo; - +import bisq.proto.grpc.MarketDepthReply; +import bisq.proto.grpc.MarketDepthRequest; import bisq.proto.grpc.MarketPriceReply; import bisq.proto.grpc.MarketPriceRequest; import bisq.proto.grpc.MarketPricesReply; @@ -81,6 +83,17 @@ class GrpcPriceService extends PriceImplBase { } } + @Override + public void getMarketDepth(MarketDepthRequest req, + StreamObserver responseObserver) { + try { + responseObserver.onNext(mapMarketDepthReply(coreApi.getMarketDepth(req.getCurrencyCode()))); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + private MarketPricesReply mapMarketPricesReply(List marketPrices) { MarketPricesReply.Builder builder = MarketPricesReply.newBuilder(); marketPrices.stream() @@ -89,6 +102,10 @@ class GrpcPriceService extends PriceImplBase { return builder.build(); } + private MarketDepthReply mapMarketDepthReply(MarketDepthInfo marketDepth) { + return MarketDepthReply.newBuilder().setMarketDepth(marketDepth.toProtoMessage()).build(); + } + final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 1b9f1148..46904467 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -506,6 +506,8 @@ service Price { } rpc GetMarketPrices (MarketPricesRequest) returns (MarketPricesReply) { } + rpc GetMarketDepth (MarketDepthRequest) returns (MarketDepthReply) { + } } message MarketPriceRequest { @@ -528,6 +530,22 @@ message MarketPriceInfo { double price = 2; } +message MarketDepthRequest { + string currency_code = 1; +} + +message MarketDepthReply { + MarketDepthInfo market_depth = 1; +} + +message MarketDepthInfo { + string currency_code = 1; + repeated double buy_prices = 2; + repeated double buy_depth = 3; + repeated double sell_prices = 4; + repeated double sell_depth = 5; +} + /////////////////////////////////////////////////////////////////////////////////////////// // GetTradeStatistics ///////////////////////////////////////////////////////////////////////////////////////////