diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index ae41786443..4dae0c6515 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -17,6 +17,7 @@ package bisq.cli; +import bisq.proto.grpc.AverageBsqTradePrice; import bisq.proto.grpc.OfferInfo; import bisq.proto.grpc.TradeInfo; @@ -63,6 +64,7 @@ import bisq.cli.opts.CreateOfferOptionParser; import bisq.cli.opts.CreatePaymentAcctOptionParser; import bisq.cli.opts.EditOfferOptionParser; import bisq.cli.opts.GetAddressBalanceOptionParser; +import bisq.cli.opts.GetAvgBsqPriceOptionParser; import bisq.cli.opts.GetBTCMarketPriceOptionParser; import bisq.cli.opts.GetBalanceOptionParser; import bisq.cli.opts.GetOffersOptionParser; @@ -210,6 +212,21 @@ public class CliMain { new TableBuilder(ADDRESS_BALANCE_TBL, addressBalance).build().print(out); return; } + case getavgbsqprice: { + var opts = new GetAvgBsqPriceOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var days = opts.getDays(); + AverageBsqTradePrice price = client.getAverageBsqTradePrice(days); + out.println(format("avg %d day btc price: %s avg %d day usd price: %s", + days, + price.getBtcPrice(), + days, + price.getUsdPrice())); + return; + } case getbtcprice: { var opts = new GetBTCMarketPriceOptionParser(args).parse(); if (opts.isForHelp()) { @@ -837,6 +854,8 @@ public class CliMain { stream.println(); stream.format(rowFormat, getaddressbalance.name(), "--address=", "Get server wallet address balance"); stream.println(); + stream.format(rowFormat, getavgbsqprice.name(), "--days=", "Get volume weighted average bsq trade price"); + stream.println(); stream.format(rowFormat, getbtcprice.name(), "--currency-code=", "Get current market btc price"); stream.println(); stream.format(rowFormat, getfundingaddresses.name(), "", "Get BTC funding addresses"); diff --git a/cli/src/main/java/bisq/cli/GrpcClient.java b/cli/src/main/java/bisq/cli/GrpcClient.java index 4f188aa359..ab44ff8776 100644 --- a/cli/src/main/java/bisq/cli/GrpcClient.java +++ b/cli/src/main/java/bisq/cli/GrpcClient.java @@ -18,9 +18,11 @@ package bisq.cli; import bisq.proto.grpc.AddressBalanceInfo; +import bisq.proto.grpc.AverageBsqTradePrice; import bisq.proto.grpc.BalancesInfo; import bisq.proto.grpc.BsqBalanceInfo; import bisq.proto.grpc.BtcBalanceInfo; +import bisq.proto.grpc.GetAverageBsqTradePriceRequest; import bisq.proto.grpc.GetMethodHelpRequest; import bisq.proto.grpc.GetTradesRequest; import bisq.proto.grpc.GetVersionRequest; @@ -98,6 +100,13 @@ public final class GrpcClient { return walletsServiceRequest.getAddressBalance(address); } + public AverageBsqTradePrice getAverageBsqTradePrice(int days) { + var request = GetAverageBsqTradePriceRequest.newBuilder() + .setDays(days) + .build(); + return grpcStubs.priceService.getAverageBsqTradePrice(request).getPrice(); + } + public double getBtcPrice(String currencyCode) { return walletsServiceRequest.getBtcPrice(currencyCode); } diff --git a/cli/src/main/java/bisq/cli/Method.java b/cli/src/main/java/bisq/cli/Method.java index 770eaa6454..cc410bd2e4 100644 --- a/cli/src/main/java/bisq/cli/Method.java +++ b/cli/src/main/java/bisq/cli/Method.java @@ -29,6 +29,7 @@ public enum Method { editoffer, createpaymentacct, createcryptopaymentacct, + getavgbsqprice, getaddressbalance, getbalance, getbtcprice, diff --git a/cli/src/main/java/bisq/cli/opts/GetAvgBsqPriceOptionParser.java b/cli/src/main/java/bisq/cli/opts/GetAvgBsqPriceOptionParser.java new file mode 100644 index 0000000000..b8b4a30d9e --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/GetAvgBsqPriceOptionParser.java @@ -0,0 +1,53 @@ +/* + * 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.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_DAYS; + +public class GetAvgBsqPriceOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec daysOpt = parser.accepts(OPT_DAYS, + "number of days in average bsq price calculation") + .withRequiredArg() + .ofType(Integer.class) + .defaultsTo(30); + + public GetAvgBsqPriceOptionParser(String[] args) { + super(args); + } + + public GetAvgBsqPriceOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(daysOpt) || options.valueOf(daysOpt) <= 0) + throw new IllegalArgumentException("no # of days specified"); + + return this; + } + + public int getDays() { + return options.valueOf(daysOpt); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/OptLabel.java b/cli/src/main/java/bisq/cli/opts/OptLabel.java index 32b9484e84..8ea22c2364 100644 --- a/cli/src/main/java/bisq/cli/opts/OptLabel.java +++ b/cli/src/main/java/bisq/cli/opts/OptLabel.java @@ -26,6 +26,7 @@ public class OptLabel { public final static String OPT_AMOUNT = "amount"; public final static String OPT_CATEGORY = "category"; public final static String OPT_CURRENCY_CODE = "currency-code"; + public final static String OPT_DAYS = "days"; public final static String OPT_DIRECTION = "direction"; public final static String OPT_DISPUTE_AGENT_TYPE = "dispute-agent-type"; public final static String OPT_ENABLE = "enable"; diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 3e29233183..58d4ac87f7 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -21,6 +21,7 @@ import bisq.core.api.model.AddressBalanceInfo; import bisq.core.api.model.BalancesInfo; import bisq.core.api.model.TxFeeRateInfo; import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.monetary.Price; import bisq.core.offer.Offer; import bisq.core.offer.OpenOffer; import bisq.core.payment.PaymentAccount; @@ -36,6 +37,7 @@ import bisq.common.app.Version; import bisq.common.config.Config; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; +import bisq.common.util.Tuple2; import bisq.proto.grpc.GetTradesRequest; @@ -282,6 +284,10 @@ public class CoreApi { corePriceService.getMarketPrice(currencyCode, resultHandler); } + public Tuple2 getAverageBsqTradePrice(int days) { + return corePriceService.getAverageBsqTradePrice(days); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Trades /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CorePriceService.java b/core/src/main/java/bisq/core/api/CorePriceService.java index a0c62ba1ca..6d2e0193d1 100644 --- a/core/src/main/java/bisq/core/api/CorePriceService.java +++ b/core/src/main/java/bisq/core/api/CorePriceService.java @@ -17,7 +17,14 @@ package bisq.core.api; +import bisq.core.api.exception.NotAvailableException; +import bisq.core.monetary.Price; 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.Tuple2; import javax.inject.Inject; import javax.inject.Singleton; @@ -38,14 +45,20 @@ class CorePriceService { private final Predicate isCurrencyCode = (c) -> isFiatCurrency(c) || isCryptoCurrency(c); + private final Preferences preferences; private final PriceFeedService priceFeedService; + private final TradeStatisticsManager tradeStatisticsManager; @Inject - public CorePriceService(PriceFeedService priceFeedService) { + public CorePriceService(Preferences preferences, + PriceFeedService priceFeedService, + TradeStatisticsManager tradeStatisticsManager) { + this.preferences = preferences; this.priceFeedService = priceFeedService; + this.tradeStatisticsManager = tradeStatisticsManager; } - public void getMarketPrice(String currencyCode, Consumer resultHandler) { + void getMarketPrice(String currencyCode, Consumer resultHandler) { String upperCaseCurrencyCode = currencyCode.toUpperCase(); if (!isCurrencyCode.test(upperCaseCurrencyCode)) @@ -72,9 +85,17 @@ class CorePriceService { format("%s price feed request should not return data for unsupported currency code", upperCaseCurrencyCode)); } else { - throw new IllegalStateException(format("%s price is not available", upperCaseCurrencyCode)); + throw new NotAvailableException(format("%s price is not available", upperCaseCurrencyCode)); } }, log::warn); } + + Tuple2 getAverageBsqTradePrice(int days) { + Tuple2 prices = AveragePriceUtil.getAveragePriceTuple(preferences, tradeStatisticsManager, days); + if (prices.first.getValue() == 0 || prices.second.getValue() == 0) + throw new NotAvailableException("average bsq price is not available"); + + return prices; + } } diff --git a/core/src/main/resources/help/getavgbsqprice-help.txt b/core/src/main/resources/help/getavgbsqprice-help.txt new file mode 100644 index 0000000000..8a8454d5e2 --- /dev/null +++ b/core/src/main/resources/help/getavgbsqprice-help.txt @@ -0,0 +1,19 @@ +getavgbsqprice + +NAME +---- +getavgbsqprice - get average BSQ price in btc and usd + +SYNOPSIS +-------- +getavgbsqprice + --days=<30|90> + +DESCRIPTION +----------- +Returns the volume weighted average BSQ trade price over n days, +in BTC and USD. + +EXAMPLES +-------- +$ ./bisq-cli --password=xyz --port=9998 getavgbsqprice --days=30 diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java index bec21b9c5b..145f972d57 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java @@ -18,15 +18,27 @@ package bisq.daemon.grpc; import bisq.core.api.CoreApi; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; +import bisq.common.util.Tuple2; + +import bisq.proto.grpc.AverageBsqTradePrice; +import bisq.proto.grpc.GetAverageBsqTradePriceReply; +import bisq.proto.grpc.GetAverageBsqTradePriceRequest; import bisq.proto.grpc.MarketPriceReply; import bisq.proto.grpc.MarketPriceRequest; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; +import org.bitcoinj.utils.Fiat; + import javax.inject.Inject; +import java.math.BigDecimal; +import java.math.RoundingMode; + import java.util.HashMap; import java.util.Optional; @@ -34,6 +46,7 @@ import lombok.extern.slf4j.Slf4j; import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import static bisq.proto.grpc.PriceGrpc.PriceImplBase; +import static bisq.proto.grpc.PriceGrpc.getGetAverageBsqTradePriceMethod; import static bisq.proto.grpc.PriceGrpc.getGetMarketPriceMethod; import static java.util.concurrent.TimeUnit.SECONDS; @@ -69,6 +82,30 @@ class GrpcPriceService extends PriceImplBase { } } + @Override + public void getAverageBsqTradePrice(GetAverageBsqTradePriceRequest req, + StreamObserver responseObserver) { + try { + var days = req.getDays(); + Tuple2 prices = coreApi.getAverageBsqTradePrice(days); + var usdPrice = new BigDecimal(prices.first.toString()) + .setScale(Fiat.SMALLEST_UNIT_EXPONENT, RoundingMode.HALF_UP); + var btcPrice = new BigDecimal(prices.second.toString()) + .setScale(Altcoin.SMALLEST_UNIT_EXPONENT, RoundingMode.HALF_UP); + var proto = AverageBsqTradePrice.newBuilder() + .setUsdPrice(usdPrice.toString()) + .setBtcPrice(btcPrice.toString()) + .build(); + var reply = GetAverageBsqTradePriceReply.newBuilder() + .setPrice(proto) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> @@ -80,6 +117,7 @@ class GrpcPriceService extends PriceImplBase { .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getGetMarketPriceMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getGetAverageBsqTradePriceMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); }} ))); } diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 53748a7455..4b447b663b 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -434,6 +434,10 @@ service Price { // Get the current market price for a crypto currency. rpc GetMarketPrice (MarketPriceRequest) returns (MarketPriceReply) { } + // Get the volume weighted average trade price for BSQ, calculated over N days. + // The response contains the average BSQ trade price in USD to 4 decimal places, and in BTC to 8 decimal places. + rpc GetAverageBsqTradePrice (GetAverageBsqTradePriceRequest) returns (GetAverageBsqTradePriceReply) { + } } message MarketPriceRequest { @@ -444,6 +448,21 @@ message MarketPriceReply { double price = 1; // The most recently available market price. } +message GetAverageBsqTradePriceRequest { + sint32 days = 1; // The number of days used in the average BSQ trade price calculations. +} + +message GetAverageBsqTradePriceReply { + // The average BSQ trade price in USD to 4 decimal places, and in BTC to 8 decimal places. + AverageBsqTradePrice price = 1; +} + +// The average BSQ trade price in USD and BTC. +message AverageBsqTradePrice { + string usdPrice = 1; // The average BSQ trade price in USD to 4 decimal places. + string btcPrice = 2; // The average BSQ trade price in BTC to 8 decimal places. +} + service ShutdownServer { // Shut down a local Bisq daemon. rpc Stop (StopRequest) returns (StopReply) {