From 49a8c61bd2a239a4e73be0d544f07f7ea8ca7165 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 28 Jun 2022 19:52:05 -0300 Subject: [PATCH 01/41] Refactor the TakeBestPricedOfferTo*Btc bots The TakeBestPricedOfferTo*Btc needed to be split up so they are specific to fiat offers, and this change adds new TakeBestPricedOfferTo*Xmr bots just for XMR. This simplifies the the non-bsq-swap bots; documenting, coding, configuring, and logging the taking of both fiat and xmr (altcoin) offers in the same bots was getting confusing, and that's going to turn away potential users. This refactoring means there are six bots now, and it forced considerable refactoring in all of them. This commit is the bulk of the work for the PR, but there will be more changes after everything is tested again, and comments are adjusted. --- .../src/main/java/bisq/bots/AbstractBot.java | 191 +++++++++- .../src/main/java/bisq/bots/BotUtils.java | 21 +- .../bots/TakeBestPricedOfferToBuyBsq.java | 105 ++---- .../bots/TakeBestPricedOfferToBuyBtc.java | 175 +++------- .../bots/TakeBestPricedOfferToBuyXmr.java | 330 ++++++++++++++++++ .../bots/TakeBestPricedOfferToSellBsq.java | 103 ++---- .../bots/TakeBestPricedOfferToSellBtc.java | 160 ++------- .../bots/TakeBestPricedOfferToSellXmr.java | 329 +++++++++++++++++ .../TakeBestPricedOfferToBuyXmr.properties | 28 ++ .../TakeBestPricedOfferToSellXmr.properties | 28 ++ 10 files changed, 1033 insertions(+), 437 deletions(-) create mode 100644 java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java create mode 100644 java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java create mode 100644 java-examples/src/main/resources/TakeBestPricedOfferToBuyXmr.properties create mode 100644 java-examples/src/main/resources/TakeBestPricedOfferToSellXmr.properties diff --git a/java-examples/src/main/java/bisq/bots/AbstractBot.java b/java-examples/src/main/java/bisq/bots/AbstractBot.java index f6ba7c9..48425f0 100644 --- a/java-examples/src/main/java/bisq/bots/AbstractBot.java +++ b/java-examples/src/main/java/bisq/bots/AbstractBot.java @@ -20,6 +20,7 @@ import bisq.bots.table.builder.TableBuilder; import bisq.proto.grpc.*; import bisq.proto.grpc.GetTradesRequest.Category; import io.grpc.StatusRuntimeException; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import protobuf.PaymentAccount; @@ -37,6 +38,7 @@ import static bisq.bots.BotUtils.*; import static bisq.bots.table.builder.TableType.BSQ_BALANCE_TBL; import static bisq.bots.table.builder.TableType.BTC_BALANCE_TBL; import static bisq.proto.grpc.GetOfferCategoryReply.OfferCategory.BSQ_SWAP; +import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED; import static io.grpc.Status.*; import static java.lang.String.format; import static java.lang.System.exit; @@ -61,6 +63,7 @@ public abstract class AbstractBot { protected final String walletPassword; protected final String conf; protected final GrpcStubs grpcStubs; + @Getter protected final boolean isDryRun; // This is an experimental option for simulating and automating protocol payment steps during bot development. // Be extremely careful in its use; You do not want to "simulate" payments when API daemon is connected to mainnet. @@ -71,8 +74,8 @@ public abstract class AbstractBot { protected final List preferredTradingPeers = new ArrayList<>(); // Used during dry runs to track offers that would be taken. - // This list should remain empty if super.dryRun = FALSE until bot can take multiple offers in one session. - protected final List offersTaken = new ArrayList<>(); + // This list should stay empty when dryRun = false. + protected final List offersTakenDuringDryRun = new ArrayList<>(); protected final boolean canUseBash = getBashPath().isPresent(); protected boolean isShutdown = false; @@ -82,9 +85,20 @@ public abstract class AbstractBot { protected final Supplier minimumTxFeeRate = () -> txFeeRates.get().getMinFeeServiceRate(); protected final Supplier mostRecentTxFeeRate = () -> txFeeRates.get().getFeeServiceRate(); - // Constructor + /** + * Constructor that optionally prompts user to enter wallet password in the console. + *

+ * The wallet password prompt will be skipped if the given program args array already contains the + * '--wallet-password' option. This situation can occur when a bot calls another bot, and passes its + * wallet password option with validated value to this constructor. + *

+ * + * @param args program arguments + */ public AbstractBot(String[] args) { - this.args = toArgsWithWalletPassword.apply(args); + this.args = hasWalletPasswordOpt.test(args) + ? args + : toArgsWithWalletPassword.apply(args); Config bisqClientOpts = new Config(this.args, defaultPropertiesFilename.get()); this.walletPassword = bisqClientOpts.getWalletPassword(); this.conf = bisqClientOpts.getConf(); @@ -113,7 +127,11 @@ public abstract class AbstractBot { var reply = grpcStubs.versionService.getVersion(request); log.info("API daemon {} is available.", reply.getVersion()); } catch (StatusRuntimeException grpcException) { - log.error("Fatal Error: {}, daemon not available. Shutting down bot.", toCleanErrorMessage.apply(grpcException)); + log.error("Fatal Error: {}, daemon not available.", toCleanErrorMessage.apply(grpcException)); + if (exceptionHasStatus.test(grpcException, UNAUTHENTICATED)) { + log.error("Make sure your bot requests' '--password' opts match the API daemon's '--apiPassword' opt."); + } + log.error("Shutting down bot."); exit(1); } } @@ -192,10 +210,10 @@ public abstract class AbstractBot { } /** - * Return true if bot has taken the offer during this session -- for dry runs only. + * Return true if bot is in dryrun mode, and has taken the offer during this session. */ protected final Predicate isAlreadyTaken = (offer) -> - offersTaken.stream().anyMatch(o -> o.getId().equals(offer.getId())); + this.isDryRun() && offersTakenDuringDryRun.stream().anyMatch(o -> o.getId().equals(offer.getId())); /** * Print a table of BSQ balance information. @@ -396,23 +414,54 @@ public abstract class AbstractBot { * XMR payment account, else throws an IllegalStateException. (The bot does not yet support BSQ Swaps.) */ protected void validatePaymentAccount(PaymentAccount paymentAccount) { + verifyPaymentAccountCurrencyIsSupported(paymentAccount); + } + + /** + * Verifies (1) the given PaymentAccount has a selected trade currency, (2) is a fiat or XMR payment account, + * and (3) the payment account's primary (selected) currency code matches the given currency code, else throws + * an IllegalStateException. + */ + protected void validatePaymentAccount(PaymentAccount paymentAccount, String currencyCode) { + verifyPaymentAccountCurrencyIsSupported(paymentAccount); + var selectedCurrencyCode = paymentAccount.getSelectedTradeCurrency().getCode(); + if (!selectedCurrencyCode.equalsIgnoreCase(currencyCode)) + throw new IllegalStateException( + format("The bot's configured paymentAccountId %s%n" + + "is the id for '%s', which was set up to trade %s, not %s.", + paymentAccount.getId(), + paymentAccount.getAccountName(), + selectedCurrencyCode, + currencyCode)); + } + + /** + * Throw an IllegalStateException if (1) the given payment account has no selected trade currency, + * or (2) the payment account's selected trade currency is not supported by this bot, or (3) the + * payment account's selected trade currency is BSQ. (Let the API daemon handle the payment + * account used for BSQ swaps.) + * + * @param paymentAccount the payment account + */ + private void verifyPaymentAccountCurrencyIsSupported(PaymentAccount paymentAccount) { if (!paymentAccount.hasSelectedTradeCurrency()) throw new IllegalStateException( - format("PaymentAccount with ID '%s' and name '%s' has no selected currency definition.", + format("Payment Account with ID '%s' and name '%s' has no selected currency definition.", paymentAccount.getId(), paymentAccount.getAccountName())); var selectedCurrencyCode = paymentAccount.getSelectedTradeCurrency().getCode(); // Hacky way to find out if this is an altcoin payment method, but there is no BLOCK_CHAINS proto enum or msg. - boolean isBlockChainsPaymentMethod = paymentAccount.getPaymentMethod().getId().equals("BLOCK_CHAINS"); + var isBlockChainsPaymentMethod = paymentAccount.getPaymentMethod().getId().equals("BLOCK_CHAINS"); if (isBlockChainsPaymentMethod && !isXmr.test(selectedCurrencyCode)) throw new IllegalStateException( format("This bot only supports fiat and monero (XMR) trading, not the %s altcoin.", selectedCurrencyCode)); if (isBsq.test(selectedCurrencyCode)) - throw new IllegalStateException("This bot does not support BSQ Swaps."); + throw new IllegalStateException("This bot supports BSQ swaps, but not BSQ v1 protocol trades\n." + + "Let the API daemon handle the (default) payment account used for BSQ swaps."); } /** @@ -632,9 +681,9 @@ public abstract class AbstractBot { * Print information about offers taken during bot simulation. */ protected void printDryRunProgress() { - if (isDryRun && offersTaken.size() > 0) { - log.info("You have \"taken\" {} offer(s) during dry run:", offersTaken.size()); - printOffersSummary(offersTaken); + if (isDryRun && offersTakenDuringDryRun.size() > 0) { + log.info("You have \"taken\" {} offer(s) during dry run:", offersTakenDuringDryRun.size()); + printOffersSummary(offersTakenDuringDryRun); } } @@ -642,7 +691,7 @@ public abstract class AbstractBot { * Add offer to list of taken offers -- for dry runs only. */ protected void addToOffersTaken(OfferInfo offer) { - offersTaken.add(offer); + offersTakenDuringDryRun.add(offer); printOfferSummary(offer); log.info("Did not actually take that offer during this simulation."); } @@ -666,6 +715,38 @@ public abstract class AbstractBot { } } + /** + * Log the non-fatal exception, and stall the bot if the NonFatalException has a stallTime value > 0. + */ + protected void handleNonFatalException(NonFatalException nonFatalException, long pollingInterval) { + log.warn(nonFatalException.getMessage()); + if (nonFatalException.hasStallTime()) { + long stallTime = nonFatalException.getStallTime(); + log.warn("A minute must pass between the previous and the next takeoffer attempt." + + " Stalling for {} seconds before the next takeoffer attempt.", + toSeconds.apply(stallTime + pollingInterval)); + runCountdown(log, stallTime); + } else { + runCountdown(log, pollingInterval); + } + } + + /** + * Lock the wallet, stop the API daemon, and terminate the bot with a non-zero status (error). + */ + protected void shutdownAfterTakeOfferFailure(StatusRuntimeException fatalException) { + log.error("", fatalException); + shutdownAfterFatalError("Shutting down API daemon and bot after fatal takeoffer error."); + } + + /** + * Log the fatal BSQ swap exception, shut down the API daemon, and terminate the bot with a non-zero status (error). + */ + protected void handleFatalBsqSwapException(StatusRuntimeException fatalException) { + log.error("", fatalException); + shutdownAfterFatalError("Shutting down API daemon and bot after failing to execute BSQ swap."); + } + /** * Lock the wallet, stop the API daemon, and terminate the bot with a non-zero status (error). */ @@ -683,6 +764,88 @@ public abstract class AbstractBot { exit(1); } + /** + * Print the day's completed trades since midnight today, then: + *

+ * If numOffersTaken >= maxTakeOffers, and trade completion is being simulated on regtest, shut down the bot. + *

+ * If numOffersTaken >= maxTakeOffers, and mainnet trade completion must be delegated to the UI, shut down the + * API daemon and the bot. + *

+ * If numOffersTaken < maxTakeOffers, just log the number of offers taken so far during the bot run. + * (Don't shut down anything.) + * + * @param numOffersTaken the number of offers taken during bot run + * @param maxTakeOffers the max number of offers that can be taken during bot run + */ + protected void maybeShutdownAfterSuccessfulSwap(int numOffersTaken, int maxTakeOffers) { + log.info("Here are today's completed trades:"); + printTradesSummaryForToday(CLOSED); + + if (!isDryRun) { + try { + lockWallet(); + } catch (NonFatalException ex) { + log.warn(ex.getMessage()); + } + } + if (numOffersTaken >= maxTakeOffers) { + isShutdown = true; + log.info("Shutting down API bot after executing {} BSQ swaps.", numOffersTaken); + exit(0); + } else { + log.info("You have completed {} BSQ swap(s) during this bot session.", numOffersTaken); + } + } + + /** + * Print the day's completed trades since midnight today, then: + *

+ * If numOffersTaken >= maxTakeOffers, and trade completion is being simulated on regtest, shut down the bot. + *

+ * If numOffersTaken >= maxTakeOffers, and mainnet trade completion must be delegated to the UI, shut down the + * API daemon and the bot. + *

+ * If numOffersTaken < maxTakeOffers, just log the number of offers taken so far during the bot run. + * (Don't shut down anything.) + * + * @param numOffersTaken the number of offers taken during bot run + * @param maxTakeOffers the max number of offers that can be taken during bot run + */ + protected void maybeShutdownAfterSuccessfulTradeCreation(int numOffersTaken, int maxTakeOffers) { + log.info("Here are today's completed trades:"); + printTradesSummaryForToday(CLOSED); + + if (!isDryRun) { + // If the bot is not in dryrun mode, lock the wallet. If dryrun=true, leave the wallet unlocked until the + // timeout expires, so the user can look at data in the daemon with the CLI (a convenience in dev/test). + try { + lockWallet(); + } catch (NonFatalException ex) { + log.warn(ex.getMessage()); + } + } + if (numOffersTaken >= maxTakeOffers) { + isShutdown = true; + if (canSimulatePaymentSteps) { + log.info("Shutting down bot after {} successful simulated trades." + + " API daemon will not be shut down.", + numOffersTaken); + sleep(2_000); + } else { + log.info("Shutting down API daemon and bot after taking {} offers." + + " Complete the trade(s) with the desktop UI.", + numOffersTaken); + sleep(2_000); + log.info("Sending stop request to daemon."); + stopDaemon(); + } + exit(0); + } else { + log.info("You have taken {} offers during this bot session.", numOffersTaken); + } + } + /** * Returns Properties object for this bot. * diff --git a/java-examples/src/main/java/bisq/bots/BotUtils.java b/java-examples/src/main/java/bisq/bots/BotUtils.java index 3582fae..ed62585 100644 --- a/java-examples/src/main/java/bisq/bots/BotUtils.java +++ b/java-examples/src/main/java/bisq/bots/BotUtils.java @@ -152,6 +152,13 @@ public class BotUtils { (offer, targetPrice) -> offer.getUseMarketBasedPrice() && new BigDecimal(offer.getPrice()).compareTo(targetPrice) >= 0; + /** + * Return true if the margin price based offer's market price margin (%) >= minxMarketPriceMargin (%). + */ + public static final BiPredicate isMarginGEMinMarketPriceMargin = + (offer, minMarketPriceMargin) -> offer.getUseMarketBasedPrice() + && offer.getMarketPriceMarginPct() >= minMarketPriceMargin.doubleValue(); + /** * Return true if the margin price based offer's market price margin (%) <= maxMarketPriceMargin (%). */ @@ -197,10 +204,16 @@ public class BotUtils { return distanceFromMarketPrice.compareTo(minMarketPriceMargin) >= 0; } + /** + * Return String "above" if minMarketPriceMargin (%) >= 0.00, else "below". + */ + public static final Function aboveOrBelowMinMarketPriceMargin = (minMarketPriceMargin) -> + minMarketPriceMargin.compareTo(ZERO) >= 0 ? "above" : "below"; + /** * Return String "below" if maxMarketPriceMargin (%) <= 0.00, else "above". */ - public static final Function aboveOrBelowMarketPrice = (maxMarketPriceMargin) -> + public static final Function aboveOrBelowMaxMarketPriceMargin = (maxMarketPriceMargin) -> maxMarketPriceMargin.compareTo(ZERO) <= 0 ? "below" : "above"; /** @@ -269,6 +282,12 @@ public class BotUtils { return appendWalletPasswordOpt(args, unvalidatedWalletPassword); }; + /** + * Return true if the '--wallet-password' option label if found in the given program args array. + */ + public static final Predicate hasWalletPasswordOpt = (args) -> + Arrays.stream(args).anyMatch(a -> a.contains("--wallet-password")); + /** * Return a wallet password read from stdin. If read from a command terminal, input will not be echoed. * If run in a virtual terminal (IDE console), the input will be echoed. diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java index 57c9ed5..2a26c14 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java @@ -25,48 +25,44 @@ import protobuf.PaymentAccount; import java.math.BigDecimal; import java.util.*; import java.util.function.BiPredicate; -import java.util.function.Predicate; import static bisq.bots.BotUtils.*; -import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED; import static java.lang.String.format; -import static java.lang.System.exit; import static java.math.RoundingMode.HALF_UP; import static protobuf.OfferDirection.SELL; /** - * Bot for swapping BSQ for BTC at an attractive (high) price. The bot sends BSQ for BTC. - *

- * I'm taking liberties with the classname by not naming it TakeBestPricedOfferToSellBtcForBsq. + * Bot for selling BSQ for BTC at an attractive (higher) price. The bot sends BSQ for BTC. */ @Slf4j @Getter public class TakeBestPricedOfferToBuyBsq extends AbstractBot { + // Taker bot's default BSQ payment account trading currency code. + private static final String CURRENCY_CODE = "BSQ"; + // Config file: resources/TakeBestPricedOfferToBuyBsq.properties. private final Properties configFile; // Taker bot's default BSQ Swap payment account. private final PaymentAccount paymentAccount; - // Taker bot's payment account trading currency code (BSQ). - private final String currencyCode; // Taker bot's minimum market price margin. A takeable BSQ Swap offer's fixed-price must be >= minMarketPriceMargin (%). // Note: all BSQ Swap offers have a fixed-price, but the bot uses a margin (%) of the 30-day price for comparison. private final BigDecimal minMarketPriceMargin; // Hard coded 30-day average BSQ trade price, used for development over regtest (ignored when running on mainnet). private final BigDecimal regtest30DayAvgBsqPrice; - // Taker bot's min BTC amount to sell (we are buying BSQ). A takeable offer's amount must be >= minAmount BTC. + // Taker bot's minimum BTC amount to trade. A takeable offer's amount must be >= minAmount BTC. private final BigDecimal minAmount; - // Taker bot's max BTC amount to sell (we are buying BSQ). A takeable offer's amount must be <= maxAmount BTC. + // Taker bot's maximum BTC amount to trade. A takeable offer's amount must be <= maxAmount BTC. private final BigDecimal maxAmount; // Taker bot's max acceptable transaction fee rate. private final long maxTxFeeRate; - // Maximum # of offers to take during one bot session (shut down bot after N swaps). + // Maximum # of offers to take during one bot session (shut down bot after taking N swap offers). private final int maxTakeOffers; // Offer polling frequency must be > 1000 ms between each getoffers request. private final long pollingInterval; - // The # of BSQ swap offers taken during the bot session (since startup). + // The # of offers taken during the bot session (since startup). private int numOffersTaken = 0; public TakeBestPricedOfferToBuyBsq(String[] args) { @@ -74,7 +70,6 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot { pingDaemon(new Date().getTime()); // Shut down now if API daemon is not available. this.configFile = loadConfigFile(); this.paymentAccount = getBsqSwapPaymentAccount(); - this.currencyCode = paymentAccount.getSelectedTradeCurrency().getCode(); this.minMarketPriceMargin = new BigDecimal(configFile.getProperty("minMarketPriceMargin")) .setScale(2, HALF_UP); this.regtest30DayAvgBsqPrice = new BigDecimal(configFile.getProperty("regtest30DayAvgBsqPrice")) @@ -105,7 +100,7 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot { } // Get all available and takeable offers, sorted by price descending. - var offers = getOffers(SELL.name(), currencyCode).stream() + var offers = getOffers(SELL.name(), CURRENCY_CODE).stream() .filter(o -> !isAlreadyTaken.test(o)) .toList(); @@ -142,7 +137,7 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot { if (isDryRun) { addToOffersTaken(offer); numOffersTaken++; - maybeShutdownAfterSuccessfulSwap(); + maybeShutdownAfterSuccessfulSwap(numOffersTaken, maxTakeOffers); } else { // An encrypted wallet must be unlocked before calling takeoffer and gettrade. // Unlock the wallet for 10 minutes. If the wallet is already unlocked, @@ -160,11 +155,11 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot { printBSQBalances("BSQ Balances After Swap Execution"); numOffersTaken++; - maybeShutdownAfterSuccessfulSwap(); + maybeShutdownAfterSuccessfulSwap(numOffersTaken, maxTakeOffers); } catch (NonFatalException nonFatalException) { - handleNonFatalException(nonFatalException); + handleNonFatalException(nonFatalException, pollingInterval); } catch (StatusRuntimeException fatalException) { - handleFatalException(fatalException); + handleFatalBsqSwapException(fatalException); } } } @@ -179,7 +174,7 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot { configsByLabel.put("My Payment Account:", ""); configsByLabel.put("\tPayment Account Id:", paymentAccount.getId()); configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName()); - configsByLabel.put("\tCurrency Code:", currencyCode); + configsByLabel.put("\tCurrency Code:", CURRENCY_CODE); configsByLabel.put("Trading Rules:", ""); configsByLabel.put("\tMax # of offers bot can take:", maxTakeOffers); configsByLabel.put("\tMax Tx Fee Rate:", maxTxFeeRate + " sats/byte"); @@ -200,70 +195,16 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot { log.info(toTable.apply("Bot Configuration", configsByLabel)); } - /** - * Log the non-fatal exception, and stall the bot if the NonFatalException has a stallTime value > 0. - */ - private void handleNonFatalException(NonFatalException nonFatalException) { - log.warn(nonFatalException.getMessage()); - if (nonFatalException.hasStallTime()) { - long stallTime = nonFatalException.getStallTime(); - log.warn("A minute must pass between the previous and the next takeoffer attempt." - + " Stalling for {} seconds before the next takeoffer attempt.", - toSeconds.apply(stallTime + pollingInterval)); - runCountdown(log, stallTime); - } else { - runCountdown(log, pollingInterval); - } - } - - /** - * Log the fatal exception, and shut down daemon and bot. - */ - private void handleFatalException(StatusRuntimeException fatalException) { - log.error("", fatalException); - shutdownAfterFatalError("Shutting down API daemon and bot after failing to execute BSQ swap."); - } - - /** - * Lock the wallet, stop the API daemon, and terminate the bot. - */ - private void maybeShutdownAfterSuccessfulSwap() { - log.info("Here are today's completed trades:"); - printTradesSummaryForToday(CLOSED); - - if (!isDryRun) { - try { - lockWallet(); - } catch (NonFatalException ex) { - log.warn(ex.getMessage()); - } - } - if (numOffersTaken >= maxTakeOffers) { - isShutdown = true; - log.info("Shutting down API bot after executing {} BSQ swaps.", numOffersTaken); - exit(0); - } else { - log.info("You have completed {} BSQ swap(s) during this bot session.", numOffersTaken); - } - } - /** * Return true is fixed-price offer's price >= the bot's max market price margin. Allows bot to take a * fixed-priced offer if the price is >= {@link #minMarketPriceMargin} (%) of the current market price. */ protected final BiPredicate isFixedPriceGEMaxMarketPriceMargin = - (offer, currentMarketPrice) -> BotUtils.isFixedPriceGEMinMarketPriceMargin( + (offer, currentMarketPrice) -> isFixedPriceGEMinMarketPriceMargin( offer, currentMarketPrice, this.getMinMarketPriceMargin()); - /** - * Return true if offer.amt >= bot.minAmt AND offer.amt <= bot.maxAmt (within the boundaries). - * TODO API's takeoffer needs to support taking offer's minAmount. - */ - protected final Predicate isWithinBTCAmountBounds = (offer) -> - BotUtils.isWithinBTCAmountBounds(offer, getMinAmount(), getMaxAmount()); - public static void main(String[] args) { TakeBestPricedOfferToBuyBsq bot = new TakeBestPricedOfferToBuyBsq(args); bot.run(); @@ -297,28 +238,28 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot { .filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount())) .filter(isMakerPreferredTradingPeer) .filter(o -> isFixedPriceGEMaxMarketPriceMargin.test(o, avgBsqPrice)) - .filter(isWithinBTCAmountBounds) + .filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount())) .findFirst(); else return offers.stream() .filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount())) .filter(o -> isFixedPriceGEMaxMarketPriceMargin.test(o, avgBsqPrice)) - .filter(isWithinBTCAmountBounds) + .filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount())) .findFirst(); } void printCriteriaSummary() { if (isZero.test(minMarketPriceMargin)) { - log.info("Looking for offers to {}, with a fixed-price at or greater than" + log.info("Looking for offers to {}, with a fixed-price at or higher than" + " the 30-day average BSQ trade price of {} BTC.", MARKET_DESCRIPTION, avgBsqPrice); } else { - log.info("Looking for offers to {}, with a fixed-price at or greater than" + log.info("Looking for offers to {}, with a fixed-price at or higher than" + " {}% {} the 30-day average BSQ trade price of {} BTC.", MARKET_DESCRIPTION, minMarketPriceMargin.abs(), // Hide the sign, text explains target price % "above or below". - aboveOrBelowMarketPrice.apply(minMarketPriceMargin), + aboveOrBelowMinMarketPriceMargin.apply(minMarketPriceMargin), avgBsqPrice); } } @@ -344,12 +285,12 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot { ? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO" : "N/A"); var fixedPriceLabel = format("Is offer fixed-price (%s) >= bot's minimum price (%s)?", - offer.getPrice() + " " + currencyCode, - targetPrice + " " + currencyCode); + offer.getPrice() + " BTC", + targetPrice + " BTC"); filterResultsByLabel.put(fixedPriceLabel, isFixedPriceGEMaxMarketPriceMargin.test(offer, avgBsqPrice)); var btcAmountBounds = format("%s BTC - %s BTC", minAmount, maxAmount); filterResultsByLabel.put("Is offer's BTC amount within bot amount bounds (" + btcAmountBounds + ")?", - isWithinBTCAmountBounds.test(offer)); + isWithinBTCAmountBounds(offer, getMinAmount(), getMaxAmount())); var title = format("Fixed price BSQ swap offer %s filter results:", offer.getId()); log.info(toTable.apply(title, filterResultsByLabel)); diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java index c281b39..8d3110b 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java @@ -25,18 +25,14 @@ import protobuf.PaymentAccount; import java.math.BigDecimal; import java.util.*; import java.util.function.BiPredicate; -import java.util.function.Predicate; -import java.util.function.Supplier; import static bisq.bots.BotUtils.*; import static java.lang.String.format; -import static java.lang.System.exit; import static java.math.RoundingMode.HALF_UP; import static protobuf.OfferDirection.BUY; -import static protobuf.OfferDirection.SELL; /** - * The TakeBestPricedOfferToBuyBtc bot waits for attractively priced BUY BTC offers to appear, takes the offers + * The TakeBestPricedOfferToBuyBtc bot waits for attractively priced BUY BTC for fiat offers to appear, takes the offers * (up to a maximum of configured {@link #maxTakeOffers}), then shuts down both the API daemon and itself (the bot), * to allow the user to start the desktop UI application and complete the trades. *

@@ -60,18 +56,7 @@ import static protobuf.OfferDirection.SELL; * the offer maker is a preferred trading peer, * and the offer's BTC amount is between 0.10 and 0.25 BTC, * and the current transaction mining fee rate is below 20 sats / byte. - * - *

- * Another possible use case for this bot is to buy BTC with XMR. (We might say "sell XMR for BTC", but we need to - * remember that all Bisq offers are for buying or selling BTC.) - *

- *      Take an offer to buy BTC with XMR at or above current market price if:
- *          the offer maker is a preferred trading peer,
- *          and the offer's BTC amount is between 0.50 and 1.00 BTC,
- *          and the current transaction mining fee rate is below 15 sats / byte.
- * 
- *

- *

+ *
  * Usage:  TakeBestPricedOfferToBuyBtc  --password=api-password --port=api-port \
  *                          [--conf=take-best-priced-offer-to-buy-btc.conf] \
  *                          [--dryrun=true|false]
@@ -90,21 +75,21 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
     private final String currencyCode;
     // Taker bot's min market price margin.  A takeable offer's price margin (%) must be >= minMarketPriceMargin (%).
     private final BigDecimal minMarketPriceMargin;
-    // Taker bot's min BTC amount to buy (or sell in case of XMR).  A takeable offer's amount must be >= minAmount BTC.
+    // Taker bot's min BTC amount to trade.  A takeable offer's amount must be >= minAmount BTC.
     private final BigDecimal minAmount;
-    // Taker bot's max BTC amount to buy (or sell in case of XMR).   A takeable offer's amount must be <= maxAmount BTC.
+    // Taker bot's max BTC amount to trade.  A takeable offer's amount must be <= maxAmount BTC.
     private final BigDecimal maxAmount;
     // Taker bot's max acceptable transaction fee rate.
     private final long maxTxFeeRate;
     // Taker bot's trading fee currency code (BSQ or BTC).
     private final String bisqTradeFeeCurrency;
-    // Maximum # of offers to take during one bot session (shut down bot after N swaps).
+    // Maximum # of offers to take during one bot session (shut down bot after taking N offers).
     private final int maxTakeOffers;
 
     // Offer polling frequency must be > 1000 ms between each getoffers request.
     private final long pollingInterval;
 
-    // The # of BSQ swap offers taken during the bot session (since startup).
+    // The # of offers taken during the bot session (since startup).
     private int numOffersTaken = 0;
 
     public TakeBestPricedOfferToBuyBtc(String[] args) {
@@ -144,12 +129,9 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
                 continue;
             }
 
-            // Taker bot's getOffers(direction) request param.  For fiat offers, is BUY (BTC), for XMR offers, is SELL (BTC).
-            String offerDirection = isXmr.test(currencyCode) ? SELL.name() : BUY.name();
-
-            // Get all available and takeable offers, sorted by price ascending.
+            // Get all available and takeable buy BTC for fiat offers, sorted by price descending.
             // The list contains both fixed-price and market price margin based offers.
-            var offers = getOffers(offerDirection, currencyCode).stream()
+            var offers = getOffers(BUY.name(), currencyCode).stream()
                     .filter(o -> !isAlreadyTaken.test(o))
                     .toList();
 
@@ -191,7 +173,7 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
         if (isDryRun) {
             addToOffersTaken(offer);
             numOffersTaken++;
-            maybeShutdownAfterSuccessfulTradeCreation();
+            maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers);
         } else {
             // An encrypted wallet must be unlocked before calling takeoffer and gettrade.
             // Unlock the wallet for 5 minutes.  If the wallet is already unlocked,
@@ -212,81 +194,15 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
                     printBTCBalances("BTC Balances After Simulated Trade Completion");
                 }
                 numOffersTaken++;
-                maybeShutdownAfterSuccessfulTradeCreation();
+                maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers);
             } catch (NonFatalException nonFatalException) {
-                handleNonFatalException(nonFatalException);
+                handleNonFatalException(nonFatalException, pollingInterval);
             } catch (StatusRuntimeException fatalException) {
-                handleFatalException(fatalException);
+                shutdownAfterTakeOfferFailure(fatalException);
             }
         }
     }
 
-    /**
-     * Log the non-fatal exception, and stall the bot if the NonFatalException has a stallTime value > 0.
-     */
-    private void handleNonFatalException(NonFatalException nonFatalException) {
-        log.warn(nonFatalException.getMessage());
-        if (nonFatalException.hasStallTime()) {
-            long stallTime = nonFatalException.getStallTime();
-            log.warn("A minute must pass between the previous and the next takeoffer attempt."
-                            + "  Stalling for {} seconds before the next takeoffer attempt.",
-                    toSeconds.apply(stallTime + pollingInterval));
-            runCountdown(log, stallTime);
-        } else {
-            runCountdown(log, pollingInterval);
-        }
-    }
-
-    /**
-     * Log the fatal exception, and shut down daemon and bot.
-     */
-    private void handleFatalException(StatusRuntimeException fatalException) {
-        log.error("", fatalException);
-        shutdownAfterFailedTradePreparation();
-    }
-
-    /**
-     * Lock the wallet, stop the API daemon, and terminate the bot.
-     */
-    private void maybeShutdownAfterSuccessfulTradeCreation() {
-        if (!isDryRun) {
-            try {
-                lockWallet();
-            } catch (NonFatalException ex) {
-                log.warn(ex.getMessage());
-            }
-        }
-        if (numOffersTaken >= maxTakeOffers) {
-            isShutdown = true;
-
-            if (canSimulatePaymentSteps) {
-                log.info("Shutting down bot after {} successful simulated trades."
-                                + "  API daemon will not be shut down.",
-                        numOffersTaken);
-                sleep(2_000);
-            } else {
-                log.info("Shutting down API daemon and bot after taking {} offers."
-                                + "  Complete the trade(s) with the desktop UI.",
-                        numOffersTaken);
-                sleep(2_000);
-                log.info("Sending stop request to daemon.");
-                stopDaemon();
-            }
-
-            exit(0);
-
-        } else {
-            log.info("You have taken {} offers during this bot session.", numOffersTaken);
-        }
-    }
-
-    /**
-     * Lock the wallet, stop the API daemon, and terminate the bot with a non-zero status (error).
-     */
-    private void shutdownAfterFailedTradePreparation() {
-        shutdownAfterFatalError("Shutting down API daemon and bot after failing to find new trade.");
-    }
-
     /**
      * Return true is fixed-price offer's price >= the bot's min market price margin.  Allows bot to take a
      * fixed-priced offer if the price is >= {@link #minMarketPriceMargin} (%) of the current market price.
@@ -297,13 +213,6 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
                     currentMarketPrice,
                     this.getMinMarketPriceMargin());
 
-    /**
-     * Return true if offer.amt >= bot.minAmt AND offer.amt <= bot.maxAmt (within the boundaries).
-     *  TODO API's takeoffer needs to support taking offer's minAmount.
-     */
-    protected final Predicate isWithinBTCAmountBounds = (offer) ->
-            BotUtils.isWithinBTCAmountBounds(offer, getMinAmount(), getMaxAmount());
-
     private void printBotConfiguration() {
         var configsByLabel = new LinkedHashMap();
         configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion());
@@ -338,17 +247,12 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
      * performs candidate offer filtering, and provides useful log statements.
      */
     private class TakeCriteria {
+        private static final String MARKET_DESCRIPTION = "Buy BTC";
+
         private final BigDecimal currentMarketPrice;
         @Getter
         private final BigDecimal targetPrice;
 
-        private final Supplier marketDescription = () -> {
-            if (isXmr.test(currencyCode))
-                return "Buy XMR (Sell BTC)";
-            else
-                return "Buy BTC";
-        };
-
         public TakeCriteria() {
             this.currentMarketPrice = getCurrentMarketPrice(currencyCode);
             this.targetPrice = calcTargetPrice(minMarketPriceMargin, currentMarketPrice, currencyCode);
@@ -367,39 +271,39 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
                         .filter(isMakerPreferredTradingPeer)
                         .filter(o -> isMarginBasedPriceGETargetPrice.test(o, targetPrice)
                                 || isFixedPriceGEMinMarketPriceMargin.test(o, currentMarketPrice))
-                        .filter(isWithinBTCAmountBounds)
+                        .filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount()))
                         .findFirst();
             else
                 return offers.stream()
                         .filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount()))
                         .filter(o -> isMarginBasedPriceGETargetPrice.test(o, targetPrice)
                                 || isFixedPriceGEMinMarketPriceMargin.test(o, currentMarketPrice))
-                        .filter(isWithinBTCAmountBounds)
+                        .filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount()))
                         .findFirst();
         }
 
         void printCriteriaSummary() {
             if (isZero.test(minMarketPriceMargin)) {
-                log.info("Looking for offers to {}, priced at or higher than the current market price {} {}.",
-                        marketDescription.get(),
+                log.info("Looking for offers to {}, priced at or higher than the current market price of {} {}.",
+                        MARKET_DESCRIPTION,
                         currentMarketPrice,
-                        isXmr.test(currencyCode) ? "BTC" : currencyCode);
+                        currencyCode);
             } else {
-                log.info("Looking for offers to {}, priced at or more than {}% {} the current market price {} {}.",
-                        marketDescription.get(),
+                log.info("Looking for offers to {}, priced at or higher than {}% {} the current market price of {} {}.",
+                        MARKET_DESCRIPTION,
                         minMarketPriceMargin.abs(), // Hide the sign, text explains target price % "above or below".
-                        aboveOrBelowMarketPrice.apply(minMarketPriceMargin),
+                        aboveOrBelowMinMarketPriceMargin.apply(minMarketPriceMargin),
                         currentMarketPrice,
-                        isXmr.test(currencyCode) ? "BTC" : currencyCode);
+                        currencyCode);
             }
         }
 
         void printOffersAgainstCriteria(List offers) {
             log.info("Currently available {} offers -- want to take {} offer with price >= {} {}.",
-                    marketDescription.get(),
+                    MARKET_DESCRIPTION,
                     currencyCode,
                     targetPrice,
-                    isXmr.test(currencyCode) ? "BTC" : currencyCode);
+                    currencyCode);
             printOffersSummary(offers);
         }
 
@@ -416,23 +320,22 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
                     iHavePreferredTradingPeers.get()
                             ? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO"
                             : "N/A");
-            var marginPriceLabel = format("Is offer's margin based price (%s) >= bot's target price (%s)?",
-                    offer.getUseMarketBasedPrice() ? offer.getPrice() : "N/A",
-                    offer.getUseMarketBasedPrice() ? targetPrice : "N/A");
-            filterResultsByLabel.put(marginPriceLabel,
-                    offer.getUseMarketBasedPrice()
-                            ? isMarginBasedPriceGETargetPrice.test(offer, targetPrice)
-                            : "N/A");
-            var fixedPriceLabel = format("Is offer's fixed-price (%s) >= bot's target price (%s)?",
-                    offer.getUseMarketBasedPrice() ? "N/A" : offer.getPrice() + " " + currencyCode,
-                    offer.getUseMarketBasedPrice() ? "N/A" : targetPrice + " " + currencyCode);
-            filterResultsByLabel.put(fixedPriceLabel,
-                    offer.getUseMarketBasedPrice()
-                            ? "N/A"
-                            : isFixedPriceGEMinMarketPriceMargin.test(offer, currentMarketPrice));
+
+            if (offer.getUseMarketBasedPrice()) {
+                var marginPriceLabel = format("Is offer's margin based price (%s) >= bot's target price (%s)?",
+                        offer.getPrice() + " " + currencyCode,
+                        targetPrice + " " + currencyCode);
+                filterResultsByLabel.put(marginPriceLabel, isMarginBasedPriceGETargetPrice.test(offer, targetPrice));
+            } else {
+                var fixedPriceLabel = format("Is offer's fixed-price (%s) >= bot's target price (%s)?",
+                        offer.getPrice() + " " + currencyCode,
+                        targetPrice + " " + currencyCode);
+                filterResultsByLabel.put(fixedPriceLabel, isFixedPriceGEMinMarketPriceMargin.test(offer, currentMarketPrice));
+            }
+
             String btcAmountBounds = format("%s BTC - %s BTC", minAmount, maxAmount);
             filterResultsByLabel.put("Is offer's BTC amount within bot amount bounds (" + btcAmountBounds + ")?",
-                    isWithinBTCAmountBounds.test(offer));
+                    isWithinBTCAmountBounds(offer, getMinAmount(), getMaxAmount()));
 
             var title = format("%s offer %s filter results:",
                     offer.getUseMarketBasedPrice() ? "Margin based" : "Fixed price",
diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java
new file mode 100644
index 0000000..1d5ea49
--- /dev/null
+++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java
@@ -0,0 +1,330 @@
+/*
+ * 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.bots;
+
+import bisq.proto.grpc.OfferInfo;
+import io.grpc.StatusRuntimeException;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import protobuf.PaymentAccount;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.function.BiPredicate;
+
+import static bisq.bots.BotUtils.*;
+import static java.lang.String.format;
+import static java.math.RoundingMode.HALF_UP;
+import static protobuf.OfferDirection.SELL;
+
+/**
+ * The use case for the TakeBestPricedOfferToBuyXmr bot is to sell XMR for BTC at a high BTC price, e.g.:
+ * 
+ *      Take an offer to buy XMR from you for BTC, at no less than #.##% above or below current market price if:
+ *          the offer maker is a preferred trading peer,
+ *          and the offer's BTC amount is between 0.50 and 1.00 BTC,
+ *          and the current transaction mining fee rate is below 15 sats / byte.
+ *
+ * Usage:  TakeBestPricedOfferToBuyXmr  --password=api-password --port=api-port \
+ *                          [--conf=take-best-priced-offer-to-buy-xmr.conf] \
+ *                          [--dryrun=true|false]
+ *                          [--simulate-regtest-payment=true|false]
+ * 
+ *

+ * The criteria for determining which offers to take are defined in the bot's configuration file + * TakeBestPricedOfferToBuyXmr.properties (located in project's src/main/resources directory). The individual + * configurations are commented in the existing TakeBestPricedOfferToBuyXmr.properties, which should be used as a + * template for your own use case. + */ +@Slf4j +@Getter +public class TakeBestPricedOfferToBuyXmr extends AbstractBot { + + // Taker bot's XMR payment account trading currency code. + private static final String CURRENCY_CODE = "XMR"; + + // Config file: resources/TakeBestPricedOfferToBuyXmr.properties. + private final Properties configFile; + // Taker bot's XMR payment account (if the configured paymentAccountId is valid). + private final PaymentAccount paymentAccount; + // Taker bot's minimum market price margin. A takeable offer's price margin (%) must be >= minMarketPriceMargin (%). + private final BigDecimal minMarketPriceMargin; + // Taker bot's min BTC amount to trade. A takeable offer's amount must be >= minAmount BTC. + private final BigDecimal minAmount; + // Taker bot's max BTC amount to trade. A takeable offer's amount must be <= maxAmount BTC. + private final BigDecimal maxAmount; + // Taker bot's max acceptable transaction fee rate. + private final long maxTxFeeRate; + // Taker bot's trading fee currency code (BSQ or BTC). + private final String bisqTradeFeeCurrency; + // Maximum # of offers to take during one bot session (shut down bot after taking N offers). + private final int maxTakeOffers; + + // Offer polling frequency must be > 1000 ms between each getoffers request. + private final long pollingInterval; + + // The # of offers taken during the bot session (since startup). + private int numOffersTaken = 0; + + public TakeBestPricedOfferToBuyXmr(String[] args) { + super(args); + pingDaemon(new Date().getTime()); // Shut down now if API daemon is not available. + this.configFile = loadConfigFile(); + this.paymentAccount = getPaymentAccount(configFile.getProperty("paymentAccountId")); + this.minMarketPriceMargin = new BigDecimal(configFile.getProperty("minMarketPriceMargin")) + .setScale(2, HALF_UP); + this.minAmount = new BigDecimal(configFile.getProperty("minAmount")); + this.maxAmount = new BigDecimal(configFile.getProperty("maxAmount")); + this.maxTxFeeRate = Long.parseLong(configFile.getProperty("maxTxFeeRate")); + this.bisqTradeFeeCurrency = configFile.getProperty("bisqTradeFeeCurrency"); + this.maxTakeOffers = Integer.parseInt(configFile.getProperty("maxTakeOffers")); + loadPreferredOnionAddresses.accept(configFile, preferredTradingPeers); + this.pollingInterval = Long.parseLong(configFile.getProperty("pollingInterval")); + } + + /** + * Checks for the most attractive offer to take every {@link #pollingInterval} ms. After {@link #maxTakeOffers} + * are taken, bot will stop the API daemon, then shut itself down, prompting the user to start the desktop UI + * to complete the trade. + */ + @Override + public void run() { + var startTime = new Date().getTime(); + validateWalletPassword(walletPassword); + validatePollingInterval(pollingInterval); + validateTradeFeeCurrencyCode(bisqTradeFeeCurrency); + validatePaymentAccount(paymentAccount, CURRENCY_CODE); + printBotConfiguration(); + + while (!isShutdown) { + if (!isBisqNetworkTxFeeRateLowEnough.test(maxTxFeeRate)) { + runCountdown(log, pollingInterval); + continue; + } + + // Get all available and takeable sell BTC for XMR offers, sorted by price descending. + // The list may contain both fixed-price and market price margin based offers. + var offers = getOffers(SELL.name(), CURRENCY_CODE).stream() + .filter(o -> !isAlreadyTaken.test(o)) + .toList(); + + if (offers.isEmpty()) { + log.info("No takeable offers found."); + runCountdown(log, pollingInterval); + continue; + } + + // Define criteria for taking an offer, based on conf file. + TakeCriteria takeCriteria = new TakeCriteria(); + takeCriteria.printCriteriaSummary(); + takeCriteria.printOffersAgainstCriteria(offers); + + // Find takeable offer based on criteria. + Optional selectedOffer = takeCriteria.findTakeableOffer(offers); + // Try to take the offer, if found, or say 'no offer found' before going to sleep. + selectedOffer.ifPresentOrElse(offer -> takeOffer(takeCriteria, offer), + () -> { + var highestPricedOffer = offers.get(0); + log.info("No acceptable offer found. Closest possible candidate did not pass filters:"); + takeCriteria.printOfferAgainstCriteria(highestPricedOffer); + }); + + printDryRunProgress(); + runCountdown(log, pollingInterval); + pingDaemon(startTime); + } + } + + /** + * Attempt to take the available offer according to configured criteria. If successful, will block until a new + * trade is fully initialized with a trade contract. Otherwise, handles a non-fatal error and allows the bot to + * stay alive, or shuts down the bot upon fatal error. + */ + private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) { + log.info("Will attempt to take offer '{}'.", offer.getId()); + takeCriteria.printOfferAgainstCriteria(offer); + if (isDryRun) { + addToOffersTaken(offer); + numOffersTaken++; + maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); + } else { + // An encrypted wallet must be unlocked before calling takeoffer and gettrade. + // Unlock the wallet for 5 minutes. If the wallet is already unlocked, + // this command will override the timeout of the previous unlock command. + try { + unlockWallet(walletPassword, 600); + printBTCBalances("BTC Balances Before Take Offer Attempt"); + // Blocks until new trade is prepared, or times out. + takeV1ProtocolOffer(offer, paymentAccount, bisqTradeFeeCurrency, pollingInterval); + printBTCBalances("BTC Balances After Take Offer Attempt"); + + if (canSimulatePaymentSteps) { + var newTrade = getTrade(offer.getId()); + RegtestTradePaymentSimulator tradePaymentSimulator = new RegtestTradePaymentSimulator(args, + newTrade.getTradeId(), + paymentAccount); + tradePaymentSimulator.run(); + printBTCBalances("BTC Balances After Simulated Trade Completion"); + } + numOffersTaken++; + maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); + } catch (NonFatalException nonFatalException) { + handleNonFatalException(nonFatalException, pollingInterval); + } catch (StatusRuntimeException fatalException) { + shutdownAfterTakeOfferFailure(fatalException); + } + } + } + + /** + * Return true is fixed-price offer's price >= the bot's min market price margin. Allows bot to take a + * fixed-priced offer if the price is >= {@link #minMarketPriceMargin} (%) of the current market price. + */ + protected final BiPredicate isFixedPriceGEMinMarketPriceMargin = + (offer, currentMarketPrice) -> BotUtils.isFixedPriceGEMinMarketPriceMargin( + offer, + currentMarketPrice, + this.getMinMarketPriceMargin()); + + private void printBotConfiguration() { + var configsByLabel = new LinkedHashMap(); + configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion()); + var network = getNetwork(); + configsByLabel.put("BTC Network:", network); + configsByLabel.put("My Payment Account:", ""); + configsByLabel.put("\tPayment Account Id:", paymentAccount.getId()); + configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName()); + configsByLabel.put("\tCurrency Code:", CURRENCY_CODE); + configsByLabel.put("Trading Rules:", ""); + configsByLabel.put("\tMax # of offers bot can take:", maxTakeOffers); + configsByLabel.put("\tMax Tx Fee Rate:", maxTxFeeRate + " sats/byte"); + configsByLabel.put("\tMin Market Price Margin:", minMarketPriceMargin + "%"); + configsByLabel.put("\tMin BTC Amount:", minAmount + " BTC"); + configsByLabel.put("\tMax BTC Amount: ", maxAmount + " BTC"); + if (iHavePreferredTradingPeers.get()) { + configsByLabel.put("\tPreferred Trading Peers:", preferredTradingPeers.toString()); + } else { + configsByLabel.put("\tPreferred Trading Peers:", "N/A"); + } + configsByLabel.put("Bot Polling Interval:", pollingInterval + " ms"); + log.info(toTable.apply("Bot Configuration", configsByLabel)); + } + + public static void main(String[] args) { + TakeBestPricedOfferToBuyXmr bot = new TakeBestPricedOfferToBuyXmr(args); + bot.run(); + } + + /** + * Calculates additional takeoffer criteria based on conf file values, + * performs candidate offer filtering, and provides useful log statements. + */ + private class TakeCriteria { + private static final String MARKET_DESCRIPTION = "Buy XMR (Sell BTC)"; + + private final BigDecimal currentMarketPrice; + @Getter + private final BigDecimal targetPrice; + + public TakeCriteria() { + this.currentMarketPrice = getCurrentMarketPrice(CURRENCY_CODE); + this.targetPrice = calcTargetPrice(minMarketPriceMargin, currentMarketPrice, CURRENCY_CODE); + } + + /** + * Returns the highest priced offer passing the filters, or Optional.empty() if not found. + * The max tx fee rate filtering should have passed prior to calling this method. + * + * @param offers to filter + */ + Optional findTakeableOffer(List offers) { + if (iHavePreferredTradingPeers.get()) + return offers.stream() + .filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount())) + .filter(isMakerPreferredTradingPeer) + .filter(o -> isMarginGEMinMarketPriceMargin.test(o, minMarketPriceMargin) + || isFixedPriceGEMinMarketPriceMargin.test(o, currentMarketPrice)) + .filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount())) + .findFirst(); + else + return offers.stream() + .filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount())) + .filter(o -> isMarginGEMinMarketPriceMargin.test(o, minMarketPriceMargin) + || isFixedPriceGEMinMarketPriceMargin.test(o, currentMarketPrice)) + .filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount())) + .findFirst(); + } + + void printCriteriaSummary() { + if (isZero.test(minMarketPriceMargin)) { + log.info("Looking for offers to {}, priced at or higher than the current market price of {} BTC.", + MARKET_DESCRIPTION, + currentMarketPrice); + } else { + log.info("Looking for offers to {}, priced at or higher than {}% {} the current market price of {} BTC.", + MARKET_DESCRIPTION, + minMarketPriceMargin.abs(), // Hide the sign, text explains target price % "above or below". + aboveOrBelowMinMarketPriceMargin.apply(minMarketPriceMargin), + currentMarketPrice); + } + } + + void printOffersAgainstCriteria(List offers) { + log.info("Currently available {} offers -- want to take {} offer with price >= {} BTC.", + MARKET_DESCRIPTION, + CURRENCY_CODE, + targetPrice); + printOffersSummary(offers); + } + + void printOfferAgainstCriteria(OfferInfo offer) { + printOfferSummary(offer); + + var filterResultsByLabel = new LinkedHashMap(); + filterResultsByLabel.put("Current Market Price:", currentMarketPrice + " BTC"); + filterResultsByLabel.put("Target Price (Min):", targetPrice + " BTC"); + filterResultsByLabel.put("Offer Price:", offer.getPrice() + " BTC"); + filterResultsByLabel.put("Offer maker used same payment method?", + usesSamePaymentMethod.test(offer, getPaymentAccount())); + filterResultsByLabel.put("Is offer maker a preferred trading peer?", + iHavePreferredTradingPeers.get() + ? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO" + : "N/A"); + + if (offer.getUseMarketBasedPrice()) { + var marginPriceLabel = format("Is offer's price margin (%s%%) >= bot's min market price margin (%s%%)?", + offer.getMarketPriceMarginPct(), + minMarketPriceMargin); + filterResultsByLabel.put(marginPriceLabel, isMarginLEMaxMarketPriceMargin.test(offer, minMarketPriceMargin)); + } else { + var fixedPriceLabel = format("Is offer's fixed-price (%s) >= bot's target price (%s)?", + offer.getPrice() + " BTC", + targetPrice + " BTC"); + filterResultsByLabel.put(fixedPriceLabel, isFixedPriceGEMinMarketPriceMargin.test(offer, currentMarketPrice)); + } + + String btcAmountBounds = format("%s BTC - %s BTC", minAmount, maxAmount); + filterResultsByLabel.put("Is offer's BTC amount within bot amount bounds (" + btcAmountBounds + ")?", + isWithinBTCAmountBounds(offer, getMinAmount(), getMaxAmount())); + + var title = format("%s offer %s filter results:", + offer.getUseMarketBasedPrice() ? "Margin based" : "Fixed price", + offer.getId()); + log.info(toTable.apply(title, filterResultsByLabel)); + } + } +} diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java index b3f809e..5622119 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java @@ -25,48 +25,44 @@ import protobuf.PaymentAccount; import java.math.BigDecimal; import java.util.*; import java.util.function.BiPredicate; -import java.util.function.Predicate; import static bisq.bots.BotUtils.*; -import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED; import static java.lang.String.format; -import static java.lang.System.exit; import static java.math.RoundingMode.HALF_UP; import static protobuf.OfferDirection.BUY; /** - * Bot for swapping BTC for BSQ at an attractive (low) price. The bot receives BSQ for BTC. - *

- * I'm taking liberties with the classname by not naming it TakeBestPricedOfferToBuyBtcForBsq. + * Bot for buying BSQ with BTC at an attractive (lower) price. The bot receives BSQ for BTC. */ @Slf4j @Getter public class TakeBestPricedOfferToSellBsq extends AbstractBot { + // Taker bot's default BSQ payment account trading currency code. + private static final String CURRENCY_CODE = "BSQ"; + // Config file: resources/TakeBestPricedOfferToSellBsq.properties. private final Properties configFile; // Taker bot's default BSQ Swap payment account. private final PaymentAccount paymentAccount; - // Taker bot's payment account trading currency code (BSQ). - private final String currencyCode; // Taker bot's max market price margin. A takeable BSQ Swap offer's fixed-price must be <= maxMarketPriceMargin (%). // Note: all BSQ Swap offers have a fixed-price, but the bot uses a margin (%) of the 30-day price for comparison. private final BigDecimal maxMarketPriceMargin; // Hard coded 30-day average BSQ trade price, used for development over regtest (ignored when running on mainnet). private final BigDecimal regtest30DayAvgBsqPrice; - // Taker bot's min BTC amount to sell (we are buying BSQ). A takeable offer's amount must be >= minAmount BTC. + // Taker bot's minimum BTC amount to trade. A takeable offer's amount must be >= minAmount BTC. private final BigDecimal minAmount; - // Taker bot's max BTC amount to sell (we are buying BSQ). A takeable offer's amount must be <= maxAmount BTC. + // Taker bot's maximum BTC amount to trade. A takeable offer's amount must be <= maxAmount BTC. private final BigDecimal maxAmount; // Taker bot's max acceptable transaction fee rate. private final long maxTxFeeRate; - // Maximum # of offers to take during one bot session (shut down bot after N swaps). + // Maximum # of offers to take during one bot session (shut down bot after taking N swap offers). private final int maxTakeOffers; // Offer polling frequency must be > 1000 ms between each getoffers request. private final long pollingInterval; - // The # of BSQ swap offers taken during the bot session (since startup). + // The # of offers taken during the bot session (since startup). private int numOffersTaken = 0; public TakeBestPricedOfferToSellBsq(String[] args) { @@ -74,7 +70,6 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { pingDaemon(new Date().getTime()); // Shut down now if API daemon is not available. this.configFile = loadConfigFile(); this.paymentAccount = getBsqSwapPaymentAccount(); - this.currencyCode = paymentAccount.getSelectedTradeCurrency().getCode(); this.maxMarketPriceMargin = new BigDecimal(configFile.getProperty("maxMarketPriceMargin")) .setScale(2, HALF_UP); this.regtest30DayAvgBsqPrice = new BigDecimal(configFile.getProperty("regtest30DayAvgBsqPrice")) @@ -105,7 +100,7 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { } // Get all available and takeable offers, sorted by price ascending. - var offers = getOffers(BUY.name(), currencyCode).stream() + var offers = getOffers(BUY.name(), CURRENCY_CODE).stream() .filter(o -> !isAlreadyTaken.test(o)) .toList(); @@ -142,7 +137,7 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { if (isDryRun) { addToOffersTaken(offer); numOffersTaken++; - maybeShutdownAfterSuccessfulSwap(); + maybeShutdownAfterSuccessfulSwap(numOffersTaken, maxTakeOffers); } else { // An encrypted wallet must be unlocked before calling takeoffer and gettrade. // Unlock the wallet for 10 minutes. If the wallet is already unlocked, @@ -160,11 +155,11 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { printBSQBalances("BSQ Balances After Swap Execution"); numOffersTaken++; - maybeShutdownAfterSuccessfulSwap(); + maybeShutdownAfterSuccessfulSwap(numOffersTaken, maxTakeOffers); } catch (NonFatalException nonFatalException) { - handleNonFatalException(nonFatalException); + handleNonFatalException(nonFatalException, pollingInterval); } catch (StatusRuntimeException fatalException) { - handleFatalException(fatalException); + handleFatalBsqSwapException(fatalException); } } } @@ -179,7 +174,7 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { configsByLabel.put("My Payment Account:", ""); configsByLabel.put("\tPayment Account Id:", paymentAccount.getId()); configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName()); - configsByLabel.put("\tCurrency Code:", currencyCode); + configsByLabel.put("\tCurrency Code:", CURRENCY_CODE); configsByLabel.put("Trading Rules:", ""); configsByLabel.put("\tMax # of offers bot can take:", maxTakeOffers); configsByLabel.put("\tMax Tx Fee Rate:", maxTxFeeRate + " sats/byte"); @@ -200,53 +195,6 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { log.info(toTable.apply("Bot Configuration", configsByLabel)); } - /** - * Log the non-fatal exception, and stall the bot if the NonFatalException has a stallTime value > 0. - */ - private void handleNonFatalException(NonFatalException nonFatalException) { - log.warn(nonFatalException.getMessage()); - if (nonFatalException.hasStallTime()) { - long stallTime = nonFatalException.getStallTime(); - log.warn("A minute must pass between the previous and the next takeoffer attempt." - + " Stalling for {} seconds before the next takeoffer attempt.", - toSeconds.apply(stallTime + pollingInterval)); - runCountdown(log, stallTime); - } else { - runCountdown(log, pollingInterval); - } - } - - /** - * Log the fatal exception, and shut down daemon and bot. - */ - private void handleFatalException(StatusRuntimeException fatalException) { - log.error("", fatalException); - shutdownAfterFatalError("Shutting down API daemon and bot after failing to execute BSQ swap."); - } - - /** - * Lock the wallet, stop the API daemon, and terminate the bot. - */ - private void maybeShutdownAfterSuccessfulSwap() { - log.info("Here are today's completed trades:"); - printTradesSummaryForToday(CLOSED); - - if (!isDryRun) { - try { - lockWallet(); - } catch (NonFatalException ex) { - log.warn(ex.getMessage()); - } - } - if (numOffersTaken >= maxTakeOffers) { - isShutdown = true; - log.info("Shutting down API bot after executing {} BSQ swaps.", numOffersTaken); - exit(0); - } else { - log.info("You have completed {} BSQ swap(s) during this bot session.", numOffersTaken); - } - } - /** * Return true is fixed-price offer's price <= the bot's max market price margin. Allows bot to take a * fixed-priced offer if the price is <= {@link #maxMarketPriceMargin} (%) of the current market price. @@ -257,13 +205,6 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { currentMarketPrice, getMaxMarketPriceMargin()); - /** - * Return true if offer.amt >= bot.minAmt AND offer.amt <= bot.maxAmt (within the boundaries). - * TODO API's takeoffer needs to support taking offer's minAmount. - */ - protected final Predicate isWithinBTCAmountBounds = (offer) -> - BotUtils.isWithinBTCAmountBounds(offer, getMinAmount(), getMaxAmount()); - public static void main(String[] args) { TakeBestPricedOfferToSellBsq bot = new TakeBestPricedOfferToSellBsq(args); bot.run(); @@ -298,28 +239,28 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { .filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount())) .filter(isMakerPreferredTradingPeer) .filter(o -> isFixedPriceLEMaxMarketPriceMargin.test(o, avgBsqPrice)) - .filter(isWithinBTCAmountBounds) + .filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount())) .findFirst(); else return offers.stream() .filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount())) .filter(o -> isFixedPriceLEMaxMarketPriceMargin.test(o, avgBsqPrice)) - .filter(isWithinBTCAmountBounds) + .filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount())) .findFirst(); } void printCriteriaSummary() { if (isZero.test(maxMarketPriceMargin)) { - log.info("Looking for offers to {}, with a fixed-price at or less than" + log.info("Looking for offers to {}, with a fixed-price at or lower than" + " the 30-day average BSQ trade price of {} BTC.", MARKET_DESCRIPTION, avgBsqPrice); } else { - log.info("Looking for offers to {}, with a fixed-price at or less than" + log.info("Looking for offers to {}, with a fixed-price at or lower than" + " {}% {} the 30-day average BSQ trade price of {} BTC.", MARKET_DESCRIPTION, maxMarketPriceMargin.abs(), // Hide the sign, text explains target price % "above or below". - aboveOrBelowMarketPrice.apply(maxMarketPriceMargin), + aboveOrBelowMaxMarketPriceMargin.apply(maxMarketPriceMargin), avgBsqPrice); } } @@ -345,12 +286,12 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { ? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO" : "N/A"); var fixedPriceLabel = format("Is offer's fixed-price (%s) <= bot's minimum price (%s)?", - offer.getPrice() + " " + currencyCode, - targetPrice + " " + currencyCode); + offer.getPrice() + " BTC", + targetPrice + " BTC"); filterResultsByLabel.put(fixedPriceLabel, isFixedPriceLEMaxMarketPriceMargin.test(offer, avgBsqPrice)); var btcAmountBounds = format("%s BTC - %s BTC", minAmount, maxAmount); filterResultsByLabel.put("Is offer's BTC amount within bot amount bounds (" + btcAmountBounds + ")?", - isWithinBTCAmountBounds.test(offer)); + isWithinBTCAmountBounds(offer, getMinAmount(), getMaxAmount())); var title = format("Fixed price BSQ swap offer %s filter results:", offer.getId()); log.info(toTable.apply(title, filterResultsByLabel)); diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java index a6bd5b5..dd9893f 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java @@ -25,14 +25,10 @@ import protobuf.PaymentAccount; import java.math.BigDecimal; import java.util.*; import java.util.function.BiPredicate; -import java.util.function.Predicate; -import java.util.function.Supplier; import static bisq.bots.BotUtils.*; import static java.lang.String.format; -import static java.lang.System.exit; import static java.math.RoundingMode.HALF_UP; -import static protobuf.OfferDirection.BUY; import static protobuf.OfferDirection.SELL; /** @@ -91,21 +87,21 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot { private final String currencyCode; // Taker bot's max market price margin. A takeable offer's price margin (%) must be <= maxMarketPriceMargin (%). private final BigDecimal maxMarketPriceMargin; - // Taker bot's min BTC amount to buy (or sell in case of XMR). A takeable offer's amount must be >= minAmount BTC. + // Taker bot's min BTC amount to trade. A takeable offer's amount must be >= minAmount BTC. private final BigDecimal minAmount; - // Taker bot's max BTC amount to buy (or sell in case of XMR). A takeable offer's amount must be <= maxAmount BTC. + // Taker bot's max BTC amount to trade. A takeable offer's amount must be <= maxAmount BTC. private final BigDecimal maxAmount; // Taker bot's max acceptable transaction fee rate. private final long maxTxFeeRate; // Taker bot's trading fee currency code (BSQ or BTC). private final String bisqTradeFeeCurrency; - // Maximum # of offers to take during one bot session (shut down bot after N swaps). + // Maximum # of offers to take during one bot session (shut down bot after taking N offers). private final int maxTakeOffers; // Offer polling frequency must be > 1000 ms between each getoffers request. private final long pollingInterval; - // The # of BSQ swap offers taken during the bot session (since startup). + // The # of offers taken during the bot session (since startup). private int numOffersTaken = 0; public TakeBestPricedOfferToSellBtc(String[] args) { @@ -145,12 +141,9 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot { continue; } - // Taker bot's getOffers(direction) request param. For fiat offers, is SELL (BTC), for XMR offers, is BUY (BTC). - String offerDirection = isXmr.test(currencyCode) ? BUY.name() : SELL.name(); - - // Get all available and takeable offers, sorted by price ascending. + // Get all available and takeable sell BTC offers, sorted by price ascending. // The list contains both fixed-price and market price margin based offers. - var offers = getOffers(offerDirection, currencyCode).stream() + var offers = getOffers(SELL.name(), currencyCode).stream() .filter(o -> !isAlreadyTaken.test(o)) .toList(); @@ -192,7 +185,7 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot { if (isDryRun) { addToOffersTaken(offer); numOffersTaken++; - maybeShutdownAfterSuccessfulTradeCreation(); + maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } else { // An encrypted wallet must be unlocked before calling takeoffer and gettrade. // Unlock the wallet for 5 minutes. If the wallet is already unlocked, @@ -213,81 +206,15 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot { printBTCBalances("BTC Balances After Simulated Trade Completion"); } numOffersTaken++; - maybeShutdownAfterSuccessfulTradeCreation(); + maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } catch (NonFatalException nonFatalException) { - handleNonFatalException(nonFatalException); + handleNonFatalException(nonFatalException, pollingInterval); } catch (StatusRuntimeException fatalException) { - handleFatalException(fatalException); + shutdownAfterTakeOfferFailure(fatalException); } } } - /** - * Log the non-fatal exception, and stall the bot if the NonFatalException has a stallTime value > 0. - */ - private void handleNonFatalException(NonFatalException nonFatalException) { - log.warn(nonFatalException.getMessage()); - if (nonFatalException.hasStallTime()) { - long stallTime = nonFatalException.getStallTime(); - log.warn("A minute must pass between the previous and the next takeoffer attempt." - + " Stalling for {} seconds before the next takeoffer attempt.", - toSeconds.apply(stallTime + pollingInterval)); - runCountdown(log, stallTime); - } else { - runCountdown(log, pollingInterval); - } - } - - /** - * Log the fatal exception, and shut down daemon and bot. - */ - private void handleFatalException(StatusRuntimeException fatalException) { - log.error("", fatalException); - shutdownAfterFailedTradePreparation(); - } - - /** - * Lock the wallet, stop the API daemon, and terminate the bot. - */ - private void maybeShutdownAfterSuccessfulTradeCreation() { - if (!isDryRun) { - try { - lockWallet(); - } catch (NonFatalException ex) { - log.warn(ex.getMessage()); - } - } - if (numOffersTaken >= maxTakeOffers) { - isShutdown = true; - - if (canSimulatePaymentSteps) { - log.info("Shutting down bot after {} successful simulated trades." - + " API daemon will not be shut down.", - numOffersTaken); - sleep(2_000); - } else { - log.info("Shutting down API daemon and bot after taking {} offers." - + " Complete the trade(s) with the desktop UI.", - numOffersTaken); - sleep(2_000); - log.info("Sending stop request to daemon."); - stopDaemon(); - } - - exit(0); - - } else { - log.info("You have taken {} offers during this bot session.", numOffersTaken); - } - } - - /** - * Lock the wallet, stop the API daemon, and terminate the bot with a non-zero status (error). - */ - private void shutdownAfterFailedTradePreparation() { - shutdownAfterFatalError("Shutting down API daemon and bot after failing to find new trade."); - } - /** * Return true is fixed-price offer's price <= the bot's max market price margin. Allows bot to take a * fixed-priced offer if the price is <= {@link #maxMarketPriceMargin} (%) of the current market price. @@ -298,13 +225,6 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot { currentMarketPrice, getMaxMarketPriceMargin()); - /** - * Return true if offer.amt >= bot.minAmt AND offer.amt <= bot.maxAmt (within the boundaries). - * TODO API's takeoffer needs to support taking offer's minAmount. - */ - protected final Predicate isWithinBTCAmountBounds = (offer) -> - BotUtils.isWithinBTCAmountBounds(offer, getMinAmount(), getMaxAmount()); - private void printBotConfiguration() { var configsByLabel = new LinkedHashMap(); configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion()); @@ -339,17 +259,12 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot { * performs candidate offer filtering, and provides useful log statements. */ private class TakeCriteria { + private static final String MARKET_DESCRIPTION = "Sell BTC"; + private final BigDecimal currentMarketPrice; @Getter private final BigDecimal targetPrice; - private final Supplier marketDescription = () -> { - if (isXmr.test(currencyCode)) - return "Sell XMR (Buy BTC)"; - else - return "Sell BTC"; - }; - public TakeCriteria() { this.currentMarketPrice = getCurrentMarketPrice(currencyCode); this.targetPrice = calcTargetPrice(maxMarketPriceMargin, currentMarketPrice, currencyCode); @@ -368,39 +283,39 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot { .filter(isMakerPreferredTradingPeer) .filter(o -> isMarginLEMaxMarketPriceMargin.test(o, maxMarketPriceMargin) || isFixedPriceLEMaxMarketPriceMargin.test(o, currentMarketPrice)) - .filter(isWithinBTCAmountBounds) + .filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount())) .findFirst(); else return offers.stream() .filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount())) .filter(o -> isMarginLEMaxMarketPriceMargin.test(o, maxMarketPriceMargin) || isFixedPriceLEMaxMarketPriceMargin.test(o, currentMarketPrice)) - .filter(isWithinBTCAmountBounds) + .filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount())) .findFirst(); } void printCriteriaSummary() { if (isZero.test(maxMarketPriceMargin)) { - log.info("Looking for offers to {}, priced at or lower than the current market price {} {}.", - marketDescription.get(), + log.info("Looking for offers to {}, priced at or lower than the current market price of {} {}.", + MARKET_DESCRIPTION, currentMarketPrice, - isXmr.test(currencyCode) ? "BTC" : currencyCode); + currencyCode); } else { - log.info("Looking for offers to {}, priced at or less than {}% {} the current market price {} {}.", - marketDescription.get(), + log.info("Looking for offers to {}, priced at or lower than {}% {} the current market price of {} {}.", + MARKET_DESCRIPTION, maxMarketPriceMargin.abs(), // Hide the sign, text explains target price % "above or below". - aboveOrBelowMarketPrice.apply(maxMarketPriceMargin), + aboveOrBelowMaxMarketPriceMargin.apply(maxMarketPriceMargin), currentMarketPrice, - isXmr.test(currencyCode) ? "BTC" : currencyCode); + currencyCode); } } void printOffersAgainstCriteria(List offers) { log.info("Currently available {} offers -- want to take {} offer with price <= {} {}.", - marketDescription.get(), + MARKET_DESCRIPTION, currencyCode, targetPrice, - isXmr.test(currencyCode) ? "BTC" : currencyCode); + currencyCode); printOffersSummary(offers); } @@ -417,23 +332,22 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot { iHavePreferredTradingPeers.get() ? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO" : "N/A"); - var marginPriceLabel = format("Is offer's price margin (%s%%) <= bot's max market price margin (%s%%)?", - offer.getMarketPriceMarginPct(), - maxMarketPriceMargin); - filterResultsByLabel.put(marginPriceLabel, - offer.getUseMarketBasedPrice() - ? isMarginLEMaxMarketPriceMargin.test(offer, maxMarketPriceMargin) - : "N/A"); - var fixedPriceLabel = format("Is offer's fixed-price (%s) <= bot's target price (%s)?", - offer.getUseMarketBasedPrice() ? "N/A" : offer.getPrice() + " " + currencyCode, - offer.getUseMarketBasedPrice() ? "N/A" : targetPrice + " " + currencyCode); - filterResultsByLabel.put(fixedPriceLabel, - offer.getUseMarketBasedPrice() - ? "N/A" - : isFixedPriceLEMaxMarketPriceMargin.test(offer, currentMarketPrice)); + + if (offer.getUseMarketBasedPrice()) { + var marginPriceLabel = format("Is offer's price margin (%s%%) <= bot's max market price margin (%s%%)?", + offer.getMarketPriceMarginPct(), + maxMarketPriceMargin); + filterResultsByLabel.put(marginPriceLabel, isMarginLEMaxMarketPriceMargin.test(offer, maxMarketPriceMargin)); + } else { + var fixedPriceLabel = format("Is offer's fixed-price (%s) <= bot's target price (%s)?", + offer.getPrice() + " " + currencyCode, + targetPrice + " " + currencyCode); + filterResultsByLabel.put(fixedPriceLabel, isFixedPriceLEMaxMarketPriceMargin.test(offer, currentMarketPrice)); + } + String btcAmountBounds = format("%s BTC - %s BTC", minAmount, maxAmount); filterResultsByLabel.put("Is offer's BTC amount within bot amount bounds (" + btcAmountBounds + ")?", - isWithinBTCAmountBounds.test(offer)); + isWithinBTCAmountBounds(offer, getMinAmount(), getMaxAmount())); var title = format("%s offer %s filter results:", offer.getUseMarketBasedPrice() ? "Margin based" : "Fixed price", diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java new file mode 100644 index 0000000..06a725e --- /dev/null +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java @@ -0,0 +1,329 @@ +/* + * 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.bots; + +import bisq.proto.grpc.OfferInfo; +import io.grpc.StatusRuntimeException; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import protobuf.PaymentAccount; + +import java.math.BigDecimal; +import java.util.*; +import java.util.function.BiPredicate; + +import static bisq.bots.BotUtils.*; +import static java.lang.String.format; +import static java.math.RoundingMode.HALF_UP; +import static protobuf.OfferDirection.BUY; + +/** + * The use case for the TakeBestPricedOfferToSellXmr bot is to buy XMR with BTC at a low BTC price, e.g.: + *

+ *      Take an offer to sell you XMR for BTC, at no more than #.##% above or below current market price if:
+ *          the offer maker is a preferred trading peer,
+ *          and the offer's BTC amount is between 0.50 and 1.00 BTC,
+ *          and the current transaction mining fee rate is below 15 sats / byte.
+ *
+ * Usage:  TakeBestPricedOfferToSellXmr  --password=api-password --port=api-port \
+ *                          [--conf=take-best-priced-offer-to-sell-xmr.conf] \
+ *                          [--dryrun=true|false]
+ *                          [--simulate-regtest-payment=true|false]
+ * 
+ *

+ * The criteria for determining which offers to take are defined in the bot's configuration file + * TakeBestPricedOfferToSellXmr.properties (located in project's src/main/resources directory). The individual + * configurations are commented in the existing TakeBestPricedOfferToSellXmr.properties, which should be used as a + * template for your own use case. + */ +@Slf4j +@Getter +public class TakeBestPricedOfferToSellXmr extends AbstractBot { + + // Taker bot's XMR payment account trading currency code. + private static final String CURRENCY_CODE = "XMR"; + + // Config file: resources/TakeBestPricedOfferToSellXmr.properties. + private final Properties configFile; + // Taker bot's XMR payment account (if the configured paymentAccountId is valid). + private final PaymentAccount paymentAccount; + // Taker bot's maximum market price margin. A takeable offer's price margin (%) must be <= maxMarketPriceMargin (%). + private final BigDecimal maxMarketPriceMargin; + // Taker bot's min BTC amount to trade. A takeable offer's amount must be >= minAmount BTC. + private final BigDecimal minAmount; + // Taker bot's max BTC amount to trade. A takeable offer's amount must be <= maxAmount BTC. + private final BigDecimal maxAmount; + // Taker bot's max acceptable transaction fee rate. + private final long maxTxFeeRate; + // Taker bot's trading fee currency code (BSQ or BTC). + private final String bisqTradeFeeCurrency; + // Maximum # of offers to take during one bot session (shut down bot after taking N offers). + private final int maxTakeOffers; + + // Offer polling frequency must be > 1000 ms between each getoffers request. + private final long pollingInterval; + + // The # of offers taken during the bot session (since startup). + private int numOffersTaken = 0; + + public TakeBestPricedOfferToSellXmr(String[] args) { + super(args); + pingDaemon(new Date().getTime()); // Shut down now if API daemon is not available. + this.configFile = loadConfigFile(); + this.paymentAccount = getPaymentAccount(configFile.getProperty("paymentAccountId")); + this.maxMarketPriceMargin = new BigDecimal(configFile.getProperty("maxMarketPriceMargin")) + .setScale(2, HALF_UP); + this.minAmount = new BigDecimal(configFile.getProperty("minAmount")); + this.maxAmount = new BigDecimal(configFile.getProperty("maxAmount")); + this.maxTxFeeRate = Long.parseLong(configFile.getProperty("maxTxFeeRate")); + this.bisqTradeFeeCurrency = configFile.getProperty("bisqTradeFeeCurrency"); + this.maxTakeOffers = Integer.parseInt(configFile.getProperty("maxTakeOffers")); + loadPreferredOnionAddresses.accept(configFile, preferredTradingPeers); + this.pollingInterval = Long.parseLong(configFile.getProperty("pollingInterval")); + } + + /** + * Checks for the most attractive offer to take every {@link #pollingInterval} ms. After {@link #maxTakeOffers} + * are taken, bot will stop the API daemon, then shut itself down, prompting the user to start the desktop UI + * to complete the trade. + */ + @Override + public void run() { + var startTime = new Date().getTime(); + validateWalletPassword(walletPassword); + validatePollingInterval(pollingInterval); + validateTradeFeeCurrencyCode(bisqTradeFeeCurrency); + validatePaymentAccount(paymentAccount, CURRENCY_CODE); + printBotConfiguration(); + + while (!isShutdown) { + if (!isBisqNetworkTxFeeRateLowEnough.test(maxTxFeeRate)) { + runCountdown(log, pollingInterval); + continue; + } + + // Get all available and takeable buy BTC for XMR offers, sorted by price ascending. + // The list may contain both fixed-price and market price margin based offers. + var offers = getOffers(BUY.name(), CURRENCY_CODE).stream() + .filter(o -> !isAlreadyTaken.test(o)) + .toList(); + + if (offers.isEmpty()) { + log.info("No takeable offers found."); + runCountdown(log, pollingInterval); + continue; + } + + // Define criteria for taking an offer, based on conf file. + TakeCriteria takeCriteria = new TakeCriteria(); + takeCriteria.printCriteriaSummary(); + takeCriteria.printOffersAgainstCriteria(offers); + + // Find takeable offer based on criteria. + Optional selectedOffer = takeCriteria.findTakeableOffer(offers); + // Try to take the offer, if found, or say 'no offer found' before going to sleep. + selectedOffer.ifPresentOrElse(offer -> takeOffer(takeCriteria, offer), + () -> { + var cheapestOffer = offers.get(0); + log.info("No acceptable offer found. Closest possible candidate did not pass filters:"); + takeCriteria.printOfferAgainstCriteria(cheapestOffer); + }); + + printDryRunProgress(); + runCountdown(log, pollingInterval); + pingDaemon(startTime); + } + } + + /** + * Attempt to take the available offer according to configured criteria. If successful, will block until a new + * trade is fully initialized with a trade contract. Otherwise, handles a non-fatal error and allows the bot to + * stay alive, or shuts down the bot upon fatal error. + */ + private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) { + log.info("Will attempt to take offer '{}'.", offer.getId()); + takeCriteria.printOfferAgainstCriteria(offer); + if (isDryRun) { + addToOffersTaken(offer); + numOffersTaken++; + maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); + } else { + // An encrypted wallet must be unlocked before calling takeoffer and gettrade. + // Unlock the wallet for 5 minutes. If the wallet is already unlocked, + // this command will override the timeout of the previous unlock command. + try { + unlockWallet(walletPassword, 600); + printBTCBalances("BTC Balances Before Take Offer Attempt"); + // Blocks until new trade is prepared, or times out. + takeV1ProtocolOffer(offer, paymentAccount, bisqTradeFeeCurrency, pollingInterval); + printBTCBalances("BTC Balances After Take Offer Attempt"); + + if (canSimulatePaymentSteps) { + var newTrade = getTrade(offer.getId()); + RegtestTradePaymentSimulator tradePaymentSimulator = new RegtestTradePaymentSimulator(args, + newTrade.getTradeId(), + paymentAccount); + tradePaymentSimulator.run(); + printBTCBalances("BTC Balances After Simulated Trade Completion"); + } + numOffersTaken++; + maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); + } catch (NonFatalException nonFatalException) { + handleNonFatalException(nonFatalException, pollingInterval); + } catch (StatusRuntimeException fatalException) { + shutdownAfterTakeOfferFailure(fatalException); + } + } + } + + /** + * Return true is fixed-price offer's price <= the bot's max market price margin. Allows bot to take a + * fixed-priced offer if the price is <= {@link #maxMarketPriceMargin} (%) of the current market price. + */ + protected final BiPredicate isFixedPriceLEMaxMarketPriceMargin = + (offer, currentMarketPrice) -> BotUtils.isFixedPriceLEMaxMarketPriceMargin( + offer, + currentMarketPrice, + this.getMaxMarketPriceMargin()); + + private void printBotConfiguration() { + var configsByLabel = new LinkedHashMap(); + configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion()); + var network = getNetwork(); + configsByLabel.put("BTC Network:", network); + configsByLabel.put("My Payment Account:", ""); + configsByLabel.put("\tPayment Account Id:", paymentAccount.getId()); + configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName()); + configsByLabel.put("\tCurrency Code:", CURRENCY_CODE); + configsByLabel.put("Trading Rules:", ""); + configsByLabel.put("\tMax # of offers bot can take:", maxTakeOffers); + configsByLabel.put("\tMax Tx Fee Rate:", maxTxFeeRate + " sats/byte"); + configsByLabel.put("\tMax Market Price Margin:", maxMarketPriceMargin + "%"); + configsByLabel.put("\tMin BTC Amount:", minAmount + " BTC"); + configsByLabel.put("\tMax BTC Amount: ", maxAmount + " BTC"); + if (iHavePreferredTradingPeers.get()) { + configsByLabel.put("\tPreferred Trading Peers:", preferredTradingPeers.toString()); + } else { + configsByLabel.put("\tPreferred Trading Peers:", "N/A"); + } + configsByLabel.put("Bot Polling Interval:", pollingInterval + " ms"); + log.info(toTable.apply("Bot Configuration", configsByLabel)); + } + + public static void main(String[] args) { + TakeBestPricedOfferToSellXmr bot = new TakeBestPricedOfferToSellXmr(args); + bot.run(); + } + + /** + * Calculates additional takeoffer criteria based on conf file values, + * performs candidate offer filtering, and provides useful log statements. + */ + private class TakeCriteria { + private static final String MARKET_DESCRIPTION = "Sell XMR (Buy BTC)"; + + private final BigDecimal currentMarketPrice; + @Getter + private final BigDecimal targetPrice; + + public TakeCriteria() { + this.currentMarketPrice = getCurrentMarketPrice(CURRENCY_CODE); + this.targetPrice = calcTargetPrice(maxMarketPriceMargin, currentMarketPrice, CURRENCY_CODE); + } + + /** + * Returns the lowest priced offer passing the filters, or Optional.empty() if not found. + * The max tx fee rate filtering should have passed prior to calling this method. + * + * @param offers to filter + */ + Optional findTakeableOffer(List offers) { + if (iHavePreferredTradingPeers.get()) + return offers.stream() + .filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount())) + .filter(isMakerPreferredTradingPeer) + .filter(o -> isMarginLEMaxMarketPriceMargin.test(o, maxMarketPriceMargin) + || isFixedPriceLEMaxMarketPriceMargin.test(o, currentMarketPrice)) + .filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount())) + .findFirst(); + else + return offers.stream() + .filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount())) + .filter(o -> isMarginLEMaxMarketPriceMargin.test(o, maxMarketPriceMargin) + || isFixedPriceLEMaxMarketPriceMargin.test(o, currentMarketPrice)) + .filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount())) + .findFirst(); + } + + void printCriteriaSummary() { + if (isZero.test(maxMarketPriceMargin)) { + log.info("Looking for offers to {}, priced at or lower than the current market price of {} BTC.", + MARKET_DESCRIPTION, + currentMarketPrice); + } else { + log.info("Looking for offers to {}, priced at or lower than {}% {} the current market price of {} BTC.", + MARKET_DESCRIPTION, + maxMarketPriceMargin.abs(), // Hide the sign, text explains target price % "above or below". + aboveOrBelowMaxMarketPriceMargin.apply(maxMarketPriceMargin), + currentMarketPrice); + } + } + + void printOffersAgainstCriteria(List offers) { + log.info("Currently available {} offers -- want to take {} offer with price <= {} BTC.", + MARKET_DESCRIPTION, + CURRENCY_CODE, + targetPrice); + printOffersSummary(offers); + } + + void printOfferAgainstCriteria(OfferInfo offer) { + printOfferSummary(offer); + + var filterResultsByLabel = new LinkedHashMap(); + filterResultsByLabel.put("Current Market Price:", currentMarketPrice + " " + CURRENCY_CODE); + filterResultsByLabel.put("Target Price (Max):", targetPrice + " " + CURRENCY_CODE); + filterResultsByLabel.put("Offer Price:", offer.getPrice() + " " + CURRENCY_CODE); + filterResultsByLabel.put("Offer maker used same payment method?", + usesSamePaymentMethod.test(offer, getPaymentAccount())); + filterResultsByLabel.put("Is offer maker a preferred trading peer?", + iHavePreferredTradingPeers.get() + ? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO" + : "N/A"); + + if (offer.getUseMarketBasedPrice()) { + var marginPriceLabel = format("Is offer's price margin (%s%%) <= bot's max market price margin (%s%%)?", + offer.getMarketPriceMarginPct(), + maxMarketPriceMargin); + filterResultsByLabel.put(marginPriceLabel, isMarginLEMaxMarketPriceMargin.test(offer, maxMarketPriceMargin)); + } else { + var fixedPriceLabel = format("Is offer's fixed-price (%s) <= bot's target price (%s)?", + offer.getPrice() + " BTC", + targetPrice + " BTC"); + filterResultsByLabel.put(fixedPriceLabel, isFixedPriceLEMaxMarketPriceMargin.test(offer, currentMarketPrice)); + } + String btcAmountBounds = format("%s BTC - %s BTC", minAmount, maxAmount); + filterResultsByLabel.put("Is offer's BTC amount within bot amount bounds (" + btcAmountBounds + ")?", + isWithinBTCAmountBounds(offer, getMinAmount(), getMaxAmount())); + + var title = format("%s offer %s filter results:", + offer.getUseMarketBasedPrice() ? "Margin based" : "Fixed price", + offer.getId()); + log.info(toTable.apply(title, filterResultsByLabel)); + } + } +} diff --git a/java-examples/src/main/resources/TakeBestPricedOfferToBuyXmr.properties b/java-examples/src/main/resources/TakeBestPricedOfferToBuyXmr.properties new file mode 100644 index 0000000..365074d --- /dev/null +++ b/java-examples/src/main/resources/TakeBestPricedOfferToBuyXmr.properties @@ -0,0 +1,28 @@ +# Maximum # of offers to take during one bot session. When reached, bot will shut down (but not the API daemon). +maxTakeOffers=50 +# +# Taker bot's payment account id. Only SELL BTC offers using the same payment method will be considered for taking. +paymentAccountId=a15d0f15-e355-4b89-8322-e262097623ae +# +# Taker bot's min market price margin. A candidate sell BTC offer's price margin must be >= minMarketPriceMargin. +minMarketPriceMargin=0 +# +# Taker bot's min BTC amount to sell. The candidate SELL BTC offer's amount must be >= minAmount BTC. +minAmount=0.01 +# +# Taker bot's max BTC amount to sell. The candidate SELL BTC offer's amount must be <= maxAmount BTC. +maxAmount=0.90 +# +# Taker bot's max acceptable transaction fee rate (sats / byte). +# Regtest fee rates are from https://price.bisq.wiz.biz/getFees +maxTxFeeRate=25 +# +# Bisq trade fee currency code (BSQ or BTC). +bisqTradeFeeCurrency=BSQ +# +# Taker bot's list of preferred trading peers (their onion addresses). +# If you do not want to constrict trading to preferred peers, comment this line out with a '#' character. +preferredTradingPeers=localhost:8888 +# +# Offer polling frequency must be >= 1s (1000ms) between each getoffers request. +pollingInterval=30000 diff --git a/java-examples/src/main/resources/TakeBestPricedOfferToSellXmr.properties b/java-examples/src/main/resources/TakeBestPricedOfferToSellXmr.properties new file mode 100644 index 0000000..28326d2 --- /dev/null +++ b/java-examples/src/main/resources/TakeBestPricedOfferToSellXmr.properties @@ -0,0 +1,28 @@ +# Maximum # of offers to take during one bot session. When reached, bot will shut down (but not the API daemon). +maxTakeOffers=1 +# +# Taker bot's payment account id. Only SELL BTC offers using the same payment method will be considered for taking. +paymentAccountId=a15d0f15-e355-4b89-8322-e262097623ae +# +# Taker bot's max market price margin. A candidate buy BTC offer's price margin must be <= maxMarketPriceMargin. +maxMarketPriceMargin=1.10 +# +# Taker bot's min BTC amount to buy. The candidate buy BTC offer's amount must be >= minAmount BTC. +minAmount=0.01 +# +# Taker bot's max BTC amount to buy. The candidate buy BTC offer's amount must be <= maxAmount BTC. +maxAmount=0.90 +# +# Taker bot's max acceptable transaction fee rate (sats / byte). +# Regtest fee rates are from https://price.bisq.wiz.biz/getFees +maxTxFeeRate=25 +# +# Bisq trade fee currency code (BSQ or BTC). +bisqTradeFeeCurrency=BSQ +# +# Taker bot's list of preferred trading peers (their onion addresses). +# If you do not want to constrict trading to preferred peers, comment this line out with a '#' character. +preferredTradingPeers=localhost:8888 +# +# Offer polling frequency must be >= 1s (1000ms) between each getoffers request. +pollingInterval=30000 From 02000c0b854c2c90d80820b946de0f33d961cc70 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 28 Jun 2022 19:53:26 -0300 Subject: [PATCH 02/41] Create runnable jars for new XMR bots --- java-examples/scripts/create-bot-jars.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/java-examples/scripts/create-bot-jars.sh b/java-examples/scripts/create-bot-jars.sh index 183ab93..6d1eb95 100755 --- a/java-examples/scripts/create-bot-jars.sh +++ b/java-examples/scripts/create-bot-jars.sh @@ -45,6 +45,9 @@ extractdistribution ./create-runnable-jar.sh "$GRADLE_DIST_NAME" bisq.bots.TakeBestPricedOfferToBuyBtc ./create-runnable-jar.sh "$GRADLE_DIST_NAME" bisq.bots.TakeBestPricedOfferToSellBtc +./create-runnable-jar.sh "$GRADLE_DIST_NAME" bisq.bots.TakeBestPricedOfferToBuyXmr +./create-runnable-jar.sh "$GRADLE_DIST_NAME" bisq.bots.TakeBestPricedOfferToSellXmr + ./create-runnable-jar.sh "$GRADLE_DIST_NAME" bisq.bots.TakeBestPricedOfferToBuyBsq ./create-runnable-jar.sh "$GRADLE_DIST_NAME" bisq.bots.TakeBestPricedOfferToSellBsq From 1f09060975a6a19581ebc838453c53542dccb6dc Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 29 Jun 2022 15:30:50 -0300 Subject: [PATCH 03/41] Extend option help col-width (no wrapping) --- .../src/main/java/bisq/bots/Config.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/Config.java b/java-examples/src/main/java/bisq/bots/Config.java index 7438073..64a1c97 100644 --- a/java-examples/src/main/java/bisq/bots/Config.java +++ b/java-examples/src/main/java/bisq/bots/Config.java @@ -17,6 +17,7 @@ package bisq.bots; +import joptsimple.BuiltinHelpFormatter; import joptsimple.OptionParser; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -63,12 +64,12 @@ public class Config { .withRequiredArg(); var confOpt = parser.accepts("conf", "Bot configuration file (required)") .withRequiredArg(); - var dryRunOpt = parser.accepts("dryrun", "Pretend to take an offer (default=false)") + var dryRunOpt = parser.accepts("dryrun", "Pretend to take an offer") .withRequiredArg() .ofType(boolean.class) .defaultsTo(FALSE); var simulateRegtestPaymentStepsOpt = - parser.accepts("simulate-regtest-payment", "Simulate regtest payment steps (default=false)") + parser.accepts("simulate-regtest-payment", "Simulate regtest payment steps") .withOptionalArg() .ofType(boolean.class) .defaultsTo(FALSE); @@ -126,10 +127,23 @@ public class Config { stream.println(); stream.println("Usage: ScriptName [options]"); stream.println(); + parser.formatHelpWith(new HelpFormatter(90, 2)); parser.printHelpOn(stream); stream.println(); } catch (IOException ex) { ex.printStackTrace(stream); } } + + private static class HelpFormatter extends BuiltinHelpFormatter { + /** + * Makes a formatter with a given overall row width and column separator width. + * + * @param desiredOverallWidth how many characters wide to make the overall help display + * @param desiredColumnSeparatorWidth how many characters wide to make the separation between option column and + */ + public HelpFormatter(int desiredOverallWidth, int desiredColumnSeparatorWidth) { + super(desiredOverallWidth, desiredColumnSeparatorWidth); + } + } } From c9443d1472db1166d230f0a8bb2f2033fe80cac6 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 29 Jun 2022 15:32:38 -0300 Subject: [PATCH 04/41] Fill out TakeBestPricedOfferToSellXmr class level doc --- .../bots/TakeBestPricedOfferToSellXmr.java | 58 ++++++++++++++----- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java index 06a725e..153d0d2 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java @@ -32,23 +32,49 @@ import static java.math.RoundingMode.HALF_UP; import static protobuf.OfferDirection.BUY; /** - * The use case for the TakeBestPricedOfferToSellXmr bot is to buy XMR with BTC at a low BTC price, e.g.: - *

- *      Take an offer to sell you XMR for BTC, at no more than #.##% above or below current market price if:
- *          the offer maker is a preferred trading peer,
- *          and the offer's BTC amount is between 0.50 and 1.00 BTC,
- *          and the current transaction mining fee rate is below 15 sats / byte.
- *
- * Usage:  TakeBestPricedOfferToSellXmr  --password=api-password --port=api-port \
- *                          [--conf=take-best-priced-offer-to-sell-xmr.conf] \
- *                          [--dryrun=true|false]
- *                          [--simulate-regtest-payment=true|false]
- * 
+ * This bot's general use case is to buy XMR with BTC at a low BTC price. It periodically checks the Sell XMR (Buy BTC) + * market, and takes a configured number of offers to sell you XMR according to criteria you define in the bot's + * configuration file TakeBestPricedOfferToSellXmr.properties (located in project's src/main/resources directory). + * You will need to replace the default values in the configuration file for your use cases. + *


+ * After the maximum number of offers have been taken (good to start with 1), the bot will shut down the API daemon, + * then the bot itself. You have to confirm the offer maker's XMR payment(s) offline, then complete the trade(s) in + * the Bisq Desktop application. *

- * The criteria for determining which offers to take are defined in the bot's configuration file - * TakeBestPricedOfferToSellXmr.properties (located in project's src/main/resources directory). The individual - * configurations are commented in the existing TakeBestPricedOfferToSellXmr.properties, which should be used as a - * template for your own use case. + * Here is one possible use case: + *

+ *  Take 1 offer to sell you XMR for BTC, priced no higher than 0.00% above or below current market price if:
+ *
+ *          the offer's BTC amount is between 0.50 and 1.00 BTC
+ *          the offer maker is one of two preferred trading peers
+ *          the current transaction mining fee rate is below 15 sats / byte
+ *
+ *  The bot configurations for these rules are set in TakeBestPricedOfferToSellXmr.properties as follows:
+ *
+ *          maxTakeOffers=1
+ *          maxMarketPriceMargin=0.00
+ *          minAmount=0.50
+ *          maxAmount=1.00
+ *          preferredTradingPeers=preferred-address-1.onion:9999,preferred-address-2.onion:9999
+ *          maxTxFeeRate=15
+ * 
+ * Usage + *


+ * There are some {@link bisq.bots.Config program options} common to all the Java bot examples, passed on the command + * line. The only one required every time the bot is run is your API daemon's password option: `--password `. + * The bot will prompt you for your wallet-password in the console. + *


+ * You can pass the '--dryrun=true' option to the program to see what offers your bot would take with a given + * configuration. This will help you avoid taking offers by mistake. + *

+ *     TakeBestPricedOfferToSellXmr  --password=api-password --port=api-port [--dryrun=true|false]
+ * 
+ * If your API daemon is running on a local regtest network (with a trading peer), you can pass the + * '--simulate-regtest-payment=true' option to the program to simulate the full trade protocol. The bot will print + * your regtest trading peer's CLI commands will be printed on the console, for you to copy/paste into another terminal. + *
+ *     TakeBestPricedOfferToSellXmr  --password=api-password --port=api-port [--simulate-regtest-payment=true|false]
+ * 
*/ @Slf4j @Getter From 897401ba0e987bca4839485306310a214db1f9ef Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 29 Jun 2022 18:05:37 -0300 Subject: [PATCH 05/41] Add null checks to print payacct/offer/trade utils --- .../src/main/java/bisq/bots/BotUtils.java | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/BotUtils.java b/java-examples/src/main/java/bisq/bots/BotUtils.java index ed62585..8bbc236 100644 --- a/java-examples/src/main/java/bisq/bots/BotUtils.java +++ b/java-examples/src/main/java/bisq/bots/BotUtils.java @@ -42,6 +42,7 @@ import static java.lang.String.format; import static java.lang.System.*; import static java.math.BigDecimal.ZERO; import static java.math.RoundingMode.HALF_UP; +import static java.util.Objects.requireNonNull; /** * Convenience methods and functions not depending on a bot's state nor the need to send requests to the API daemon. @@ -407,6 +408,7 @@ public class BotUtils { * @param offer printed offer */ public static void printOfferSummary(OfferInfo offer) { + requireNonNull(offer, "OfferInfo offer param cannot be null."); new TableBuilder(OFFER_TBL, offer).build().print(out); } @@ -416,7 +418,12 @@ public class BotUtils { * @param offers printed offer list */ public static void printOffersSummary(List offers) { - new TableBuilder(OFFER_TBL, offers).build().print(out); + requireNonNull(offers, "List offers param cannot be null."); + if (offers.isEmpty()) { + log.info("No offers to print."); + } else { + new TableBuilder(OFFER_TBL, offers).build().print(out); + } } /** @@ -425,6 +432,7 @@ public class BotUtils { * @param trade printed trade */ public static void printTradeSummary(TradeInfo trade) { + requireNonNull(trade, "TradeInfo trade param cannot be null."); new TableBuilder(TRADE_DETAIL_TBL, trade).build().print(out); } @@ -435,10 +443,15 @@ public class BotUtils { * @param trades list of trades */ public static void printTradesSummary(GetTradesRequest.Category category, List trades) { - switch (category) { - case CLOSED -> new TableBuilder(CLOSED_TRADES_TBL, trades).build().print(out); - case FAILED -> new TableBuilder(FAILED_TRADES_TBL, trades).build().print(out); - default -> new TableBuilder(OPEN_TRADES_TBL, trades).build().print(out); + requireNonNull(trades, "List trades param cannot be null."); + if (trades.isEmpty()) { + log.info("No trades to print."); + } else { + switch (category) { + case CLOSED -> new TableBuilder(CLOSED_TRADES_TBL, trades).build().print(out); + case FAILED -> new TableBuilder(FAILED_TRADES_TBL, trades).build().print(out); + default -> new TableBuilder(OPEN_TRADES_TBL, trades).build().print(out); + } } } @@ -448,6 +461,7 @@ public class BotUtils { * @param paymentAccount the printed PaymentAccount */ public static void printPaymentAccountSummary(PaymentAccount paymentAccount) { + requireNonNull(paymentAccount, "PaymentAccount paymentAccount param cannot be null."); new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount).build().print(out); } From 9307c08d4ded8d4f1ecc50227526151c77f9733b Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 29 Jun 2022 18:07:34 -0300 Subject: [PATCH 06/41] Treat some wallet-is-locked errors as trivial --- .../src/main/java/bisq/bots/AbstractBot.java | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/AbstractBot.java b/java-examples/src/main/java/bisq/bots/AbstractBot.java index 48425f0..94d8413 100644 --- a/java-examples/src/main/java/bisq/bots/AbstractBot.java +++ b/java-examples/src/main/java/bisq/bots/AbstractBot.java @@ -345,6 +345,19 @@ public abstract class AbstractBot { } } + /** + * Return true if gRPC StatusRuntimeException indicates wallet is locked. Sometimes this is a trivial error. + * + * @param grpcException a StatusRuntimeException + * @return true if gRPC StatusRuntimeException indicates wallet is locked + */ + protected boolean walletIsLocked(StatusRuntimeException grpcException) { + var errorMsg = toCleanErrorMessage.apply(grpcException).toLowerCase(); + return (exceptionHasStatus.test(grpcException, FAILED_PRECONDITION) + && errorMsg.contains("wallet") + && errorMsg.contains("locked")); + } + /** * Attempt to unlock the wallet for 1 second using the given password, and shut down the bot if the * password check fails for any reason. @@ -575,6 +588,22 @@ public abstract class AbstractBot { BotUtils.printTradesSummary(category, trades); } + /** + * Print the completed trades since midnight today, if the wallet is unlocked, else log an error message. + */ + protected void printTradesSummaryForTodayIfWalletIsUnlocked() { + try { + log.info("Here are today's completed trades:"); + printTradesSummaryForToday(CLOSED); + } catch (StatusRuntimeException grpcException) { + if (walletIsLocked(grpcException)) { + log.error("Cannot retrieve trades while API daemon's wallet is locked."); + } else { + throw grpcException; + } + } + } + /** * Print list of today's trade summaries to stdout. * @@ -813,9 +842,7 @@ public abstract class AbstractBot { * @param maxTakeOffers the max number of offers that can be taken during bot run */ protected void maybeShutdownAfterSuccessfulTradeCreation(int numOffersTaken, int maxTakeOffers) { - log.info("Here are today's completed trades:"); - printTradesSummaryForToday(CLOSED); - + printTradesSummaryForTodayIfWalletIsUnlocked(); if (!isDryRun) { // If the bot is not in dryrun mode, lock the wallet. If dryrun=true, leave the wallet unlocked until the // timeout expires, so the user can look at data in the daemon with the CLI (a convenience in dev/test). From 8ed39eb8bf8102100f2990ceed3f13f89f49ce70 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 29 Jun 2022 18:50:57 -0300 Subject: [PATCH 07/41] Improve some of the progress logging --- .../src/main/java/bisq/bots/AbstractBot.java | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/AbstractBot.java b/java-examples/src/main/java/bisq/bots/AbstractBot.java index 94d8413..6c4936e 100644 --- a/java-examples/src/main/java/bisq/bots/AbstractBot.java +++ b/java-examples/src/main/java/bisq/bots/AbstractBot.java @@ -710,8 +710,7 @@ public abstract class AbstractBot { * Print information about offers taken during bot simulation. */ protected void printDryRunProgress() { - if (isDryRun && offersTakenDuringDryRun.size() > 0) { - log.info("You have \"taken\" {} offer(s) during dry run:", offersTakenDuringDryRun.size()); + if (isDryRun && !offersTakenDuringDryRun.isEmpty()) { printOffersSummary(offersTakenDuringDryRun); } } @@ -721,8 +720,6 @@ public abstract class AbstractBot { */ protected void addToOffersTaken(OfferInfo offer) { offersTakenDuringDryRun.add(offer); - printOfferSummary(offer); - log.info("Did not actually take that offer during this simulation."); } /** @@ -828,7 +825,7 @@ public abstract class AbstractBot { } /** - * Print the day's completed trades since midnight today, then: + * Print the day's completed trades since midnight today, and number of offers taken during the bot session, then, *

* If numOffersTaken >= maxTakeOffers, and trade completion is being simulated on regtest, shut down the bot. *

@@ -843,15 +840,15 @@ public abstract class AbstractBot { */ protected void maybeShutdownAfterSuccessfulTradeCreation(int numOffersTaken, int maxTakeOffers) { printTradesSummaryForTodayIfWalletIsUnlocked(); - if (!isDryRun) { - // If the bot is not in dryrun mode, lock the wallet. If dryrun=true, leave the wallet unlocked until the - // timeout expires, so the user can look at data in the daemon with the CLI (a convenience in dev/test). - try { - lockWallet(); - } catch (NonFatalException ex) { - log.warn(ex.getMessage()); - } + + log.info("You {} have taken {} offer(s) during this bot's {}", + isDryRun ? "would" : "", + numOffersTaken, + isDryRun ? "dryrun:" : "session."); + if (isDryRun) { + printDryRunProgress(); } + if (numOffersTaken >= maxTakeOffers) { isShutdown = true; if (canSimulatePaymentSteps) { @@ -868,8 +865,6 @@ public abstract class AbstractBot { stopDaemon(); } exit(0); - } else { - log.info("You have taken {} offers during this bot session.", numOffersTaken); } } From e87d3e31918d04916c8da1cbdaf3ab148cf72797 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 29 Jun 2022 19:06:36 -0300 Subject: [PATCH 08/41] Move some log statements to debug level --- .../src/main/java/bisq/bots/AbstractBot.java | 12 ++++++++++-- java-examples/src/main/java/bisq/bots/GrpcStubs.java | 6 ++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/AbstractBot.java b/java-examples/src/main/java/bisq/bots/AbstractBot.java index 6c4936e..1aaf28c 100644 --- a/java-examples/src/main/java/bisq/bots/AbstractBot.java +++ b/java-examples/src/main/java/bisq/bots/AbstractBot.java @@ -890,7 +890,11 @@ public abstract class AbstractBot { try { var defaultFilename = defaultPropertiesFilename.get(); properties.load(this.getClass().getClassLoader().getResourceAsStream(defaultFilename)); - log.info("Internal configuration loaded from {}.", defaultFilename); + if (this instanceof RegtestTradePaymentSimulator) { + log.debug("Internal configuration loaded from {}.", defaultFilename); + } else { + log.info("Internal configuration loaded from {}.", defaultFilename); + } } catch (Exception ex) { throw new IllegalStateException(ex); } @@ -911,7 +915,11 @@ public abstract class AbstractBot { Properties properties = new java.util.Properties(); try { properties.load(is); - log.info("External configuration loaded from {}.", confFile.getAbsolutePath()); + if (this instanceof RegtestTradePaymentSimulator) { + log.debug("External configuration loaded from {}.", confFile.getAbsolutePath()); + } else { + log.info("External configuration loaded from {}.", confFile.getAbsolutePath()); + } return properties; } catch (FileNotFoundException ignored) { // Cannot happen here. Ignore FileNotFoundException because confFile.exists() == true. diff --git a/java-examples/src/main/java/bisq/bots/GrpcStubs.java b/java-examples/src/main/java/bisq/bots/GrpcStubs.java index 2f6d101..6a35aa2 100644 --- a/java-examples/src/main/java/bisq/bots/GrpcStubs.java +++ b/java-examples/src/main/java/bisq/bots/GrpcStubs.java @@ -63,9 +63,11 @@ final class GrpcStubs { public void close() { try { if (!channel.isShutdown()) { - log.info("Shutting down bot's grpc channel."); + log.debug("Shutting down bot's grpc channel."); channel.shutdown().awaitTermination(1, SECONDS); - log.info("Bot channel shutdown complete."); + log.debug("Bot channel shutdown complete."); + } else { + log.warn("Bot channel already shut down!"); } } catch (InterruptedException ex) { throw new IllegalStateException(ex); From 9cb798f427596a859cb0390a800829f9ad9805a1 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 29 Jun 2022 19:16:29 -0300 Subject: [PATCH 09/41] Back out log statement --- java-examples/src/main/java/bisq/bots/GrpcStubs.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/GrpcStubs.java b/java-examples/src/main/java/bisq/bots/GrpcStubs.java index 6a35aa2..73b19b7 100644 --- a/java-examples/src/main/java/bisq/bots/GrpcStubs.java +++ b/java-examples/src/main/java/bisq/bots/GrpcStubs.java @@ -66,8 +66,6 @@ final class GrpcStubs { log.debug("Shutting down bot's grpc channel."); channel.shutdown().awaitTermination(1, SECONDS); log.debug("Bot channel shutdown complete."); - } else { - log.warn("Bot channel already shut down!"); } } catch (InterruptedException ex) { throw new IllegalStateException(ex); From a355138b4713f091b614937c2edb4eb0fdfac82d Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 29 Jun 2022 19:26:08 -0300 Subject: [PATCH 10/41] Remove some verbose log statements, and a sleep. --- .../main/java/bisq/bots/RegtestTradePaymentSimulator.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/RegtestTradePaymentSimulator.java b/java-examples/src/main/java/bisq/bots/RegtestTradePaymentSimulator.java index f117ea9..c48e364 100644 --- a/java-examples/src/main/java/bisq/bots/RegtestTradePaymentSimulator.java +++ b/java-examples/src/main/java/bisq/bots/RegtestTradePaymentSimulator.java @@ -24,7 +24,6 @@ import protobuf.PaymentAccount; import java.util.Properties; import static bisq.bots.BotUtils.*; -import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED; import static io.grpc.Status.Code.PERMISSION_DENIED; /** @@ -113,11 +112,7 @@ public class RegtestTradePaymentSimulator extends AbstractBot { log.warn(copyPasteCliCommands); log.warn("##############################################################################"); - sleep(pollingInterval); - log.info("Trade is completed. Here are today's completed trades:"); - printTradesSummaryForToday(CLOSED); - - log.info("Closing {}'s gRPC channel.", this.getClass().getSimpleName()); + log.debug("Closing {}'s gRPC channel.", this.getClass().getSimpleName()); super.grpcStubs.close(); } From 195b62c4772fe9ef1a734c3813bac802067e563e Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 29 Jun 2022 19:28:13 -0300 Subject: [PATCH 11/41] Tidy up and javadoc the TakeBestPricedOfferToSellXmr bot --- .../bots/TakeBestPricedOfferToSellXmr.java | 32 ++++++++++++------- .../TakeBestPricedOfferToSellXmr.properties | 10 +++--- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java index 153d0d2..c7a8761 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java @@ -33,12 +33,12 @@ import static protobuf.OfferDirection.BUY; /** * This bot's general use case is to buy XMR with BTC at a low BTC price. It periodically checks the Sell XMR (Buy BTC) - * market, and takes a configured number of offers to sell you XMR according to criteria you define in the bot's - * configuration file TakeBestPricedOfferToSellXmr.properties (located in project's src/main/resources directory). + * market, and takes a configured maximum number of offers to sell you XMR according to criteria you define in the bot's + * configuration file: TakeBestPricedOfferToSellXmr.properties (located in project's src/main/resources directory). * You will need to replace the default values in the configuration file for your use cases. *


* After the maximum number of offers have been taken (good to start with 1), the bot will shut down the API daemon, - * then the bot itself. You have to confirm the offer maker's XMR payment(s) offline, then complete the trade(s) in + * then itself. You have to confirm the offer maker's XMR payment(s) outside Bisq, then complete the trade(s) in * the Bisq Desktop application. *

* Here is one possible use case: @@ -60,9 +60,13 @@ import static protobuf.OfferDirection.BUY; *

* Usage *


+ * You must encrypt your wallet password before running this bot. If it is not already, you can use the CLI: + *

+ *     $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
+ * 
* There are some {@link bisq.bots.Config program options} common to all the Java bot examples, passed on the command - * line. The only one required every time the bot is run is your API daemon's password option: `--password `. - * The bot will prompt you for your wallet-password in the console. + * line. The only one you must provide (no default value) is your API daemon's password option: + * `--password `. The bot will prompt you for your wallet-password in the console. *


* You can pass the '--dryrun=true' option to the program to see what offers your bot would take with a given * configuration. This will help you avoid taking offers by mistake. @@ -169,7 +173,6 @@ public class TakeBestPricedOfferToSellXmr extends AbstractBot { takeCriteria.printOfferAgainstCriteria(cheapestOffer); }); - printDryRunProgress(); runCountdown(log, pollingInterval); pingDaemon(startTime); } @@ -183,16 +186,21 @@ public class TakeBestPricedOfferToSellXmr extends AbstractBot { private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) { log.info("Will attempt to take offer '{}'.", offer.getId()); takeCriteria.printOfferAgainstCriteria(offer); + + // An encrypted wallet must be unlocked before calling takeoffer and gettrade(s). + // Unlock the wallet for 5 minutes. If the wallet is already unlocked, this request + // will override the timeout of the previous unlock request. + try { + unlockWallet(walletPassword, 300); + } catch (NonFatalException nonFatalException) { + handleNonFatalException(nonFatalException, pollingInterval); + } + if (isDryRun) { addToOffersTaken(offer); numOffersTaken++; - maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } else { - // An encrypted wallet must be unlocked before calling takeoffer and gettrade. - // Unlock the wallet for 5 minutes. If the wallet is already unlocked, - // this command will override the timeout of the previous unlock command. try { - unlockWallet(walletPassword, 600); printBTCBalances("BTC Balances Before Take Offer Attempt"); // Blocks until new trade is prepared, or times out. takeV1ProtocolOffer(offer, paymentAccount, bisqTradeFeeCurrency, pollingInterval); @@ -207,12 +215,12 @@ public class TakeBestPricedOfferToSellXmr extends AbstractBot { printBTCBalances("BTC Balances After Simulated Trade Completion"); } numOffersTaken++; - maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } catch (NonFatalException nonFatalException) { handleNonFatalException(nonFatalException, pollingInterval); } catch (StatusRuntimeException fatalException) { shutdownAfterTakeOfferFailure(fatalException); } + maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } } diff --git a/java-examples/src/main/resources/TakeBestPricedOfferToSellXmr.properties b/java-examples/src/main/resources/TakeBestPricedOfferToSellXmr.properties index 28326d2..7affb46 100644 --- a/java-examples/src/main/resources/TakeBestPricedOfferToSellXmr.properties +++ b/java-examples/src/main/resources/TakeBestPricedOfferToSellXmr.properties @@ -1,17 +1,17 @@ # Maximum # of offers to take during one bot session. When reached, bot will shut down (but not the API daemon). -maxTakeOffers=1 +maxTakeOffers=2 # # Taker bot's payment account id. Only SELL BTC offers using the same payment method will be considered for taking. -paymentAccountId=a15d0f15-e355-4b89-8322-e262097623ae +paymentAccountId=fafeec6e-fb95-4ff5-a537-ea7e9d1ad683 # # Taker bot's max market price margin. A candidate buy BTC offer's price margin must be <= maxMarketPriceMargin. -maxMarketPriceMargin=1.10 +maxMarketPriceMargin=1 # # Taker bot's min BTC amount to buy. The candidate buy BTC offer's amount must be >= minAmount BTC. minAmount=0.01 # # Taker bot's max BTC amount to buy. The candidate buy BTC offer's amount must be <= maxAmount BTC. -maxAmount=0.90 +maxAmount=1.00 # # Taker bot's max acceptable transaction fee rate (sats / byte). # Regtest fee rates are from https://price.bisq.wiz.biz/getFees @@ -25,4 +25,4 @@ bisqTradeFeeCurrency=BSQ preferredTradingPeers=localhost:8888 # # Offer polling frequency must be >= 1s (1000ms) between each getoffers request. -pollingInterval=30000 +pollingInterval=60000 From 28deeb81e0ae3fb4aafb6245859bd72e8be781bd Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 29 Jun 2022 19:38:59 -0300 Subject: [PATCH 12/41] [WIP] Save java-examples/README.md --- java-examples/README.md | 208 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 202 insertions(+), 6 deletions(-) diff --git a/java-examples/README.md b/java-examples/README.md index 9106750..70ef305 100644 --- a/java-examples/README.md +++ b/java-examples/README.md @@ -1,11 +1,207 @@ -# Bisq API Java Examples +# Bisq API Java Examples And Bots -This subproject contains Java classes demonstrating API gRPC method calls. +This subproject contains: +* [Java API rpc examples](https://github.com/bisq-network/bisq-api-reference/tree/main/java-examples/src/main/java/bisq/rpccalls) + demonstrating how to send API gRPC requests, and handle responses. -Each class in +* [Java API bots](https://github.com/bisq-network/bisq-api-reference/tree/main/java-examples/src/main/java/bisq/bots) + to use as is on mainnet, and demonstrate how to write new API bots. If interested in porting any bot code to + other languages supported by [gRPC](https://grpc.io/docs/languages), please use these Java bot examples, not the + Python examples. The Python examples were written by a Python noob, and don't handle errors. + + TODO Add warning about this to Python Examples README. + +* A [Gradle build file](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/build.gradle) + that could be used as a template for your own API bot project. + +## Risks And Warnings + +### Never Run API Daemon and [Bisq GUI](https://bisq.network) On Same Host At Same Time + +The API daemon and the GUI share the same default wallet and connection ports. Beyond inevitable failures due to +fighting over the wallet and ports, doing so will probably corrupt your wallet. Before starting the API daemon, +make sure your GUI is shut down, and vice-versa. Please back up your mainnet wallet early and often with the GUI. + +TODO Add warning about this to Python Examples README, Api Reference, and Beta Test Guide. + +### Go Slow (But Much Faster Than You Click Buttons In The GUI) + +[Bisq](https://bisq.network) was designed to respond to manual clicks in the user interface. It is not a +high-throughput, high-performance system supporting atomic transactions. Care must be taken to avoid +problems due to slow wallet updates on your disk, and Tor network latency. The API daemon enforces limits on request +frequency via call rate metering, but you cannot assume bots can perform tasks as rapidly as the API daemon's call +rate meters allow. + +### Run Bots On Mainnet At Your Own Risk + +This document would not state "these bots can be run on mainnet, as is" without reasonable confidence on the part of +the code's author, but if you do, you do so at your own risk. Copious details about running them on a local BTC +regtest network, running on mainnet in **dryrun** mode, and each bot's configuration is provided in this document. +Please put some effort into understanding a bot's code and its configuration before trying it on mainnet. + +TODO Add warning about this to Python Examples README. + +## Generating Protobuf Code + +### Download IDL (.proto) Files From The [Bisq Repo](https://github.com/bisq-network/bisq) + +The protobuf IDL files are not part of this project, and must be downloaded from the +[Bisq repo](https://github.com/bisq-network/bisq/tree/master/proto/src/main/proto). + +You can download them by running +[this script](https://github.com/bisq-network/bisq-api-reference/blob/main/proto-downloader/download-bisq-protos.sh) +from your IDE or a shell: +```asciidoc +$ proto-downloader/download-bisq-protos.sh +``` + +The java-examples [build file](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/build.gradle) +will generate the code from the downloaded IDL (.proto) files. + +You should be able to generate the protobuf Java sources and all Java examples in your IDE. + +In a terminal: +```asciidoc +$ cd java-examples +$ ./gradlew clean build +``` + +## Java API Method Examples + +Each class in the [bisq.rpccalls](https://github.com/bisq-network/bisq-api-reference/tree/main/java-examples/src/main/java/bisq/rpccalls) package is named for the RPC method call being demonstrated. -The subproject uses a -a [Gradle build file](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/build.gradle), also -demonstrating how to generate the necessary protobuf classes from the Bisq .proto files. +Their purpose is to show how to construct a gRPC service method request with parameters, send the +request, and print a response object if there is one. As a rule, the request was successful if +no gRPC StatusRuntimeException was received from the API daemon. + +Their usage is simple; there are no program arguments for any of them. Just run them with an IDE +program launcher or your shell. + +However, you will often need to edit the Java class and re-compile it before running it because these +examples know nothing about real Payment Account IDs, Offer IDs, etc. To run the +[GetOffer.java](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/src/main/java/bisq/rpccalls/GetOffer.java) +example, your will need to change the hard-coded offer ID to a real offer ID to avoid a "not found" +exception. + +## Java API Bots + +### Purpose + +The Java API bots in this project are meant run on mainnet, provide a base for more complex bots, and guide you in +developing your own bots. + +### Run on mainnet at your own risk! + +You need to understand a bot's code and its configuration before trying it on mainnet. While you get familiar with +a bot example, you can run it in **dryrun** mode to see how it behaves with different configurations (more later). +Even better: run it while your Bisq API daemons (seednode, arbitration-node, and a trading peer) are connected to a +local BTC regtest network. +The [API test harness](https://github.com/bisq-network/bisq/blob/master/apitest/docs/api-beta-test-guide.md) is +convenient for this. + +#### Quick And Dirty Test Harness + +TODO: Create issue for out of date https://bisq.wiki/Downloading_and_installing#Build_from_source? + +If you are already familiar with [building Bisq source code](https://github.com/bisq-network/bisq/blob/master/docs/build.md), +and have [bitcoin-core binaries](https://github.com/bitcoin/bitcoin) on your system's $PATH, you might skip the +[API test harness setup guide](https://github.com/bisq-network/bisq/blob/master/apitest/docs/api-beta-test-guide.md). + +Before you try the test harness, make sure your host is not running any bitcoind or Bisq nodes. + +Clone the Bisq master branch to your host, build and start it: +```asciidoc +#Clone Bisq source repo. +$ git clone https://github.com/bisq-network/bisq.git [some folder] +$ cd [some folder] + +#Build Bisq source, install DAO/Regtest wallet files. +$ ./gradlew clean build :apitest:installDaoSetup -x test + +#Start local bitcoind (regtest) node and headless test harness nodes. +$ ./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false +``` +To shut down the test harness, enter **^C**. + +#### Creating And Using Runnable Bot Jars + +TODO (more later) + +### Take BSQ / BTC / XMR / Offer Bots + +There are four bots for taking offers: + +* [TakeBestPricedOfferToBuyBsq (From You)](https://github.com/bisq-network/bisq-api-reference/blob/update-java-examples-readme/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java) +* [TakeBestPricedOfferToSellBsq (To You)](https://github.com/bisq-network/bisq-api-reference/blob/update-java-examples-readme/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java) + +* [TakeBestPricedOfferToBuyBtc (From You)](https://github.com/bisq-network/bisq-api-reference/blob/update-java-examples-readme/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java) +* [TakeBestPricedOfferToSellBtc (To You)](https://github.com/bisq-network/bisq-api-reference/blob/update-java-examples-readme/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java) + +* [TakeBestPricedOfferToBuyXmr (From You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java) +* [TakeBestPricedOfferToSellXmr (To You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java) + + +The **Take Buy/Sell BTC** bots take 1 or more offers for a given criteria as defined in each bot's configuration file +(more later). After the configured maximum number of offers have been taken, the bot shuts down the API daemon and +itself because trade payments are made outside Bisq. The Bisq API *cannot automate the fiat or XMR trade payment and +receipt confirmation steps of the protocol*. When an offer is taken by the API, the trade payment steps of the protocol +must be performed in the UI. + +The **Take Buy/Sell BSQ** bots take 1 or more offers for a given criteria as defined in each bot's configuration file +(more later). After the configured maximum number of offers have been taken, the bot shuts down itself, but not the +API daemon. BSQ Swaps can be fully automated by the API because the swap transaction is completed within seconds of +taking a BSQ Swap offer. + +#### [TakeBestPricedOfferToSellBtc (To You)](https://github.com/bisq-network/bisq-api-reference/blob/update-java-examples-readme/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java) + +**Purpose** + +This bot waits for attractively priced SELL BTC offers to appear, takes the offers, then shuts down both the API daemon +and itself (the bot). The user should then proceed to start the desktop UI application and complete the new trades. + +The benefit this bot provides is freeing up the user time spent watching the offer book in the UI, waiting for the +right offer to take. Low-priced BTC offers are taken relatively quickly; this bot increases the chance of beating +the other nodes at taking the offer. + +The disadvantage is that if the user takes offers with the API, she must complete the trades with the desktop UI. +This problem is due to the inability of the API to fully automate every step of the trading protocol. Sending fiat or +XMR payments, and confirming their receipt, are manual activities performed outside the Bisq daemon and desktop UI. +Also, the API and the desktop UI cannot run at the same time. Care must be taken to shut down one before starting +the other. + +The criteria for determining which offers to take are defined in the bot's configuration file +TakeBestPricedOfferToSellBtc.properties (located in project's src/main/resources directory). The individual +configurations are commented in the existing TakeBestPricedOfferToSellBtc.properties, which should be used as a +template for your own use case. + +**Use Cases** + +One possible use case for this bot is to buy BTC with GBP, e.g., take a "Faster Payment" offer to sell BTC (to you) for +GBP at or below the current market GBP price if: +* the offer maker is a preferred trading peer +* the offer's BTC amount is between 0.10 and 0.25 BTC +* the current transaction mining fee rate is less than or equal 20 sats / byte + +**Class Doc Link** + +**Configuration** + +* Param 1 +* Param 2 +* Param 3 +* ... + +**Usage** + +```asciidoc +$ java -jar x.jar --password=xyz --conf=[path.conf] --dryrun=true +``` + +## Gradle Build File + +This project's [Gradle build file](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/build.gradle), +shows how to generate the necessary protobuf classes from the Bisq .proto files. + + From 44ca04af870a180126e384fb17becc65efdec1cc Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 30 Jun 2022 12:54:46 -0300 Subject: [PATCH 13/41] Log a CLI gettrade command for a simulated trading peer --- .../src/main/java/bisq/bots/BotUtils.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/java-examples/src/main/java/bisq/bots/BotUtils.java b/java-examples/src/main/java/bisq/bots/BotUtils.java index 8bbc236..b433ace 100644 --- a/java-examples/src/main/java/bisq/bots/BotUtils.java +++ b/java-examples/src/main/java/bisq/bots/BotUtils.java @@ -552,6 +552,28 @@ public class BotUtils { log.warn(BANNER); } + + /** + * Log a CLI gettrade command for a simulated trading peer. + * + * @param log calling bot's logger + * @param tradingPeerApiPassword trading peer's CLI --password param value + * @param tradingPeerApiPort trading peer's CLI --port param value + * @param tradeId trade's unique identifier (cannot be short-id) + */ + public static void printCliGetTradeCommand(Logger log, + String tradingPeerApiPassword, + int tradingPeerApiPort, + String tradeId) { + log.warn(BANNER); + log.warn("Trading peer can view a trade with a gettrade CLI command:"); + log.warn("./bisq-cli --password={} --port={} gettrade --trade-id={}", + tradingPeerApiPassword, + tradingPeerApiPort, + tradeId); + log.warn(BANNER); + } + /** * Run a bash script to count down the given number of seconds, printing each character of output from stdout. *

From dd2619bd3bc8cbb1a584b3e04299214c36d927ef Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 30 Jun 2022 12:55:32 -0300 Subject: [PATCH 14/41] Tidy up "Here are today's completed trades" logging --- .../src/main/java/bisq/bots/AbstractBot.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/AbstractBot.java b/java-examples/src/main/java/bisq/bots/AbstractBot.java index 1aaf28c..18a6d81 100644 --- a/java-examples/src/main/java/bisq/bots/AbstractBot.java +++ b/java-examples/src/main/java/bisq/bots/AbstractBot.java @@ -593,11 +593,10 @@ public abstract class AbstractBot { */ protected void printTradesSummaryForTodayIfWalletIsUnlocked() { try { - log.info("Here are today's completed trades:"); printTradesSummaryForToday(CLOSED); } catch (StatusRuntimeException grpcException) { if (walletIsLocked(grpcException)) { - log.error("Cannot retrieve trades while API daemon's wallet is locked."); + log.error("Cannot show today's trades while API daemon's wallet is locked."); } else { throw grpcException; } @@ -614,7 +613,12 @@ public abstract class AbstractBot { var trades = getTrades(category).stream() .filter(t -> t.getDate() >= midnightToday) .collect(Collectors.toList()); - BotUtils.printTradesSummary(category, trades); + if (trades.isEmpty()) { + log.info("No trades have been completed today."); + } else { + log.info("Here are today's completed trades:"); + BotUtils.printTradesSummary(category, trades); + } } /** @@ -805,7 +809,6 @@ public abstract class AbstractBot { * @param maxTakeOffers the max number of offers that can be taken during bot run */ protected void maybeShutdownAfterSuccessfulSwap(int numOffersTaken, int maxTakeOffers) { - log.info("Here are today's completed trades:"); printTradesSummaryForToday(CLOSED); if (!isDryRun) { From dee8e5baa8dfa9ce4dbcb2921d3835d87a11f250 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 30 Jun 2022 13:13:35 -0300 Subject: [PATCH 15/41] Add convenience for lgging combination of copy/paste CLI cmds for peer --- .../src/main/java/bisq/bots/BotUtils.java | 18 +++++++++++++++++- .../bots/RegtestTradePaymentSimulator.java | 9 ++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/BotUtils.java b/java-examples/src/main/java/bisq/bots/BotUtils.java index b433ace..2db1f38 100644 --- a/java-examples/src/main/java/bisq/bots/BotUtils.java +++ b/java-examples/src/main/java/bisq/bots/BotUtils.java @@ -552,7 +552,6 @@ public class BotUtils { log.warn(BANNER); } - /** * Log a CLI gettrade command for a simulated trading peer. * @@ -574,6 +573,23 @@ public class BotUtils { log.warn(BANNER); } + /** + * Log 1 or more CLI commands for a simulated trading peer. + * Commands need to be separated by newlines to be legible. + * + * @param log calling bot's logger + * @param description description of CLI commands + * @param commands CLI commands separated by newlines. + */ + public static void printCliCommands(Logger log, + String description, + String commands) { + log.warn(BANNER); + log.warn(description); + log.warn(commands); + log.warn(BANNER); + } + /** * Run a bash script to count down the given number of seconds, printing each character of output from stdout. *

diff --git a/java-examples/src/main/java/bisq/bots/RegtestTradePaymentSimulator.java b/java-examples/src/main/java/bisq/bots/RegtestTradePaymentSimulator.java index c48e364..a57eac3 100644 --- a/java-examples/src/main/java/bisq/bots/RegtestTradePaymentSimulator.java +++ b/java-examples/src/main/java/bisq/bots/RegtestTradePaymentSimulator.java @@ -105,12 +105,11 @@ public class RegtestTradePaymentSimulator extends AbstractBot { closeTrade(tradeId); log.info("You closed the trade here in the bot (mandatory, to move trades to history list)."); - log.warn("##############################################################################"); - log.warn("Bob closes trade in the CLI (mandatory, to move trades to history list):"); - String copyPasteCliCommands = "./bisq-cli --password=xyz --port=9999 closetrade --trade-id=" + trade.getTradeId() + String cliCommandDescription = "Trading peer inspects and closes trade in the CLI (mandatory, to move trades to history list):"; + String copyPasteCliCommands = "./bisq-cli --password=xyz --port=9999 gettrade --trade-id=" + trade.getTradeId() + + "\n" + "./bisq-cli --password=xyz --port=9999 closetrade --trade-id=" + trade.getTradeId() + "\n" + "./bisq-cli --password=xyz --port=9999 gettrades --category=closed"; - log.warn(copyPasteCliCommands); - log.warn("##############################################################################"); + printCliCommands(log, cliCommandDescription, copyPasteCliCommands); log.debug("Closing {}'s gRPC channel.", this.getClass().getSimpleName()); super.grpcStubs.close(); From d59dc725eeb396005b3a1988a13320f490c86873 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 30 Jun 2022 13:16:14 -0300 Subject: [PATCH 16/41] Tweak class javadoc, log more bot config details --- .../bisq/bots/TakeBestPricedOfferToSellXmr.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java index c7a8761..f5ec8ca 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java @@ -32,10 +32,11 @@ import static java.math.RoundingMode.HALF_UP; import static protobuf.OfferDirection.BUY; /** - * This bot's general use case is to buy XMR with BTC at a low BTC price. It periodically checks the Sell XMR (Buy BTC) - * market, and takes a configured maximum number of offers to sell you XMR according to criteria you define in the bot's - * configuration file: TakeBestPricedOfferToSellXmr.properties (located in project's src/main/resources directory). - * You will need to replace the default values in the configuration file for your use cases. + * This bot's general use case is to buy XMR with your BTC at a low BTC price. It periodically checks the + * Sell XMR (Buy BTC) market, and takes a configured maximum number of offers to sell you XMR according to criteria you + * define in the bot's configuration file: TakeBestPricedOfferToSellXmr.properties (located in project's + * src/main/resources directory). You will need to replace the default values in the configuration file for your + * use cases. *


* After the maximum number of offers have been taken (good to start with 1), the bot will shut down the API daemon, * then itself. You have to confirm the offer maker's XMR payment(s) outside Bisq, then complete the trade(s) in @@ -220,8 +221,8 @@ public class TakeBestPricedOfferToSellXmr extends AbstractBot { } catch (StatusRuntimeException fatalException) { shutdownAfterTakeOfferFailure(fatalException); } - maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } + maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } /** @@ -239,6 +240,8 @@ public class TakeBestPricedOfferToSellXmr extends AbstractBot { configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion()); var network = getNetwork(); configsByLabel.put("BTC Network:", network); + configsByLabel.put("Dry Run?", isDryRun ? "YES" : "NO"); + configsByLabel.put("Simulate Regtest Trade?", canSimulatePaymentSteps ? "YES" : "NO"); configsByLabel.put("My Payment Account:", ""); configsByLabel.put("\tPayment Account Id:", paymentAccount.getId()); configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName()); From fdc96bc455ec1f4032bcf434cfa373ffd6b59e27 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 30 Jun 2022 13:17:46 -0300 Subject: [PATCH 17/41] Document and tidy up TakeBestPricedOfferToBuyXmr bot --- .../bots/TakeBestPricedOfferToBuyXmr.java | 87 +++++++++++++------ .../TakeBestPricedOfferToBuyXmr.properties | 12 +-- 2 files changed, 68 insertions(+), 31 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java index 1d5ea49..d9cd71c 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java @@ -32,23 +32,54 @@ import static java.math.RoundingMode.HALF_UP; import static protobuf.OfferDirection.SELL; /** - * The use case for the TakeBestPricedOfferToBuyXmr bot is to sell XMR for BTC at a high BTC price, e.g.: - *

- *      Take an offer to buy XMR from you for BTC, at no less than #.##% above or below current market price if:
- *          the offer maker is a preferred trading peer,
- *          and the offer's BTC amount is between 0.50 and 1.00 BTC,
- *          and the current transaction mining fee rate is below 15 sats / byte.
- *
- * Usage:  TakeBestPricedOfferToBuyXmr  --password=api-password --port=api-port \
- *                          [--conf=take-best-priced-offer-to-buy-xmr.conf] \
- *                          [--dryrun=true|false]
- *                          [--simulate-regtest-payment=true|false]
- * 
+ * This bot's general use case is to sell your XMR for BTC at a high BTC price. It periodically checks the + * Buy XMR (Sell BTC) market, and takes a configured maximum number of offers to buy XMR from you according to criteria + * you define in the bot's configuration file: TakeBestPricedOfferToBuyXmr.properties (located in project's + * src/main/resources directory). You will need to replace the default values in the configuration file for your + * use cases. + *


+ * After the maximum number of offers have been taken (good to start with 1), the bot will shut down the API daemon, + * then itself. You have to send XMR payment to the offer maker(s) outside Bisq, then complete the trade(s) in the + * Bisq Desktop application. *

- * The criteria for determining which offers to take are defined in the bot's configuration file - * TakeBestPricedOfferToBuyXmr.properties (located in project's src/main/resources directory). The individual - * configurations are commented in the existing TakeBestPricedOfferToBuyXmr.properties, which should be used as a - * template for your own use case. + * Here is one possible use case: + *

+ *  Take 2 offers to buy your XMR for BTC, priced no lower than -1.50% above or below current market price if:
+ *
+ *          the offer's BTC amount is between 0.50 and 1.00 BTC
+ *          the offer maker is one of two preferred trading peers
+ *          the current transaction mining fee rate is below 20 sats / byte
+ *
+ *  The bot configurations for these rules are set in TakeBestPricedOfferToBuyXmr.properties as follows:
+ *
+ *          maxTakeOffers=2
+ *          minMarketPriceMargin=-1.50
+ *          minAmount=0.50
+ *          maxAmount=1.00
+ *          preferredTradingPeers=preferred-address-1.onion:9999,preferred-address-2.onion:9999
+ *          maxTxFeeRate=20
+ * 
+ * Usage + *


+ * You must encrypt your wallet password before running this bot. If it is not already, you can use the CLI: + *

+ *     $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
+ * 
+ * There are some {@link bisq.bots.Config program options} common to all the Java bot examples, passed on the command + * line. The only one you must provide (no default value) is your API daemon's password option: + * `--password `. The bot will prompt you for your wallet-password in the console. + *


+ * You can pass the '--dryrun=true' option to the program to see what offers your bot would take with a given + * configuration. This will help you avoid taking offers by mistake. + *

+ *     TakeBestPricedOfferToBuyXmr  --password=api-password --port=api-port [--dryrun=true|false]
+ * 
+ * If your API daemon is running on a local regtest network (with a trading peer), you can pass the + * '--simulate-regtest-payment=true' option to the program to simulate the full trade protocol. The bot will print + * your regtest trading peer's CLI commands will be printed on the console, for you to copy/paste into another terminal. + *
+ *     TakeBestPricedOfferToBuyXmr  --password=api-password --port=api-port [--simulate-regtest-payment=true|false]
+ * 
*/ @Slf4j @Getter @@ -143,7 +174,6 @@ public class TakeBestPricedOfferToBuyXmr extends AbstractBot { takeCriteria.printOfferAgainstCriteria(highestPricedOffer); }); - printDryRunProgress(); runCountdown(log, pollingInterval); pingDaemon(startTime); } @@ -157,16 +187,21 @@ public class TakeBestPricedOfferToBuyXmr extends AbstractBot { private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) { log.info("Will attempt to take offer '{}'.", offer.getId()); takeCriteria.printOfferAgainstCriteria(offer); + + // An encrypted wallet must be unlocked before calling takeoffer and gettrade(s). + // Unlock the wallet for 5 minutes. If the wallet is already unlocked, this request + // will override the timeout of the previous unlock request. + try { + unlockWallet(walletPassword, 300); + } catch (NonFatalException nonFatalException) { + handleNonFatalException(nonFatalException, pollingInterval); + } + if (isDryRun) { addToOffersTaken(offer); numOffersTaken++; - maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } else { - // An encrypted wallet must be unlocked before calling takeoffer and gettrade. - // Unlock the wallet for 5 minutes. If the wallet is already unlocked, - // this command will override the timeout of the previous unlock command. try { - unlockWallet(walletPassword, 600); printBTCBalances("BTC Balances Before Take Offer Attempt"); // Blocks until new trade is prepared, or times out. takeV1ProtocolOffer(offer, paymentAccount, bisqTradeFeeCurrency, pollingInterval); @@ -181,13 +216,13 @@ public class TakeBestPricedOfferToBuyXmr extends AbstractBot { printBTCBalances("BTC Balances After Simulated Trade Completion"); } numOffersTaken++; - maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } catch (NonFatalException nonFatalException) { handleNonFatalException(nonFatalException, pollingInterval); } catch (StatusRuntimeException fatalException) { shutdownAfterTakeOfferFailure(fatalException); } } + maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } /** @@ -205,6 +240,8 @@ public class TakeBestPricedOfferToBuyXmr extends AbstractBot { configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion()); var network = getNetwork(); configsByLabel.put("BTC Network:", network); + configsByLabel.put("Dry Run?", isDryRun ? "YES" : "NO"); + configsByLabel.put("Simulate Regtest Trade?", canSimulatePaymentSteps ? "YES" : "NO"); configsByLabel.put("My Payment Account:", ""); configsByLabel.put("\tPayment Account Id:", paymentAccount.getId()); configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName()); @@ -309,14 +346,14 @@ public class TakeBestPricedOfferToBuyXmr extends AbstractBot { var marginPriceLabel = format("Is offer's price margin (%s%%) >= bot's min market price margin (%s%%)?", offer.getMarketPriceMarginPct(), minMarketPriceMargin); - filterResultsByLabel.put(marginPriceLabel, isMarginLEMaxMarketPriceMargin.test(offer, minMarketPriceMargin)); + filterResultsByLabel.put(marginPriceLabel, isMarginGEMinMarketPriceMargin.test(offer, minMarketPriceMargin)); } else { var fixedPriceLabel = format("Is offer's fixed-price (%s) >= bot's target price (%s)?", offer.getPrice() + " BTC", targetPrice + " BTC"); filterResultsByLabel.put(fixedPriceLabel, isFixedPriceGEMinMarketPriceMargin.test(offer, currentMarketPrice)); } - + String btcAmountBounds = format("%s BTC - %s BTC", minAmount, maxAmount); filterResultsByLabel.put("Is offer's BTC amount within bot amount bounds (" + btcAmountBounds + ")?", isWithinBTCAmountBounds(offer, getMinAmount(), getMaxAmount())); diff --git a/java-examples/src/main/resources/TakeBestPricedOfferToBuyXmr.properties b/java-examples/src/main/resources/TakeBestPricedOfferToBuyXmr.properties index 365074d..d7ef4f4 100644 --- a/java-examples/src/main/resources/TakeBestPricedOfferToBuyXmr.properties +++ b/java-examples/src/main/resources/TakeBestPricedOfferToBuyXmr.properties @@ -1,21 +1,21 @@ # Maximum # of offers to take during one bot session. When reached, bot will shut down (but not the API daemon). -maxTakeOffers=50 +maxTakeOffers=5 # # Taker bot's payment account id. Only SELL BTC offers using the same payment method will be considered for taking. -paymentAccountId=a15d0f15-e355-4b89-8322-e262097623ae +paymentAccountId=f32546cd-bb47-4bce-acc8-5227a13e2516 # # Taker bot's min market price margin. A candidate sell BTC offer's price margin must be >= minMarketPriceMargin. -minMarketPriceMargin=0 +minMarketPriceMargin=-5.00 # # Taker bot's min BTC amount to sell. The candidate SELL BTC offer's amount must be >= minAmount BTC. minAmount=0.01 # # Taker bot's max BTC amount to sell. The candidate SELL BTC offer's amount must be <= maxAmount BTC. -maxAmount=0.90 +maxAmount=1.00 # # Taker bot's max acceptable transaction fee rate (sats / byte). # Regtest fee rates are from https://price.bisq.wiz.biz/getFees -maxTxFeeRate=25 +maxTxFeeRate=50 # # Bisq trade fee currency code (BSQ or BTC). bisqTradeFeeCurrency=BSQ @@ -25,4 +25,4 @@ bisqTradeFeeCurrency=BSQ preferredTradingPeers=localhost:8888 # # Offer polling frequency must be >= 1s (1000ms) between each getoffers request. -pollingInterval=30000 +pollingInterval=10000 From 4f3ff57ca9e3fdb1c27f56c823f9f5db7782a7d7 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 30 Jun 2022 18:07:20 -0300 Subject: [PATCH 18/41] Log comparison of margin price based offers' prices, not margins --- .../main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java | 8 ++++---- .../main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java index d9cd71c..9863f46 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java @@ -76,7 +76,7 @@ import static protobuf.OfferDirection.SELL; * * If your API daemon is running on a local regtest network (with a trading peer), you can pass the * '--simulate-regtest-payment=true' option to the program to simulate the full trade protocol. The bot will print - * your regtest trading peer's CLI commands will be printed on the console, for you to copy/paste into another terminal. + * your regtest trading peer's CLI commands in the console, for you to copy/paste into another terminal. *
  *     TakeBestPricedOfferToBuyXmr  --password=api-password --port=api-port [--simulate-regtest-payment=true|false]
  * 
@@ -343,9 +343,9 @@ public class TakeBestPricedOfferToBuyXmr extends AbstractBot { : "N/A"); if (offer.getUseMarketBasedPrice()) { - var marginPriceLabel = format("Is offer's price margin (%s%%) >= bot's min market price margin (%s%%)?", - offer.getMarketPriceMarginPct(), - minMarketPriceMargin); + var marginPriceLabel = format("Is offer's margin based price (%s) >= bot's target price (%s)?", + offer.getPrice() + " BTC", + targetPrice + " BTC"); filterResultsByLabel.put(marginPriceLabel, isMarginGEMinMarketPriceMargin.test(offer, minMarketPriceMargin)); } else { var fixedPriceLabel = format("Is offer's fixed-price (%s) >= bot's target price (%s)?", diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java index f5ec8ca..f42f42a 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java @@ -76,7 +76,7 @@ import static protobuf.OfferDirection.BUY; * * If your API daemon is running on a local regtest network (with a trading peer), you can pass the * '--simulate-regtest-payment=true' option to the program to simulate the full trade protocol. The bot will print - * your regtest trading peer's CLI commands will be printed on the console, for you to copy/paste into another terminal. + * your regtest trading peer's CLI commands in the console, for you to copy/paste into another terminal. *
  *     TakeBestPricedOfferToSellXmr  --password=api-password --port=api-port [--simulate-regtest-payment=true|false]
  * 
@@ -343,9 +343,9 @@ public class TakeBestPricedOfferToSellXmr extends AbstractBot { : "N/A"); if (offer.getUseMarketBasedPrice()) { - var marginPriceLabel = format("Is offer's price margin (%s%%) <= bot's max market price margin (%s%%)?", - offer.getMarketPriceMarginPct(), - maxMarketPriceMargin); + var marginPriceLabel = format("Is offer's margin based price (%s) <= bot's target price (%s)?", + offer.getPrice() + " BTC", + targetPrice + " BTC"); filterResultsByLabel.put(marginPriceLabel, isMarginLEMaxMarketPriceMargin.test(offer, maxMarketPriceMargin)); } else { var fixedPriceLabel = format("Is offer's fixed-price (%s) <= bot's target price (%s)?", From 52a6d29687cf547e9ab13f003c20867ebec9f421 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 30 Jun 2022 18:08:55 -0300 Subject: [PATCH 19/41] Show how to set preferredTradingPeers config --- .../resources/TakeBestPricedOfferToBuyBsq.properties | 1 + .../resources/TakeBestPricedOfferToBuyBtc.properties | 11 +++++------ .../resources/TakeBestPricedOfferToBuyXmr.properties | 1 + .../resources/TakeBestPricedOfferToSellBsq.properties | 1 + .../resources/TakeBestPricedOfferToSellBtc.properties | 1 + .../resources/TakeBestPricedOfferToSellXmr.properties | 1 + 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/java-examples/src/main/resources/TakeBestPricedOfferToBuyBsq.properties b/java-examples/src/main/resources/TakeBestPricedOfferToBuyBsq.properties index 444bc92..286bd30 100644 --- a/java-examples/src/main/resources/TakeBestPricedOfferToBuyBsq.properties +++ b/java-examples/src/main/resources/TakeBestPricedOfferToBuyBsq.properties @@ -19,6 +19,7 @@ maxAmount=0.90 maxTxFeeRate=25 # # Taker bot's list of preferred trading peers (their onion addresses). +# Example: preferred-address-1.onion:9999,preferred-address-2.onion:9999 # If you do not want to constrict trading to preferred peers, comment this line out with a '#' character. preferredTradingPeers=localhost:8888 # diff --git a/java-examples/src/main/resources/TakeBestPricedOfferToBuyBtc.properties b/java-examples/src/main/resources/TakeBestPricedOfferToBuyBtc.properties index 89783e7..50f6552 100644 --- a/java-examples/src/main/resources/TakeBestPricedOfferToBuyBtc.properties +++ b/java-examples/src/main/resources/TakeBestPricedOfferToBuyBtc.properties @@ -1,13 +1,11 @@ -# # Maximum # of offers to take during one bot session. When reached, bot will shut down API daemon then itself. -maxTakeOffers=1 +maxTakeOffers=5 # # Taker bot's payment account id. Only BUY BTC offers using the same payment method will be considered for taking. -paymentAccountId=6e58f3d9-e7a3-4799-aa38-e28e624d79a3 +paymentAccountId=9f791b7b-9b34-4931-8c93-8e7b0dc71612 # # Taker bot's min market price margin. A candidate BUY BTC offer's price margin must be >= minMarketPriceMargin. -# -minMarketPriceMargin=0 +minMarketPriceMargin=-5.00 # # Taker bot's min BTC amount to sell. The candidate BUY offer's amount must be >= minAmount BTC. minAmount=0.01 @@ -23,6 +21,7 @@ maxTxFeeRate=25 bisqTradeFeeCurrency=BSQ # # Taker bot's list of preferred trading peers (their onion addresses). +# Example: preferred-address-1.onion:9999,preferred-address-2.onion:9999 # If you do not want to constrict trading to preferred peers, comment this line out with a '#' character. preferredTradingPeers=localhost:8888, \ nysf2pknaaxfh26k42ego5mnfzpbozyi3nuoxdu745unvva4pvywffyd.onion:9999, \ @@ -35,4 +34,4 @@ preferredTradingPeers=localhost:8888, \ x6x2o3m6rxhkfuf2v6lalbharf3whwvkts5rdn3jkhgieqvnq6mvdfyd.onion:9999 # # Offer polling frequency must be >= 1s (1000ms) between each getoffers request. -pollingInterval=20000 +pollingInterval=60000 diff --git a/java-examples/src/main/resources/TakeBestPricedOfferToBuyXmr.properties b/java-examples/src/main/resources/TakeBestPricedOfferToBuyXmr.properties index d7ef4f4..98f24e7 100644 --- a/java-examples/src/main/resources/TakeBestPricedOfferToBuyXmr.properties +++ b/java-examples/src/main/resources/TakeBestPricedOfferToBuyXmr.properties @@ -21,6 +21,7 @@ maxTxFeeRate=50 bisqTradeFeeCurrency=BSQ # # Taker bot's list of preferred trading peers (their onion addresses). +# Example: preferred-address-1.onion:9999,preferred-address-2.onion:9999 # If you do not want to constrict trading to preferred peers, comment this line out with a '#' character. preferredTradingPeers=localhost:8888 # diff --git a/java-examples/src/main/resources/TakeBestPricedOfferToSellBsq.properties b/java-examples/src/main/resources/TakeBestPricedOfferToSellBsq.properties index 97f13a0..ee55ba2 100644 --- a/java-examples/src/main/resources/TakeBestPricedOfferToSellBsq.properties +++ b/java-examples/src/main/resources/TakeBestPricedOfferToSellBsq.properties @@ -19,6 +19,7 @@ maxAmount=0.90 maxTxFeeRate=25 # # Taker bot's list of preferred trading peers (their onion addresses). +# Example: preferred-address-1.onion:9999,preferred-address-2.onion:9999 # If you do not want to constrict trading to preferred peers, comment this line out with a '#' character. preferredTradingPeers=localhost:8888 # diff --git a/java-examples/src/main/resources/TakeBestPricedOfferToSellBtc.properties b/java-examples/src/main/resources/TakeBestPricedOfferToSellBtc.properties index a4f51e2..47d6e76 100644 --- a/java-examples/src/main/resources/TakeBestPricedOfferToSellBtc.properties +++ b/java-examples/src/main/resources/TakeBestPricedOfferToSellBtc.properties @@ -22,6 +22,7 @@ maxTxFeeRate=100 bisqTradeFeeCurrency=BSQ # # Taker bot's list of preferred trading peers (their onion addresses). +# Example: preferred-address-1.onion:9999,preferred-address-2.onion:9999 # If you do not want to constrict trading to preferred peers, comment this line out with a '#' character. preferredTradingPeers=localhost:8888, \ nysf2pknaaxfh26k42ego5mnfzpbozyi3nuoxdu745unvva4pvywffyd.onion:9999, \ diff --git a/java-examples/src/main/resources/TakeBestPricedOfferToSellXmr.properties b/java-examples/src/main/resources/TakeBestPricedOfferToSellXmr.properties index 7affb46..01db613 100644 --- a/java-examples/src/main/resources/TakeBestPricedOfferToSellXmr.properties +++ b/java-examples/src/main/resources/TakeBestPricedOfferToSellXmr.properties @@ -21,6 +21,7 @@ maxTxFeeRate=25 bisqTradeFeeCurrency=BSQ # # Taker bot's list of preferred trading peers (their onion addresses). +# Example: preferred-address-1.onion:9999,preferred-address-2.onion:9999 # If you do not want to constrict trading to preferred peers, comment this line out with a '#' character. preferredTradingPeers=localhost:8888 # From e138d35761f9c0bfad427e46b56b8fe6f645fa1c Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 30 Jun 2022 18:10:01 -0300 Subject: [PATCH 20/41] Tidy up and document TakeBestPricedOfferToBuyBtc bot --- .../bots/TakeBestPricedOfferToBuyBtc.java | 95 ++++++++++++------- 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java index 8d3110b..2ad0f87 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java @@ -32,35 +32,56 @@ import static java.math.RoundingMode.HALF_UP; import static protobuf.OfferDirection.BUY; /** - * The TakeBestPricedOfferToBuyBtc bot waits for attractively priced BUY BTC for fiat offers to appear, takes the offers - * (up to a maximum of configured {@link #maxTakeOffers}), then shuts down both the API daemon and itself (the bot), - * to allow the user to start the desktop UI application and complete the trades. + * This bot's general use case is to sell your BTC for fiat at a high BTC price. It periodically checks the + * Buy BTC market, and takes a configured maximum number of offers to buy BTC from you according to criteria you + * define in the bot's configuration file: TakeBestPricedOfferToBuyBtc.properties (located in project's + * src/main/resources directory). You will need to replace the default values in the configuration file for your + * use cases. + *


+ * After the maximum number of offers have been taken (good to start with 1), the bot will shut down the API daemon, + * then itself. You have to confirm the offer maker's fiat payment(s) outside Bisq, then complete the trade(s) in + * the Bisq Desktop application. *

- * The benefit this bot provides is freeing up the user time spent watching the offer book in the UI, waiting for the - * right offer to take. This bot increases the chance of beating the other nodes at taking the offer. - *

- * The disadvantage is that if the user takes offers with the API, she must complete the trades with the desktop UI. - * This problem is due to the inability of the API to fully automate every step of the trading protocol. Sending fiat - * payments, and confirming their receipt, are manual activities performed outside the Bisq daemon and desktop UI. - * Also, the API and the desktop UI cannot run at the same time. Care must be taken to shut down one before starting - * the other. - *

- * The criteria for determining which offers to take are defined in the bot's configuration file - * TakeBestPricedOfferToBuyBtc.properties (located in project's src/main/resources directory). The individual - * configurations are commented in the existing TakeBestPricedOfferToBuyBtc.properties, which should be used as a - * template for your own use case. - *

- * One possible use case for this bot is sell BTC for GBP: - *

- *      Take a "Faster Payment (Santander)" offer to buy BTC with GBP at or above current market price if:
- *          the offer maker is a preferred trading peer,
- *          and the offer's BTC amount is between 0.10 and 0.25 BTC,
- *          and the current transaction mining fee rate is below 20 sats / byte.
+ * Here is one possible use case:
  *
- * Usage:  TakeBestPricedOfferToBuyBtc  --password=api-password --port=api-port \
- *                          [--conf=take-best-priced-offer-to-buy-btc.conf] \
- *                          [--dryrun=true|false]
- *                          [--simulate-regtest-payment=true|false]
+ *
+ * 
+ *      Take 3 "Faster Payment" offers to buy BTC with GBP, priced no lower than 2.00% above the current market
+ *      price if:
+ *
+ *          the offer's BTC amount is between 0.10 and 0.25 BTC
+ *          the offer maker is one of two preferred trading peers
+ *          the current transaction mining fee rate is below 20 sats / byte
+ *
+ *  The bot configurations for these rules are set in TakeBestPricedOfferToBuyBtc.properties as follows:
+ *
+ *          maxTakeOffers=3
+ *          minMarketPriceMargin=2.00
+ *          minAmount=0.10
+ *          maxAmount=0.25
+ *          preferredTradingPeers=preferred-address-1.onion:9999,preferred-address-2.onion:9999
+ *          maxTxFeeRate=20
+ * 
+ * Usage + *


+ * You must encrypt your wallet password before running this bot. If it is not already, you can use the CLI: + *

+ *     $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
+ * 
+ * There are some {@link bisq.bots.Config program options} common to all the Java bot examples, passed on the command + * line. The only one you must provide (no default value) is your API daemon's password option: + * `--password `. The bot will prompt you for your wallet-password in the console. + *


+ * You can pass the '--dryrun=true' option to the program to see what offers your bot would take with a given + * configuration. This will help you avoid taking offers by mistake. + *

+ *     TakeBestPricedOfferToBuyBtc  --password=api-password --port=api-port [--dryrun=true|false]
+ * 
+ * If your API daemon is running on a local regtest network (with a trading peer), you can pass the + * '--simulate-regtest-payment=true' option to the program to simulate the full trade protocol. The bot will print + * your regtest trading peer's CLI commands in the console, for you to copy/paste into another terminal. + *
+ *     TakeBestPricedOfferToBuyBtc  --password=api-password --port=api-port [--simulate-regtest-payment=true|false]
  * 
*/ @Slf4j @@ -156,7 +177,6 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot { takeCriteria.printOfferAgainstCriteria(highestPricedOffer); }); - printDryRunProgress(); runCountdown(log, pollingInterval); pingDaemon(startTime); } @@ -170,16 +190,21 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot { private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) { log.info("Will attempt to take offer '{}'.", offer.getId()); takeCriteria.printOfferAgainstCriteria(offer); + + // An encrypted wallet must be unlocked before calling takeoffer and gettrade(s). + // Unlock the wallet for 5 minutes. If the wallet is already unlocked, this request + // will override the timeout of the previous unlock request. + try { + unlockWallet(walletPassword, 300); + } catch (NonFatalException nonFatalException) { + handleNonFatalException(nonFatalException, pollingInterval); + } + if (isDryRun) { addToOffersTaken(offer); numOffersTaken++; - maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } else { - // An encrypted wallet must be unlocked before calling takeoffer and gettrade. - // Unlock the wallet for 5 minutes. If the wallet is already unlocked, - // this command will override the timeout of the previous unlock command. try { - unlockWallet(walletPassword, 600); printBTCBalances("BTC Balances Before Take Offer Attempt"); // Blocks until new trade is prepared, or times out. takeV1ProtocolOffer(offer, paymentAccount, bisqTradeFeeCurrency, pollingInterval); @@ -194,13 +219,13 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot { printBTCBalances("BTC Balances After Simulated Trade Completion"); } numOffersTaken++; - maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } catch (NonFatalException nonFatalException) { handleNonFatalException(nonFatalException, pollingInterval); } catch (StatusRuntimeException fatalException) { shutdownAfterTakeOfferFailure(fatalException); } } + maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } /** @@ -218,6 +243,8 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot { configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion()); var network = getNetwork(); configsByLabel.put("BTC Network:", network); + configsByLabel.put("Dry Run?", isDryRun ? "YES" : "NO"); + configsByLabel.put("Simulate Regtest Trade?", canSimulatePaymentSteps ? "YES" : "NO"); configsByLabel.put("My Payment Account:", ""); configsByLabel.put("\tPayment Account Id:", paymentAccount.getId()); configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName()); From 034172b8f4286db6244e334531979bf01f6425f7 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 1 Jul 2022 11:20:43 -0300 Subject: [PATCH 21/41] Tweak comment about encrypting wallet with CLI --- .../src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java | 4 +--- .../src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java | 2 +- .../src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java index 2ad0f87..039ed84 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java @@ -43,8 +43,6 @@ import static protobuf.OfferDirection.BUY; * the Bisq Desktop application. *

* Here is one possible use case: - * - * *

  *      Take 3 "Faster Payment" offers to buy BTC with GBP, priced no lower than 2.00% above the current market
  *      price if:
@@ -64,7 +62,7 @@ import static protobuf.OfferDirection.BUY;
  * 
* Usage *


- * You must encrypt your wallet password before running this bot. If it is not already, you can use the CLI: + * You must encrypt your wallet password before running this bot. If it is not already encrypted, you can use the CLI: *

  *     $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
  * 
diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java index 9863f46..684fb57 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java @@ -61,7 +61,7 @@ import static protobuf.OfferDirection.SELL; *
* Usage *


- * You must encrypt your wallet password before running this bot. If it is not already, you can use the CLI: + * You must encrypt your wallet password before running this bot. If it is not already encrypted, you can use the CLI: *

  *     $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
  * 
diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java index f42f42a..1ba8bdd 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java @@ -61,7 +61,7 @@ import static protobuf.OfferDirection.BUY; * * Usage *


- * You must encrypt your wallet password before running this bot. If it is not already, you can use the CLI: + * You must encrypt your wallet password before running this bot. If it is not already encrypted, you can use the CLI: *

  *     $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
  * 
From 1f45116849cce681ecfcb429842beb99801c036f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 1 Jul 2022 11:21:31 -0300 Subject: [PATCH 22/41] Tidy up and document TakeBestPricedOfferToSellBtc bot --- .../bots/TakeBestPricedOfferToSellBtc.java | 103 ++++++++++-------- .../TakeBestPricedOfferToSellBtc.properties | 10 +- 2 files changed, 63 insertions(+), 50 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java index dd9893f..4dc9e17 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java @@ -32,47 +32,54 @@ import static java.math.RoundingMode.HALF_UP; import static protobuf.OfferDirection.SELL; /** - * The TakeBestPricedOfferToSellBtc bot waits for attractively priced SELL BTC offers to appear, takes the offers - * (up to a maximum of configured {@link #maxTakeOffers}), then shuts down both the API daemon and itself (the bot), - * to allow the user to start the desktop UI application and complete the trades. + * This bot's general use case is to buy BTC with fiat at a low fiat price. It periodically checks the + * Sell BTC market, and takes a configured maximum number of offers to sell BTC to you according to criteria you + * define in the bot's configuration file: TakeBestPricedOfferToSellBtc.properties (located in project's + * src/main/resources directory). You will need to replace the default values in the configuration file for your + * use cases. + *


+ * After the maximum number of offers have been taken (good to start with 1), the bot will shut down the API daemon, + * then itself. You have to send fiat payment(s) to the offer maker(s) outside Bisq, then complete the trade(s) in + * the Bisq Desktop application. *

- * The benefit this bot provides is freeing up the user time spent watching the offer book in the UI, waiting for the - * right offer to take. Low-priced offers are taken relatively quickly; this bot increases the chance of beating - * the other nodes at taking the offer. - *

- * The disadvantage is that if the user takes offers with the API, she must complete the trades with the desktop UI. - * This problem is due to the inability of the API to fully automate every step of the trading protocol. Sending fiat - * payments, and confirming their receipt, are manual activities performed outside the Bisq daemon and desktop UI. - * Also, the API and the desktop UI cannot run at the same time. Care must be taken to shut down one before starting - * the other. - *

- * The criteria for determining which offers to take are defined in the bot's configuration file - * TakeBestPricedOfferToSellBtc.properties (located in project's src/main/resources directory). The individual - * configurations are commented in the existing TakeBestPricedOfferToSellBtc.properties, which should be used as a - * template for your own use case. - *

- * One possible use case for this bot is buy BTC with GBP: + * Here is one possible use case: *

- *      Take a "Faster Payment (Santander)" offer to sell BTC for GBP at or below current market price if:
- *          the offer maker is a preferred trading peer,
- *          and the offer's BTC amount is between 0.10 and 0.25 BTC,
- *          and the current transaction mining fee rate is below 20 sats / byte.
+ *      Take 4 "Faster Payment" offers to sell BTC for GBP, priced no higher than 1.00% above the current market
+ *      price if:
+ *
+ *          the offer's BTC amount is between 0.10 and 0.25 BTC
+ *          the offer maker is one of two preferred trading peers
+ *          the current transaction mining fee rate is below 20 sats / byte
+ *
+ *  The bot configurations for these rules are set in TakeBestPricedOfferToSellBtc.properties as follows:
+ *
+ *          maxTakeOffers=4
+ *          minMarketPriceMargin=1.00
+ *          minAmount=0.10
+ *          maxAmount=0.25
+ *          preferredTradingPeers=preferred-address-1.onion:9999,preferred-address-2.onion:9999
+ *          maxTxFeeRate=20
  * 
- *

- * Another possible use case for this bot is to sell BTC for XMR. (We might say "buy XMR with BTC", but we need to - * remember that all Bisq offers are for buying or selling BTC.) + * Usage + *


+ * You must encrypt your wallet password before running this bot. If it is not already encrypted, you can use the CLI: *

- *      Take an offer to sell BTC for XMR at or below current market price if:
- *          the offer maker is a preferred trading peer,
- *          and the offer's BTC amount is between 0.50 and 1.00 BTC,
- *          and the current transaction mining fee rate is below 15 sats / byte.
+ *     $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
  * 
- *

+ * There are some {@link bisq.bots.Config program options} common to all the Java bot examples, passed on the command + * line. The only one you must provide (no default value) is your API daemon's password option: + * `--password `. The bot will prompt you for your wallet-password in the console. + *


+ * You can pass the '--dryrun=true' option to the program to see what offers your bot would take with a given + * configuration. This will help you avoid taking offers by mistake. *

- * Usage:  TakeBestPricedOfferToSellBtc  --password=api-password --port=api-port \
- *                          [--conf=take-best-priced-offer-to-sell-btc.conf] \
- *                          [--dryrun=true|false]
- *                          [--simulate-regtest-payment=true|false]
+ *     TakeBestPricedOfferToBuyBtc  --password=api-password --port=api-port [--dryrun=true|false]
+ * 
+ * If your API daemon is running on a local regtest network (with a trading peer), you can pass the + * '--simulate-regtest-payment=true' option to the program to simulate the full trade protocol. The bot will print + * your regtest trading peer's CLI commands in the console, for you to copy/paste into another terminal. + *
+ *     TakeBestPricedOfferToBuyBtc  --password=api-password --port=api-port [--simulate-regtest-payment=true|false]
  * 
*/ @Slf4j @@ -168,7 +175,6 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot { takeCriteria.printOfferAgainstCriteria(cheapestOffer); }); - printDryRunProgress(); runCountdown(log, pollingInterval); pingDaemon(startTime); } @@ -182,16 +188,21 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot { private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) { log.info("Will attempt to take offer '{}'.", offer.getId()); takeCriteria.printOfferAgainstCriteria(offer); + + // An encrypted wallet must be unlocked before calling takeoffer and gettrade(s). + // Unlock the wallet for 5 minutes. If the wallet is already unlocked, this request + // will override the timeout of the previous unlock request. + try { + unlockWallet(walletPassword, 300); + } catch (NonFatalException nonFatalException) { + handleNonFatalException(nonFatalException, pollingInterval); + } + if (isDryRun) { addToOffersTaken(offer); numOffersTaken++; - maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } else { - // An encrypted wallet must be unlocked before calling takeoffer and gettrade. - // Unlock the wallet for 5 minutes. If the wallet is already unlocked, - // this command will override the timeout of the previous unlock command. try { - unlockWallet(walletPassword, 600); printBTCBalances("BTC Balances Before Take Offer Attempt"); // Blocks until new trade is prepared, or times out. takeV1ProtocolOffer(offer, paymentAccount, bisqTradeFeeCurrency, pollingInterval); @@ -206,13 +217,13 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot { printBTCBalances("BTC Balances After Simulated Trade Completion"); } numOffersTaken++; - maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } catch (NonFatalException nonFatalException) { handleNonFatalException(nonFatalException, pollingInterval); } catch (StatusRuntimeException fatalException) { shutdownAfterTakeOfferFailure(fatalException); } } + maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers); } /** @@ -230,6 +241,8 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot { configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion()); var network = getNetwork(); configsByLabel.put("BTC Network:", network); + configsByLabel.put("Dry Run?", isDryRun ? "YES" : "NO"); + configsByLabel.put("Simulate Regtest Trade?", canSimulatePaymentSteps ? "YES" : "NO"); configsByLabel.put("My Payment Account:", ""); configsByLabel.put("\tPayment Account Id:", paymentAccount.getId()); configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName()); @@ -334,9 +347,9 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot { : "N/A"); if (offer.getUseMarketBasedPrice()) { - var marginPriceLabel = format("Is offer's price margin (%s%%) <= bot's max market price margin (%s%%)?", - offer.getMarketPriceMarginPct(), - maxMarketPriceMargin); + var marginPriceLabel = format("Is offer's margin based price (%s) <= bot's target price (%s)?", + offer.getPrice() + " " + currencyCode, + targetPrice + " " + currencyCode); filterResultsByLabel.put(marginPriceLabel, isMarginLEMaxMarketPriceMargin.test(offer, maxMarketPriceMargin)); } else { var fixedPriceLabel = format("Is offer's fixed-price (%s) <= bot's target price (%s)?", diff --git a/java-examples/src/main/resources/TakeBestPricedOfferToSellBtc.properties b/java-examples/src/main/resources/TakeBestPricedOfferToSellBtc.properties index 47d6e76..3f5099c 100644 --- a/java-examples/src/main/resources/TakeBestPricedOfferToSellBtc.properties +++ b/java-examples/src/main/resources/TakeBestPricedOfferToSellBtc.properties @@ -1,12 +1,12 @@ # # Maximum # of offers to take during one bot session. When reached, bot will shut down API daemon then itself. -maxTakeOffers=4 +maxTakeOffers=1 # # Taker bot's payment account id. Only SELL BTC offers using the same payment method will be considered for taking. -paymentAccountId=6e58f3d9-e7a3-4799-aa38-e28e624d79a3 +paymentAccountId=09dbadfd-c2ff-4bf4-b8d7-1d63e11d0238 # # Taker bot's max market price margin. A candidate SELL BTC offer's price margin must be <= maxMarketPriceMargin. -maxMarketPriceMargin=3.00 +maxMarketPriceMargin=0.00 # # Taker bot's min BTC amount to buy. The candidate SELL offer's amount must be >= minAmount BTC. minAmount=0.01 @@ -16,7 +16,7 @@ maxAmount=0.50 # # Taker bot's max acceptable transaction fee rate (sats / byte). # Regtest fee rates are from https://price.bisq.wiz.biz/getFees -maxTxFeeRate=100 +maxTxFeeRate=20 # # Bisq trade fee currency code (BSQ or BTC). bisqTradeFeeCurrency=BSQ @@ -35,4 +35,4 @@ preferredTradingPeers=localhost:8888, \ x6x2o3m6rxhkfuf2v6lalbharf3whwvkts5rdn3jkhgieqvnq6mvdfyd.onion:9999 # # Offer polling frequency must be >= 1s (1000ms) between each getoffers request. -pollingInterval=20000 +pollingInterval=60000 From 5384d3ac59de5928a4842d1c4248a699ffa42ae1 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 1 Jul 2022 12:39:17 -0300 Subject: [PATCH 23/41] Fix logging in AbstractBot's maybeShutdownAfterSuccessfulSwap() --- .../src/main/java/bisq/bots/AbstractBot.java | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/AbstractBot.java b/java-examples/src/main/java/bisq/bots/AbstractBot.java index 18a6d81..2e8a9a4 100644 --- a/java-examples/src/main/java/bisq/bots/AbstractBot.java +++ b/java-examples/src/main/java/bisq/bots/AbstractBot.java @@ -795,35 +795,30 @@ public abstract class AbstractBot { } /** - * Print the day's completed trades since midnight today, then: + * Print the day's completed trades since midnight today, and number of offers taken during the bot session, then, *

- * If numOffersTaken >= maxTakeOffers, and trade completion is being simulated on regtest, shut down the bot. + * If numOffersTaken >= maxTakeOffers, shut down the bot. *

- * If numOffersTaken >= maxTakeOffers, and mainnet trade completion must be delegated to the UI, shut down the - * API daemon and the bot. - *

- * If numOffersTaken < maxTakeOffers, just log the number of offers taken so far during the bot run. - * (Don't shut down anything.) + * If numOffersTaken < maxTakeOffers, log the number of offers taken so far during the bot run. * * @param numOffersTaken the number of offers taken during bot run * @param maxTakeOffers the max number of offers that can be taken during bot run */ protected void maybeShutdownAfterSuccessfulSwap(int numOffersTaken, int maxTakeOffers) { - printTradesSummaryForToday(CLOSED); + printTradesSummaryForTodayIfWalletIsUnlocked(); - if (!isDryRun) { - try { - lockWallet(); - } catch (NonFatalException ex) { - log.warn(ex.getMessage()); - } + log.info("You {} have taken {} swap offer(s) during this bot's {}", + isDryRun ? "would" : "", + numOffersTaken, + isDryRun ? "dryrun:" : "session."); + if (isDryRun) { + printDryRunProgress(); } + if (numOffersTaken >= maxTakeOffers) { isShutdown = true; log.info("Shutting down API bot after executing {} BSQ swaps.", numOffersTaken); exit(0); - } else { - log.info("You have completed {} BSQ swap(s) during this bot session.", numOffersTaken); } } From 7df2533469ddb72d8f3d81374c09694e32945061 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 1 Jul 2022 12:40:21 -0300 Subject: [PATCH 24/41] Tidy up and document TakeBestPricedOfferToBuyBsq bot --- .../bots/TakeBestPricedOfferToBuyBsq.java | 68 ++++++++++++++++--- .../TakeBestPricedOfferToBuyBsq.properties | 6 +- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java index 2a26c14..4a3e7fd 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java @@ -32,7 +32,50 @@ import static java.math.RoundingMode.HALF_UP; import static protobuf.OfferDirection.SELL; /** - * Bot for selling BSQ for BTC at an attractive (higher) price. The bot sends BSQ for BTC. + * This bot's general use case is to sell your BSQ for BTC at a high BTC price. It periodically checks the + * Buy BSQ (Sell BTC) market, and takes a configured maximum number of offers to buy BSQ from you according to criteria + * you define in the bot's configuration file: TakeBestPricedOfferToBuyBsq.properties (located in project's + * src/main/resources directory). You will need to replace the default values in the configuration file for your + * use cases. + *


+ * After the maximum number of BSQ swap offers have been taken (good to start with 1), the bot will shut down. The API + * daemon will not be shut down because swaps do not require additional payment related steps taken outside Bisq, or + * in the GUI. + *

+ * Here is one possible use case: + *

+ *      Take 1 BSQ swap offer to buy BSQ with BTC, priced no lower than 0.50% above the 30-day average BSQ price if:
+ *
+ *          the offer's BTC amount is between 0.10 and 0.25 BTC
+ *          the offer maker is one of two preferred trading peers
+ *          the current transaction mining fee rate is below 20 sats / byte
+ *
+ *  The bot configurations for these rules are set in TakeBestPricedOfferToBuyBsq.properties as follows:
+ *
+ *          maxTakeOffers=1
+ *          minMarketPriceMargin=0.50
+ *          minAmount=0.10
+ *          maxAmount=0.25
+ *          preferredTradingPeers=preferred-address-1.onion:9999,preferred-address-2.onion:9999
+ *          maxTxFeeRate=20
+ * 
+ * Usage + *


+ * You must encrypt your wallet password before running this bot. If it is not already encrypted, you can use the CLI: + *

+ *     $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
+ * 
+ * There are some {@link bisq.bots.Config program options} common to all the Java bot examples, passed on the command + * line. The only one you must provide (no default value) is your API daemon's password option: + * `--password `. The bot will prompt you for your wallet-password in the console. + *


+ * You can pass the '--dryrun=true' option to the program to see what offers your bot would take with a given + * configuration. This will help you avoid taking offers by mistake. + *

+ *     TakeBestPricedOfferToBuyBsq  --password=api-password --port=api-port [--dryrun=true|false]
+ * 
+ *

+ * The '--simulate-regtest-payment=true' option is ignored by this bot. Taking a swap triggers execution of the swap. */ @Slf4j @Getter @@ -99,7 +142,8 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot { continue; } - // Get all available and takeable offers, sorted by price descending. + // Get all available sell BTC for BSQ offers, sorted by price descending. + // The list contains only fixed-priced offers. var offers = getOffers(SELL.name(), CURRENCY_CODE).stream() .filter(o -> !isAlreadyTaken.test(o)) .toList(); @@ -125,7 +169,6 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot { takeCriteria.printOfferAgainstCriteria(highestPricedOffer); }); - printDryRunProgress(); runCountdown(log, pollingInterval); pingDaemon(startTime); } @@ -134,17 +177,21 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot { private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) { log.info("Will attempt to take offer '{}'.", offer.getId()); takeCriteria.printOfferAgainstCriteria(offer); + + // An encrypted wallet must be unlocked before calling takeoffer and gettrade(s). + // Unlock the wallet for 5 minutes. If the wallet is already unlocked, this request + // will override the timeout of the previous unlock request. + try { + unlockWallet(walletPassword, 300); + } catch (NonFatalException nonFatalException) { + handleNonFatalException(nonFatalException, pollingInterval); + } + if (isDryRun) { addToOffersTaken(offer); numOffersTaken++; - maybeShutdownAfterSuccessfulSwap(numOffersTaken, maxTakeOffers); } else { - // An encrypted wallet must be unlocked before calling takeoffer and gettrade. - // Unlock the wallet for 10 minutes. If the wallet is already unlocked, - // this command will override the timeout of the previous unlock command. try { - unlockWallet(walletPassword, 600); - printBTCBalances("BTC Balances Before Swap Execution"); printBSQBalances("BSQ Balances Before Swap Execution"); @@ -155,13 +202,13 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot { printBSQBalances("BSQ Balances After Swap Execution"); numOffersTaken++; - maybeShutdownAfterSuccessfulSwap(numOffersTaken, maxTakeOffers); } catch (NonFatalException nonFatalException) { handleNonFatalException(nonFatalException, pollingInterval); } catch (StatusRuntimeException fatalException) { handleFatalBsqSwapException(fatalException); } } + maybeShutdownAfterSuccessfulSwap(numOffersTaken, maxTakeOffers); } private void printBotConfiguration() { @@ -169,6 +216,7 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot { configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion()); var network = getNetwork(); configsByLabel.put("BTC Network:", network); + configsByLabel.put("Dry Run?", isDryRun ? "YES" : "NO"); var isMainnet = network.equalsIgnoreCase("mainnet"); var mainnet30DayAvgBsqPrice = isMainnet ? get30DayAvgBsqPriceInBtc() : null; configsByLabel.put("My Payment Account:", ""); diff --git a/java-examples/src/main/resources/TakeBestPricedOfferToBuyBsq.properties b/java-examples/src/main/resources/TakeBestPricedOfferToBuyBsq.properties index 286bd30..89e3682 100644 --- a/java-examples/src/main/resources/TakeBestPricedOfferToBuyBsq.properties +++ b/java-examples/src/main/resources/TakeBestPricedOfferToBuyBsq.properties @@ -1,12 +1,12 @@ # Maximum # of offers to take during one bot session. When reached, bot will shut down (but not the API daemon). -maxTakeOffers=50 +maxTakeOffers=5 # # Minimum distance from 30-day average BSQ trade price. # Note: all BSQ Swap offers have a fixed-price, but the bot uses a margin (%) of the 30-day price for comparison. minMarketPriceMargin=0.00 # # Hard coded 30-day average BSQ trade price, used for development over regtest. -regtest30DayAvgBsqPrice=0.00005 +regtest30DayAvgBsqPrice=0.00004 # # Taker bot's min BTC amount to sell. The candidate SELL BTC offer's amount must be >= minAmount BTC. minAmount=0.01 @@ -24,4 +24,4 @@ maxTxFeeRate=25 preferredTradingPeers=localhost:8888 # # Offer polling frequency must be >= 1s (1000ms) between each getoffers request. -pollingInterval=30000 +pollingInterval=60000 From 661ef2fa418a2e8423182f07de551f683843a5ba Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 1 Jul 2022 15:29:02 -0300 Subject: [PATCH 25/41] Add link to Config.java in class docs Hideous, but navigable. --- .../src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java | 2 ++ .../src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java | 2 ++ .../src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java | 2 ++ .../src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java | 2 ++ .../src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java | 2 ++ 5 files changed, 10 insertions(+) diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java index 4a3e7fd..0cc1301 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java @@ -76,6 +76,8 @@ import static protobuf.OfferDirection.SELL; * *

* The '--simulate-regtest-payment=true' option is ignored by this bot. Taking a swap triggers execution of the swap. + * + * @see bisq.bots.Config.java */ @Slf4j @Getter diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java index 039ed84..b636ae9 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java @@ -81,6 +81,8 @@ import static protobuf.OfferDirection.BUY; *

  *     TakeBestPricedOfferToBuyBtc  --password=api-password --port=api-port [--simulate-regtest-payment=true|false]
  * 
+ * + * @see bisq.bots.Config.java */ @Slf4j @Getter diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java index 684fb57..b601734 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java @@ -80,6 +80,8 @@ import static protobuf.OfferDirection.SELL; *
  *     TakeBestPricedOfferToBuyXmr  --password=api-password --port=api-port [--simulate-regtest-payment=true|false]
  * 
+ * + * @see bisq.bots.Config.java */ @Slf4j @Getter diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java index 4dc9e17..e70595a 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java @@ -81,6 +81,8 @@ import static protobuf.OfferDirection.SELL; *
  *     TakeBestPricedOfferToBuyBtc  --password=api-password --port=api-port [--simulate-regtest-payment=true|false]
  * 
+ * + * @see bisq.bots.Config.java */ @Slf4j @Getter diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java index 1ba8bdd..07739a7 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java @@ -80,6 +80,8 @@ import static protobuf.OfferDirection.BUY; *
  *     TakeBestPricedOfferToSellXmr  --password=api-password --port=api-port [--simulate-regtest-payment=true|false]
  * 
+ * + * @see bisq.bots.Config.java */ @Slf4j @Getter From 3c08b0bf4627e0bf632e36d37041172f7da973bb Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 1 Jul 2022 15:45:14 -0300 Subject: [PATCH 26/41] Tweak log statement --- .../src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java index 0cc1301..d1c4e84 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java @@ -334,7 +334,7 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot { iHavePreferredTradingPeers.get() ? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO" : "N/A"); - var fixedPriceLabel = format("Is offer fixed-price (%s) >= bot's minimum price (%s)?", + var fixedPriceLabel = format("Is offer fixed-price (%s) >= bot's minimum price of (%s)?", offer.getPrice() + " BTC", targetPrice + " BTC"); filterResultsByLabel.put(fixedPriceLabel, isFixedPriceGEMaxMarketPriceMargin.test(offer, avgBsqPrice)); From 9907d8f5f82c9b2ab08fd0fc42e4e2a0aefd61ab Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 1 Jul 2022 16:00:17 -0300 Subject: [PATCH 27/41] Fix javadoc about maximum tx fee rate constraint --- .../src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java | 2 +- .../src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java | 2 +- .../src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java | 2 +- .../src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java | 2 +- .../src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java index d1c4e84..ac862bc 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java @@ -48,7 +48,7 @@ import static protobuf.OfferDirection.SELL; * * the offer's BTC amount is between 0.10 and 0.25 BTC * the offer maker is one of two preferred trading peers - * the current transaction mining fee rate is below 20 sats / byte + * the current transaction mining fee rate is less than or equal 20 sats / byte * * The bot configurations for these rules are set in TakeBestPricedOfferToBuyBsq.properties as follows: * diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java index b636ae9..476af30 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java @@ -49,7 +49,7 @@ import static protobuf.OfferDirection.BUY; * * the offer's BTC amount is between 0.10 and 0.25 BTC * the offer maker is one of two preferred trading peers - * the current transaction mining fee rate is below 20 sats / byte + * the current transaction mining fee rate is less than or equal 20 sats / byte * * The bot configurations for these rules are set in TakeBestPricedOfferToBuyBtc.properties as follows: * diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java index b601734..168274b 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java @@ -48,7 +48,7 @@ import static protobuf.OfferDirection.SELL; * * the offer's BTC amount is between 0.50 and 1.00 BTC * the offer maker is one of two preferred trading peers - * the current transaction mining fee rate is below 20 sats / byte + * the current transaction mining fee rate is less than or equal 20 sats / byte * * The bot configurations for these rules are set in TakeBestPricedOfferToBuyXmr.properties as follows: * diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java index e70595a..d44c37f 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java @@ -49,7 +49,7 @@ import static protobuf.OfferDirection.SELL; * * the offer's BTC amount is between 0.10 and 0.25 BTC * the offer maker is one of two preferred trading peers - * the current transaction mining fee rate is below 20 sats / byte + * the current transaction mining fee rate is less than or equal 20 sats / byte * * The bot configurations for these rules are set in TakeBestPricedOfferToSellBtc.properties as follows: * diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java index 07739a7..92f0944 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java @@ -48,7 +48,7 @@ import static protobuf.OfferDirection.BUY; * * the offer's BTC amount is between 0.50 and 1.00 BTC * the offer maker is one of two preferred trading peers - * the current transaction mining fee rate is below 15 sats / byte + * the current transaction mining fee rate is less than or equal 15 sats / byte * * The bot configurations for these rules are set in TakeBestPricedOfferToSellXmr.properties as follows: * From 8456fd9fb6a3c358f6c4d32957a4680fba95bb77 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 1 Jul 2022 16:10:38 -0300 Subject: [PATCH 28/41] Tweak log statement --- java-examples/src/main/java/bisq/bots/AbstractBot.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/AbstractBot.java b/java-examples/src/main/java/bisq/bots/AbstractBot.java index 2e8a9a4..c9279c3 100644 --- a/java-examples/src/main/java/bisq/bots/AbstractBot.java +++ b/java-examples/src/main/java/bisq/bots/AbstractBot.java @@ -807,8 +807,8 @@ public abstract class AbstractBot { protected void maybeShutdownAfterSuccessfulSwap(int numOffersTaken, int maxTakeOffers) { printTradesSummaryForTodayIfWalletIsUnlocked(); - log.info("You {} have taken {} swap offer(s) during this bot's {}", - isDryRun ? "would" : "", + log.info("You {}have taken {} swap offer(s) during this bot's {}", + isDryRun ? "would " : "", numOffersTaken, isDryRun ? "dryrun:" : "session."); if (isDryRun) { From 4133a4c0474807394aa513a00c71e13e7db2cff8 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 1 Jul 2022 16:18:23 -0300 Subject: [PATCH 29/41] Tidy up and document TakeBestPricedOfferToSellBsq bot --- .../bots/TakeBestPricedOfferToSellBsq.java | 75 +++++++++++++++---- .../TakeBestPricedOfferToSellBsq.properties | 8 +- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java index 5622119..c8084b0 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java @@ -32,7 +32,52 @@ import static java.math.RoundingMode.HALF_UP; import static protobuf.OfferDirection.BUY; /** - * Bot for buying BSQ with BTC at an attractive (lower) price. The bot receives BSQ for BTC. + * This bot's general use case is to buy BSQ with BTC at a low BTC price. It periodically checks the + * Sell BSQ (Buy BTC) market, and takes a configured maximum number of offers to buy BSQ from you according to criteria + * you define in the bot's configuration file: TakeBestPricedOfferToSellBsq.properties (located in project's + * src/main/resources directory). You will need to replace the default values in the configuration file for your + * use cases. + *


+ * After the maximum number of BSQ swap offers have been taken (good to start with 1), the bot will shut down. The API + * daemon will not be shut down because swaps do not require additional payment related steps taken outside Bisq, or + * in the GUI. + *

+ * Here is one possible use case: + *

+ *      Take 5 BSQ swap offers to sell BSQ for BTC, priced no higher than -1.00% below the 30-day average BSQ price if:
+ *
+ *          the offer's BTC amount is between 0.10 and 0.25 BTC
+ *          the offer maker is one of two preferred trading peers
+ *          the current transaction mining fee rate is less than or equal 25 sats / byte
+ *
+ *  The bot configurations for these rules are set in TakeBestPricedOfferToSellBsq.properties as follows:
+ *
+ *          maxTakeOffers=5
+ *          minMarketPriceMargin=-1.00
+ *          minAmount=0.10
+ *          maxAmount=0.25
+ *          preferredTradingPeers=preferred-address-1.onion:9999,preferred-address-2.onion:9999
+ *          maxTxFeeRate=25
+ * 
+ * Usage + *


+ * You must encrypt your wallet password before running this bot. If it is not already encrypted, you can use the CLI: + *

+ *     $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
+ * 
+ * There are some {@link bisq.bots.Config program options} common to all the Java bot examples, passed on the command + * line. The only one you must provide (no default value) is your API daemon's password option: + * `--password `. The bot will prompt you for your wallet-password in the console. + *


+ * You can pass the '--dryrun=true' option to the program to see what offers your bot would take with a given + * configuration. This will help you avoid taking offers by mistake. + *

+ *     TakeBestPricedOfferToBuyBsq  --password=api-password --port=api-port [--dryrun=true|false]
+ * 
+ *

+ * The '--simulate-regtest-payment=true' option is ignored by this bot. Taking a swap triggers execution of the swap. + * + * @see bisq.bots.Config.java */ @Slf4j @Getter @@ -99,7 +144,8 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { continue; } - // Get all available and takeable offers, sorted by price ascending. + // Get all available buy BTC with BSQ offers, sorted by price ascending. + // The list contains only fixed-priced offers. var offers = getOffers(BUY.name(), CURRENCY_CODE).stream() .filter(o -> !isAlreadyTaken.test(o)) .toList(); @@ -125,7 +171,6 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { takeCriteria.printOfferAgainstCriteria(cheapestOffer); }); - printDryRunProgress(); runCountdown(log, pollingInterval); pingDaemon(startTime); } @@ -134,17 +179,21 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) { log.info("Will attempt to take offer '{}'.", offer.getId()); takeCriteria.printOfferAgainstCriteria(offer); + + // An encrypted wallet must be unlocked before calling takeoffer and gettrade(s). + // Unlock the wallet for 5 minutes. If the wallet is already unlocked, this request + // will override the timeout of the previous unlock request. + try { + unlockWallet(walletPassword, 300); + } catch (NonFatalException nonFatalException) { + handleNonFatalException(nonFatalException, pollingInterval); + } + if (isDryRun) { addToOffersTaken(offer); numOffersTaken++; - maybeShutdownAfterSuccessfulSwap(numOffersTaken, maxTakeOffers); } else { - // An encrypted wallet must be unlocked before calling takeoffer and gettrade. - // Unlock the wallet for 10 minutes. If the wallet is already unlocked, - // this command will override the timeout of the previous unlock command. try { - unlockWallet(walletPassword, 600); - printBTCBalances("BTC Balances Before Swap Execution"); printBSQBalances("BSQ Balances Before Swap Execution"); @@ -155,13 +204,13 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { printBSQBalances("BSQ Balances After Swap Execution"); numOffersTaken++; - maybeShutdownAfterSuccessfulSwap(numOffersTaken, maxTakeOffers); } catch (NonFatalException nonFatalException) { handleNonFatalException(nonFatalException, pollingInterval); } catch (StatusRuntimeException fatalException) { handleFatalBsqSwapException(fatalException); } } + maybeShutdownAfterSuccessfulSwap(numOffersTaken, maxTakeOffers); } private void printBotConfiguration() { @@ -169,6 +218,7 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion()); var network = getNetwork(); configsByLabel.put("BTC Network:", network); + configsByLabel.put("Dry Run?", isDryRun ? "YES" : "NO"); var isMainnet = network.equalsIgnoreCase("mainnet"); var mainnet30DayAvgBsqPrice = isMainnet ? get30DayAvgBsqPriceInBtc() : null; configsByLabel.put("My Payment Account:", ""); @@ -226,7 +276,6 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { this.targetPrice = calcTargetBsqPrice(maxMarketPriceMargin, avgBsqPrice); } - /** * Returns the lowest priced offer passing the filters, or Optional.empty() if not found. * Max tx fee rate filtering should have passed prior to calling this method. @@ -277,7 +326,7 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { var filterResultsByLabel = new LinkedHashMap(); filterResultsByLabel.put("30-day Avg BSQ trade price:", avgBsqPrice + " BTC"); - filterResultsByLabel.put("Target Price (Min):", targetPrice + " BTC"); + filterResultsByLabel.put("Target Price (Max):", targetPrice + " BTC"); filterResultsByLabel.put("Offer Price:", offer.getPrice() + " BTC"); filterResultsByLabel.put("Offer maker used same payment method?", usesSamePaymentMethod.test(offer, getPaymentAccount())); @@ -285,7 +334,7 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot { iHavePreferredTradingPeers.get() ? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO" : "N/A"); - var fixedPriceLabel = format("Is offer's fixed-price (%s) <= bot's minimum price (%s)?", + var fixedPriceLabel = format("Is offer's fixed-price (%s) <= bot's maximum price of (%s)?", offer.getPrice() + " BTC", targetPrice + " BTC"); filterResultsByLabel.put(fixedPriceLabel, isFixedPriceLEMaxMarketPriceMargin.test(offer, avgBsqPrice)); diff --git a/java-examples/src/main/resources/TakeBestPricedOfferToSellBsq.properties b/java-examples/src/main/resources/TakeBestPricedOfferToSellBsq.properties index ee55ba2..3d44768 100644 --- a/java-examples/src/main/resources/TakeBestPricedOfferToSellBsq.properties +++ b/java-examples/src/main/resources/TakeBestPricedOfferToSellBsq.properties @@ -1,12 +1,12 @@ # Maximum # of offers to take during one bot session. When reached, bot will shut down (but not the API daemon). -maxTakeOffers=50 +maxTakeOffers=5 # # Maximum distance from 30-day average BSQ trade price. # Note: all BSQ Swap offers have a fixed-price, but the bot uses a margin (%) of the 30-day price for comparison. -maxMarketPriceMargin=0.00 +maxMarketPriceMargin=-1.0 # # Hard coded 30-day average BSQ trade price, used for development over regtest. -regtest30DayAvgBsqPrice=0.00037 +regtest30DayAvgBsqPrice=0.00060 # # Taker bot's min BTC amount to buy. The candidate BUY BTC offer's amount must be >= minAmount BTC. minAmount=0.01 @@ -24,4 +24,4 @@ maxTxFeeRate=25 preferredTradingPeers=localhost:8888 # # Offer polling frequency must be >= 1s (1000ms) between each getoffers request. -pollingInterval=30000 +pollingInterval=60000 From 483b2d8eab0c44a6922054e31d1eba0dd435ab63 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 2 Jul 2022 11:59:17 -0300 Subject: [PATCH 30/41] Save README work in progress --- java-examples/README.md | 168 ++++++++++++++++++++-------------------- 1 file changed, 86 insertions(+), 82 deletions(-) diff --git a/java-examples/README.md b/java-examples/README.md index 70ef305..2565002 100644 --- a/java-examples/README.md +++ b/java-examples/README.md @@ -1,117 +1,120 @@ # Bisq API Java Examples And Bots This subproject contains: + * [Java API rpc examples](https://github.com/bisq-network/bisq-api-reference/tree/main/java-examples/src/main/java/bisq/rpccalls) demonstrating how to send API gRPC requests, and handle responses. * [Java API bots](https://github.com/bisq-network/bisq-api-reference/tree/main/java-examples/src/main/java/bisq/bots) - to use as is on mainnet, and demonstrate how to write new API bots. If interested in porting any bot code to - other languages supported by [gRPC](https://grpc.io/docs/languages), please use these Java bot examples, not the - Python examples. The Python examples were written by a Python noob, and don't handle errors. - - TODO Add warning about this to Python Examples README. + to use as is on mainnet, and demonstrate how to write new API bots. If interested in porting any bot code to other + languages supported by [gRPC](https://grpc.io/docs/languages), please use these Java bot examples, not the Python + examples. The Python examples were written by a Python noob, and don't handle errors. * A [Gradle build file](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/build.gradle) - that could be used as a template for your own API bot project. + that could be used as a template for your own Java API bot project. -## Risks And Warnings +## Risks, Warnings and Flaws ### Never Run API Daemon and [Bisq GUI](https://bisq.network) On Same Host At Same Time -The API daemon and the GUI share the same default wallet and connection ports. Beyond inevitable failures due to -fighting over the wallet and ports, doing so will probably corrupt your wallet. Before starting the API daemon, -make sure your GUI is shut down, and vice-versa. Please back up your mainnet wallet early and often with the GUI. - -TODO Add warning about this to Python Examples README, Api Reference, and Beta Test Guide. +The API daemon and the GUI share the same default wallet and connection ports. Beyond inevitable failures due to +fighting over the wallet and ports, doing so will probably corrupt your wallet. Before starting the API daemon, make +sure your GUI is shut down, and vice-versa. Please back up your mainnet wallet early and often with the GUI. ### Go Slow (But Much Faster Than You Click Buttons In The GUI) -[Bisq](https://bisq.network) was designed to respond to manual clicks in the user interface. It is not a -high-throughput, high-performance system supporting atomic transactions. Care must be taken to avoid -problems due to slow wallet updates on your disk, and Tor network latency. The API daemon enforces limits on request -frequency via call rate metering, but you cannot assume bots can perform tasks as rapidly as the API daemon's call -rate meters allow. +[Bisq](https://bisq.network) was designed to respond to manual clicks in the user interface. It is not a +high-throughput, high-performance system supporting atomic transactions. Care must be taken to avoid problems due to +slow wallet updates on your disk, and Tor network latency. The API daemon enforces limits on request frequency via call +rate metering, but you cannot assume bots can perform tasks as rapidly as the API daemon's call rate meters allow. ### Run Bots On Mainnet At Your Own Risk -This document would not state "these bots can be run on mainnet, as is" without reasonable confidence on the part of -the code's author, but if you do, you do so at your own risk. Copious details about running them on a local BTC -regtest network, running on mainnet in **dryrun** mode, and each bot's configuration is provided in this document. -Please put some effort into understanding a bot's code and its configuration before trying it on mainnet. +This document would not state "these bots can be run on mainnet, as is" without reasonable confidence on the part of the +code's author, but if you do, you do so at your own risk. Copious details about running them on a local BTC regtest +network, running on mainnet in **dryrun** mode, and each bot's configuration is provided in this document. Please put +some effort into understanding a bot's code and its configuration before trying it on mainnet. -TODO Add warning about this to Python Examples README. +### Why There Is Duplicated Code In The Bots + +The TakeBestPricedOffer* bots could be combined into a single class, and use multithreaded task scheduling instead of +loops with Thread sleep instructions, but we want them to be easily understood by people who are not necessarily +experienced Java coders (and be easily portable to other [gRPC supported language bindings](https://grpc.io/docs/languages)). +For non-developers, splitting up a one-size-fits-all TakeBestPricedOffer also makes them easier to configure for various +BTC/Fiat, XMR/BTC, and BSQ/BTC market use cases. ## Generating Protobuf Code ### Download IDL (.proto) Files From The [Bisq Repo](https://github.com/bisq-network/bisq) -The protobuf IDL files are not part of this project, and must be downloaded from the -[Bisq repo](https://github.com/bisq-network/bisq/tree/master/proto/src/main/proto). +The protobuf IDL files are not part of this project, and must be downloaded from the Bisq repository's +[protobuf file directory](https://github.com/bisq-network/bisq/tree/master/proto/src/main/proto). -You can download them by running +TODO @ripcurlx, please review https://github.com/ghubstan/bisq-api-reference/pull/11 + +You can download them by running [this script](https://github.com/bisq-network/bisq-api-reference/blob/main/proto-downloader/download-bisq-protos.sh) from your IDE or a shell: + ```asciidoc $ proto-downloader/download-bisq-protos.sh ``` -The java-examples [build file](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/build.gradle) +The java-examples [build file](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/build.gradle) will generate the code from the downloaded IDL (.proto) files. You should be able to generate the protobuf Java sources and all Java examples in your IDE. In a terminal: + ```asciidoc -$ cd java-examples -$ ./gradlew clean build +$ cd java-examples $ ./gradlew clean build ``` ## Java API Method Examples -Each class in +Each class in the [bisq.rpccalls](https://github.com/bisq-network/bisq-api-reference/tree/main/java-examples/src/main/java/bisq/rpccalls) package is named for the RPC method call being demonstrated. -Their purpose is to show how to construct a gRPC service method request with parameters, send the -request, and print a response object if there is one. As a rule, the request was successful if -no gRPC StatusRuntimeException was received from the API daemon. +Their purpose is to show how to construct a gRPC service method request with parameters, send the request, and print a +response object if there is one. As a rule, the request is successful if a gRPC StatusRuntimeException is not thrown +by the API daemon. -Their usage is simple; there are no program arguments for any of them. Just run them with an IDE -program launcher or your shell. - -However, you will often need to edit the Java class and re-compile it before running it because these -examples know nothing about real Payment Account IDs, Offer IDs, etc. To run the +Their usage is simple; there are no program arguments for any of them. Just run them with an IDE program launcher or +your shell. However, you will usually need to edit the Java class and re-compile it before running it because these +examples know nothing about real Payment Account IDs, Offer IDs, etc. To run the [GetOffer.java](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/src/main/java/bisq/rpccalls/GetOffer.java) -example, your will need to change the hard-coded offer ID to a real offer ID to avoid a "not found" -exception. +example, your will need to change the hard-coded offer ID to a real offer ID to avoid a "not found" gRPC +StatusRuntimeException from the API daemon. ## Java API Bots ### Purpose -The Java API bots in this project are meant run on mainnet, provide a base for more complex bots, and guide you in -developing your own bots. +The Java API bots in this project are meant to be run on mainnet, provide a base for more complex bots, and guide you +in developing your own bots. ### Run on mainnet at your own risk! -You need to understand a bot's code and its configuration before trying it on mainnet. While you get familiar with -a bot example, you can run it in **dryrun** mode to see how it behaves with different configurations (more later). -Even better: run it while your Bisq API daemons (seednode, arbitration-node, and a trading peer) are connected to a -local BTC regtest network. +You need to understand a bot's code and its configuration before trying it on mainnet. While you get familiar with a bot +example, you can run it in **dryrun** mode to see how it behaves with different configurations (more later). Even +better: run it while your Bisq API daemons (seednode, arbitration-node, and a trading peer) are connected to a local +BTC regtest network. The [API test harness](https://github.com/bisq-network/bisq/blob/master/apitest/docs/api-beta-test-guide.md) is -convenient for this. +convenient for this. -#### Quick And Dirty Test Harness +#### Quick And Dirty Test Harness -TODO: Create issue for out of date https://bisq.wiki/Downloading_and_installing#Build_from_source? - -If you are already familiar with [building Bisq source code](https://github.com/bisq-network/bisq/blob/master/docs/build.md), -and have [bitcoin-core binaries](https://github.com/bitcoin/bitcoin) on your system's $PATH, you might skip the +If you are already familiar +with [building Bisq source code](https://github.com/bisq-network/bisq/blob/master/docs/build.md), and +have [bitcoin-core binaries](https://github.com/bitcoin/bitcoin) on your system's $PATH, you might skip the [API test harness setup guide](https://github.com/bisq-network/bisq/blob/master/apitest/docs/api-beta-test-guide.md). Before you try the test harness, make sure your host is not running any bitcoind or Bisq nodes. Clone the Bisq master branch to your host, build and start it: + ```asciidoc #Clone Bisq source repo. $ git clone https://github.com/bisq-network/bisq.git [some folder] @@ -120,9 +123,10 @@ $ cd [some folder] #Build Bisq source, install DAO/Regtest wallet files. $ ./gradlew clean build :apitest:installDaoSetup -x test -#Start local bitcoind (regtest) node and headless test harness nodes. -$ ./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false +#Start local bitcoind (regtest) node and headless test harness nodes. +$ ./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false ``` + To shut down the test harness, enter **^C**. #### Creating And Using Runnable Bot Jars @@ -133,53 +137,52 @@ TODO (more later) There are four bots for taking offers: -* [TakeBestPricedOfferToBuyBsq (From You)](https://github.com/bisq-network/bisq-api-reference/blob/update-java-examples-readme/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java) -* [TakeBestPricedOfferToSellBsq (To You)](https://github.com/bisq-network/bisq-api-reference/blob/update-java-examples-readme/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java) +* [TakeBestPricedOfferToBuyBsq (From You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java) +* [TakeBestPricedOfferToSellBsq (To You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java) -* [TakeBestPricedOfferToBuyBtc (From You)](https://github.com/bisq-network/bisq-api-reference/blob/update-java-examples-readme/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java) -* [TakeBestPricedOfferToSellBtc (To You)](https://github.com/bisq-network/bisq-api-reference/blob/update-java-examples-readme/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java) +* [TakeBestPricedOfferToBuyBtc (From You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java) +* [TakeBestPricedOfferToSellBtc (To You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java) * [TakeBestPricedOfferToBuyXmr (From You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java) * [TakeBestPricedOfferToSellXmr (To You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java) - The **Take Buy/Sell BTC** bots take 1 or more offers for a given criteria as defined in each bot's configuration file -(more later). After the configured maximum number of offers have been taken, the bot shuts down the API daemon and -itself because trade payments are made outside Bisq. The Bisq API *cannot automate the fiat or XMR trade payment and -receipt confirmation steps of the protocol*. When an offer is taken by the API, the trade payment steps of the protocol +(more later). After the configured maximum number of offers have been taken, the bot shuts down the API daemon and +itself because trade payments are made outside Bisq. The Bisq API *cannot automate the fiat or XMR trade payment and +receipt confirmation steps of the protocol*. When an offer is taken by the API, the trade payment steps of the protocol must be performed in the UI. -The **Take Buy/Sell BSQ** bots take 1 or more offers for a given criteria as defined in each bot's configuration file -(more later). After the configured maximum number of offers have been taken, the bot shuts down itself, but not the -API daemon. BSQ Swaps can be fully automated by the API because the swap transaction is completed within seconds of -taking a BSQ Swap offer. +The **Take Buy/Sell BSQ** bots take 1 or more offers for a given criteria as defined in each bot's configuration file +(more later). After the configured maximum number of offers have been taken, the bot shuts down itself, but not the API +daemon. BSQ Swaps can be fully automated by the API because the swap transaction is completed within seconds of taking a +BSQ Swap offer. -#### [TakeBestPricedOfferToSellBtc (To You)](https://github.com/bisq-network/bisq-api-reference/blob/update-java-examples-readme/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java) +#### [TakeBestPricedOfferToSellBtc (To You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java) **Purpose** -This bot waits for attractively priced SELL BTC offers to appear, takes the offers, then shuts down both the API daemon -and itself (the bot). The user should then proceed to start the desktop UI application and complete the new trades. +This bot waits for attractively priced SELL BTC offers to appear, takes the offers, then shuts down both the API daemon +and itself (the bot). The user should then proceed to start the desktop UI application and complete the new trades. -The benefit this bot provides is freeing up the user time spent watching the offer book in the UI, waiting for the -right offer to take. Low-priced BTC offers are taken relatively quickly; this bot increases the chance of beating -the other nodes at taking the offer. +The benefit this bot provides is freeing up the user time spent watching the offer book in the UI, waiting for the right +offer to take. Low-priced BTC offers are taken relatively quickly; this bot increases the chance of beating the other +nodes at taking the offer. -The disadvantage is that if the user takes offers with the API, she must complete the trades with the desktop UI. -This problem is due to the inability of the API to fully automate every step of the trading protocol. Sending fiat or -XMR payments, and confirming their receipt, are manual activities performed outside the Bisq daemon and desktop UI. -Also, the API and the desktop UI cannot run at the same time. Care must be taken to shut down one before starting -the other. +The disadvantage is that if the user takes offers with the API, she must complete the trades with the desktop UI. This +problem is due to the inability of the API to fully automate every step of the trading protocol. Sending fiat or XMR +payments, and confirming their receipt, are manual activities performed outside the Bisq daemon and desktop UI. Also, +the API and the desktop UI cannot run at the same time. Care must be taken to shut down one before starting the other. -The criteria for determining which offers to take are defined in the bot's configuration file -TakeBestPricedOfferToSellBtc.properties (located in project's src/main/resources directory). The individual -configurations are commented in the existing TakeBestPricedOfferToSellBtc.properties, which should be used as a -template for your own use case. +The criteria for determining which offers to take are defined in the bot's configuration file +TakeBestPricedOfferToSellBtc.properties (located in project's src/main/resources directory). The individual +configurations are commented in the existing TakeBestPricedOfferToSellBtc.properties, which should be used as a template +for your own use case. **Use Cases** -One possible use case for this bot is to buy BTC with GBP, e.g., take a "Faster Payment" offer to sell BTC (to you) for +One possible use case for this bot is to buy BTC with GBP, e.g., take a "Faster Payment" offer to sell BTC (to you) for GBP at or below the current market GBP price if: + * the offer maker is a preferred trading peer * the offer's BTC amount is between 0.10 and 0.25 BTC * the current transaction mining fee rate is less than or equal 20 sats / byte @@ -201,7 +204,8 @@ $ java -jar x.jar --password=xyz --conf=[path.conf] --dryrun=true ## Gradle Build File -This project's [Gradle build file](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/build.gradle), +This +project's [Gradle build file](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/build.gradle), shows how to generate the necessary protobuf classes from the Bisq .proto files. From 24a7e098aaf71a2c5b24d59cd1b0939b4a3995e3 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 3 Jul 2022 14:29:10 -0300 Subject: [PATCH 31/41] Improve logging, fix bad file list (ls) cmd --- java-examples/scripts/create-runnable-jar.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/java-examples/scripts/create-runnable-jar.sh b/java-examples/scripts/create-runnable-jar.sh index 10dc197..1538494 100755 --- a/java-examples/scripts/create-runnable-jar.sh +++ b/java-examples/scripts/create-runnable-jar.sh @@ -84,15 +84,15 @@ MAINCLASS_FILE_PATH=$(getmainclassfilepath "$FULLY_QUALIFIED_CLASSNAME") # Extract the Main-Class from the distribution jar, to the current working directory. jar xfv "lib/$GRADLE_DIST_NAME.jar" "$MAINCLASS_FILE_PATH" "$SIMPLE_CLASSNAME.properties" -echo "Extracted one class:" -ls -l bisq/bots/pazza +echo "Extracted $SIMPLE_CLASSNAME.class:" +ls -l "bisq/bots/$SIMPLE_CLASSNAME.class" mv "$SIMPLE_CLASSNAME.properties" "$JAR_BASENAME.conf" -echo "Extracted one properties file and renamed it $JAR_BASENAME.conf" -ls -l *.conf +echo "Extracted $SIMPLE_CLASSNAME.properties and renamed it $JAR_BASENAME.conf" +ls -l "$JAR_BASENAME.conf" # Now it can be added to the empty jar with the correct path. jar uf "$JAR_BASENAME.jar" "$MAINCLASS_FILE_PATH" -# Remove bisq (bisq/bots/junk). +# Remove workarea. rm -rf bisq echo "Runnable $JAR_BASENAME.jar is ready to use." From fd31922e97885fc34f069b8015cac45fe8bcf525 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 3 Jul 2022 16:34:48 -0300 Subject: [PATCH 32/41] Explain runnable jars, create header links --- java-examples/README.md | 107 +++++++++++++++++++++++++++++----------- 1 file changed, 78 insertions(+), 29 deletions(-) diff --git a/java-examples/README.md b/java-examples/README.md index 2565002..ab12cd5 100644 --- a/java-examples/README.md +++ b/java-examples/README.md @@ -13,7 +13,7 @@ This subproject contains: * A [Gradle build file](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/build.gradle) that could be used as a template for your own Java API bot project. -## Risks, Warnings and Flaws +## [Risks, Warnings and Flaws](#risks-warnings-and-flaws) ### Never Run API Daemon and [Bisq GUI](https://bisq.network) On Same Host At Same Time @@ -43,7 +43,7 @@ experienced Java coders (and be easily portable to other [gRPC supported languag For non-developers, splitting up a one-size-fits-all TakeBestPricedOffer also makes them easier to configure for various BTC/Fiat, XMR/BTC, and BSQ/BTC market use cases. -## Generating Protobuf Code +## [Generating Protobuf Code](#generating-protobuf-code) ### Download IDL (.proto) Files From The [Bisq Repo](https://github.com/bisq-network/bisq) @@ -71,7 +71,7 @@ In a terminal: $ cd java-examples $ ./gradlew clean build ``` -## Java API Method Examples +## [Java API Method Examples](#java-api-method-examples) Each class in the [bisq.rpccalls](https://github.com/bisq-network/bisq-api-reference/tree/main/java-examples/src/main/java/bisq/rpccalls) @@ -88,19 +88,17 @@ examples know nothing about real Payment Account IDs, Offer IDs, etc. To run the example, your will need to change the hard-coded offer ID to a real offer ID to avoid a "not found" gRPC StatusRuntimeException from the API daemon. -## Java API Bots +## [Java API Bots](#java-api-bots) ### Purpose The Java API bots in this project are meant to be run on mainnet, provide a base for more complex bots, and guide you in developing your own bots. -### Run on mainnet at your own risk! - -You need to understand a bot's code and its configuration before trying it on mainnet. While you get familiar with a bot -example, you can run it in **dryrun** mode to see how it behaves with different configurations (more later). Even -better: run it while your Bisq API daemons (seednode, arbitration-node, and a trading peer) are connected to a local -BTC regtest network. +Put some effort into understanding a bot's code and its configuration before trying it on mainnet. While you get +familiar with a bot example, you can run it in **dryrun** mode to see how it behaves with different configurations +(more later). Even better: run it while your Bisq API daemons (seednode, arbitration-node, and a trading peer) are +connected to a local BTC regtest network. The [API test harness](https://github.com/bisq-network/bisq/blob/master/apitest/docs/api-beta-test-guide.md) is convenient for this. @@ -116,26 +114,22 @@ Before you try the test harness, make sure your host is not running any bitcoind Clone the Bisq master branch to your host, build and start it: ```asciidoc -#Clone Bisq source repo. +# Clone Bisq source repo. $ git clone https://github.com/bisq-network/bisq.git [some folder] $ cd [some folder] -#Build Bisq source, install DAO/Regtest wallet files. +# Build Bisq source, install DAO/Regtest wallet files (with coins). $ ./gradlew clean build :apitest:installDaoSetup -x test -#Start local bitcoind (regtest) node and headless test harness nodes. +# Start local bitcoind (regtest) node and headless test harness nodes. $ ./bisq-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false ``` To shut down the test harness, enter **^C**. -#### Creating And Using Runnable Bot Jars - -TODO (more later) - ### Take BSQ / BTC / XMR / Offer Bots -There are four bots for taking offers: +There are six bots for taking offers: * [TakeBestPricedOfferToBuyBsq (From You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java) * [TakeBestPricedOfferToSellBsq (To You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java) @@ -146,11 +140,23 @@ There are four bots for taking offers: * [TakeBestPricedOfferToBuyXmr (From You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java) * [TakeBestPricedOfferToSellXmr (To You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java) -The **Take Buy/Sell BTC** bots take 1 or more offers for a given criteria as defined in each bot's configuration file -(more later). After the configured maximum number of offers have been taken, the bot shuts down the API daemon and -itself because trade payments are made outside Bisq. The Bisq API *cannot automate the fiat or XMR trade payment and -receipt confirmation steps of the protocol*. When an offer is taken by the API, the trade payment steps of the protocol -must be performed in the UI. +The **Take Buy/Sell BTC and XMR Offer** bots take 1 or more offers for a given criteria as defined in each bot's +configuration file (more later). After the configured maximum number of offers have been taken, the bot shuts down +the API daemon and itself because trade payments are made outside Bisq. Bisq nodes (UI or API) do not communicate +with your banks and XMR wallets, and *cannot automate fiat and XMR trade payments and deposit confirmations*. + +The Bisq trade payment protocol steps of the Bisq protocol can be performed in the UI, or you can finish the trade with +an API daemon and manual CLI commands: +```asciidoc +# If you are a BTC buyer, notify peer you have initiated fiat or XMR payment. +$ ./bisq-cli --password=xyz --port=9998 confirmpaymentstarted --trade-id= + +# If you are a BTC seller, notify peer your have received fiat or XMR payment. +$ ./bisq-cli --password=xyz --port=9998 confirmpaymentreceived --trade-id= + +# Close your completed trade (move it to your trade history). +$ ./bisq-cli --password=xyz --port=9998 closetrade --trade-id= +``` The **Take Buy/Sell BSQ** bots take 1 or more offers for a given criteria as defined in each bot's configuration file (more later). After the configured maximum number of offers have been taken, the bot shuts down itself, but not the API @@ -170,8 +176,8 @@ nodes at taking the offer. The disadvantage is that if the user takes offers with the API, she must complete the trades with the desktop UI. This problem is due to the inability of the API to fully automate every step of the trading protocol. Sending fiat or XMR -payments, and confirming their receipt, are manual activities performed outside the Bisq daemon and desktop UI. Also, -the API and the desktop UI cannot run at the same time. Care must be taken to shut down one before starting the other. +payments, and confirming their receipt, are manual activities performed outside the Bisq daemon and desktop UI. When you +switch back and forth between the API daemon and UI, be careful not to let them run at the same time. The criteria for determining which offers to take are defined in the bot's configuration file TakeBestPricedOfferToSellBtc.properties (located in project's src/main/resources directory). The individual @@ -187,7 +193,7 @@ GBP at or below the current market GBP price if: * the offer's BTC amount is between 0.10 and 0.25 BTC * the current transaction mining fee rate is less than or equal 20 sats / byte -**Class Doc Link** +**Usage** **Configuration** @@ -196,13 +202,56 @@ GBP at or below the current market GBP price if: * Param 3 * ... -**Usage** +**Creating And Using Runnable TakeBestPricedOfferToSellBtc Jar** +To create the runnable jar, see [Creating Runnable Jars](#creating-runnable-jars). + +To run the jar, edit the conf file for your use case, and run it: ```asciidoc -$ java -jar x.jar --password=xyz --conf=[path.conf] --dryrun=true +$ java -jar take-best-priced-offer-to-buy-btc.jar \ + --password=xyz \ + --dryrun=false \ + --conf=take-best-priced-offer-to-buy-btc.conf ``` -## Gradle Build File +### [Creating Runnable Jars](#creating-runnable-jars) + +You can create runnable jars for these bots and run them in a terminal. After building the java-examples project, run +the script **java-examples/scripts/create-bot-jars.sh**. You can run the jar from the +**java-examples/scripts/java-examples** folder created by the script, or copy that folder where you like and run it +from there. + +Here are the steps to create runnable bot jars. +```asciidoc +# Build the java-examples project. +$ cd java-examples +$ ./gradlew clean build + +# Build the runnable bot jars. Each jar contains one class, with its dependencies defined in its MANIFEST.MF. +$ scripts/create-bot-jars.sh +``` + +Each jar has its own conf file, generated from the bot source code's properties file. For example, + +* take-best-priced-offer-to-buy-btc.jar +* take-best-priced-offer-to-buy-btc.conf + +are created from +* TakeBestPricedOfferToBuyBtc.java +* TakeBestPricedOfferToBuyBtc.properties + +To run it, edit the conf file for your use case and run a java -jar command: + +```asciidoc +$ java -jar take-best-priced-offer-to-buy-btc.jar \ + --password=xyz \ + --dryrun=false \ + --conf=take-best-priced-offer-to-buy-btc.conf +``` + +You can rename a conf file as you like, and save several copies for specific use cases. + +## [Gradle Build File](#gradle-build-file) This project's [Gradle build file](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/build.gradle), From 618268805ffd56498da2cfaab280b03c81433c8a Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 4 Jul 2022 15:04:22 -0300 Subject: [PATCH 33/41] Describe btc bots, refer to class javadocs --- java-examples/README.md | 153 +++++++++++++++++++++++----------------- 1 file changed, 87 insertions(+), 66 deletions(-) diff --git a/java-examples/README.md b/java-examples/README.md index ab12cd5..2f78b97 100644 --- a/java-examples/README.md +++ b/java-examples/README.md @@ -39,15 +39,16 @@ some effort into understanding a bot's code and its configuration before trying The TakeBestPricedOffer* bots could be combined into a single class, and use multithreaded task scheduling instead of loops with Thread sleep instructions, but we want them to be easily understood by people who are not necessarily -experienced Java coders (and be easily portable to other [gRPC supported language bindings](https://grpc.io/docs/languages)). -For non-developers, splitting up a one-size-fits-all TakeBestPricedOffer also makes them easier to configure for various -BTC/Fiat, XMR/BTC, and BSQ/BTC market use cases. +experienced Java coders (and be easily portable to +other [gRPC supported language bindings](https://grpc.io/docs/languages)). For non-developers, splitting up a +one-size-fits-all TakeBestPricedOffer also makes them easier to configure for various BTC/Fiat, XMR/BTC, and BSQ/BTC +market use cases. ## [Generating Protobuf Code](#generating-protobuf-code) ### Download IDL (.proto) Files From The [Bisq Repo](https://github.com/bisq-network/bisq) -The protobuf IDL files are not part of this project, and must be downloaded from the Bisq repository's +The protobuf IDL files are not part of this project, and must be downloaded from the Bisq repository's [protobuf file directory](https://github.com/bisq-network/bisq/tree/master/proto/src/main/proto). TODO @ripcurlx, please review https://github.com/ghubstan/bisq-api-reference/pull/11 @@ -78,27 +79,27 @@ the [bisq.rpccalls](https://github.com/bisq-network/bisq-api-reference/tree/main package is named for the RPC method call being demonstrated. Their purpose is to show how to construct a gRPC service method request with parameters, send the request, and print a -response object if there is one. As a rule, the request is successful if a gRPC StatusRuntimeException is not thrown -by the API daemon. +response object if there is one. As a rule, the request is successful if a gRPC StatusRuntimeException is not thrown by +the API daemon. Their usage is simple; there are no program arguments for any of them. Just run them with an IDE program launcher or -your shell. However, you will usually need to edit the Java class and re-compile it before running it because these +your shell. However, you will usually need to edit the Java class and re-compile it before running it because these examples know nothing about real Payment Account IDs, Offer IDs, etc. To run the [GetOffer.java](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/src/main/java/bisq/rpccalls/GetOffer.java) -example, your will need to change the hard-coded offer ID to a real offer ID to avoid a "not found" gRPC +example, your will need to change the hard-coded offer ID to a real offer ID to avoid a "not found" gRPC StatusRuntimeException from the API daemon. ## [Java API Bots](#java-api-bots) ### Purpose -The Java API bots in this project are meant to be run on mainnet, provide a base for more complex bots, and guide you -in developing your own bots. +The Java API bots in this project are meant to be run on mainnet, provide a base for more complex bots, and guide you in +developing your own bots. -Put some effort into understanding a bot's code and its configuration before trying it on mainnet. While you get -familiar with a bot example, you can run it in **dryrun** mode to see how it behaves with different configurations -(more later). Even better: run it while your Bisq API daemons (seednode, arbitration-node, and a trading peer) are -connected to a local BTC regtest network. +Put some effort into understanding a bot's code and its configuration before trying it on mainnet. While you get +familiar with a bot example, you can run it in **dryrun** mode to see how it behaves with different configurations +(more later). Even better: run it while your Bisq API daemons (seednode, arbitration-node, and a trading peer) are +connected to a local BTC regtest network. The [API test harness](https://github.com/bisq-network/bisq/blob/master/apitest/docs/api-beta-test-guide.md) is convenient for this. @@ -131,22 +132,23 @@ To shut down the test harness, enter **^C**. There are six bots for taking offers: +* [TakeBestPricedOfferToBuyBtc](#take-best-priced-offer-to-buy-btc) +* [TakeBestPricedOfferToSellBtc](#take-best-priced-offer-to-sell-btc) + * [TakeBestPricedOfferToBuyBsq (From You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java) * [TakeBestPricedOfferToSellBsq (To You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java) -* [TakeBestPricedOfferToBuyBtc (From You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java) -* [TakeBestPricedOfferToSellBtc (To You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java) - * [TakeBestPricedOfferToBuyXmr (From You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java) * [TakeBestPricedOfferToSellXmr (To You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java) -The **Take Buy/Sell BTC and XMR Offer** bots take 1 or more offers for a given criteria as defined in each bot's -configuration file (more later). After the configured maximum number of offers have been taken, the bot shuts down -the API daemon and itself because trade payments are made outside Bisq. Bisq nodes (UI or API) do not communicate -with your banks and XMR wallets, and *cannot automate fiat and XMR trade payments and deposit confirmations*. +The **Take Buy/Sell BTC and XMR Offer** bots take 1 or more offers for a given criteria as defined in each bot's +configuration file (more later). After the configured maximum number of offers have been taken, the bot shuts down the +API daemon and itself because trade payments are made outside Bisq. Bisq nodes (UI or API) do not communicate with your +banks and XMR wallets, and *cannot automate fiat and XMR trade payments and deposit confirmations*. -The Bisq trade payment protocol steps of the Bisq protocol can be performed in the UI, or you can finish the trade with +The Bisq trade payment protocol steps of the Bisq protocol can be performed in the UI, or you can finish the trade with an API daemon and manual CLI commands: + ```asciidoc # If you are a BTC buyer, notify peer you have initiated fiat or XMR payment. $ ./bisq-cli --password=xyz --port=9998 confirmpaymentstarted --trade-id= @@ -163,50 +165,70 @@ The **Take Buy/Sell BSQ** bots take 1 or more offers for a given criteria as def daemon. BSQ Swaps can be fully automated by the API because the swap transaction is completed within seconds of taking a BSQ Swap offer. -#### [TakeBestPricedOfferToSellBtc (To You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java) +### [TakeBestPricedOfferToBuyBtc (From You)](#take-best-priced-offer-to-buy-btc) -**Purpose** +**Purpose (Sell High)** -This bot waits for attractively priced SELL BTC offers to appear, takes the offers, then shuts down both the API daemon -and itself (the bot). The user should then proceed to start the desktop UI application and complete the new trades. +This bot waits for attractively priced BUY BTC (with fiat) offers to appear, and takes 1 or more of them according to +the bot's configuration. The bot will consider only those offers created with the same payment method associated with +your bot's configured payment account id. -The benefit this bot provides is freeing up the user time spent watching the offer book in the UI, waiting for the right -offer to take. Low-priced BTC offers are taken relatively quickly; this bot increases the chance of beating the other -nodes at taking the offer. +The benefit this bot provides is freeing up user time spent watching the offer book in the UI, waiting for the right +offer to take. The disadvantage is having to perform some trade protocol steps outside the API daemon. +Fiat payment confirmations must be done as they are when using the Bisq UI -- outside the Bisq application. After bank +payments are confirmed in your online banking system, trades can be completed in the desktop UI, or via API CLI commands. -The disadvantage is that if the user takes offers with the API, she must complete the trades with the desktop UI. This -problem is due to the inability of the API to fully automate every step of the trading protocol. Sending fiat or XMR -payments, and confirming their receipt, are manual activities performed outside the Bisq daemon and desktop UI. When you -switch back and forth between the API daemon and UI, be careful not to let them run at the same time. +**Warning** -The criteria for determining which offers to take are defined in the bot's configuration file -TakeBestPricedOfferToSellBtc.properties (located in project's src/main/resources directory). The individual -configurations are commented in the existing TakeBestPricedOfferToSellBtc.properties, which should be used as a template -for your own use case. +Take special care to not run the Bisq API daemon and the desktop application at the same time on the same host. -**Use Cases** +**Use Cases, Usage and Configuration** -One possible use case for this bot is to buy BTC with GBP, e.g., take a "Faster Payment" offer to sell BTC (to you) for -GBP at or below the current market GBP price if: - -* the offer maker is a preferred trading peer -* the offer's BTC amount is between 0.10 and 0.25 BTC -* the current transaction mining fee rate is less than or equal 20 sats / byte - -**Usage** - -**Configuration** - -* Param 1 -* Param 2 -* Param 3 -* ... +This information is found in +the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java) +. **Creating And Using Runnable TakeBestPricedOfferToSellBtc Jar** -To create the runnable jar, see [Creating Runnable Jars](#creating-runnable-jars). +To create a runnable jar, see [Creating Runnable Jars](#creating-runnable-jars). To run the jar, edit the conf file for +your use case, and use the java -jar command: + +```asciidoc +$ java -jar take-best-priced-offer-to-sell-btc.jar \ + --password=xyz \ + --dryrun=false \ + --conf=take-best-priced-offer-to-sell-btc.conf +``` + +### [TakeBestPricedOfferToSellBtc (To You)](#take-best-priced-offer-to-sell-btc) + +**Purpose (Buy Low)** + +This bot waits for attractively priced SELL BTC (for fiat) offers to appear, and takes 1 or more of them according to +the bot's configuration. The bot will consider only those offers created with the same payment method associated with +your bot's configured payment account id. + +The benefit this bot provides is freeing up user time spent watching the offer book in the UI, waiting for the right +offer to take. Low-priced BTC offers are taken relatively quickly; this bot increases the chance of beating other nodes +to the offer. The disadvantage is having to perform some trade protocol steps outside the API daemon. Fiat +payments must be sent as they are when using the Bisq UI -- outside the Bisq application. After bank payments are made +in your online banking system, trades can be completed in the desktop UI, or via API CLI commands. + +**Warning** + +Take special care to not run the Bisq API daemon and the desktop application at the same time on the same host. + +**Use Cases, Usage and Configuration** + +This information is found in +the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java) +. + +**Creating And Using Runnable TakeBestPricedOfferToSellBtc Jar** + +To create a runnable jar, see [Creating Runnable Jars](#creating-runnable-jars). To run the jar, edit the conf file for +your use case, and use the java -jar command: -To run the jar, edit the conf file for your use case, and run it: ```asciidoc $ java -jar take-best-priced-offer-to-buy-btc.jar \ --password=xyz \ @@ -216,12 +238,13 @@ $ java -jar take-best-priced-offer-to-buy-btc.jar \ ### [Creating Runnable Jars](#creating-runnable-jars) -You can create runnable jars for these bots and run them in a terminal. After building the java-examples project, run -the script **java-examples/scripts/create-bot-jars.sh**. You can run the jar from the -**java-examples/scripts/java-examples** folder created by the script, or copy that folder where you like and run it -from there. +You can create runnable jars for these bots and run them in a terminal. After building the java-examples project, run +the script **java-examples/scripts/create-bot-jars.sh**. You can run the jar from the +**java-examples/scripts/java-examples** folder created by the script, or copy that folder where you like and run it from +there. Here are the steps to create runnable bot jars. + ```asciidoc # Build the java-examples project. $ cd java-examples @@ -231,22 +254,20 @@ $ ./gradlew clean build $ scripts/create-bot-jars.sh ``` -Each jar has its own conf file, generated from the bot source code's properties file. For example, +Each jar has its own conf file, generated from the bot source code's properties file. For example, -* take-best-priced-offer-to-buy-btc.jar +* take-best-priced-offer-to-buy-btc.jar * take-best-priced-offer-to-buy-btc.conf are created from -* TakeBestPricedOfferToBuyBtc.java + +* TakeBestPricedOfferToBuyBtc.java * TakeBestPricedOfferToBuyBtc.properties To run it, edit the conf file for your use case and run a java -jar command: ```asciidoc -$ java -jar take-best-priced-offer-to-buy-btc.jar \ - --password=xyz \ - --dryrun=false \ - --conf=take-best-priced-offer-to-buy-btc.conf +$ java -jar take-best-priced-offer-to-sell-btc.jar \ --password=xyz \ --dryrun=false \ --conf=take-best-priced-offer-to-sell-btc.conf ``` You can rename a conf file as you like, and save several copies for specific use cases. From 25b6e5721e9786b2d291b4a17854c787fdede23c Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 4 Jul 2022 15:50:14 -0300 Subject: [PATCH 34/41] Fix header links --- java-examples/README.md | 57 ++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/java-examples/README.md b/java-examples/README.md index 2f78b97..4c84b37 100644 --- a/java-examples/README.md +++ b/java-examples/README.md @@ -132,14 +132,15 @@ To shut down the test harness, enter **^C**. There are six bots for taking offers: -* [TakeBestPricedOfferToBuyBtc](#take-best-priced-offer-to-buy-btc) -* [TakeBestPricedOfferToSellBtc](#take-best-priced-offer-to-sell-btc) +* [Take Best Priced Offer To Buy Btc](#take-best-priced-offer-to-buy-btc) +* [Take Best Priced Offer To Sell Btc](#take-best-priced-offer-to-sell-btc) -* [TakeBestPricedOfferToBuyBsq (From You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java) -* [TakeBestPricedOfferToSellBsq (To You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java) -* [TakeBestPricedOfferToBuyXmr (From You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java) -* [TakeBestPricedOfferToSellXmr (To You)](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java) +* [Take Best Priced Offer To Buy Bsq](#take-best-priced-offer-to-buy-bsq) +* [Take Best Priced Offer To Sell Bsq](#take-best-priced-offer-to-sell-bsq) + +* [Take Best Priced Offer To Buy Xmr](#take-best-priced-offer-to-buy-xmr) +* [Take Best Priced Offer To Sell Xmr](#take-best-priced-offer-to-sell-xmr) The **Take Buy/Sell BTC and XMR Offer** bots take 1 or more offers for a given criteria as defined in each bot's configuration file (more later). After the configured maximum number of offers have been taken, the bot shuts down the @@ -165,7 +166,7 @@ The **Take Buy/Sell BSQ** bots take 1 or more offers for a given criteria as def daemon. BSQ Swaps can be fully automated by the API because the swap transaction is completed within seconds of taking a BSQ Swap offer. -### [TakeBestPricedOfferToBuyBtc (From You)](#take-best-priced-offer-to-buy-btc) +### [Take Best Priced Offer To Buy Btc](#take-best-priced-offer-to-buy-btc) **Purpose (Sell High)** @@ -200,7 +201,7 @@ $ java -jar take-best-priced-offer-to-sell-btc.jar \ --conf=take-best-priced-offer-to-sell-btc.conf ``` -### [TakeBestPricedOfferToSellBtc (To You)](#take-best-priced-offer-to-sell-btc) +### [Take Best Priced Offer To Sell Btc](#take-best-priced-offer-to-sell-btc) **Purpose (Buy Low)** @@ -236,6 +237,41 @@ $ java -jar take-best-priced-offer-to-buy-btc.jar \ --conf=take-best-priced-offer-to-buy-btc.conf ``` + +### [Take Best Priced Offer To Buy Bsq](#take-best-priced-offer-to-buy-bsq) + +**Use Cases, Usage and Configuration** + +This information is found in +the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java) +. + +### [Take Best Priced Offer To Sell Bsq](#take-best-priced-offer-to-sell-bsq) + +**Use Cases, Usage and Configuration** + +This information is found in +the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java) +. + +### [Take Best Priced Offer To Buy Xmr](#take-best-priced-offer-to-buy-xmr) + +**Use Cases, Usage and Configuration** + +This information is found in +the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java) +. + +### [Take Best Priced Offer To Sell Xmr](#take-best-priced-offer-to-sell-xmr) + +**Use Cases, Usage and Configuration** + +This information is found in +the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java) +. + + + ### [Creating Runnable Jars](#creating-runnable-jars) You can create runnable jars for these bots and run them in a terminal. After building the java-examples project, run @@ -267,7 +303,10 @@ are created from To run it, edit the conf file for your use case and run a java -jar command: ```asciidoc -$ java -jar take-best-priced-offer-to-sell-btc.jar \ --password=xyz \ --dryrun=false \ --conf=take-best-priced-offer-to-sell-btc.conf +$ java -jar take-best-priced-offer-to-sell-btc.jar \ + --password=xyz \ + --dryrun=false \ + --conf=take-best-priced-offer-to-sell-btc.conf ``` You can rename a conf file as you like, and save several copies for specific use cases. From ed1b5f78626b137f36bf38382fe3bd90d4a05f4e Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 4 Jul 2022 15:56:12 -0300 Subject: [PATCH 35/41] Fix javadoc --- .../src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java index c8084b0..e30d76a 100644 --- a/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java +++ b/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java @@ -53,7 +53,7 @@ import static protobuf.OfferDirection.BUY; * The bot configurations for these rules are set in TakeBestPricedOfferToSellBsq.properties as follows: * * maxTakeOffers=5 - * minMarketPriceMargin=-1.00 + * maxMarketPriceMargin=-1.00 * minAmount=0.10 * maxAmount=0.25 * preferredTradingPeers=preferred-address-1.onion:9999,preferred-address-2.onion:9999 From 566013da1c30e3f7398e8273e9b679c0d659a1ba Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 4 Jul 2022 16:08:13 -0300 Subject: [PATCH 36/41] Fill out bsq/xmr bot descriptions --- java-examples/README.md | 98 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/java-examples/README.md b/java-examples/README.md index 4c84b37..78417e4 100644 --- a/java-examples/README.md +++ b/java-examples/README.md @@ -135,7 +135,6 @@ There are six bots for taking offers: * [Take Best Priced Offer To Buy Btc](#take-best-priced-offer-to-buy-btc) * [Take Best Priced Offer To Sell Btc](#take-best-priced-offer-to-sell-btc) - * [Take Best Priced Offer To Buy Bsq](#take-best-priced-offer-to-buy-bsq) * [Take Best Priced Offer To Sell Bsq](#take-best-priced-offer-to-sell-bsq) @@ -189,16 +188,16 @@ This information is found in the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java) . -**Creating And Using Runnable TakeBestPricedOfferToSellBtc Jar** +**Creating And Using Runnable TakeBestPricedOfferToBuyBtc Jar** To create a runnable jar, see [Creating Runnable Jars](#creating-runnable-jars). To run the jar, edit the conf file for your use case, and use the java -jar command: ```asciidoc -$ java -jar take-best-priced-offer-to-sell-btc.jar \ +$ java -jar take-best-priced-offer-to-buy-btc.jar \ --password=xyz \ --dryrun=false \ - --conf=take-best-priced-offer-to-sell-btc.conf + --conf=take-best-priced-offer-to-buy-btc.conf ``` ### [Take Best Priced Offer To Sell Btc](#take-best-priced-offer-to-sell-btc) @@ -231,46 +230,131 @@ To create a runnable jar, see [Creating Runnable Jars](#creating-runnable-jars). your use case, and use the java -jar command: ```asciidoc -$ java -jar take-best-priced-offer-to-buy-btc.jar \ +$ java -jar take-best-priced-offer-to-sell-btc.jar \ --password=xyz \ --dryrun=false \ - --conf=take-best-priced-offer-to-buy-btc.conf + --conf=take-best-priced-offer-to-sell-btc.conf ``` - ### [Take Best Priced Offer To Buy Bsq](#take-best-priced-offer-to-buy-bsq) +**Purpose (Sell High)** + +This bot waits for attractively priced BUY BSQ (with BTC) offers to appear, and takes 1 or more of them according to +the bot's configuration. The bot will consider only those offers created with the same payment method associated with +your bot's configured payment account id. + +**Warning** + +Take special care to not run the Bisq API daemon and the desktop application at the same time on the same host. + **Use Cases, Usage and Configuration** This information is found in the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java) . +**Creating And Using Runnable TakeBestPricedOfferToBuyBsq Jar** + +To create a runnable jar, see [Creating Runnable Jars](#creating-runnable-jars). To run the jar, edit the conf file for +your use case, and use the java -jar command: + +```asciidoc +$ java -jar take-best-priced-offer-to-buy-bsq.jar \ + --password=xyz \ + --dryrun=false \ + --conf=take-best-priced-offer-to-buy-bsq.conf +``` + ### [Take Best Priced Offer To Sell Bsq](#take-best-priced-offer-to-sell-bsq) +**Purpose (Buy Low)** + +This bot waits for attractively priced SELL BSQ (for BTC) offers to appear, and takes 1 or more of them according to +the bot's configuration. The bot will consider only those offers created with the same payment method associated with +your bot's configured payment account id. + +**Warning** + +Take special care to not run the Bisq API daemon and the desktop application at the same time on the same host. + **Use Cases, Usage and Configuration** This information is found in the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java) . +**Creating And Using Runnable TakeBestPricedOfferToSellBsq Jar** + +To create a runnable jar, see [Creating Runnable Jars](#creating-runnable-jars). To run the jar, edit the conf file for +your use case, and use the java -jar command: + +```asciidoc +$ java -jar take-best-priced-offer-to-sell-bsq.jar \ + --password=xyz \ + --dryrun=false \ + --conf=take-best-priced-offer-to-sell-bsq.conf +``` + ### [Take Best Priced Offer To Buy Xmr](#take-best-priced-offer-to-buy-xmr) +**Purpose (Sell High)** + +This bot waits for attractively priced BUY XMR (with BTC) offers to appear, and takes 1 or more of them according to +the bot's configuration. The bot will consider only those offers created with the same payment method associated with +your bot's configured payment account id. + +**Warning** + +Take special care to not run the Bisq API daemon and the desktop application at the same time on the same host. + **Use Cases, Usage and Configuration** This information is found in the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java) . +**Creating And Using Runnable TakeBestPricedOfferToBuyXmr Jar** + +To create a runnable jar, see [Creating Runnable Jars](#creating-runnable-jars). To run the jar, edit the conf file for +your use case, and use the java -jar command: + +```asciidoc +$ java -jar take-best-priced-offer-to-buy-xmr.jar \ + --password=xyz \ + --dryrun=false \ + --conf=take-best-priced-offer-to-buy-xmr.conf +``` + ### [Take Best Priced Offer To Sell Xmr](#take-best-priced-offer-to-sell-xmr) +**Purpose (Buy Low)** + +This bot waits for attractively priced SELL XMR (for BTC) offers to appear, and takes 1 or more of them according to +the bot's configuration. The bot will consider only those offers created with the same payment method associated with +your bot's configured payment account id. + +**Warning** + +Take special care to not run the Bisq API daemon and the desktop application at the same time on the same host. + **Use Cases, Usage and Configuration** This information is found in the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java) . +**Creating And Using Runnable TakeBestPricedOfferToSellXmr Jar** +To create a runnable jar, see [Creating Runnable Jars](#creating-runnable-jars). To run the jar, edit the conf file for +your use case, and use the java -jar command: + +```asciidoc +$ java -jar take-best-priced-offer-to-sell-xmr.jar \ + --password=xyz \ + --dryrun=false \ + --conf=take-best-priced-offer-to-sell-xmr.conf +``` ### [Creating Runnable Jars](#creating-runnable-jars) From e8e4cc0d5f9d2fd18abade3e7b6b6beffa691b7d Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 4 Jul 2022 16:17:25 -0300 Subject: [PATCH 37/41] Improve bot Purpose --- java-examples/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/java-examples/README.md b/java-examples/README.md index 78417e4..699d192 100644 --- a/java-examples/README.md +++ b/java-examples/README.md @@ -167,7 +167,7 @@ BSQ Swap offer. ### [Take Best Priced Offer To Buy Btc](#take-best-priced-offer-to-buy-btc) -**Purpose (Sell High)** +**Purpose:** Sell your BTC at a higher fiat price This bot waits for attractively priced BUY BTC (with fiat) offers to appear, and takes 1 or more of them according to the bot's configuration. The bot will consider only those offers created with the same payment method associated with @@ -202,7 +202,7 @@ $ java -jar take-best-priced-offer-to-buy-btc.jar \ ### [Take Best Priced Offer To Sell Btc](#take-best-priced-offer-to-sell-btc) -**Purpose (Buy Low)** +**Purpose:** Buy BTC at a lower fiat price This bot waits for attractively priced SELL BTC (for fiat) offers to appear, and takes 1 or more of them according to the bot's configuration. The bot will consider only those offers created with the same payment method associated with @@ -238,7 +238,7 @@ $ java -jar take-best-priced-offer-to-sell-btc.jar \ ### [Take Best Priced Offer To Buy Bsq](#take-best-priced-offer-to-buy-bsq) -**Purpose (Sell High)** +**Purpose:** Sell your BSQ at a higher BTC price This bot waits for attractively priced BUY BSQ (with BTC) offers to appear, and takes 1 or more of them according to the bot's configuration. The bot will consider only those offers created with the same payment method associated with @@ -268,7 +268,7 @@ $ java -jar take-best-priced-offer-to-buy-bsq.jar \ ### [Take Best Priced Offer To Sell Bsq](#take-best-priced-offer-to-sell-bsq) -**Purpose (Buy Low)** +**Purpose:** Buy BSQ at a lower BTC price This bot waits for attractively priced SELL BSQ (for BTC) offers to appear, and takes 1 or more of them according to the bot's configuration. The bot will consider only those offers created with the same payment method associated with @@ -298,7 +298,7 @@ $ java -jar take-best-priced-offer-to-sell-bsq.jar \ ### [Take Best Priced Offer To Buy Xmr](#take-best-priced-offer-to-buy-xmr) -**Purpose (Sell High)** +**Purpose:** Sell your XMR at a higher BTC price This bot waits for attractively priced BUY XMR (with BTC) offers to appear, and takes 1 or more of them according to the bot's configuration. The bot will consider only those offers created with the same payment method associated with @@ -328,7 +328,7 @@ $ java -jar take-best-priced-offer-to-buy-xmr.jar \ ### [Take Best Priced Offer To Sell Xmr](#take-best-priced-offer-to-sell-xmr) -**Purpose (Buy Low)** +**Purpose:** Buy XMR at a lower BTC price This bot waits for attractively priced SELL XMR (for BTC) offers to appear, and takes 1 or more of them according to the bot's configuration. The bot will consider only those offers created with the same payment method associated with From 23b09932727e991ec5d59920f42bc339c6997123 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 4 Jul 2022 16:35:44 -0300 Subject: [PATCH 38/41] Add warning and java-example bot link --- python-examples/README.md | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/python-examples/README.md b/python-examples/README.md index da93181..16c2c2d 100644 --- a/python-examples/README.md +++ b/python-examples/README.md @@ -3,8 +3,37 @@ This subproject contains Python3 classes demonstrating API gRPC method calls, and some sample bots. Each class in -the [bisq.rpccalls](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples/bisq/rpccalls) package is -named for the RPC method call being demonstrated. +the [bisq.rpccalls](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples/bisq/rpccalls) package +is named for the RPC method call being demonstrated. + +The [bisq.bots](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples/bisq/rpccalls) package +contains some simple bots. Please do not run the Python bot examples on mainnet. +See [warning](#bot-not-ready-for-mainnet). The `run-setup.sh` script in this directory can install Python3 dependencies and example packages into a local venv. +## Risks, Warnings and Flaws + +### Never Run API Daemon and [Bisq GUI](https://bisq.network) On Same Host At Same Time + +The API daemon and the GUI share the same default wallet and connection ports. Beyond inevitable failures due to +fighting over the wallet and ports, doing so will probably corrupt your wallet. Before starting the API daemon, make +sure your GUI is shut down, and vice-versa. Please back up your mainnet wallet early and often with the GUI. + +### Go Slow (But Much Faster Than You Click Buttons In The GUI) + +[Bisq](https://bisq.network) was designed to respond to manual clicks in the user interface. It is not a +high-throughput, high-performance system supporting atomic transactions. Care must be taken to avoid problems due to +slow wallet updates on your disk, and Tor network latency. The API daemon enforces limits on request frequency via call +rate metering, but you cannot assume bots can perform tasks as rapidly as the API daemon's call rate meters allow. + +### [Do Not Run Python Bot Examples On Mainnet](#bot-not-ready-for-mainnet) + +The scripts in the [bisq.bots](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples/bisq/bots) +package should not be run on mainnet. They do not properly handle errors, and were written by a Python noob. + +The [Java Bot Examples](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/README.md) +are intended to be run on mainnet. An experienced Python developer could port these examples to Python for running on +mainnet, and offer them as a contribution to +the [Bisq API Reference](https://github.com/bisq-network/bisq-api-reference) +project. If accepted, they could be [compensated](https://bisq.wiki/Making_a_compensation_request). \ No newline at end of file From aa88b90c6aedb40989bfc434ce7b4fe666fb61bb Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 4 Jul 2022 16:38:30 -0300 Subject: [PATCH 39/41] Fix internal link --- python-examples/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python-examples/README.md b/python-examples/README.md index 16c2c2d..907c06e 100644 --- a/python-examples/README.md +++ b/python-examples/README.md @@ -8,7 +8,7 @@ is named for the RPC method call being demonstrated. The [bisq.bots](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples/bisq/rpccalls) package contains some simple bots. Please do not run the Python bot examples on mainnet. -See [warning](#bot-not-ready-for-mainnet). +See [warning](#do-not-run-python-bot-examples-on-mainnet). The `run-setup.sh` script in this directory can install Python3 dependencies and example packages into a local venv. @@ -27,7 +27,7 @@ high-throughput, high-performance system supporting atomic transactions. Care mu slow wallet updates on your disk, and Tor network latency. The API daemon enforces limits on request frequency via call rate metering, but you cannot assume bots can perform tasks as rapidly as the API daemon's call rate meters allow. -### [Do Not Run Python Bot Examples On Mainnet](#bot-not-ready-for-mainnet) +### [Do Not Run Python Bot Examples On Mainnet](#do-not-run-python-bot-examples-on-mainnet) The scripts in the [bisq.bots](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples/bisq/bots) package should not be run on mainnet. They do not properly handle errors, and were written by a Python noob. From 0e7eeaedb7d250f729707e3d06b06141427bfe27 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 4 Jul 2022 16:49:15 -0300 Subject: [PATCH 40/41] Add link to Java bots --- README.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 07f2207..83337c0 100644 --- a/README.md +++ b/README.md @@ -10,20 +10,23 @@ client example code, and developing new Java and Python clients and bots. It contains four subprojects: -1. [reference-doc-builder](https://github.com/bisq-network/bisq-api-reference/tree/main/reference-doc-builder) -- The Java - application that produces the [API Reference](https://bisq-network.github.io/slate) content, from Bisq protobuf +1. [reference-doc-builder](https://github.com/bisq-network/bisq-api-reference/tree/main/reference-doc-builder) -- The + Java application that produces the [API Reference](https://bisq-network.github.io/slate) content, from Bisq protobuf definition files. 2. [cli-examples](https://github.com/bisq-network/bisq-api-reference/tree/main/cli-examples) -- A folder of bash scripts demonstrating how to run API CLI commands. Each script is named for the RPC method call being demonstrated. 3. [java-examples](https://github.com/bisq-network/bisq-api-reference/tree/main/java-examples) -- A Java project - demonstrating how to call the API from Java gRPC clients. Each class in - the [bisq.rpccalls](https://github.com/bisq-network/bisq-api-reference/tree/main/java-examples/src/main/java/bisq/rpccalls) - package is named for the RPC method call being demonstrated. -4. [python-examples](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples) -- A Python3 project + demonstrating how to call the API from Java gRPC clients. Each class in the + [bisq.rpccalls](https://github.com/bisq-network/bisq-api-reference/tree/main/java-examples/src/main/java/bisq/rpccalls) + package is named for the RPC method call being demonstrated. There are also some mainnet-ready Java API bots in the + [bisq.bots](https://github.com/bisq-network/bisq-api-reference/tree/main/java-examples/src/main/java/bisq/bots) + package. +5. [python-examples](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples) -- A Python3 project demonstrating how to call the API from Python3 gRPC clients. Each class in - the [bisq.rpccalls](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples/bisq/rpccalls) package - is named for the RPC method call being demonstrated. There are also some simple bot examples in - the [bisq.bots](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples/bisq/bots) package. + the [bisq.rpccalls](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples/bisq/rpccalls) + package is named for the RPC method call being demonstrated. There are also some simple (not-ready-for-mainnet) bot + examples in the [bisq.bots](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples/bisq/bots) + package. The RPC method examples are also displayed in the [API Reference](https://bisq-network.github.io/slate). While navigating the RPC method links in the reference's table of contents on the left side of the page, they appear in the From 7da06a32c42a3b8103d09cc7f680081525f96e45 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 5 Jul 2022 08:16:49 -0300 Subject: [PATCH 41/41] Change links from PR branch to main branch Links to xmr bot src added to the PR branch will work after PR is merged. --- java-examples/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/java-examples/README.md b/java-examples/README.md index 699d192..6d5cd0f 100644 --- a/java-examples/README.md +++ b/java-examples/README.md @@ -185,7 +185,7 @@ Take special care to not run the Bisq API daemon and the desktop application at **Use Cases, Usage and Configuration** This information is found in -the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java) +the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java) . **Creating And Using Runnable TakeBestPricedOfferToBuyBtc Jar** @@ -221,7 +221,7 @@ Take special care to not run the Bisq API daemon and the desktop application at **Use Cases, Usage and Configuration** This information is found in -the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java) +the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBtc.java) . **Creating And Using Runnable TakeBestPricedOfferToSellBtc Jar** @@ -251,7 +251,7 @@ Take special care to not run the Bisq API daemon and the desktop application at **Use Cases, Usage and Configuration** This information is found in -the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java) +the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBsq.java) . **Creating And Using Runnable TakeBestPricedOfferToBuyBsq Jar** @@ -281,7 +281,7 @@ Take special care to not run the Bisq API daemon and the desktop application at **Use Cases, Usage and Configuration** This information is found in -the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java) +the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellBsq.java) . **Creating And Using Runnable TakeBestPricedOfferToSellBsq Jar** @@ -311,7 +311,7 @@ Take special care to not run the Bisq API daemon and the desktop application at **Use Cases, Usage and Configuration** This information is found in -the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java) +the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyXmr.java) . **Creating And Using Runnable TakeBestPricedOfferToBuyXmr Jar** @@ -341,7 +341,7 @@ Take special care to not run the Bisq API daemon and the desktop application at **Use Cases, Usage and Configuration** This information is found in -the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/split-up-take-btc-offer-bots/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java) +the [source code's Java class level documentation](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToSellXmr.java) . **Creating And Using Runnable TakeBestPricedOfferToSellXmr Jar**