mirror of
https://github.com/bisq-network/bisq-api-reference.git
synced 2026-05-20 12:24: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:
|
It contains four subprojects:
|
||||||
|
|
||||||
1. [reference-doc-builder](https://github.com/bisq-network/bisq-api-reference/tree/main/reference-doc-builder) -- The Java
|
1. [reference-doc-builder](https://github.com/bisq-network/bisq-api-reference/tree/main/reference-doc-builder) -- The
|
||||||
application that produces the [API Reference](https://bisq-network.github.io/slate) content, from Bisq protobuf
|
Java application that produces the [API Reference](https://bisq-network.github.io/slate) content, from Bisq protobuf
|
||||||
definition files.
|
definition files.
|
||||||
2. [cli-examples](https://github.com/bisq-network/bisq-api-reference/tree/main/cli-examples) -- A folder of bash scripts
|
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.
|
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
|
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
|
demonstrating how to call the API from Java gRPC clients. Each class in the
|
||||||
the [bisq.rpccalls](https://github.com/bisq-network/bisq-api-reference/tree/main/java-examples/src/main/java/bisq/rpccalls)
|
[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.
|
package is named for the RPC method call being demonstrated. There are also some mainnet-ready Java API bots in the
|
||||||
4. [python-examples](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples) -- A Python3 project
|
[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
|
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
|
the [bisq.rpccalls](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples/bisq/rpccalls)
|
||||||
is named for the RPC method call being demonstrated. There are also some simple bot examples in
|
package is named for the RPC method call being demonstrated. There are also some simple (not-ready-for-mainnet) bot
|
||||||
the [bisq.bots](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples/bisq/bots) package.
|
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
|
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
|
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
|
Each class in
|
||||||
the [bisq.rpccalls](https://github.com/bisq-network/bisq-api-reference/tree/main/java-examples/src/main/java/bisq/rpccalls)
|
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.
|
package is named for the RPC method call being demonstrated.
|
||||||
|
|
||||||
The subproject uses a
|
Their purpose is to show how to construct a gRPC service method request with parameters, send the request, and print a
|
||||||
a [Gradle build file](https://github.com/bisq-network/bisq-api-reference/blob/main/java-examples/build.gradle), also
|
response object if there is one. As a rule, the request is successful if a gRPC StatusRuntimeException is not thrown by
|
||||||
demonstrating how to generate the necessary protobuf classes from the Bisq .proto files.
|
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.TakeBestPricedOfferToBuyBtc
|
||||||
./create-runnable-jar.sh "$GRADLE_DIST_NAME" bisq.bots.TakeBestPricedOfferToSellBtc
|
./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.TakeBestPricedOfferToBuyBsq
|
||||||
./create-runnable-jar.sh "$GRADLE_DIST_NAME" bisq.bots.TakeBestPricedOfferToSellBsq
|
./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.
|
# 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"
|
jar xfv "lib/$GRADLE_DIST_NAME.jar" "$MAINCLASS_FILE_PATH" "$SIMPLE_CLASSNAME.properties"
|
||||||
echo "Extracted one class:"
|
echo "Extracted $SIMPLE_CLASSNAME.class:"
|
||||||
ls -l bisq/bots/pazza
|
ls -l "bisq/bots/$SIMPLE_CLASSNAME.class"
|
||||||
mv "$SIMPLE_CLASSNAME.properties" "$JAR_BASENAME.conf"
|
mv "$SIMPLE_CLASSNAME.properties" "$JAR_BASENAME.conf"
|
||||||
echo "Extracted one properties file and renamed it $JAR_BASENAME.conf"
|
echo "Extracted $SIMPLE_CLASSNAME.properties and renamed it $JAR_BASENAME.conf"
|
||||||
ls -l *.conf
|
ls -l "$JAR_BASENAME.conf"
|
||||||
|
|
||||||
# Now it can be added to the empty jar with the correct path.
|
# Now it can be added to the empty jar with the correct path.
|
||||||
jar uf "$JAR_BASENAME.jar" "$MAINCLASS_FILE_PATH"
|
jar uf "$JAR_BASENAME.jar" "$MAINCLASS_FILE_PATH"
|
||||||
# Remove bisq (bisq/bots/junk).
|
# Remove workarea.
|
||||||
rm -rf bisq
|
rm -rf bisq
|
||||||
|
|
||||||
echo "Runnable $JAR_BASENAME.jar is ready to use."
|
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.*;
|
||||||
import bisq.proto.grpc.GetTradesRequest.Category;
|
import bisq.proto.grpc.GetTradesRequest.Category;
|
||||||
import io.grpc.StatusRuntimeException;
|
import io.grpc.StatusRuntimeException;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import protobuf.PaymentAccount;
|
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.BSQ_BALANCE_TBL;
|
||||||
import static bisq.bots.table.builder.TableType.BTC_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.GetOfferCategoryReply.OfferCategory.BSQ_SWAP;
|
||||||
|
import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED;
|
||||||
import static io.grpc.Status.*;
|
import static io.grpc.Status.*;
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import static java.lang.System.exit;
|
import static java.lang.System.exit;
|
||||||
@ -61,6 +63,7 @@ public abstract class AbstractBot {
|
|||||||
protected final String walletPassword;
|
protected final String walletPassword;
|
||||||
protected final String conf;
|
protected final String conf;
|
||||||
protected final GrpcStubs grpcStubs;
|
protected final GrpcStubs grpcStubs;
|
||||||
|
@Getter
|
||||||
protected final boolean isDryRun;
|
protected final boolean isDryRun;
|
||||||
// This is an experimental option for simulating and automating protocol payment steps during bot development.
|
// 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.
|
// 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<>();
|
protected final List<String> preferredTradingPeers = new ArrayList<>();
|
||||||
|
|
||||||
// Used during dry runs to track offers that would be taken.
|
// 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.
|
// This list should stay empty when dryRun = false.
|
||||||
protected final List<OfferInfo> offersTaken = new ArrayList<>();
|
protected final List<OfferInfo> offersTakenDuringDryRun = new ArrayList<>();
|
||||||
|
|
||||||
protected final boolean canUseBash = getBashPath().isPresent();
|
protected final boolean canUseBash = getBashPath().isPresent();
|
||||||
protected boolean isShutdown = false;
|
protected boolean isShutdown = false;
|
||||||
@ -82,9 +85,20 @@ public abstract class AbstractBot {
|
|||||||
protected final Supplier<Long> minimumTxFeeRate = () -> txFeeRates.get().getMinFeeServiceRate();
|
protected final Supplier<Long> minimumTxFeeRate = () -> txFeeRates.get().getMinFeeServiceRate();
|
||||||
protected final Supplier<Long> mostRecentTxFeeRate = () -> txFeeRates.get().getFeeServiceRate();
|
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) {
|
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());
|
Config bisqClientOpts = new Config(this.args, defaultPropertiesFilename.get());
|
||||||
this.walletPassword = bisqClientOpts.getWalletPassword();
|
this.walletPassword = bisqClientOpts.getWalletPassword();
|
||||||
this.conf = bisqClientOpts.getConf();
|
this.conf = bisqClientOpts.getConf();
|
||||||
@ -113,7 +127,11 @@ public abstract class AbstractBot {
|
|||||||
var reply = grpcStubs.versionService.getVersion(request);
|
var reply = grpcStubs.versionService.getVersion(request);
|
||||||
log.info("API daemon {} is available.", reply.getVersion());
|
log.info("API daemon {} is available.", reply.getVersion());
|
||||||
} catch (StatusRuntimeException grpcException) {
|
} 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);
|
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) ->
|
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.
|
* 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
|
* 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.
|
* 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.)
|
* XMR payment account, else throws an IllegalStateException. (The bot does not yet support BSQ Swaps.)
|
||||||
*/
|
*/
|
||||||
protected void validatePaymentAccount(PaymentAccount paymentAccount) {
|
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())
|
if (!paymentAccount.hasSelectedTradeCurrency())
|
||||||
throw new IllegalStateException(
|
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.getId(),
|
||||||
paymentAccount.getAccountName()));
|
paymentAccount.getAccountName()));
|
||||||
|
|
||||||
var selectedCurrencyCode = paymentAccount.getSelectedTradeCurrency().getCode();
|
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.
|
// 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))
|
if (isBlockChainsPaymentMethod && !isXmr.test(selectedCurrencyCode))
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
format("This bot only supports fiat and monero (XMR) trading, not the %s altcoin.",
|
format("This bot only supports fiat and monero (XMR) trading, not the %s altcoin.",
|
||||||
selectedCurrencyCode));
|
selectedCurrencyCode));
|
||||||
|
|
||||||
if (isBsq.test(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);
|
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.
|
* Print list of today's trade summaries to stdout.
|
||||||
*
|
*
|
||||||
@ -536,7 +613,12 @@ public abstract class AbstractBot {
|
|||||||
var trades = getTrades(category).stream()
|
var trades = getTrades(category).stream()
|
||||||
.filter(t -> t.getDate() >= midnightToday)
|
.filter(t -> t.getDate() >= midnightToday)
|
||||||
.collect(Collectors.toList());
|
.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.
|
* Print information about offers taken during bot simulation.
|
||||||
*/
|
*/
|
||||||
protected void printDryRunProgress() {
|
protected void printDryRunProgress() {
|
||||||
if (isDryRun && offersTaken.size() > 0) {
|
if (isDryRun && !offersTakenDuringDryRun.isEmpty()) {
|
||||||
log.info("You have \"taken\" {} offer(s) during dry run:", offersTaken.size());
|
printOffersSummary(offersTakenDuringDryRun);
|
||||||
printOffersSummary(offersTaken);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -642,9 +723,7 @@ public abstract class AbstractBot {
|
|||||||
* Add offer to list of taken offers -- for dry runs only.
|
* Add offer to list of taken offers -- for dry runs only.
|
||||||
*/
|
*/
|
||||||
protected void addToOffersTaken(OfferInfo offer) {
|
protected void addToOffersTaken(OfferInfo offer) {
|
||||||
offersTaken.add(offer);
|
offersTakenDuringDryRun.add(offer);
|
||||||
printOfferSummary(offer);
|
|
||||||
log.info("Did not actually take that offer during this simulation.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -666,6 +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).
|
* 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);
|
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.
|
* Returns Properties object for this bot.
|
||||||
*
|
*
|
||||||
@ -705,7 +888,11 @@ public abstract class AbstractBot {
|
|||||||
try {
|
try {
|
||||||
var defaultFilename = defaultPropertiesFilename.get();
|
var defaultFilename = defaultPropertiesFilename.get();
|
||||||
properties.load(this.getClass().getClassLoader().getResourceAsStream(defaultFilename));
|
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) {
|
} catch (Exception ex) {
|
||||||
throw new IllegalStateException(ex);
|
throw new IllegalStateException(ex);
|
||||||
}
|
}
|
||||||
@ -726,7 +913,11 @@ public abstract class AbstractBot {
|
|||||||
Properties properties = new java.util.Properties();
|
Properties properties = new java.util.Properties();
|
||||||
try {
|
try {
|
||||||
properties.load(is);
|
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;
|
return properties;
|
||||||
} catch (FileNotFoundException ignored) {
|
} catch (FileNotFoundException ignored) {
|
||||||
// Cannot happen here. Ignore FileNotFoundException because confFile.exists() == true.
|
// 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.lang.System.*;
|
||||||
import static java.math.BigDecimal.ZERO;
|
import static java.math.BigDecimal.ZERO;
|
||||||
import static java.math.RoundingMode.HALF_UP;
|
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.
|
* 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()
|
(offer, targetPrice) -> offer.getUseMarketBasedPrice()
|
||||||
&& new BigDecimal(offer.getPrice()).compareTo(targetPrice) >= 0;
|
&& 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 (%).
|
* 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 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".
|
* 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";
|
maxMarketPriceMargin.compareTo(ZERO) <= 0 ? "below" : "above";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -269,6 +283,12 @@ public class BotUtils {
|
|||||||
return appendWalletPasswordOpt(args, unvalidatedWalletPassword);
|
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.
|
* 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.
|
* If run in a virtual terminal (IDE console), the input will be echoed.
|
||||||
@ -388,6 +408,7 @@ public class BotUtils {
|
|||||||
* @param offer printed offer
|
* @param offer printed offer
|
||||||
*/
|
*/
|
||||||
public static void printOfferSummary(OfferInfo offer) {
|
public static void printOfferSummary(OfferInfo offer) {
|
||||||
|
requireNonNull(offer, "OfferInfo offer param cannot be null.");
|
||||||
new TableBuilder(OFFER_TBL, offer).build().print(out);
|
new TableBuilder(OFFER_TBL, offer).build().print(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,7 +418,12 @@ public class BotUtils {
|
|||||||
* @param offers printed offer list
|
* @param offers printed offer list
|
||||||
*/
|
*/
|
||||||
public static void printOffersSummary(List<OfferInfo> offers) {
|
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
|
* @param trade printed trade
|
||||||
*/
|
*/
|
||||||
public static void printTradeSummary(TradeInfo trade) {
|
public static void printTradeSummary(TradeInfo trade) {
|
||||||
|
requireNonNull(trade, "TradeInfo trade param cannot be null.");
|
||||||
new TableBuilder(TRADE_DETAIL_TBL, trade).build().print(out);
|
new TableBuilder(TRADE_DETAIL_TBL, trade).build().print(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -416,10 +443,15 @@ public class BotUtils {
|
|||||||
* @param trades list of trades
|
* @param trades list of trades
|
||||||
*/
|
*/
|
||||||
public static void printTradesSummary(GetTradesRequest.Category category, List<TradeInfo> trades) {
|
public static void printTradesSummary(GetTradesRequest.Category category, List<TradeInfo> trades) {
|
||||||
switch (category) {
|
requireNonNull(trades, "List<TradeInfo> trades param cannot be null.");
|
||||||
case CLOSED -> new TableBuilder(CLOSED_TRADES_TBL, trades).build().print(out);
|
if (trades.isEmpty()) {
|
||||||
case FAILED -> new TableBuilder(FAILED_TRADES_TBL, trades).build().print(out);
|
log.info("No trades to print.");
|
||||||
default -> new TableBuilder(OPEN_TRADES_TBL, trades).build().print(out);
|
} 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
|
* @param paymentAccount the printed PaymentAccount
|
||||||
*/
|
*/
|
||||||
public static void printPaymentAccountSummary(PaymentAccount paymentAccount) {
|
public static void printPaymentAccountSummary(PaymentAccount paymentAccount) {
|
||||||
|
requireNonNull(paymentAccount, "PaymentAccount paymentAccount param cannot be null.");
|
||||||
new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount).build().print(out);
|
new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount).build().print(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -519,6 +552,44 @@ public class BotUtils {
|
|||||||
log.warn(BANNER);
|
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.
|
* Run a bash script to count down the given number of seconds, printing each character of output from stdout.
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
package bisq.bots;
|
package bisq.bots;
|
||||||
|
|
||||||
|
|
||||||
|
import joptsimple.BuiltinHelpFormatter;
|
||||||
import joptsimple.OptionParser;
|
import joptsimple.OptionParser;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -63,12 +64,12 @@ public class Config {
|
|||||||
.withRequiredArg();
|
.withRequiredArg();
|
||||||
var confOpt = parser.accepts("conf", "Bot configuration file (required)")
|
var confOpt = parser.accepts("conf", "Bot configuration file (required)")
|
||||||
.withRequiredArg();
|
.withRequiredArg();
|
||||||
var dryRunOpt = parser.accepts("dryrun", "Pretend to take an offer (default=false)")
|
var dryRunOpt = parser.accepts("dryrun", "Pretend to take an offer")
|
||||||
.withRequiredArg()
|
.withRequiredArg()
|
||||||
.ofType(boolean.class)
|
.ofType(boolean.class)
|
||||||
.defaultsTo(FALSE);
|
.defaultsTo(FALSE);
|
||||||
var simulateRegtestPaymentStepsOpt =
|
var simulateRegtestPaymentStepsOpt =
|
||||||
parser.accepts("simulate-regtest-payment", "Simulate regtest payment steps (default=false)")
|
parser.accepts("simulate-regtest-payment", "Simulate regtest payment steps")
|
||||||
.withOptionalArg()
|
.withOptionalArg()
|
||||||
.ofType(boolean.class)
|
.ofType(boolean.class)
|
||||||
.defaultsTo(FALSE);
|
.defaultsTo(FALSE);
|
||||||
@ -126,10 +127,23 @@ public class Config {
|
|||||||
stream.println();
|
stream.println();
|
||||||
stream.println("Usage: ScriptName [options]");
|
stream.println("Usage: ScriptName [options]");
|
||||||
stream.println();
|
stream.println();
|
||||||
|
parser.formatHelpWith(new HelpFormatter(90, 2));
|
||||||
parser.printHelpOn(stream);
|
parser.printHelpOn(stream);
|
||||||
stream.println();
|
stream.println();
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
ex.printStackTrace(stream);
|
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() {
|
public void close() {
|
||||||
try {
|
try {
|
||||||
if (!channel.isShutdown()) {
|
if (!channel.isShutdown()) {
|
||||||
log.info("Shutting down bot's grpc channel.");
|
log.debug("Shutting down bot's grpc channel.");
|
||||||
channel.shutdown().awaitTermination(1, SECONDS);
|
channel.shutdown().awaitTermination(1, SECONDS);
|
||||||
log.info("Bot channel shutdown complete.");
|
log.debug("Bot channel shutdown complete.");
|
||||||
}
|
}
|
||||||
} catch (InterruptedException ex) {
|
} catch (InterruptedException ex) {
|
||||||
throw new IllegalStateException(ex);
|
throw new IllegalStateException(ex);
|
||||||
|
|||||||
@ -24,7 +24,6 @@ import protobuf.PaymentAccount;
|
|||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
import static bisq.bots.BotUtils.*;
|
import static bisq.bots.BotUtils.*;
|
||||||
import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED;
|
|
||||||
import static io.grpc.Status.Code.PERMISSION_DENIED;
|
import static io.grpc.Status.Code.PERMISSION_DENIED;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -106,18 +105,13 @@ public class RegtestTradePaymentSimulator extends AbstractBot {
|
|||||||
closeTrade(tradeId);
|
closeTrade(tradeId);
|
||||||
log.info("You closed the trade here in the bot (mandatory, to move trades to history list).");
|
log.info("You closed the trade here in the bot (mandatory, to move trades to history list).");
|
||||||
|
|
||||||
log.warn("##############################################################################");
|
String cliCommandDescription = "Trading peer inspects and closes trade in the CLI (mandatory, to move trades to history list):";
|
||||||
log.warn("Bob 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()
|
||||||
String copyPasteCliCommands = "./bisq-cli --password=xyz --port=9999 closetrade --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";
|
+ "\n" + "./bisq-cli --password=xyz --port=9999 gettrades --category=closed";
|
||||||
log.warn(copyPasteCliCommands);
|
printCliCommands(log, cliCommandDescription, copyPasteCliCommands);
|
||||||
log.warn("##############################################################################");
|
|
||||||
|
|
||||||
sleep(pollingInterval);
|
log.debug("Closing {}'s gRPC channel.", this.getClass().getSimpleName());
|
||||||
log.info("Trade is completed. Here are today's completed trades:");
|
|
||||||
printTradesSummaryForToday(CLOSED);
|
|
||||||
|
|
||||||
log.info("Closing {}'s gRPC channel.", this.getClass().getSimpleName());
|
|
||||||
super.grpcStubs.close();
|
super.grpcStubs.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,48 +25,89 @@ import protobuf.PaymentAccount;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.BiPredicate;
|
import java.util.function.BiPredicate;
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
import static bisq.bots.BotUtils.*;
|
import static bisq.bots.BotUtils.*;
|
||||||
import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED;
|
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import static java.lang.System.exit;
|
|
||||||
import static java.math.RoundingMode.HALF_UP;
|
import static java.math.RoundingMode.HALF_UP;
|
||||||
import static protobuf.OfferDirection.SELL;
|
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>
|
* <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
|
@Slf4j
|
||||||
@Getter
|
@Getter
|
||||||
public class TakeBestPricedOfferToBuyBsq extends AbstractBot {
|
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.
|
// Config file: resources/TakeBestPricedOfferToBuyBsq.properties.
|
||||||
private final Properties configFile;
|
private final Properties configFile;
|
||||||
// Taker bot's default BSQ Swap payment account.
|
// Taker bot's default BSQ Swap payment account.
|
||||||
private final PaymentAccount paymentAccount;
|
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 (%).
|
// 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.
|
// 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;
|
private final BigDecimal minMarketPriceMargin;
|
||||||
// Hard coded 30-day average BSQ trade price, used for development over regtest (ignored when running on mainnet).
|
// Hard coded 30-day average BSQ trade price, used for development over regtest (ignored when running on mainnet).
|
||||||
private final BigDecimal regtest30DayAvgBsqPrice;
|
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;
|
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;
|
private final BigDecimal maxAmount;
|
||||||
// Taker bot's max acceptable transaction fee rate.
|
// Taker bot's max acceptable transaction fee rate.
|
||||||
private final long maxTxFeeRate;
|
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;
|
private final int maxTakeOffers;
|
||||||
|
|
||||||
// Offer polling frequency must be > 1000 ms between each getoffers request.
|
// Offer polling frequency must be > 1000 ms between each getoffers request.
|
||||||
private final long pollingInterval;
|
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;
|
private int numOffersTaken = 0;
|
||||||
|
|
||||||
public TakeBestPricedOfferToBuyBsq(String[] args) {
|
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.
|
pingDaemon(new Date().getTime()); // Shut down now if API daemon is not available.
|
||||||
this.configFile = loadConfigFile();
|
this.configFile = loadConfigFile();
|
||||||
this.paymentAccount = getBsqSwapPaymentAccount();
|
this.paymentAccount = getBsqSwapPaymentAccount();
|
||||||
this.currencyCode = paymentAccount.getSelectedTradeCurrency().getCode();
|
|
||||||
this.minMarketPriceMargin = new BigDecimal(configFile.getProperty("minMarketPriceMargin"))
|
this.minMarketPriceMargin = new BigDecimal(configFile.getProperty("minMarketPriceMargin"))
|
||||||
.setScale(2, HALF_UP);
|
.setScale(2, HALF_UP);
|
||||||
this.regtest30DayAvgBsqPrice = new BigDecimal(configFile.getProperty("regtest30DayAvgBsqPrice"))
|
this.regtest30DayAvgBsqPrice = new BigDecimal(configFile.getProperty("regtest30DayAvgBsqPrice"))
|
||||||
@ -104,8 +144,9 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all available and takeable offers, sorted by price descending.
|
// Get all available sell BTC for BSQ offers, sorted by price descending.
|
||||||
var offers = getOffers(SELL.name(), currencyCode).stream()
|
// The list contains only fixed-priced offers.
|
||||||
|
var offers = getOffers(SELL.name(), CURRENCY_CODE).stream()
|
||||||
.filter(o -> !isAlreadyTaken.test(o))
|
.filter(o -> !isAlreadyTaken.test(o))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@ -130,7 +171,6 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot {
|
|||||||
takeCriteria.printOfferAgainstCriteria(highestPricedOffer);
|
takeCriteria.printOfferAgainstCriteria(highestPricedOffer);
|
||||||
});
|
});
|
||||||
|
|
||||||
printDryRunProgress();
|
|
||||||
runCountdown(log, pollingInterval);
|
runCountdown(log, pollingInterval);
|
||||||
pingDaemon(startTime);
|
pingDaemon(startTime);
|
||||||
}
|
}
|
||||||
@ -139,17 +179,21 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot {
|
|||||||
private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) {
|
private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) {
|
||||||
log.info("Will attempt to take offer '{}'.", offer.getId());
|
log.info("Will attempt to take offer '{}'.", offer.getId());
|
||||||
takeCriteria.printOfferAgainstCriteria(offer);
|
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) {
|
if (isDryRun) {
|
||||||
addToOffersTaken(offer);
|
addToOffersTaken(offer);
|
||||||
numOffersTaken++;
|
numOffersTaken++;
|
||||||
maybeShutdownAfterSuccessfulSwap();
|
|
||||||
} else {
|
} 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 {
|
try {
|
||||||
unlockWallet(walletPassword, 600);
|
|
||||||
|
|
||||||
printBTCBalances("BTC Balances Before Swap Execution");
|
printBTCBalances("BTC Balances Before Swap Execution");
|
||||||
printBSQBalances("BSQ 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");
|
printBSQBalances("BSQ Balances After Swap Execution");
|
||||||
|
|
||||||
numOffersTaken++;
|
numOffersTaken++;
|
||||||
maybeShutdownAfterSuccessfulSwap();
|
|
||||||
} catch (NonFatalException nonFatalException) {
|
} catch (NonFatalException nonFatalException) {
|
||||||
handleNonFatalException(nonFatalException);
|
handleNonFatalException(nonFatalException, pollingInterval);
|
||||||
} catch (StatusRuntimeException fatalException) {
|
} catch (StatusRuntimeException fatalException) {
|
||||||
handleFatalException(fatalException);
|
handleFatalBsqSwapException(fatalException);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
maybeShutdownAfterSuccessfulSwap(numOffersTaken, maxTakeOffers);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void printBotConfiguration() {
|
private void printBotConfiguration() {
|
||||||
@ -174,12 +218,13 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot {
|
|||||||
configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion());
|
configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion());
|
||||||
var network = getNetwork();
|
var network = getNetwork();
|
||||||
configsByLabel.put("BTC Network:", network);
|
configsByLabel.put("BTC Network:", network);
|
||||||
|
configsByLabel.put("Dry Run?", isDryRun ? "YES" : "NO");
|
||||||
var isMainnet = network.equalsIgnoreCase("mainnet");
|
var isMainnet = network.equalsIgnoreCase("mainnet");
|
||||||
var mainnet30DayAvgBsqPrice = isMainnet ? get30DayAvgBsqPriceInBtc() : null;
|
var mainnet30DayAvgBsqPrice = isMainnet ? get30DayAvgBsqPriceInBtc() : null;
|
||||||
configsByLabel.put("My Payment Account:", "");
|
configsByLabel.put("My Payment Account:", "");
|
||||||
configsByLabel.put("\tPayment Account Id:", paymentAccount.getId());
|
configsByLabel.put("\tPayment Account Id:", paymentAccount.getId());
|
||||||
configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName());
|
configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName());
|
||||||
configsByLabel.put("\tCurrency Code:", currencyCode);
|
configsByLabel.put("\tCurrency Code:", CURRENCY_CODE);
|
||||||
configsByLabel.put("Trading Rules:", "");
|
configsByLabel.put("Trading Rules:", "");
|
||||||
configsByLabel.put("\tMax # of offers bot can take:", maxTakeOffers);
|
configsByLabel.put("\tMax # of offers bot can take:", maxTakeOffers);
|
||||||
configsByLabel.put("\tMax Tx Fee Rate:", maxTxFeeRate + " sats/byte");
|
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.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
|
* 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.
|
* fixed-priced offer if the price is >= {@link #minMarketPriceMargin} (%) of the current market price.
|
||||||
*/
|
*/
|
||||||
protected final BiPredicate<OfferInfo, BigDecimal> isFixedPriceGEMaxMarketPriceMargin =
|
protected final BiPredicate<OfferInfo, BigDecimal> isFixedPriceGEMaxMarketPriceMargin =
|
||||||
(offer, currentMarketPrice) -> BotUtils.isFixedPriceGEMinMarketPriceMargin(
|
(offer, currentMarketPrice) -> isFixedPriceGEMinMarketPriceMargin(
|
||||||
offer,
|
offer,
|
||||||
currentMarketPrice,
|
currentMarketPrice,
|
||||||
this.getMinMarketPriceMargin());
|
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) {
|
public static void main(String[] args) {
|
||||||
TakeBestPricedOfferToBuyBsq bot = new TakeBestPricedOfferToBuyBsq(args);
|
TakeBestPricedOfferToBuyBsq bot = new TakeBestPricedOfferToBuyBsq(args);
|
||||||
bot.run();
|
bot.run();
|
||||||
@ -297,28 +288,28 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot {
|
|||||||
.filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount()))
|
.filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount()))
|
||||||
.filter(isMakerPreferredTradingPeer)
|
.filter(isMakerPreferredTradingPeer)
|
||||||
.filter(o -> isFixedPriceGEMaxMarketPriceMargin.test(o, avgBsqPrice))
|
.filter(o -> isFixedPriceGEMaxMarketPriceMargin.test(o, avgBsqPrice))
|
||||||
.filter(isWithinBTCAmountBounds)
|
.filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount()))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
else
|
else
|
||||||
return offers.stream()
|
return offers.stream()
|
||||||
.filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount()))
|
.filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount()))
|
||||||
.filter(o -> isFixedPriceGEMaxMarketPriceMargin.test(o, avgBsqPrice))
|
.filter(o -> isFixedPriceGEMaxMarketPriceMargin.test(o, avgBsqPrice))
|
||||||
.filter(isWithinBTCAmountBounds)
|
.filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount()))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
void printCriteriaSummary() {
|
void printCriteriaSummary() {
|
||||||
if (isZero.test(minMarketPriceMargin)) {
|
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.",
|
+ " the 30-day average BSQ trade price of {} BTC.",
|
||||||
MARKET_DESCRIPTION,
|
MARKET_DESCRIPTION,
|
||||||
avgBsqPrice);
|
avgBsqPrice);
|
||||||
} else {
|
} 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.",
|
+ " {}% {} the 30-day average BSQ trade price of {} BTC.",
|
||||||
MARKET_DESCRIPTION,
|
MARKET_DESCRIPTION,
|
||||||
minMarketPriceMargin.abs(), // Hide the sign, text explains target price % "above or below".
|
minMarketPriceMargin.abs(), // Hide the sign, text explains target price % "above or below".
|
||||||
aboveOrBelowMarketPrice.apply(minMarketPriceMargin),
|
aboveOrBelowMinMarketPriceMargin.apply(minMarketPriceMargin),
|
||||||
avgBsqPrice);
|
avgBsqPrice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -343,13 +334,13 @@ public class TakeBestPricedOfferToBuyBsq extends AbstractBot {
|
|||||||
iHavePreferredTradingPeers.get()
|
iHavePreferredTradingPeers.get()
|
||||||
? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO"
|
? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO"
|
||||||
: "N/A");
|
: "N/A");
|
||||||
var fixedPriceLabel = format("Is offer fixed-price (%s) >= bot's minimum price (%s)?",
|
var fixedPriceLabel = format("Is offer fixed-price (%s) >= bot's minimum price of (%s)?",
|
||||||
offer.getPrice() + " " + currencyCode,
|
offer.getPrice() + " BTC",
|
||||||
targetPrice + " " + currencyCode);
|
targetPrice + " BTC");
|
||||||
filterResultsByLabel.put(fixedPriceLabel, isFixedPriceGEMaxMarketPriceMargin.test(offer, avgBsqPrice));
|
filterResultsByLabel.put(fixedPriceLabel, isFixedPriceGEMaxMarketPriceMargin.test(offer, avgBsqPrice));
|
||||||
var btcAmountBounds = format("%s BTC - %s BTC", minAmount, maxAmount);
|
var btcAmountBounds = format("%s BTC - %s BTC", minAmount, maxAmount);
|
||||||
filterResultsByLabel.put("Is offer's BTC amount within bot amount bounds (" + btcAmountBounds + ")?",
|
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());
|
var title = format("Fixed price BSQ swap offer %s filter results:", offer.getId());
|
||||||
log.info(toTable.apply(title, filterResultsByLabel));
|
log.info(toTable.apply(title, filterResultsByLabel));
|
||||||
|
|||||||
@ -25,58 +25,64 @@ import protobuf.PaymentAccount;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.BiPredicate;
|
import java.util.function.BiPredicate;
|
||||||
import java.util.function.Predicate;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import static bisq.bots.BotUtils.*;
|
import static bisq.bots.BotUtils.*;
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import static java.lang.System.exit;
|
|
||||||
import static java.math.RoundingMode.HALF_UP;
|
import static java.math.RoundingMode.HALF_UP;
|
||||||
import static protobuf.OfferDirection.BUY;
|
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
|
* This bot's general use case is to sell your BTC for fiat at a high BTC price. It periodically checks the
|
||||||
* (up to a maximum of configured {@link #maxTakeOffers}), then shuts down both the API daemon and itself (the bot),
|
* Buy BTC market, and takes a configured maximum number of offers to buy BTC from you according to criteria you
|
||||||
* to allow the user to start the desktop UI application and complete the trades.
|
* 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>
|
* <p>
|
||||||
* The benefit this bot provides is freeing up the user time spent watching the offer book in the UI, waiting for the
|
* Here is one possible use case:
|
||||||
* 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:
|
|
||||||
* <pre>
|
* <pre>
|
||||||
* Take a "Faster Payment (Santander)" offer to buy BTC with GBP at or above current market price if:
|
* Take 3 "Faster Payment" offers to buy BTC with GBP, priced no lower than 2.00% above the current market
|
||||||
* the offer maker is a preferred trading peer,
|
* price if:
|
||||||
* 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.
|
* 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>
|
* </pre>
|
||||||
* <p>
|
* <b>Usage</b>
|
||||||
* Another possible use case for this bot is to buy BTC with XMR. (We might say "sell XMR for BTC", but we need to
|
* <p><br/>
|
||||||
* remember that all Bisq offers are for buying or selling BTC.)
|
* You must encrypt your wallet password before running this bot. If it is not already encrypted, you can use the CLI:
|
||||||
* <pre>
|
* <pre>
|
||||||
* Take an offer to buy BTC with XMR at or above current market price if:
|
* $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
|
||||||
* 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.
|
|
||||||
* </pre>
|
* </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>
|
* <pre>
|
||||||
* Usage: TakeBestPricedOfferToBuyBtc --password=api-password --port=api-port \
|
* TakeBestPricedOfferToBuyBtc --password=api-password --port=api-port [--dryrun=true|false]
|
||||||
* [--conf=take-best-priced-offer-to-buy-btc.conf] \
|
|
||||||
* [--dryrun=true|false]
|
|
||||||
* [--simulate-regtest-payment=true|false]
|
|
||||||
* </pre>
|
* </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
|
@Slf4j
|
||||||
@Getter
|
@Getter
|
||||||
@ -90,21 +96,21 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
|
|||||||
private final String currencyCode;
|
private final String currencyCode;
|
||||||
// Taker bot's min market price margin. A takeable offer's price margin (%) must be >= minMarketPriceMargin (%).
|
// Taker bot's min market price margin. A takeable offer's price margin (%) must be >= minMarketPriceMargin (%).
|
||||||
private final BigDecimal 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;
|
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;
|
private final BigDecimal maxAmount;
|
||||||
// Taker bot's max acceptable transaction fee rate.
|
// Taker bot's max acceptable transaction fee rate.
|
||||||
private final long maxTxFeeRate;
|
private final long maxTxFeeRate;
|
||||||
// Taker bot's trading fee currency code (BSQ or BTC).
|
// Taker bot's trading fee currency code (BSQ or BTC).
|
||||||
private final String bisqTradeFeeCurrency;
|
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;
|
private final int maxTakeOffers;
|
||||||
|
|
||||||
// Offer polling frequency must be > 1000 ms between each getoffers request.
|
// Offer polling frequency must be > 1000 ms between each getoffers request.
|
||||||
private final long pollingInterval;
|
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;
|
private int numOffersTaken = 0;
|
||||||
|
|
||||||
public TakeBestPricedOfferToBuyBtc(String[] args) {
|
public TakeBestPricedOfferToBuyBtc(String[] args) {
|
||||||
@ -144,12 +150,9 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Taker bot's getOffers(direction) request param. For fiat offers, is BUY (BTC), for XMR offers, is SELL (BTC).
|
// Get all available and takeable buy BTC for fiat offers, sorted by price descending.
|
||||||
String offerDirection = isXmr.test(currencyCode) ? SELL.name() : BUY.name();
|
|
||||||
|
|
||||||
// Get all available and takeable offers, sorted by price ascending.
|
|
||||||
// The list contains both fixed-price and market price margin based offers.
|
// 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))
|
.filter(o -> !isAlreadyTaken.test(o))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@ -174,7 +177,6 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
|
|||||||
takeCriteria.printOfferAgainstCriteria(highestPricedOffer);
|
takeCriteria.printOfferAgainstCriteria(highestPricedOffer);
|
||||||
});
|
});
|
||||||
|
|
||||||
printDryRunProgress();
|
|
||||||
runCountdown(log, pollingInterval);
|
runCountdown(log, pollingInterval);
|
||||||
pingDaemon(startTime);
|
pingDaemon(startTime);
|
||||||
}
|
}
|
||||||
@ -188,16 +190,21 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
|
|||||||
private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) {
|
private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) {
|
||||||
log.info("Will attempt to take offer '{}'.", offer.getId());
|
log.info("Will attempt to take offer '{}'.", offer.getId());
|
||||||
takeCriteria.printOfferAgainstCriteria(offer);
|
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) {
|
if (isDryRun) {
|
||||||
addToOffersTaken(offer);
|
addToOffersTaken(offer);
|
||||||
numOffersTaken++;
|
numOffersTaken++;
|
||||||
maybeShutdownAfterSuccessfulTradeCreation();
|
|
||||||
} else {
|
} 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 {
|
try {
|
||||||
unlockWallet(walletPassword, 600);
|
|
||||||
printBTCBalances("BTC Balances Before Take Offer Attempt");
|
printBTCBalances("BTC Balances Before Take Offer Attempt");
|
||||||
// Blocks until new trade is prepared, or times out.
|
// Blocks until new trade is prepared, or times out.
|
||||||
takeV1ProtocolOffer(offer, paymentAccount, bisqTradeFeeCurrency, pollingInterval);
|
takeV1ProtocolOffer(offer, paymentAccount, bisqTradeFeeCurrency, pollingInterval);
|
||||||
@ -212,79 +219,13 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
|
|||||||
printBTCBalances("BTC Balances After Simulated Trade Completion");
|
printBTCBalances("BTC Balances After Simulated Trade Completion");
|
||||||
}
|
}
|
||||||
numOffersTaken++;
|
numOffersTaken++;
|
||||||
maybeShutdownAfterSuccessfulTradeCreation();
|
|
||||||
} catch (NonFatalException nonFatalException) {
|
} catch (NonFatalException nonFatalException) {
|
||||||
handleNonFatalException(nonFatalException);
|
handleNonFatalException(nonFatalException, pollingInterval);
|
||||||
} catch (StatusRuntimeException fatalException) {
|
} catch (StatusRuntimeException fatalException) {
|
||||||
handleFatalException(fatalException);
|
shutdownAfterTakeOfferFailure(fatalException);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers);
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -297,18 +238,13 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
|
|||||||
currentMarketPrice,
|
currentMarketPrice,
|
||||||
this.getMinMarketPriceMargin());
|
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() {
|
private void printBotConfiguration() {
|
||||||
var configsByLabel = new LinkedHashMap<String, Object>();
|
var configsByLabel = new LinkedHashMap<String, Object>();
|
||||||
configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion());
|
configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion());
|
||||||
var network = getNetwork();
|
var network = getNetwork();
|
||||||
configsByLabel.put("BTC Network:", network);
|
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("My Payment Account:", "");
|
||||||
configsByLabel.put("\tPayment Account Id:", paymentAccount.getId());
|
configsByLabel.put("\tPayment Account Id:", paymentAccount.getId());
|
||||||
configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName());
|
configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName());
|
||||||
@ -338,17 +274,12 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
|
|||||||
* performs candidate offer filtering, and provides useful log statements.
|
* performs candidate offer filtering, and provides useful log statements.
|
||||||
*/
|
*/
|
||||||
private class TakeCriteria {
|
private class TakeCriteria {
|
||||||
|
private static final String MARKET_DESCRIPTION = "Buy BTC";
|
||||||
|
|
||||||
private final BigDecimal currentMarketPrice;
|
private final BigDecimal currentMarketPrice;
|
||||||
@Getter
|
@Getter
|
||||||
private final BigDecimal targetPrice;
|
private final BigDecimal targetPrice;
|
||||||
|
|
||||||
private final Supplier<String> marketDescription = () -> {
|
|
||||||
if (isXmr.test(currencyCode))
|
|
||||||
return "Buy XMR (Sell BTC)";
|
|
||||||
else
|
|
||||||
return "Buy BTC";
|
|
||||||
};
|
|
||||||
|
|
||||||
public TakeCriteria() {
|
public TakeCriteria() {
|
||||||
this.currentMarketPrice = getCurrentMarketPrice(currencyCode);
|
this.currentMarketPrice = getCurrentMarketPrice(currencyCode);
|
||||||
this.targetPrice = calcTargetPrice(minMarketPriceMargin, currentMarketPrice, currencyCode);
|
this.targetPrice = calcTargetPrice(minMarketPriceMargin, currentMarketPrice, currencyCode);
|
||||||
@ -367,39 +298,39 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
|
|||||||
.filter(isMakerPreferredTradingPeer)
|
.filter(isMakerPreferredTradingPeer)
|
||||||
.filter(o -> isMarginBasedPriceGETargetPrice.test(o, targetPrice)
|
.filter(o -> isMarginBasedPriceGETargetPrice.test(o, targetPrice)
|
||||||
|| isFixedPriceGEMinMarketPriceMargin.test(o, currentMarketPrice))
|
|| isFixedPriceGEMinMarketPriceMargin.test(o, currentMarketPrice))
|
||||||
.filter(isWithinBTCAmountBounds)
|
.filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount()))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
else
|
else
|
||||||
return offers.stream()
|
return offers.stream()
|
||||||
.filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount()))
|
.filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount()))
|
||||||
.filter(o -> isMarginBasedPriceGETargetPrice.test(o, targetPrice)
|
.filter(o -> isMarginBasedPriceGETargetPrice.test(o, targetPrice)
|
||||||
|| isFixedPriceGEMinMarketPriceMargin.test(o, currentMarketPrice))
|
|| isFixedPriceGEMinMarketPriceMargin.test(o, currentMarketPrice))
|
||||||
.filter(isWithinBTCAmountBounds)
|
.filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount()))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
void printCriteriaSummary() {
|
void printCriteriaSummary() {
|
||||||
if (isZero.test(minMarketPriceMargin)) {
|
if (isZero.test(minMarketPriceMargin)) {
|
||||||
log.info("Looking for offers to {}, priced at or higher than the current market price {} {}.",
|
log.info("Looking for offers to {}, priced at or higher than the current market price of {} {}.",
|
||||||
marketDescription.get(),
|
MARKET_DESCRIPTION,
|
||||||
currentMarketPrice,
|
currentMarketPrice,
|
||||||
isXmr.test(currencyCode) ? "BTC" : currencyCode);
|
currencyCode);
|
||||||
} else {
|
} else {
|
||||||
log.info("Looking for offers to {}, priced at or more than {}% {} the current market price {} {}.",
|
log.info("Looking for offers to {}, priced at or higher than {}% {} the current market price of {} {}.",
|
||||||
marketDescription.get(),
|
MARKET_DESCRIPTION,
|
||||||
minMarketPriceMargin.abs(), // Hide the sign, text explains target price % "above or below".
|
minMarketPriceMargin.abs(), // Hide the sign, text explains target price % "above or below".
|
||||||
aboveOrBelowMarketPrice.apply(minMarketPriceMargin),
|
aboveOrBelowMinMarketPriceMargin.apply(minMarketPriceMargin),
|
||||||
currentMarketPrice,
|
currentMarketPrice,
|
||||||
isXmr.test(currencyCode) ? "BTC" : currencyCode);
|
currencyCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void printOffersAgainstCriteria(List<OfferInfo> offers) {
|
void printOffersAgainstCriteria(List<OfferInfo> offers) {
|
||||||
log.info("Currently available {} offers -- want to take {} offer with price >= {} {}.",
|
log.info("Currently available {} offers -- want to take {} offer with price >= {} {}.",
|
||||||
marketDescription.get(),
|
MARKET_DESCRIPTION,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
targetPrice,
|
targetPrice,
|
||||||
isXmr.test(currencyCode) ? "BTC" : currencyCode);
|
currencyCode);
|
||||||
printOffersSummary(offers);
|
printOffersSummary(offers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -416,23 +347,22 @@ public class TakeBestPricedOfferToBuyBtc extends AbstractBot {
|
|||||||
iHavePreferredTradingPeers.get()
|
iHavePreferredTradingPeers.get()
|
||||||
? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO"
|
? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO"
|
||||||
: "N/A");
|
: "N/A");
|
||||||
var marginPriceLabel = format("Is offer's margin based price (%s) >= bot's target price (%s)?",
|
|
||||||
offer.getUseMarketBasedPrice() ? offer.getPrice() : "N/A",
|
if (offer.getUseMarketBasedPrice()) {
|
||||||
offer.getUseMarketBasedPrice() ? targetPrice : "N/A");
|
var marginPriceLabel = format("Is offer's margin based price (%s) >= bot's target price (%s)?",
|
||||||
filterResultsByLabel.put(marginPriceLabel,
|
offer.getPrice() + " " + currencyCode,
|
||||||
offer.getUseMarketBasedPrice()
|
targetPrice + " " + currencyCode);
|
||||||
? isMarginBasedPriceGETargetPrice.test(offer, targetPrice)
|
filterResultsByLabel.put(marginPriceLabel, isMarginBasedPriceGETargetPrice.test(offer, targetPrice));
|
||||||
: "N/A");
|
} else {
|
||||||
var fixedPriceLabel = format("Is offer's fixed-price (%s) >= bot's target price (%s)?",
|
var fixedPriceLabel = format("Is offer's fixed-price (%s) >= bot's target price (%s)?",
|
||||||
offer.getUseMarketBasedPrice() ? "N/A" : offer.getPrice() + " " + currencyCode,
|
offer.getPrice() + " " + currencyCode,
|
||||||
offer.getUseMarketBasedPrice() ? "N/A" : targetPrice + " " + currencyCode);
|
targetPrice + " " + currencyCode);
|
||||||
filterResultsByLabel.put(fixedPriceLabel,
|
filterResultsByLabel.put(fixedPriceLabel, isFixedPriceGEMinMarketPriceMargin.test(offer, currentMarketPrice));
|
||||||
offer.getUseMarketBasedPrice()
|
}
|
||||||
? "N/A"
|
|
||||||
: isFixedPriceGEMinMarketPriceMargin.test(offer, currentMarketPrice));
|
|
||||||
String btcAmountBounds = format("%s BTC - %s BTC", minAmount, maxAmount);
|
String btcAmountBounds = format("%s BTC - %s BTC", minAmount, maxAmount);
|
||||||
filterResultsByLabel.put("Is offer's BTC amount within bot amount bounds (" + btcAmountBounds + ")?",
|
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:",
|
var title = format("%s offer %s filter results:",
|
||||||
offer.getUseMarketBasedPrice() ? "Margin based" : "Fixed price",
|
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.math.BigDecimal;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.BiPredicate;
|
import java.util.function.BiPredicate;
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
import static bisq.bots.BotUtils.*;
|
import static bisq.bots.BotUtils.*;
|
||||||
import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED;
|
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import static java.lang.System.exit;
|
|
||||||
import static java.math.RoundingMode.HALF_UP;
|
import static java.math.RoundingMode.HALF_UP;
|
||||||
import static protobuf.OfferDirection.BUY;
|
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>
|
* <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
|
@Slf4j
|
||||||
@Getter
|
@Getter
|
||||||
public class TakeBestPricedOfferToSellBsq extends AbstractBot {
|
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.
|
// Config file: resources/TakeBestPricedOfferToSellBsq.properties.
|
||||||
private final Properties configFile;
|
private final Properties configFile;
|
||||||
// Taker bot's default BSQ Swap payment account.
|
// Taker bot's default BSQ Swap payment account.
|
||||||
private final PaymentAccount paymentAccount;
|
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 (%).
|
// 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.
|
// 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;
|
private final BigDecimal maxMarketPriceMargin;
|
||||||
// Hard coded 30-day average BSQ trade price, used for development over regtest (ignored when running on mainnet).
|
// Hard coded 30-day average BSQ trade price, used for development over regtest (ignored when running on mainnet).
|
||||||
private final BigDecimal regtest30DayAvgBsqPrice;
|
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;
|
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;
|
private final BigDecimal maxAmount;
|
||||||
// Taker bot's max acceptable transaction fee rate.
|
// Taker bot's max acceptable transaction fee rate.
|
||||||
private final long maxTxFeeRate;
|
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;
|
private final int maxTakeOffers;
|
||||||
|
|
||||||
// Offer polling frequency must be > 1000 ms between each getoffers request.
|
// Offer polling frequency must be > 1000 ms between each getoffers request.
|
||||||
private final long pollingInterval;
|
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;
|
private int numOffersTaken = 0;
|
||||||
|
|
||||||
public TakeBestPricedOfferToSellBsq(String[] args) {
|
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.
|
pingDaemon(new Date().getTime()); // Shut down now if API daemon is not available.
|
||||||
this.configFile = loadConfigFile();
|
this.configFile = loadConfigFile();
|
||||||
this.paymentAccount = getBsqSwapPaymentAccount();
|
this.paymentAccount = getBsqSwapPaymentAccount();
|
||||||
this.currencyCode = paymentAccount.getSelectedTradeCurrency().getCode();
|
|
||||||
this.maxMarketPriceMargin = new BigDecimal(configFile.getProperty("maxMarketPriceMargin"))
|
this.maxMarketPriceMargin = new BigDecimal(configFile.getProperty("maxMarketPriceMargin"))
|
||||||
.setScale(2, HALF_UP);
|
.setScale(2, HALF_UP);
|
||||||
this.regtest30DayAvgBsqPrice = new BigDecimal(configFile.getProperty("regtest30DayAvgBsqPrice"))
|
this.regtest30DayAvgBsqPrice = new BigDecimal(configFile.getProperty("regtest30DayAvgBsqPrice"))
|
||||||
@ -104,8 +144,9 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all available and takeable offers, sorted by price ascending.
|
// Get all available buy BTC with BSQ offers, sorted by price ascending.
|
||||||
var offers = getOffers(BUY.name(), currencyCode).stream()
|
// The list contains only fixed-priced offers.
|
||||||
|
var offers = getOffers(BUY.name(), CURRENCY_CODE).stream()
|
||||||
.filter(o -> !isAlreadyTaken.test(o))
|
.filter(o -> !isAlreadyTaken.test(o))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@ -130,7 +171,6 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot {
|
|||||||
takeCriteria.printOfferAgainstCriteria(cheapestOffer);
|
takeCriteria.printOfferAgainstCriteria(cheapestOffer);
|
||||||
});
|
});
|
||||||
|
|
||||||
printDryRunProgress();
|
|
||||||
runCountdown(log, pollingInterval);
|
runCountdown(log, pollingInterval);
|
||||||
pingDaemon(startTime);
|
pingDaemon(startTime);
|
||||||
}
|
}
|
||||||
@ -139,17 +179,21 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot {
|
|||||||
private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) {
|
private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) {
|
||||||
log.info("Will attempt to take offer '{}'.", offer.getId());
|
log.info("Will attempt to take offer '{}'.", offer.getId());
|
||||||
takeCriteria.printOfferAgainstCriteria(offer);
|
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) {
|
if (isDryRun) {
|
||||||
addToOffersTaken(offer);
|
addToOffersTaken(offer);
|
||||||
numOffersTaken++;
|
numOffersTaken++;
|
||||||
maybeShutdownAfterSuccessfulSwap();
|
|
||||||
} else {
|
} 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 {
|
try {
|
||||||
unlockWallet(walletPassword, 600);
|
|
||||||
|
|
||||||
printBTCBalances("BTC Balances Before Swap Execution");
|
printBTCBalances("BTC Balances Before Swap Execution");
|
||||||
printBSQBalances("BSQ 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");
|
printBSQBalances("BSQ Balances After Swap Execution");
|
||||||
|
|
||||||
numOffersTaken++;
|
numOffersTaken++;
|
||||||
maybeShutdownAfterSuccessfulSwap();
|
|
||||||
} catch (NonFatalException nonFatalException) {
|
} catch (NonFatalException nonFatalException) {
|
||||||
handleNonFatalException(nonFatalException);
|
handleNonFatalException(nonFatalException, pollingInterval);
|
||||||
} catch (StatusRuntimeException fatalException) {
|
} catch (StatusRuntimeException fatalException) {
|
||||||
handleFatalException(fatalException);
|
handleFatalBsqSwapException(fatalException);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
maybeShutdownAfterSuccessfulSwap(numOffersTaken, maxTakeOffers);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void printBotConfiguration() {
|
private void printBotConfiguration() {
|
||||||
@ -174,12 +218,13 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot {
|
|||||||
configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion());
|
configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion());
|
||||||
var network = getNetwork();
|
var network = getNetwork();
|
||||||
configsByLabel.put("BTC Network:", network);
|
configsByLabel.put("BTC Network:", network);
|
||||||
|
configsByLabel.put("Dry Run?", isDryRun ? "YES" : "NO");
|
||||||
var isMainnet = network.equalsIgnoreCase("mainnet");
|
var isMainnet = network.equalsIgnoreCase("mainnet");
|
||||||
var mainnet30DayAvgBsqPrice = isMainnet ? get30DayAvgBsqPriceInBtc() : null;
|
var mainnet30DayAvgBsqPrice = isMainnet ? get30DayAvgBsqPriceInBtc() : null;
|
||||||
configsByLabel.put("My Payment Account:", "");
|
configsByLabel.put("My Payment Account:", "");
|
||||||
configsByLabel.put("\tPayment Account Id:", paymentAccount.getId());
|
configsByLabel.put("\tPayment Account Id:", paymentAccount.getId());
|
||||||
configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName());
|
configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName());
|
||||||
configsByLabel.put("\tCurrency Code:", currencyCode);
|
configsByLabel.put("\tCurrency Code:", CURRENCY_CODE);
|
||||||
configsByLabel.put("Trading Rules:", "");
|
configsByLabel.put("Trading Rules:", "");
|
||||||
configsByLabel.put("\tMax # of offers bot can take:", maxTakeOffers);
|
configsByLabel.put("\tMax # of offers bot can take:", maxTakeOffers);
|
||||||
configsByLabel.put("\tMax Tx Fee Rate:", maxTxFeeRate + " sats/byte");
|
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.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
|
* 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.
|
* fixed-priced offer if the price is <= {@link #maxMarketPriceMargin} (%) of the current market price.
|
||||||
@ -257,13 +255,6 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot {
|
|||||||
currentMarketPrice,
|
currentMarketPrice,
|
||||||
getMaxMarketPriceMargin());
|
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) {
|
public static void main(String[] args) {
|
||||||
TakeBestPricedOfferToSellBsq bot = new TakeBestPricedOfferToSellBsq(args);
|
TakeBestPricedOfferToSellBsq bot = new TakeBestPricedOfferToSellBsq(args);
|
||||||
bot.run();
|
bot.run();
|
||||||
@ -285,7 +276,6 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot {
|
|||||||
this.targetPrice = calcTargetBsqPrice(maxMarketPriceMargin, avgBsqPrice);
|
this.targetPrice = calcTargetBsqPrice(maxMarketPriceMargin, avgBsqPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the lowest priced offer passing the filters, or Optional.empty() if not found.
|
* 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.
|
* 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(o -> usesSamePaymentMethod.test(o, getPaymentAccount()))
|
||||||
.filter(isMakerPreferredTradingPeer)
|
.filter(isMakerPreferredTradingPeer)
|
||||||
.filter(o -> isFixedPriceLEMaxMarketPriceMargin.test(o, avgBsqPrice))
|
.filter(o -> isFixedPriceLEMaxMarketPriceMargin.test(o, avgBsqPrice))
|
||||||
.filter(isWithinBTCAmountBounds)
|
.filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount()))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
else
|
else
|
||||||
return offers.stream()
|
return offers.stream()
|
||||||
.filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount()))
|
.filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount()))
|
||||||
.filter(o -> isFixedPriceLEMaxMarketPriceMargin.test(o, avgBsqPrice))
|
.filter(o -> isFixedPriceLEMaxMarketPriceMargin.test(o, avgBsqPrice))
|
||||||
.filter(isWithinBTCAmountBounds)
|
.filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount()))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
void printCriteriaSummary() {
|
void printCriteriaSummary() {
|
||||||
if (isZero.test(maxMarketPriceMargin)) {
|
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.",
|
+ " the 30-day average BSQ trade price of {} BTC.",
|
||||||
MARKET_DESCRIPTION,
|
MARKET_DESCRIPTION,
|
||||||
avgBsqPrice);
|
avgBsqPrice);
|
||||||
} else {
|
} 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.",
|
+ " {}% {} the 30-day average BSQ trade price of {} BTC.",
|
||||||
MARKET_DESCRIPTION,
|
MARKET_DESCRIPTION,
|
||||||
maxMarketPriceMargin.abs(), // Hide the sign, text explains target price % "above or below".
|
maxMarketPriceMargin.abs(), // Hide the sign, text explains target price % "above or below".
|
||||||
aboveOrBelowMarketPrice.apply(maxMarketPriceMargin),
|
aboveOrBelowMaxMarketPriceMargin.apply(maxMarketPriceMargin),
|
||||||
avgBsqPrice);
|
avgBsqPrice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -336,7 +326,7 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot {
|
|||||||
|
|
||||||
var filterResultsByLabel = new LinkedHashMap<String, Object>();
|
var filterResultsByLabel = new LinkedHashMap<String, Object>();
|
||||||
filterResultsByLabel.put("30-day Avg BSQ trade price:", avgBsqPrice + " BTC");
|
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 Price:", offer.getPrice() + " BTC");
|
||||||
filterResultsByLabel.put("Offer maker used same payment method?",
|
filterResultsByLabel.put("Offer maker used same payment method?",
|
||||||
usesSamePaymentMethod.test(offer, getPaymentAccount()));
|
usesSamePaymentMethod.test(offer, getPaymentAccount()));
|
||||||
@ -344,13 +334,13 @@ public class TakeBestPricedOfferToSellBsq extends AbstractBot {
|
|||||||
iHavePreferredTradingPeers.get()
|
iHavePreferredTradingPeers.get()
|
||||||
? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO"
|
? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO"
|
||||||
: "N/A");
|
: "N/A");
|
||||||
var fixedPriceLabel = format("Is offer's fixed-price (%s) <= bot's minimum price (%s)?",
|
var fixedPriceLabel = format("Is offer's fixed-price (%s) <= bot's maximum price of (%s)?",
|
||||||
offer.getPrice() + " " + currencyCode,
|
offer.getPrice() + " BTC",
|
||||||
targetPrice + " " + currencyCode);
|
targetPrice + " BTC");
|
||||||
filterResultsByLabel.put(fixedPriceLabel, isFixedPriceLEMaxMarketPriceMargin.test(offer, avgBsqPrice));
|
filterResultsByLabel.put(fixedPriceLabel, isFixedPriceLEMaxMarketPriceMargin.test(offer, avgBsqPrice));
|
||||||
var btcAmountBounds = format("%s BTC - %s BTC", minAmount, maxAmount);
|
var btcAmountBounds = format("%s BTC - %s BTC", minAmount, maxAmount);
|
||||||
filterResultsByLabel.put("Is offer's BTC amount within bot amount bounds (" + btcAmountBounds + ")?",
|
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());
|
var title = format("Fixed price BSQ swap offer %s filter results:", offer.getId());
|
||||||
log.info(toTable.apply(title, filterResultsByLabel));
|
log.info(toTable.apply(title, filterResultsByLabel));
|
||||||
|
|||||||
@ -25,59 +25,64 @@ import protobuf.PaymentAccount;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.BiPredicate;
|
import java.util.function.BiPredicate;
|
||||||
import java.util.function.Predicate;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import static bisq.bots.BotUtils.*;
|
import static bisq.bots.BotUtils.*;
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import static java.lang.System.exit;
|
|
||||||
import static java.math.RoundingMode.HALF_UP;
|
import static java.math.RoundingMode.HALF_UP;
|
||||||
import static protobuf.OfferDirection.BUY;
|
|
||||||
import static protobuf.OfferDirection.SELL;
|
import static protobuf.OfferDirection.SELL;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The TakeBestPricedOfferToSellBtc bot waits for attractively priced SELL BTC offers to appear, takes the offers
|
* This bot's general use case is to buy BTC with fiat at a low fiat price. It periodically checks the
|
||||||
* (up to a maximum of configured {@link #maxTakeOffers}), then shuts down both the API daemon and itself (the bot),
|
* Sell BTC market, and takes a configured maximum number of offers to sell BTC to you according to criteria you
|
||||||
* to allow the user to start the desktop UI application and complete the trades.
|
* 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>
|
* <p>
|
||||||
* The benefit this bot provides is freeing up the user time spent watching the offer book in the UI, waiting for the
|
* Here is one possible use case:
|
||||||
* 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:
|
|
||||||
* <pre>
|
* <pre>
|
||||||
* Take a "Faster Payment (Santander)" offer to sell BTC for GBP at or below current market price if:
|
* Take 4 "Faster Payment" offers to sell BTC for GBP, priced no higher than 1.00% above the current market
|
||||||
* the offer maker is a preferred trading peer,
|
* price if:
|
||||||
* 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.
|
* 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>
|
* </pre>
|
||||||
* <p>
|
* <b>Usage</b>
|
||||||
* Another possible use case for this bot is to sell BTC for XMR. (We might say "buy XMR with BTC", but we need to
|
* <p><br/>
|
||||||
* remember that all Bisq offers are for buying or selling BTC.)
|
* You must encrypt your wallet password before running this bot. If it is not already encrypted, you can use the CLI:
|
||||||
* <pre>
|
* <pre>
|
||||||
* Take an offer to sell BTC for XMR at or below current market price if:
|
* $ ./bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="be careful"
|
||||||
* 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.
|
|
||||||
* </pre>
|
* </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>
|
* <pre>
|
||||||
* Usage: TakeBestPricedOfferToSellBtc --password=api-password --port=api-port \
|
* TakeBestPricedOfferToBuyBtc --password=api-password --port=api-port [--dryrun=true|false]
|
||||||
* [--conf=take-best-priced-offer-to-sell-btc.conf] \
|
|
||||||
* [--dryrun=true|false]
|
|
||||||
* [--simulate-regtest-payment=true|false]
|
|
||||||
* </pre>
|
* </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
|
@Slf4j
|
||||||
@Getter
|
@Getter
|
||||||
@ -91,21 +96,21 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot {
|
|||||||
private final String currencyCode;
|
private final String currencyCode;
|
||||||
// Taker bot's max market price margin. A takeable offer's price margin (%) must be <= maxMarketPriceMargin (%).
|
// Taker bot's max market price margin. A takeable offer's price margin (%) must be <= maxMarketPriceMargin (%).
|
||||||
private final BigDecimal 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;
|
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;
|
private final BigDecimal maxAmount;
|
||||||
// Taker bot's max acceptable transaction fee rate.
|
// Taker bot's max acceptable transaction fee rate.
|
||||||
private final long maxTxFeeRate;
|
private final long maxTxFeeRate;
|
||||||
// Taker bot's trading fee currency code (BSQ or BTC).
|
// Taker bot's trading fee currency code (BSQ or BTC).
|
||||||
private final String bisqTradeFeeCurrency;
|
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;
|
private final int maxTakeOffers;
|
||||||
|
|
||||||
// Offer polling frequency must be > 1000 ms between each getoffers request.
|
// Offer polling frequency must be > 1000 ms between each getoffers request.
|
||||||
private final long pollingInterval;
|
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;
|
private int numOffersTaken = 0;
|
||||||
|
|
||||||
public TakeBestPricedOfferToSellBtc(String[] args) {
|
public TakeBestPricedOfferToSellBtc(String[] args) {
|
||||||
@ -145,12 +150,9 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Taker bot's getOffers(direction) request param. For fiat offers, is SELL (BTC), for XMR offers, is BUY (BTC).
|
// Get all available and takeable sell BTC offers, sorted by price ascending.
|
||||||
String offerDirection = isXmr.test(currencyCode) ? BUY.name() : SELL.name();
|
|
||||||
|
|
||||||
// Get all available and takeable offers, sorted by price ascending.
|
|
||||||
// The list contains both fixed-price and market price margin based offers.
|
// 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))
|
.filter(o -> !isAlreadyTaken.test(o))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@ -175,7 +177,6 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot {
|
|||||||
takeCriteria.printOfferAgainstCriteria(cheapestOffer);
|
takeCriteria.printOfferAgainstCriteria(cheapestOffer);
|
||||||
});
|
});
|
||||||
|
|
||||||
printDryRunProgress();
|
|
||||||
runCountdown(log, pollingInterval);
|
runCountdown(log, pollingInterval);
|
||||||
pingDaemon(startTime);
|
pingDaemon(startTime);
|
||||||
}
|
}
|
||||||
@ -189,16 +190,21 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot {
|
|||||||
private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) {
|
private void takeOffer(TakeCriteria takeCriteria, OfferInfo offer) {
|
||||||
log.info("Will attempt to take offer '{}'.", offer.getId());
|
log.info("Will attempt to take offer '{}'.", offer.getId());
|
||||||
takeCriteria.printOfferAgainstCriteria(offer);
|
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) {
|
if (isDryRun) {
|
||||||
addToOffersTaken(offer);
|
addToOffersTaken(offer);
|
||||||
numOffersTaken++;
|
numOffersTaken++;
|
||||||
maybeShutdownAfterSuccessfulTradeCreation();
|
|
||||||
} else {
|
} 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 {
|
try {
|
||||||
unlockWallet(walletPassword, 600);
|
|
||||||
printBTCBalances("BTC Balances Before Take Offer Attempt");
|
printBTCBalances("BTC Balances Before Take Offer Attempt");
|
||||||
// Blocks until new trade is prepared, or times out.
|
// Blocks until new trade is prepared, or times out.
|
||||||
takeV1ProtocolOffer(offer, paymentAccount, bisqTradeFeeCurrency, pollingInterval);
|
takeV1ProtocolOffer(offer, paymentAccount, bisqTradeFeeCurrency, pollingInterval);
|
||||||
@ -213,79 +219,13 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot {
|
|||||||
printBTCBalances("BTC Balances After Simulated Trade Completion");
|
printBTCBalances("BTC Balances After Simulated Trade Completion");
|
||||||
}
|
}
|
||||||
numOffersTaken++;
|
numOffersTaken++;
|
||||||
maybeShutdownAfterSuccessfulTradeCreation();
|
|
||||||
} catch (NonFatalException nonFatalException) {
|
} catch (NonFatalException nonFatalException) {
|
||||||
handleNonFatalException(nonFatalException);
|
handleNonFatalException(nonFatalException, pollingInterval);
|
||||||
} catch (StatusRuntimeException fatalException) {
|
} catch (StatusRuntimeException fatalException) {
|
||||||
handleFatalException(fatalException);
|
shutdownAfterTakeOfferFailure(fatalException);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
maybeShutdownAfterSuccessfulTradeCreation(numOffersTaken, maxTakeOffers);
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -298,18 +238,13 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot {
|
|||||||
currentMarketPrice,
|
currentMarketPrice,
|
||||||
getMaxMarketPriceMargin());
|
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() {
|
private void printBotConfiguration() {
|
||||||
var configsByLabel = new LinkedHashMap<String, Object>();
|
var configsByLabel = new LinkedHashMap<String, Object>();
|
||||||
configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion());
|
configsByLabel.put("Bot OS:", getOSName() + " " + getOSVersion());
|
||||||
var network = getNetwork();
|
var network = getNetwork();
|
||||||
configsByLabel.put("BTC Network:", network);
|
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("My Payment Account:", "");
|
||||||
configsByLabel.put("\tPayment Account Id:", paymentAccount.getId());
|
configsByLabel.put("\tPayment Account Id:", paymentAccount.getId());
|
||||||
configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName());
|
configsByLabel.put("\tAccount Name:", paymentAccount.getAccountName());
|
||||||
@ -339,17 +274,12 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot {
|
|||||||
* performs candidate offer filtering, and provides useful log statements.
|
* performs candidate offer filtering, and provides useful log statements.
|
||||||
*/
|
*/
|
||||||
private class TakeCriteria {
|
private class TakeCriteria {
|
||||||
|
private static final String MARKET_DESCRIPTION = "Sell BTC";
|
||||||
|
|
||||||
private final BigDecimal currentMarketPrice;
|
private final BigDecimal currentMarketPrice;
|
||||||
@Getter
|
@Getter
|
||||||
private final BigDecimal targetPrice;
|
private final BigDecimal targetPrice;
|
||||||
|
|
||||||
private final Supplier<String> marketDescription = () -> {
|
|
||||||
if (isXmr.test(currencyCode))
|
|
||||||
return "Sell XMR (Buy BTC)";
|
|
||||||
else
|
|
||||||
return "Sell BTC";
|
|
||||||
};
|
|
||||||
|
|
||||||
public TakeCriteria() {
|
public TakeCriteria() {
|
||||||
this.currentMarketPrice = getCurrentMarketPrice(currencyCode);
|
this.currentMarketPrice = getCurrentMarketPrice(currencyCode);
|
||||||
this.targetPrice = calcTargetPrice(maxMarketPriceMargin, currentMarketPrice, currencyCode);
|
this.targetPrice = calcTargetPrice(maxMarketPriceMargin, currentMarketPrice, currencyCode);
|
||||||
@ -368,39 +298,39 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot {
|
|||||||
.filter(isMakerPreferredTradingPeer)
|
.filter(isMakerPreferredTradingPeer)
|
||||||
.filter(o -> isMarginLEMaxMarketPriceMargin.test(o, maxMarketPriceMargin)
|
.filter(o -> isMarginLEMaxMarketPriceMargin.test(o, maxMarketPriceMargin)
|
||||||
|| isFixedPriceLEMaxMarketPriceMargin.test(o, currentMarketPrice))
|
|| isFixedPriceLEMaxMarketPriceMargin.test(o, currentMarketPrice))
|
||||||
.filter(isWithinBTCAmountBounds)
|
.filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount()))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
else
|
else
|
||||||
return offers.stream()
|
return offers.stream()
|
||||||
.filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount()))
|
.filter(o -> usesSamePaymentMethod.test(o, getPaymentAccount()))
|
||||||
.filter(o -> isMarginLEMaxMarketPriceMargin.test(o, maxMarketPriceMargin)
|
.filter(o -> isMarginLEMaxMarketPriceMargin.test(o, maxMarketPriceMargin)
|
||||||
|| isFixedPriceLEMaxMarketPriceMargin.test(o, currentMarketPrice))
|
|| isFixedPriceLEMaxMarketPriceMargin.test(o, currentMarketPrice))
|
||||||
.filter(isWithinBTCAmountBounds)
|
.filter(o -> isWithinBTCAmountBounds(o, getMinAmount(), getMaxAmount()))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
void printCriteriaSummary() {
|
void printCriteriaSummary() {
|
||||||
if (isZero.test(maxMarketPriceMargin)) {
|
if (isZero.test(maxMarketPriceMargin)) {
|
||||||
log.info("Looking for offers to {}, priced at or lower than the current market price {} {}.",
|
log.info("Looking for offers to {}, priced at or lower than the current market price of {} {}.",
|
||||||
marketDescription.get(),
|
MARKET_DESCRIPTION,
|
||||||
currentMarketPrice,
|
currentMarketPrice,
|
||||||
isXmr.test(currencyCode) ? "BTC" : currencyCode);
|
currencyCode);
|
||||||
} else {
|
} else {
|
||||||
log.info("Looking for offers to {}, priced at or less than {}% {} the current market price {} {}.",
|
log.info("Looking for offers to {}, priced at or lower than {}% {} the current market price of {} {}.",
|
||||||
marketDescription.get(),
|
MARKET_DESCRIPTION,
|
||||||
maxMarketPriceMargin.abs(), // Hide the sign, text explains target price % "above or below".
|
maxMarketPriceMargin.abs(), // Hide the sign, text explains target price % "above or below".
|
||||||
aboveOrBelowMarketPrice.apply(maxMarketPriceMargin),
|
aboveOrBelowMaxMarketPriceMargin.apply(maxMarketPriceMargin),
|
||||||
currentMarketPrice,
|
currentMarketPrice,
|
||||||
isXmr.test(currencyCode) ? "BTC" : currencyCode);
|
currencyCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void printOffersAgainstCriteria(List<OfferInfo> offers) {
|
void printOffersAgainstCriteria(List<OfferInfo> offers) {
|
||||||
log.info("Currently available {} offers -- want to take {} offer with price <= {} {}.",
|
log.info("Currently available {} offers -- want to take {} offer with price <= {} {}.",
|
||||||
marketDescription.get(),
|
MARKET_DESCRIPTION,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
targetPrice,
|
targetPrice,
|
||||||
isXmr.test(currencyCode) ? "BTC" : currencyCode);
|
currencyCode);
|
||||||
printOffersSummary(offers);
|
printOffersSummary(offers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,23 +347,22 @@ public class TakeBestPricedOfferToSellBtc extends AbstractBot {
|
|||||||
iHavePreferredTradingPeers.get()
|
iHavePreferredTradingPeers.get()
|
||||||
? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO"
|
? isMakerPreferredTradingPeer.test(offer) ? "YES" : "NO"
|
||||||
: "N/A");
|
: "N/A");
|
||||||
var marginPriceLabel = format("Is offer's price margin (%s%%) <= bot's max market price margin (%s%%)?",
|
|
||||||
offer.getMarketPriceMarginPct(),
|
if (offer.getUseMarketBasedPrice()) {
|
||||||
maxMarketPriceMargin);
|
var marginPriceLabel = format("Is offer's margin based price (%s) <= bot's target price (%s)?",
|
||||||
filterResultsByLabel.put(marginPriceLabel,
|
offer.getPrice() + " " + currencyCode,
|
||||||
offer.getUseMarketBasedPrice()
|
targetPrice + " " + currencyCode);
|
||||||
? isMarginLEMaxMarketPriceMargin.test(offer, maxMarketPriceMargin)
|
filterResultsByLabel.put(marginPriceLabel, isMarginLEMaxMarketPriceMargin.test(offer, maxMarketPriceMargin));
|
||||||
: "N/A");
|
} else {
|
||||||
var fixedPriceLabel = format("Is offer's fixed-price (%s) <= bot's target price (%s)?",
|
var fixedPriceLabel = format("Is offer's fixed-price (%s) <= bot's target price (%s)?",
|
||||||
offer.getUseMarketBasedPrice() ? "N/A" : offer.getPrice() + " " + currencyCode,
|
offer.getPrice() + " " + currencyCode,
|
||||||
offer.getUseMarketBasedPrice() ? "N/A" : targetPrice + " " + currencyCode);
|
targetPrice + " " + currencyCode);
|
||||||
filterResultsByLabel.put(fixedPriceLabel,
|
filterResultsByLabel.put(fixedPriceLabel, isFixedPriceLEMaxMarketPriceMargin.test(offer, currentMarketPrice));
|
||||||
offer.getUseMarketBasedPrice()
|
}
|
||||||
? "N/A"
|
|
||||||
: isFixedPriceLEMaxMarketPriceMargin.test(offer, currentMarketPrice));
|
|
||||||
String btcAmountBounds = format("%s BTC - %s BTC", minAmount, maxAmount);
|
String btcAmountBounds = format("%s BTC - %s BTC", minAmount, maxAmount);
|
||||||
filterResultsByLabel.put("Is offer's BTC amount within bot amount bounds (" + btcAmountBounds + ")?",
|
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:",
|
var title = format("%s offer %s filter results:",
|
||||||
offer.getUseMarketBasedPrice() ? "Margin based" : "Fixed price",
|
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).
|
# 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.
|
# 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.
|
# 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
|
minMarketPriceMargin=0.00
|
||||||
#
|
#
|
||||||
# Hard coded 30-day average BSQ trade price, used for development over regtest.
|
# 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.
|
# Taker bot's min BTC amount to sell. The candidate SELL BTC offer's amount must be >= minAmount BTC.
|
||||||
minAmount=0.01
|
minAmount=0.01
|
||||||
@ -19,8 +19,9 @@ maxAmount=0.90
|
|||||||
maxTxFeeRate=25
|
maxTxFeeRate=25
|
||||||
#
|
#
|
||||||
# Taker bot's list of preferred trading peers (their onion addresses).
|
# 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.
|
# If you do not want to constrict trading to preferred peers, comment this line out with a '#' character.
|
||||||
preferredTradingPeers=localhost:8888
|
preferredTradingPeers=localhost:8888
|
||||||
#
|
#
|
||||||
# Offer polling frequency must be >= 1s (1000ms) between each getoffers request.
|
# 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.
|
# 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.
|
# 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.
|
# Taker bot's min market price margin. A candidate BUY BTC offer's price margin must be >= minMarketPriceMargin.
|
||||||
#
|
minMarketPriceMargin=-5.00
|
||||||
minMarketPriceMargin=0
|
|
||||||
#
|
#
|
||||||
# Taker bot's min BTC amount to sell. The candidate BUY offer's amount must be >= minAmount BTC.
|
# Taker bot's min BTC amount to sell. The candidate BUY offer's amount must be >= minAmount BTC.
|
||||||
minAmount=0.01
|
minAmount=0.01
|
||||||
@ -23,6 +21,7 @@ maxTxFeeRate=25
|
|||||||
bisqTradeFeeCurrency=BSQ
|
bisqTradeFeeCurrency=BSQ
|
||||||
#
|
#
|
||||||
# Taker bot's list of preferred trading peers (their onion addresses).
|
# 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.
|
# If you do not want to constrict trading to preferred peers, comment this line out with a '#' character.
|
||||||
preferredTradingPeers=localhost:8888, \
|
preferredTradingPeers=localhost:8888, \
|
||||||
nysf2pknaaxfh26k42ego5mnfzpbozyi3nuoxdu745unvva4pvywffyd.onion:9999, \
|
nysf2pknaaxfh26k42ego5mnfzpbozyi3nuoxdu745unvva4pvywffyd.onion:9999, \
|
||||||
@ -35,4 +34,4 @@ preferredTradingPeers=localhost:8888, \
|
|||||||
x6x2o3m6rxhkfuf2v6lalbharf3whwvkts5rdn3jkhgieqvnq6mvdfyd.onion:9999
|
x6x2o3m6rxhkfuf2v6lalbharf3whwvkts5rdn3jkhgieqvnq6mvdfyd.onion:9999
|
||||||
#
|
#
|
||||||
# Offer polling frequency must be >= 1s (1000ms) between each getoffers request.
|
# 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).
|
# 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.
|
# 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.
|
# 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.
|
# 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.
|
# Taker bot's min BTC amount to buy. The candidate BUY BTC offer's amount must be >= minAmount BTC.
|
||||||
minAmount=0.01
|
minAmount=0.01
|
||||||
@ -19,8 +19,9 @@ maxAmount=0.90
|
|||||||
maxTxFeeRate=25
|
maxTxFeeRate=25
|
||||||
#
|
#
|
||||||
# Taker bot's list of preferred trading peers (their onion addresses).
|
# 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.
|
# If you do not want to constrict trading to preferred peers, comment this line out with a '#' character.
|
||||||
preferredTradingPeers=localhost:8888
|
preferredTradingPeers=localhost:8888
|
||||||
#
|
#
|
||||||
# Offer polling frequency must be >= 1s (1000ms) between each getoffers request.
|
# 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.
|
# 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.
|
# 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.
|
# 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.
|
# Taker bot's min BTC amount to buy. The candidate SELL offer's amount must be >= minAmount BTC.
|
||||||
minAmount=0.01
|
minAmount=0.01
|
||||||
@ -16,12 +16,13 @@ maxAmount=0.50
|
|||||||
#
|
#
|
||||||
# Taker bot's max acceptable transaction fee rate (sats / byte).
|
# Taker bot's max acceptable transaction fee rate (sats / byte).
|
||||||
# Regtest fee rates are from https://price.bisq.wiz.biz/getFees
|
# Regtest fee rates are from https://price.bisq.wiz.biz/getFees
|
||||||
maxTxFeeRate=100
|
maxTxFeeRate=20
|
||||||
#
|
#
|
||||||
# Bisq trade fee currency code (BSQ or BTC).
|
# Bisq trade fee currency code (BSQ or BTC).
|
||||||
bisqTradeFeeCurrency=BSQ
|
bisqTradeFeeCurrency=BSQ
|
||||||
#
|
#
|
||||||
# Taker bot's list of preferred trading peers (their onion addresses).
|
# 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.
|
# If you do not want to constrict trading to preferred peers, comment this line out with a '#' character.
|
||||||
preferredTradingPeers=localhost:8888, \
|
preferredTradingPeers=localhost:8888, \
|
||||||
nysf2pknaaxfh26k42ego5mnfzpbozyi3nuoxdu745unvva4pvywffyd.onion:9999, \
|
nysf2pknaaxfh26k42ego5mnfzpbozyi3nuoxdu745unvva4pvywffyd.onion:9999, \
|
||||||
@ -34,4 +35,4 @@ preferredTradingPeers=localhost:8888, \
|
|||||||
x6x2o3m6rxhkfuf2v6lalbharf3whwvkts5rdn3jkhgieqvnq6mvdfyd.onion:9999
|
x6x2o3m6rxhkfuf2v6lalbharf3whwvkts5rdn3jkhgieqvnq6mvdfyd.onion:9999
|
||||||
#
|
#
|
||||||
# Offer polling frequency must be >= 1s (1000ms) between each getoffers request.
|
# 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.
|
This subproject contains Python3 classes demonstrating API gRPC method calls, and some sample bots.
|
||||||
|
|
||||||
Each class in
|
Each class in
|
||||||
the [bisq.rpccalls](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples/bisq/rpccalls) package is
|
the [bisq.rpccalls](https://github.com/bisq-network/bisq-api-reference/tree/main/python-examples/bisq/rpccalls) package
|
||||||
named for the RPC method call being demonstrated.
|
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.
|
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