mirror of
https://github.com/bisq-network/bisq-api-reference.git
synced 2026-05-19 12:14:14 +00:00
Merge pull request #12 from bisq-network/split-up-take-btc-offer-bots
Split take BTC offer bots into take BTC-FIAT and XMR-BTC offer bots
This commit is contained in:
commit
3170e71167
21
README.md
21
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
|
||||
|
||||
@ -1,11 +1,404 @@
|
||||
# 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.
|
||||
|
||||
* [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.
|
||||
|
||||
* 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)
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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](#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
|
||||
[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
|
||||
|
||||
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](#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 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
|
||||
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
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
#### Quick And Dirty Test Harness
|
||||
|
||||
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 (with coins).
|
||||
$ ./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**.
|
||||
|
||||
### Take BSQ / BTC / XMR / Offer Bots
|
||||
|
||||
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)
|
||||
|
||||
* [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
|
||||
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=<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=<trade-d>
|
||||
|
||||
# Close your completed trade (move it to your trade history).
|
||||
$ ./bisq-cli --password=xyz --port=9998 closetrade --trade-id=<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
|
||||
daemon. BSQ Swaps can be fully automated by the API because the swap transaction is completed within seconds of taking a
|
||||
BSQ Swap offer.
|
||||
|
||||
### [Take Best Priced Offer To Buy Btc](#take-best-priced-offer-to-buy-btc)
|
||||
|
||||
**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
|
||||
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. 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.
|
||||
|
||||
**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/main/java-examples/src/main/java/bisq/bots/TakeBestPricedOfferToBuyBtc.java)
|
||||
.
|
||||
|
||||
**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-buy-btc.jar \
|
||||
--password=xyz \
|
||||
--dryrun=false \
|
||||
--conf=take-best-priced-offer-to-buy-btc.conf
|
||||
```
|
||||
|
||||
### [Take Best Priced Offer To Sell Btc](#take-best-priced-offer-to-sell-btc)
|
||||
|
||||
**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
|
||||
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/main/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:
|
||||
|
||||
```asciidoc
|
||||
$ java -jar take-best-priced-offer-to-sell-btc.jar \
|
||||
--password=xyz \
|
||||
--dryrun=false \
|
||||
--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 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
|
||||
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/main/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 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
|
||||
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/main/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 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
|
||||
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/main/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 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
|
||||
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/main/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)
|
||||
|
||||
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-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.
|
||||
|
||||
## [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),
|
||||
shows how to generate the necessary protobuf classes from the Bisq .proto files.
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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<String> 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<OfferInfo> offersTaken = new ArrayList<>();
|
||||
// This list should stay empty when dryRun = false.
|
||||
protected final List<OfferInfo> offersTakenDuringDryRun = new ArrayList<>();
|
||||
|
||||
protected final boolean canUseBash = getBashPath().isPresent();
|
||||
protected boolean isShutdown = false;
|
||||
@ -82,9 +85,20 @@ public abstract class AbstractBot {
|
||||
protected final Supplier<Long> minimumTxFeeRate = () -> txFeeRates.get().getMinFeeServiceRate();
|
||||
protected final Supplier<Long> mostRecentTxFeeRate = () -> txFeeRates.get().getFeeServiceRate();
|
||||
|
||||
// Constructor
|
||||
/**
|
||||
* Constructor that optionally prompts user to enter wallet password in the console.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
*
|
||||
* @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<OfferInfo> 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.
|
||||
@ -327,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.
|
||||
@ -396,23 +427,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.");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -526,6 +588,21 @@ 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 {
|
||||
printTradesSummaryForToday(CLOSED);
|
||||
} catch (StatusRuntimeException grpcException) {
|
||||
if (walletIsLocked(grpcException)) {
|
||||
log.error("Cannot show today's trades while API daemon's wallet is locked.");
|
||||
} else {
|
||||
throw grpcException;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print list of today's trade summaries to stdout.
|
||||
*
|
||||
@ -536,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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -632,9 +714,8 @@ 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.isEmpty()) {
|
||||
printOffersSummary(offersTakenDuringDryRun);
|
||||
}
|
||||
}
|
||||
|
||||
@ -642,9 +723,7 @@ public abstract class AbstractBot {
|
||||
* Add offer to list of taken offers -- for dry runs only.
|
||||
*/
|
||||
protected void addToOffersTaken(OfferInfo offer) {
|
||||
offersTaken.add(offer);
|
||||
printOfferSummary(offer);
|
||||
log.info("Did not actually take that offer during this simulation.");
|
||||
offersTakenDuringDryRun.add(offer);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -666,6 +745,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 +794,78 @@ public abstract class AbstractBot {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the day's completed trades since midnight today, and number of offers taken during the bot session, then,
|
||||
* <p>
|
||||
* If numOffersTaken >= maxTakeOffers, shut down the bot.
|
||||
* <p>
|
||||
* 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) {
|
||||
printTradesSummaryForTodayIfWalletIsUnlocked();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the day's completed trades since midnight today, and number of offers taken during the bot session, then,
|
||||
* <p>
|
||||
* If numOffersTaken >= maxTakeOffers, and trade completion is being simulated on regtest, shut down the bot.
|
||||
* <p>
|
||||
* If numOffersTaken >= maxTakeOffers, and mainnet trade completion must be delegated to the UI, shut down the
|
||||
* API daemon and the bot.
|
||||
* <p>
|
||||
* 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) {
|
||||
printTradesSummaryForTodayIfWalletIsUnlocked();
|
||||
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Properties object for this bot.
|
||||
*
|
||||
@ -705,7 +888,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);
|
||||
}
|
||||
@ -726,7 +913,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.
|
||||
|
||||
@ -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.
|
||||
@ -152,6 +153,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<OfferInfo, BigDecimal> isMarginGEMinMarketPriceMargin =
|
||||
(offer, minMarketPriceMargin) -> offer.getUseMarketBasedPrice()
|
||||
&& offer.getMarketPriceMarginPct() >= minMarketPriceMargin.doubleValue();
|
||||
|
||||
/**
|
||||
* Return true if the margin price based offer's market price margin (%) <= maxMarketPriceMargin (%).
|
||||
*/
|
||||
@ -197,10 +205,16 @@ public class BotUtils {
|
||||
return distanceFromMarketPrice.compareTo(minMarketPriceMargin) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return String "above" if minMarketPriceMargin (%) >= 0.00, else "below".
|
||||
*/
|
||||
public static final Function<BigDecimal, String> aboveOrBelowMinMarketPriceMargin = (minMarketPriceMargin) ->
|
||||
minMarketPriceMargin.compareTo(ZERO) >= 0 ? "above" : "below";
|
||||
|
||||
/**
|
||||
* Return String "below" if maxMarketPriceMargin (%) <= 0.00, else "above".
|
||||
*/
|
||||
public static final Function<BigDecimal, String> aboveOrBelowMarketPrice = (maxMarketPriceMargin) ->
|
||||
public static final Function<BigDecimal, String> aboveOrBelowMaxMarketPriceMargin = (maxMarketPriceMargin) ->
|
||||
maxMarketPriceMargin.compareTo(ZERO) <= 0 ? "below" : "above";
|
||||
|
||||
/**
|
||||
@ -269,6 +283,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<String[]> 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.
|
||||
@ -388,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);
|
||||
}
|
||||
|
||||
@ -397,7 +418,12 @@ public class BotUtils {
|
||||
* @param offers printed offer list
|
||||
*/
|
||||
public static void printOffersSummary(List<OfferInfo> offers) {
|
||||
new TableBuilder(OFFER_TBL, offers).build().print(out);
|
||||
requireNonNull(offers, "List<OfferInfo> offers param cannot be null.");
|
||||
if (offers.isEmpty()) {
|
||||
log.info("No offers to print.");
|
||||
} else {
|
||||
new TableBuilder(OFFER_TBL, offers).build().print(out);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -406,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);
|
||||
}
|
||||
|
||||
@ -416,10 +443,15 @@ public class BotUtils {
|
||||
* @param trades list of trades
|
||||
*/
|
||||
public static void printTradesSummary(GetTradesRequest.Category category, List<TradeInfo> 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<TradeInfo> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -429,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);
|
||||
}
|
||||
|
||||
@ -519,6 +552,44 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* <p>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,9 +63,9 @@ 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.");
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
@ -106,18 +105,13 @@ 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);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@ -25,48 +25,89 @@ 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.
|
||||
* 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: <b>TakeBestPricedOfferToBuyBsq.properties</b> (located in project's
|
||||
* src/main/resources directory). You will need to replace the default values in the configuration file for your
|
||||
* use cases.
|
||||
* <p><br/>
|
||||
* 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.
|
||||
* <p>
|
||||
* I'm taking liberties with the classname by not naming it TakeBestPricedOfferToSellBtcForBsq.
|
||||
* Here is one possible use case:
|
||||
* <pre>
|
||||
* 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 less than or equal 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
|
||||
* </pre>
|
||||
* <b>Usage</b>
|
||||
* <p><br/>
|
||||
* You must encrypt your wallet password before running this bot. If it is not already encrypted, you can use the CLI:
|
||||
* <pre>
|
||||
* $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
|
||||
* </pre>
|
||||
* 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 <String>`. The bot will prompt you for your wallet-password in the console.
|
||||
* <p><br/>
|
||||
* You can pass the '--dryrun=true' option to the program to see what offers your bot <i>would take</i> with a given
|
||||
* configuration. This will help you avoid taking offers by mistake.
|
||||
* <pre>
|
||||
* TakeBestPricedOfferToBuyBsq --password=api-password --port=api-port [--dryrun=true|false]
|
||||
* </pre>
|
||||
* <p>
|
||||
* The '--simulate-regtest-payment=true' option is ignored by this bot. Taking a swap triggers execution of the swap.
|
||||
*
|
||||
* @see <a href="https://github.com/bisq-network/bisq-api-reference/blob/make-proto-downloader-runnable-from-any-dir/java-examples/src/main/java/bisq/bots/Config.java">bisq.bots.Config.java</a>
|
||||
*/
|
||||
@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 +115,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"))
|
||||
@ -104,8 +144,9 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all available and takeable offers, sorted by price descending.
|
||||
var offers = getOffers(SELL.name(), currencyCode).stream()
|
||||
// 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();
|
||||
|
||||
@ -130,7 +171,6 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot {
|
||||
takeCriteria.printOfferAgainstCriteria(highestPricedOffer);
|
||||
});
|
||||
|
||||
printDryRunProgress();
|
||||
runCountdown(log, pollingInterval);
|
||||
pingDaemon(startTime);
|
||||
}
|
||||
@ -139,17 +179,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();
|
||||
} 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");
|
||||
|
||||
@ -160,13 +204,13 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot {
|
||||
printBSQBalances("BSQ Balances After Swap Execution");
|
||||
|
||||
numOffersTaken++;
|
||||
maybeShutdownAfterSuccessfulSwap();
|
||||
} catch (NonFatalException nonFatalException) {
|
||||
handleNonFatalException(nonFatalException);
|
||||
handleNonFatalException(nonFatalException, pollingInterval);
|
||||
} catch (StatusRuntimeException fatalException) {
|
||||
handleFatalException(fatalException);
|
||||
handleFatalBsqSwapException(fatalException);
|
||||
}
|
||||
}
|
||||
maybeShutdownAfterSuccessfulSwap(numOffersTaken, maxTakeOffers);
|
||||
}
|
||||
|
||||
private void printBotConfiguration() {
|
||||
@ -174,12 +218,13 @@ 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:", "");
|
||||
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 +245,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<OfferInfo, BigDecimal> 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<OfferInfo> isWithinBTCAmountBounds = (offer) ->
|
||||
BotUtils.isWithinBTCAmountBounds(offer, getMinAmount(), getMaxAmount());
|
||||
|
||||
public static void main(String[] args) {
|
||||
TakeBestPricedOfferToBuyBsq bot = new TakeBestPricedOfferToBuyBsq(args);
|
||||
bot.run();
|
||||
@ -297,28 +288,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);
|
||||
}
|
||||
}
|
||||
@ -343,13 +334,13 @@ 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)?",
|
||||
offer.getPrice() + " " + currencyCode,
|
||||
targetPrice + " " + currencyCode);
|
||||
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));
|
||||
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));
|
||||
|
||||
@ -25,58 +25,64 @@ 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
|
||||
* (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: <b>TakeBestPricedOfferToBuyBtc.properties</b> (located in project's
|
||||
* src/main/resources directory). You will need to replace the default values in the configuration file for your
|
||||
* use cases.
|
||||
* <p><br/>
|
||||
* 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 <a href="https://bisq.network">Bisq Desktop</a> application.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* One possible use case for this bot is sell BTC for GBP:
|
||||
* Here is one possible use case:
|
||||
* <pre>
|
||||
* 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.
|
||||
* 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 less than or equal 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
|
||||
* </pre>
|
||||
* <p>
|
||||
* 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.)
|
||||
* <b>Usage</b>
|
||||
* <p><br/>
|
||||
* You must encrypt your wallet password before running this bot. If it is not already encrypted, you can use the CLI:
|
||||
* <pre>
|
||||
* 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.
|
||||
* $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
|
||||
* </pre>
|
||||
* <p>
|
||||
* 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 <String>`. The bot will prompt you for your wallet-password in the console.
|
||||
* <p><br/>
|
||||
* You can pass the '--dryrun=true' option to the program to see what offers your bot <i>would take</i> with a given
|
||||
* configuration. This will help you avoid taking offers by mistake.
|
||||
* <pre>
|
||||
* 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]
|
||||
* TakeBestPricedOfferToBuyBtc --password=api-password --port=api-port [--dryrun=true|false]
|
||||
* </pre>
|
||||
* 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.
|
||||
* <pre>
|
||||
* TakeBestPricedOfferToBuyBtc --password=api-password --port=api-port [--simulate-regtest-payment=true|false]
|
||||
* </pre>
|
||||
*
|
||||
* @see <a href="https://github.com/bisq-network/bisq-api-reference/blob/make-proto-downloader-runnable-from-any-dir/java-examples/src/main/java/bisq/bots/Config.java">bisq.bots.Config.java</a>
|
||||
*/
|
||||
@Slf4j
|
||||
@Getter
|
||||
@ -90,21 +96,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 +150,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();
|
||||
|
||||
@ -174,7 +177,6 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
|
||||
takeCriteria.printOfferAgainstCriteria(highestPricedOffer);
|
||||
});
|
||||
|
||||
printDryRunProgress();
|
||||
runCountdown(log, pollingInterval);
|
||||
pingDaemon(startTime);
|
||||
}
|
||||
@ -188,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();
|
||||
} 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);
|
||||
@ -212,79 +219,13 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
|
||||
printBTCBalances("BTC Balances After Simulated Trade Completion");
|
||||
}
|
||||
numOffersTaken++;
|
||||
maybeShutdownAfterSuccessfulTradeCreation();
|
||||
} 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.");
|
||||
maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -297,18 +238,13 @@ 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<OfferInfo> isWithinBTCAmountBounds = (offer) ->
|
||||
BotUtils.isWithinBTCAmountBounds(offer, getMinAmount(), getMaxAmount());
|
||||
|
||||
private void printBotConfiguration() {
|
||||
var configsByLabel = new LinkedHashMap<String, Object>();
|
||||
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());
|
||||
@ -338,17 +274,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<String> 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 +298,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<OfferInfo> 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 +347,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",
|
||||
|
||||
@ -0,0 +1,369 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package bisq.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;
|
||||
|
||||
/**
|
||||
* 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: <b>TakeBestPricedOfferToBuyXmr.properties</b> (located in project's
|
||||
* src/main/resources directory). You will need to replace the default values in the configuration file for your
|
||||
* use cases.
|
||||
* <p><br/>
|
||||
* 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
|
||||
* <a href="https://bisq.network">Bisq Desktop</a> application.
|
||||
* <p>
|
||||
* Here is one possible use case:
|
||||
* <pre>
|
||||
* 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 less than or equal 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
|
||||
* </pre>
|
||||
* <b>Usage</b>
|
||||
* <p><br/>
|
||||
* You must encrypt your wallet password before running this bot. If it is not already encrypted, you can use the CLI:
|
||||
* <pre>
|
||||
* $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
|
||||
* </pre>
|
||||
* 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 <String>`. The bot will prompt you for your wallet-password in the console.
|
||||
* <p><br/>
|
||||
* You can pass the '--dryrun=true' option to the program to see what offers your bot <i>would take</i> with a given
|
||||
* configuration. This will help you avoid taking offers by mistake.
|
||||
* <pre>
|
||||
* TakeBestPricedOfferToBuyXmr --password=api-password --port=api-port [--dryrun=true|false]
|
||||
* </pre>
|
||||
* 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.
|
||||
* <pre>
|
||||
* TakeBestPricedOfferToBuyXmr --password=api-password --port=api-port [--simulate-regtest-payment=true|false]
|
||||
* </pre>
|
||||
*
|
||||
* @see <a href="https://github.com/bisq-network/bisq-api-reference/blob/make-proto-downloader-runnable-from-any-dir/java-examples/src/main/java/bisq/bots/Config.java">bisq.bots.Config.java</a>
|
||||
*/
|
||||
@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<OfferInfo> 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);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
// 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++;
|
||||
} else {
|
||||
try {
|
||||
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++;
|
||||
} catch (NonFatalException nonFatalException) {
|
||||
handleNonFatalException(nonFatalException, pollingInterval);
|
||||
} catch (StatusRuntimeException fatalException) {
|
||||
shutdownAfterTakeOfferFailure(fatalException);
|
||||
}
|
||||
}
|
||||
maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<OfferInfo, BigDecimal> isFixedPriceGEMinMarketPriceMargin =
|
||||
(offer, currentMarketPrice) -> BotUtils.isFixedPriceGEMinMarketPriceMargin(
|
||||
offer,
|
||||
currentMarketPrice,
|
||||
this.getMinMarketPriceMargin());
|
||||
|
||||
private void printBotConfiguration() {
|
||||
var configsByLabel = new LinkedHashMap<String, Object>();
|
||||
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());
|
||||
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<OfferInfo> findTakeableOffer(List<OfferInfo> 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<OfferInfo> 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<String, Object>();
|
||||
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 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)?",
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -25,48 +25,89 @@ 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.
|
||||
* 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: <b>TakeBestPricedOfferToSellBsq.properties</b> (located in project's
|
||||
* src/main/resources directory). You will need to replace the default values in the configuration file for your
|
||||
* use cases.
|
||||
* <p><br/>
|
||||
* 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.
|
||||
* <p>
|
||||
* I'm taking liberties with the classname by not naming it TakeBestPricedOfferToBuyBtcForBsq.
|
||||
* Here is one possible use case:
|
||||
* <pre>
|
||||
* 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
|
||||
* maxMarketPriceMargin=-1.00
|
||||
* minAmount=0.10
|
||||
* maxAmount=0.25
|
||||
* preferredTradingPeers=preferred-address-1.onion:9999,preferred-address-2.onion:9999
|
||||
* maxTxFeeRate=25
|
||||
* </pre>
|
||||
* <b>Usage</b>
|
||||
* <p><br/>
|
||||
* You must encrypt your wallet password before running this bot. If it is not already encrypted, you can use the CLI:
|
||||
* <pre>
|
||||
* $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
|
||||
* </pre>
|
||||
* 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 <String>`. The bot will prompt you for your wallet-password in the console.
|
||||
* <p><br/>
|
||||
* You can pass the '--dryrun=true' option to the program to see what offers your bot <i>would take</i> with a given
|
||||
* configuration. This will help you avoid taking offers by mistake.
|
||||
* <pre>
|
||||
* TakeBestPricedOfferToBuyBsq --password=api-password --port=api-port [--dryrun=true|false]
|
||||
* </pre>
|
||||
* <p>
|
||||
* The '--simulate-regtest-payment=true' option is ignored by this bot. Taking a swap triggers execution of the swap.
|
||||
*
|
||||
* @see <a href="https://github.com/bisq-network/bisq-api-reference/blob/make-proto-downloader-runnable-from-any-dir/java-examples/src/main/java/bisq/bots/Config.java">bisq.bots.Config.java</a>
|
||||
*/
|
||||
@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 +115,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"))
|
||||
@ -104,8 +144,9 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all available and takeable offers, sorted by price ascending.
|
||||
var offers = getOffers(BUY.name(), currencyCode).stream()
|
||||
// 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();
|
||||
|
||||
@ -130,7 +171,6 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot {
|
||||
takeCriteria.printOfferAgainstCriteria(cheapestOffer);
|
||||
});
|
||||
|
||||
printDryRunProgress();
|
||||
runCountdown(log, pollingInterval);
|
||||
pingDaemon(startTime);
|
||||
}
|
||||
@ -139,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();
|
||||
} 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");
|
||||
|
||||
@ -160,13 +204,13 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot {
|
||||
printBSQBalances("BSQ Balances After Swap Execution");
|
||||
|
||||
numOffersTaken++;
|
||||
maybeShutdownAfterSuccessfulSwap();
|
||||
} catch (NonFatalException nonFatalException) {
|
||||
handleNonFatalException(nonFatalException);
|
||||
handleNonFatalException(nonFatalException, pollingInterval);
|
||||
} catch (StatusRuntimeException fatalException) {
|
||||
handleFatalException(fatalException);
|
||||
handleFatalBsqSwapException(fatalException);
|
||||
}
|
||||
}
|
||||
maybeShutdownAfterSuccessfulSwap(numOffersTaken, maxTakeOffers);
|
||||
}
|
||||
|
||||
private void printBotConfiguration() {
|
||||
@ -174,12 +218,13 @@ 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:", "");
|
||||
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 +245,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 +255,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<OfferInfo> isWithinBTCAmountBounds = (offer) ->
|
||||
BotUtils.isWithinBTCAmountBounds(offer, getMinAmount(), getMaxAmount());
|
||||
|
||||
public static void main(String[] args) {
|
||||
TakeBestPricedOfferToSellBsq bot = new TakeBestPricedOfferToSellBsq(args);
|
||||
bot.run();
|
||||
@ -285,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.
|
||||
@ -298,28 +288,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);
|
||||
}
|
||||
}
|
||||
@ -336,7 +326,7 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot {
|
||||
|
||||
var filterResultsByLabel = new LinkedHashMap<String, Object>();
|
||||
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()));
|
||||
@ -344,13 +334,13 @@ 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)?",
|
||||
offer.getPrice() + " " + currencyCode,
|
||||
targetPrice + " " + currencyCode);
|
||||
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));
|
||||
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));
|
||||
|
||||
@ -25,59 +25,64 @@ 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 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: <b>TakeBestPricedOfferToSellBtc.properties</b> (located in project's
|
||||
* src/main/resources directory). You will need to replace the default values in the configuration file for your
|
||||
* use cases.
|
||||
* <p><br/>
|
||||
* 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 <a href="https://bisq.network">Bisq Desktop</a> application.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* One possible use case for this bot is buy BTC with GBP:
|
||||
* Here is one possible use case:
|
||||
* <pre>
|
||||
* 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 less than or equal 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
|
||||
* </pre>
|
||||
* <p>
|
||||
* 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.)
|
||||
* <b>Usage</b>
|
||||
* <p><br/>
|
||||
* You must encrypt your wallet password before running this bot. If it is not already encrypted, you can use the CLI:
|
||||
* <pre>
|
||||
* 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"
|
||||
* </pre>
|
||||
* <p>
|
||||
* 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 <String>`. The bot will prompt you for your wallet-password in the console.
|
||||
* <p><br/>
|
||||
* You can pass the '--dryrun=true' option to the program to see what offers your bot <i>would take</i> with a given
|
||||
* configuration. This will help you avoid taking offers by mistake.
|
||||
* <pre>
|
||||
* 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]
|
||||
* </pre>
|
||||
* 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.
|
||||
* <pre>
|
||||
* TakeBestPricedOfferToBuyBtc --password=api-password --port=api-port [--simulate-regtest-payment=true|false]
|
||||
* </pre>
|
||||
*
|
||||
* @see <a href="https://github.com/bisq-network/bisq-api-reference/blob/make-proto-downloader-runnable-from-any-dir/java-examples/src/main/java/bisq/bots/Config.java">bisq.bots.Config.java</a>
|
||||
*/
|
||||
@Slf4j
|
||||
@Getter
|
||||
@ -91,21 +96,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 +150,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();
|
||||
|
||||
@ -175,7 +177,6 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot {
|
||||
takeCriteria.printOfferAgainstCriteria(cheapestOffer);
|
||||
});
|
||||
|
||||
printDryRunProgress();
|
||||
runCountdown(log, pollingInterval);
|
||||
pingDaemon(startTime);
|
||||
}
|
||||
@ -189,16 +190,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();
|
||||
} 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);
|
||||
@ -213,79 +219,13 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot {
|
||||
printBTCBalances("BTC Balances After Simulated Trade Completion");
|
||||
}
|
||||
numOffersTaken++;
|
||||
maybeShutdownAfterSuccessfulTradeCreation();
|
||||
} 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.");
|
||||
maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -298,18 +238,13 @@ 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<OfferInfo> isWithinBTCAmountBounds = (offer) ->
|
||||
BotUtils.isWithinBTCAmountBounds(offer, getMinAmount(), getMaxAmount());
|
||||
|
||||
private void printBotConfiguration() {
|
||||
var configsByLabel = new LinkedHashMap<String, Object>();
|
||||
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());
|
||||
@ -339,17 +274,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<String> 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 +298,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<OfferInfo> 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 +347,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 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)?",
|
||||
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",
|
||||
|
||||
@ -0,0 +1,368 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package bisq.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;
|
||||
|
||||
/**
|
||||
* 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: <b>TakeBestPricedOfferToSellXmr.properties</b> (located in project's
|
||||
* src/main/resources directory). You will need to replace the default values in the configuration file for your
|
||||
* use cases.
|
||||
* <p><br/>
|
||||
* 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
|
||||
* the <a href="https://bisq.network">Bisq Desktop</a> application.
|
||||
* <p>
|
||||
* Here is one possible use case:
|
||||
* <pre>
|
||||
* 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 less than or equal 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
|
||||
* </pre>
|
||||
* <b>Usage</b>
|
||||
* <p><br/>
|
||||
* You must encrypt your wallet password before running this bot. If it is not already encrypted, you can use the CLI:
|
||||
* <pre>
|
||||
* $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
|
||||
* </pre>
|
||||
* 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 <String>`. The bot will prompt you for your wallet-password in the console.
|
||||
* <p><br/>
|
||||
* You can pass the '--dryrun=true' option to the program to see what offers your bot <i>would take</i> with a given
|
||||
* configuration. This will help you avoid taking offers by mistake.
|
||||
* <pre>
|
||||
* TakeBestPricedOfferToSellXmr --password=api-password --port=api-port [--dryrun=true|false]
|
||||
* </pre>
|
||||
* 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.
|
||||
* <pre>
|
||||
* TakeBestPricedOfferToSellXmr --password=api-password --port=api-port [--simulate-regtest-payment=true|false]
|
||||
* </pre>
|
||||
*
|
||||
* @see <a href="https://github.com/bisq-network/bisq-api-reference/blob/make-proto-downloader-runnable-from-any-dir/java-examples/src/main/java/bisq/bots/Config.java">bisq.bots.Config.java</a>
|
||||
*/
|
||||
@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<OfferInfo> 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);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
// 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++;
|
||||
} else {
|
||||
try {
|
||||
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++;
|
||||
} catch (NonFatalException nonFatalException) {
|
||||
handleNonFatalException(nonFatalException, pollingInterval);
|
||||
} catch (StatusRuntimeException fatalException) {
|
||||
shutdownAfterTakeOfferFailure(fatalException);
|
||||
}
|
||||
}
|
||||
maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<OfferInfo, BigDecimal> isFixedPriceLEMaxMarketPriceMargin =
|
||||
(offer, currentMarketPrice) -> BotUtils.isFixedPriceLEMaxMarketPriceMargin(
|
||||
offer,
|
||||
currentMarketPrice,
|
||||
this.getMaxMarketPriceMargin());
|
||||
|
||||
private void printBotConfiguration() {
|
||||
var configsByLabel = new LinkedHashMap<String, Object>();
|
||||
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());
|
||||
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<OfferInfo> findTakeableOffer(List<OfferInfo> 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<OfferInfo> 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<String, Object>();
|
||||
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 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)?",
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -19,8 +19,9 @@ 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
|
||||
#
|
||||
# Offer polling frequency must be >= 1s (1000ms) between each getoffers request.
|
||||
pollingInterval=30000
|
||||
pollingInterval=60000
|
||||
|
||||
@ -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
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
# Maximum # of offers to take during one bot session. When reached, bot will shut down (but not the API daemon).
|
||||
maxTakeOffers=5
|
||||
#
|
||||
# Taker bot's payment account id. Only SELL BTC offers using the same payment method will be considered for taking.
|
||||
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=-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=1.00
|
||||
#
|
||||
# Taker bot's max acceptable transaction fee rate (sats / byte).
|
||||
# Regtest fee rates are from https://price.bisq.wiz.biz/getFees
|
||||
maxTxFeeRate=50
|
||||
#
|
||||
# Bisq trade fee currency code (BSQ or BTC).
|
||||
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
|
||||
#
|
||||
# Offer polling frequency must be >= 1s (1000ms) between each getoffers request.
|
||||
pollingInterval=10000
|
||||
@ -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
|
||||
@ -19,8 +19,9 @@ 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
|
||||
#
|
||||
# Offer polling frequency must be >= 1s (1000ms) between each getoffers request.
|
||||
pollingInterval=30000
|
||||
pollingInterval=60000
|
||||
|
||||
@ -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,12 +16,13 @@ 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
|
||||
#
|
||||
# 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, \
|
||||
@ -34,4 +35,4 @@ preferredTradingPeers=localhost:8888, \
|
||||
x6x2o3m6rxhkfuf2v6lalbharf3whwvkts5rdn3jkhgieqvnq6mvdfyd.onion:9999
|
||||
#
|
||||
# Offer polling frequency must be >= 1s (1000ms) between each getoffers request.
|
||||
pollingInterval=20000
|
||||
pollingInterval=60000
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
# Maximum # of offers to take during one bot session. When reached, bot will shut down (but not the API daemon).
|
||||
maxTakeOffers=2
|
||||
#
|
||||
# Taker bot's payment account id. Only SELL BTC offers using the same payment method will be considered for taking.
|
||||
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
|
||||
#
|
||||
# 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=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
|
||||
#
|
||||
# Bisq trade fee currency code (BSQ or BTC).
|
||||
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
|
||||
#
|
||||
# Offer polling frequency must be >= 1s (1000ms) between each getoffers request.
|
||||
pollingInterval=60000
|
||||
@ -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](#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.
|
||||
|
||||
## 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](#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.
|
||||
|
||||
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).
|
||||
Loading…
x
Reference in New Issue
Block a user