Add market depth info API call (#190)

This commit is contained in:
Randall B 2022-02-11 17:13:41 -06:00 committed by GitHub
parent e3b9a9962b
commit 5b038697c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 187 additions and 5 deletions

View File

@ -19,6 +19,7 @@ package bisq.core.api;
import bisq.core.api.model.AddressBalanceInfo; import bisq.core.api.model.AddressBalanceInfo;
import bisq.core.api.model.BalancesInfo; import bisq.core.api.model.BalancesInfo;
import bisq.core.api.model.MarketDepthInfo;
import bisq.core.api.model.MarketPriceInfo; import bisq.core.api.model.MarketPriceInfo;
import bisq.core.api.model.TxFeeRateInfo; import bisq.core.api.model.TxFeeRateInfo;
import bisq.core.app.AppStartupState; import bisq.core.app.AppStartupState;
@ -31,7 +32,6 @@ import bisq.core.payment.payload.PaymentMethod;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.statistics.TradeStatistics3; import bisq.core.trade.statistics.TradeStatistics3;
import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.common.app.Version; import bisq.common.app.Version;
import bisq.common.config.Config; import bisq.common.config.Config;
import bisq.common.crypto.IncorrectPasswordException; import bisq.common.crypto.IncorrectPasswordException;
@ -461,6 +461,10 @@ public class CoreApi {
return corePriceService.getMarketPrices(); return corePriceService.getMarketPrices();
} }
public MarketDepthInfo getMarketDepth(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException {
return corePriceService.getMarketDepth(currencyCode);
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Trades // Trades
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -17,29 +17,41 @@
package bisq.core.api; package bisq.core.api;
import bisq.core.api.model.MarketDepthInfo;
import bisq.core.api.model.MarketPriceInfo; import bisq.core.api.model.MarketPriceInfo;
import bisq.core.locale.CurrencyUtil; 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 bisq.core.provider.price.PriceFeedService;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import com.google.common.math.LongMath;
import java.util.List; import java.util.List;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Singleton @Singleton
@Slf4j @Slf4j
class CorePriceService { class CorePriceService {
private final PriceFeedService priceFeedService; private final PriceFeedService priceFeedService;
private final OfferBookService offerBookService;
@Inject @Inject
public CorePriceService(PriceFeedService priceFeedService) { public CorePriceService(PriceFeedService priceFeedService, OfferBookService offerBookService) {
this.priceFeedService = priceFeedService; this.priceFeedService = priceFeedService;
this.offerBookService = offerBookService;
} }
/** /**
@ -65,6 +77,71 @@ class CorePriceService {
.collect(Collectors.toList()); .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<Offer> 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<Offer> 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<Offer> buyOffers = offerBookService.getOffersByCurrency(Direction.BUY.name(), currencyCode).stream().sorted(buyOfferSortComparator).collect(Collectors.toList());
List<Offer> 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<Double,Double> buyTM = new LinkedHashMap<Double,Double>();
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<Double,Double> sellTM = new LinkedHashMap<Double,Double>();
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, * PriceProvider returns different values for crypto and fiat,
* e.g. 1 XMR = X USD * 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 // TODO PriceProvider.getAll() could provide these values directly when the original values are not needed for the 'desktop' UI anymore
} }
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -20,6 +20,8 @@ package bisq.core.monetary;
import bisq.core.locale.CurrencyUtil; import bisq.core.locale.CurrencyUtil;
import bisq.core.util.ParsingUtils; import bisq.core.util.ParsingUtils;
import java.math.BigDecimal;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Monetary; import org.bitcoinj.core.Monetary;
import org.bitcoinj.utils.ExchangeRate; import org.bitcoinj.utils.ExchangeRate;
@ -103,10 +105,18 @@ public class Price extends MonetaryWrapper implements Comparable<Price> {
return monetary instanceof Altcoin ? ((Altcoin) monetary).getCurrencyCode() : ((Fiat) monetary).getCurrencyCode(); return monetary instanceof Altcoin ? ((Altcoin) monetary).getCurrencyCode() : ((Fiat) monetary).getCurrencyCode();
} }
@Override
public long getValue() { public long getValue() {
return monetary.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 @Override
public int compareTo(@NotNull Price other) { public int compareTo(@NotNull Price other) {
if (!this.getCurrencyCode().equals(other.getCurrencyCode())) if (!this.getCurrencyCode().equals(other.getCurrencyCode()))

View File

@ -202,6 +202,12 @@ public class OfferBookService {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public List<Offer> 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) { public void removeOfferAtShutDown(OfferPayload offerPayload) {
removeOffer(offerPayload, null, null); removeOffer(offerPayload, null, null);
} }

View File

@ -195,7 +195,7 @@ class GrpcOffersService extends OffersImplBase {
put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getGetMyOffersMethod().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)); put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
}} }}
))); )));

View File

@ -174,7 +174,7 @@ class GrpcPaymentAccountsService extends PaymentAccountsImplBase {
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
new HashMap<>() {{ new HashMap<>() {{
put(getCreatePaymentAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); 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(getGetPaymentAccountsMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getGetPaymentMethodsMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getGetPaymentMethodsMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getGetPaymentAccountFormMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getGetPaymentAccountFormMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));

View File

@ -18,8 +18,10 @@
package bisq.daemon.grpc; package bisq.daemon.grpc;
import bisq.core.api.CoreApi; import bisq.core.api.CoreApi;
import bisq.core.api.model.MarketDepthInfo;
import bisq.core.api.model.MarketPriceInfo; 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.MarketPriceReply;
import bisq.proto.grpc.MarketPriceRequest; import bisq.proto.grpc.MarketPriceRequest;
import bisq.proto.grpc.MarketPricesReply; import bisq.proto.grpc.MarketPricesReply;
@ -81,6 +83,17 @@ class GrpcPriceService extends PriceImplBase {
} }
} }
@Override
public void getMarketDepth(MarketDepthRequest req,
StreamObserver<MarketDepthReply> responseObserver) {
try {
responseObserver.onNext(mapMarketDepthReply(coreApi.getMarketDepth(req.getCurrencyCode())));
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
private MarketPricesReply mapMarketPricesReply(List<MarketPriceInfo> marketPrices) { private MarketPricesReply mapMarketPricesReply(List<MarketPriceInfo> marketPrices) {
MarketPricesReply.Builder builder = MarketPricesReply.newBuilder(); MarketPricesReply.Builder builder = MarketPricesReply.newBuilder();
marketPrices.stream() marketPrices.stream()
@ -89,6 +102,10 @@ class GrpcPriceService extends PriceImplBase {
return builder.build(); return builder.build();
} }
private MarketDepthReply mapMarketDepthReply(MarketDepthInfo marketDepth) {
return MarketDepthReply.newBuilder().setMarketDepth(marketDepth.toProtoMessage()).build();
}
final ServerInterceptor[] interceptors() { final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor(); Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor -> return rateMeteringInterceptor.map(serverInterceptor ->

View File

@ -506,6 +506,8 @@ service Price {
} }
rpc GetMarketPrices (MarketPricesRequest) returns (MarketPricesReply) { rpc GetMarketPrices (MarketPricesRequest) returns (MarketPricesReply) {
} }
rpc GetMarketDepth (MarketDepthRequest) returns (MarketDepthReply) {
}
} }
message MarketPriceRequest { message MarketPriceRequest {
@ -528,6 +530,22 @@ message MarketPriceInfo {
double price = 2; 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 // GetTradeStatistics
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////