Add API method 'getavgbsqprice'

Returns volume weighted average BSQ price in BTC and USD over N days.

The need for this arose while creating a Java BSQ Swap bot example
(PR pending).  API bots can use this to determine whether or not to
take swap offers based on their price distance above or below the 30
(or 90) day trade price average.
This commit is contained in:
ghubstan 2022-06-17 18:26:10 -03:00
parent 71365be48c
commit a12dd52b81
No known key found for this signature in database
GPG key ID: E35592D6800A861E
10 changed files with 189 additions and 3 deletions

View file

@ -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=<btc-address>", "Get server wallet address balance");
stream.println();
stream.format(rowFormat, getavgbsqprice.name(), "--days=<days>", "Get volume weighted average bsq trade price");
stream.println();
stream.format(rowFormat, getbtcprice.name(), "--currency-code=<currency-code>", "Get current market btc price");
stream.println();
stream.format(rowFormat, getfundingaddresses.name(), "", "Get BTC funding addresses");

View file

@ -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);
}

View file

@ -29,6 +29,7 @@ public enum Method {
editoffer,
createpaymentacct,
createcryptopaymentacct,
getavgbsqprice,
getaddressbalance,
getbalance,
getbtcprice,

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_DAYS;
public class GetAvgBsqPriceOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<Integer> 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);
}
}

View file

@ -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";

View file

@ -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<Price, Price> getAverageBsqTradePrice(int days) {
return corePriceService.getAverageBsqTradePrice(days);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Trades
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -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<String> 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<Double> resultHandler) {
void getMarketPrice(String currencyCode, Consumer<Double> 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<Price, Price> getAverageBsqTradePrice(int days) {
Tuple2<Price, Price> 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;
}
}

View file

@ -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

View file

@ -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<GetAverageBsqTradePriceReply> responseObserver) {
try {
var days = req.getDays();
Tuple2<Price, Price> 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<ServerInterceptor> 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));
}}
)));
}

View file

@ -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) {