-
Notifications
You must be signed in to change notification settings - Fork 81
WIP Produce the simplest-possible getting started guide for the Bisq HTTP API #54
Changes from all commits
95a2f88
e778141
37f2b19
70ce251
7ee80ee
ae5b54a
24ffd06
eea7498
911f6ca
17a48d1
d73a565
b73db83
10be120
7d8e78c
7eb1466
56d3185
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,256 @@ | ||
= Monitoring Bisq offers with Http API | ||
|
||
This guide walks you through the process of creating a simple bot that monitors available offers. | ||
|
||
|
||
== What you'll build | ||
|
||
You'll build a NodeJS-based script that connects to Bisq over an HTTP API to get offers and market prices and then displays "interesting" offers on the console if any are found. | ||
|
||
CAUTION: The Bisq HTTP API is currently incubating. Things may be rough around the edges and should not be considered production-ready. Your feedback is extremely valuable at this stage—see the link:#next-steps[next steps] section at bottom for details how to get in touch with us. Thanks! | ||
|
||
|
||
== What you’ll need | ||
|
||
* About 15 minutes | ||
* A favorite text editor or IDE | ||
* NodeJS 6+ (to execute the API client script that you will write) | ||
* Either: | ||
** Docker (to run from an image) or | ||
** Git, Maven and JDK8 (to build from source) | ||
|
||
NOTE: Using the Bisq HTTP API in no way requires the use of NodeJS on the client side. It is just used for example purposes in this guide. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest removing the parenthetical note above ("to execute the API client script...") and making this note more detailed. Specifically, explicitly state:
|
||
|
||
== Run the API | ||
|
||
There are two ways you can run an instance of the Bisq HTTP API: from a Docker image or from source. | ||
|
||
=== Run the API using Docker | ||
|
||
The easiest way to run the API in headless mode is by using a Docker image: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could 'headless' be clarified here? Knowing virtually nothing about how this API works, my initial impression was that the API would query my local Bisq client for data, and that 'headless' simply meant 'no GUI' but that's clearly not the case. Basically I wasn't sure if the API needed my Bisq client to be running in the background. A quick 1 or 2 lines about what these images actually do / how the API works might help? Also, how does one know when the Docker image is done setting up? Is it when the Bisq ASCII art shows? I watched the terminal for a while until I realized that what I was seeing was actually streaming data, and API calls were already working. |
||
|
||
docker run -it --rm -p 8080:8080 -e BISQ_API_HOST=0.0.0.0 bisq/api | ||
|
||
=== Run the API from source | ||
|
||
For more hard-core developers that want to run from source: | ||
|
||
git clone https://github.com/mrosseel/bisq-api | ||
cd bisq-api | ||
mvn compile exec:java \ | ||
-Dexec.mainClass="network.bisq.api.app.ApiMain" \ | ||
-Dexec.args="--appName=http-api-monitor-offers" | ||
|
||
[NOTE] | ||
Since the API is incubating we suggest to run it against a different database directory from the default one. | ||
To do that, use the `--appName` parameter. You can set it to whatever you like (just keep it different from `Bisq`). | ||
|
||
=== Verify the API is running | ||
|
||
The API should now be running on port 8080. You can verify that by executing: | ||
|
||
curl http://localhost:8080/api/v1/version | ||
|
||
You should receive a response like the following: | ||
|
||
[source,json] | ||
---- | ||
{ | ||
"application": "0.6.7", | ||
"network": 1, | ||
"p2PMessage": 10, | ||
"localDB": 1, | ||
"tradeProtocol": 1 | ||
} | ||
---- | ||
|
||
[NOTE] | ||
Complete interactive API documentation is available at http://localhost:8080/swagger. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd make this stand out a bit more. I don't know if it was just me, but I glazed over it the first few times I saw it. (I also wasn't aware of Swagger, so I just thought it was a cool pathname). Maybe take it out of a NOTE and make it something like " |
||
|
||
== API overview | ||
|
||
First we will look at the endpoints we need to fetch the data from and how their responses look. | ||
|
||
=== List available offers | ||
|
||
Open offers are available at http://localhost:8080/api/v1/offers. | ||
|
||
The response should look like this: | ||
|
||
[source,json] | ||
---- | ||
{ | ||
"offers": [OfferDetail], | ||
"total": integer | ||
} | ||
---- | ||
|
||
The `offers` property is an array with individual offers and `total` is the number of all offers. The model for each `OfferDetail` element is defined as follows: | ||
|
||
[source,json] | ||
---- | ||
{ | ||
"acceptedBankIds": [string] | ||
"acceptedCountryCodes": [string] | ||
"amount": integer | ||
"arbitratorNodeAddresses": [string] | ||
"bankId": string | ||
"baseCurrencyCode": string | ||
"blockHeightAtOfferCreation": integer | ||
"buyerSecurityDeposit": integer | ||
"counterCurrencyCode": string | ||
"countryCode": string | ||
"currencyCode": string | ||
"date": string | ||
"direction": string Enum: [ BUY, SELL ] | ||
"hashOfChallenge": string | ||
"id": string | ||
"isCurrencyForMakerFeeBtc": boolean | ||
"isPrivateOffer": boolean | ||
"lowerClosePrice": integer | ||
"makerFee": integer | ||
"makerPaymentAccountId": string | ||
"marketPriceMargin": number | ||
"maxTradeLimit": integer | ||
"maxTradePeriod": integer | ||
"minAmount": integer | ||
"offerFeePaymentTxId": string | ||
"ownerNodeAddress": string | ||
"paymentMethodId": string | ||
"price": integer | ||
"protocolVersion": integer | ||
"sellerSecurityDeposit": integer | ||
"state": string Enum: [ UNKNOWN, OFFER_FEE_PAID, AVAILABLE, NOT_AVAILABLE, REMOVED, MAKER_OFFLINE ] | ||
"txFee": integer | ||
"upperClosePrice": integer | ||
"useAutoClose": boolean | ||
"useMarketBasedPrice": boolean | ||
"useReOpenAfterAutoClose": boolean | ||
"versionNr": string | ||
} | ||
---- | ||
|
||
Now let's assume that we want to buy bitcoin, and let's define "interesting" offers as those that: | ||
|
||
. sell bitcoin at a 1% discount or more under the current market price, and | ||
. accept payment in Euros to a Polish SEPA account | ||
|
||
First we need to filter those offers using following static criteria: | ||
|
||
[source,json] | ||
---- | ||
{ | ||
"baseCurrencyCode": "BTC", | ||
"counterCurrencyCode": "EUR", | ||
"direction": "SELL", | ||
"paymentMethodId": "SEPA" | ||
} | ||
---- | ||
|
||
Next we need to filter those offers by price. There are two types of offers: _market-based price offers_ and _fixed price offers_. They are distinguished by the `useMarketBasedPrice` attribute. In the case of market-based price offers the filtering criteria is easy: the `marketPriceMargin` value must be above 0.1. In the case of fixed price offers we have to fetch the market price of BTC in EUR, calculate whether the price is 1% or less, and then filter offers which have a price _less_ than that calculated price. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "On Bisq, there are two types of offers..." [Suggestion] "In the case of fixed price offers we have to fetch the market price of BTC in EUR, calculate a threshold price that fits our needs (i.e., the price that's 1% lower than the market price), and then find all offers which have a price less than that threshold price." [Suggestion—I found this wording confusing] |
||
|
||
=== Get the market price | ||
|
||
In order to get the market price of BTC in EUR, execute the following query: | ||
|
||
curl http://localhost:8080/api/v1/currencies/prices?currencyCodes=EUR | ||
|
||
You should receive a response like the following: | ||
|
||
[source,json] | ||
---- | ||
{ | ||
"prices": { | ||
"EUR": 7035.62 | ||
} | ||
} | ||
---- | ||
|
||
|
||
== Write the monitoring bot code | ||
|
||
Let's install some dependencies: | ||
|
||
npm install lodash http-as-promised | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be helpful to mention adding the I built the script as I read along, and ended up with errors because I didn't |
||
|
||
In general our script will look like this: | ||
|
||
.http-api-monitor-offers.js | ||
[source,javascript] | ||
---- | ||
include::http-api-monitor-offers.js[tags=flow] | ||
---- | ||
|
||
So first 2 things to do (concurrently and asynchronously) is to fetch offers and market price of BTC from our API. Then we need to filter those offers and display the results. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure "(concurrently and asynchronously)" adds any value. |
||
|
||
Getting offers is a simple call that returns a promise with an array of offers: | ||
|
||
.http-api-monitor-offers.js | ||
[source,javascript] | ||
---- | ||
include::http-api-monitor-offers.js[tags=getOffers] | ||
---- | ||
|
||
Similarly with `getMarketPrice`: | ||
|
||
.http-api-monitor-offers.js | ||
[source,javascript] | ||
---- | ||
include::http-api-monitor-offers.js[tags=getMarketPrice] | ||
---- | ||
|
||
Now our `filterOffers` function is ready to receive an array of results from the two functions described above: | ||
|
||
.http-api-monitor-offers.js | ||
[source,javascript] | ||
---- | ||
include::http-api-monitor-offers.js[tags=filterOffers] | ||
---- | ||
|
||
This function filters offers to match our criteria. It returns matching offers and maps them to a simpler structure that contains as little data as needed for the `notify` function. We are using the `lodash` library to simplify filtering. | ||
|
||
The `getPriceFilter` function creates the actual filter function and looks like this: | ||
|
||
.http-api-monitor-offers.js | ||
[source,javascript] | ||
---- | ||
include::http-api-monitor-offers.js[tags=getPriceFilter] | ||
---- | ||
|
||
We are multiplying `marketPrice` by `10000` because that is the format in which the API returns the price. | ||
|
||
Here is a full script. | ||
|
||
.http-api-monitor-offers.js | ||
[source,javascript] | ||
---- | ||
include::http-api-monitor-offers.js[tags=**;*] | ||
---- | ||
|
||
Now run | ||
|
||
node http-api-monitor-offers.js | ||
|
||
If there are any matching offers you should see something like this: | ||
|
||
5 interesting BTC offers from Bisq | ||
0.0625 BTC (-2%) | ||
0.01 BTC (-2%) | ||
0.01 BTC (-5%) | ||
0.033 BTC (-3%) | ||
0.02 BTC (-1.5%) | ||
0.25 BTC (-6%) | ||
|
||
If there are no matching offers, try fiddling with the value of `threshold`. Try setting it to -20, 0.01, etc. | ||
|
||
|
||
== Summary | ||
|
||
Congratulations! You are now able to monitor Bisq offers via the Bisq HTTP API. From here, you can explore the complete API documentation and build trading bots of any complexity and in any language you like. | ||
|
||
|
||
== Next steps | ||
|
||
* Try adding a loop to keep the process repeating the search periodically (use _setInterval_) | ||
* Join our slack at https://bisq.slack.com and leave feedback on the API and this guide | ||
* If you find any issues please report them https://github.com/mrosseel/bisq-api/issues[here] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
const _ = require('lodash'); | ||
const $http = require('http-as-promised'); | ||
|
||
const threshold = 0.1; | ||
|
||
//tag::getPriceFilter[] | ||
function getPriceFilter(marketPrice) { | ||
const maxPrice = marketPrice * (1 - threshold) * 10000; | ||
return offer => { | ||
if (offer.useMarketBasedPrice) | ||
return offer.marketPriceMargin >= threshold; | ||
return offer.price < maxPrice; | ||
} | ||
} | ||
//end::getPriceFilter[] | ||
|
||
//tag::getMarketPrice[] | ||
function getMarketPrice() { | ||
return $http.get('http://localhost:8080/api/v1/currencies/prices?currencyCodes=EUR', {resolve: 'body', json: true}) | ||
.then(body => _.get(body, 'prices.EUR')) | ||
} | ||
//end::getMarketPrice[] | ||
|
||
//tag::getOffers[] | ||
function getOffers() { | ||
return $http.get('http://localhost:8080/api/v1/offers', {resolve: 'body', json: true}).then(body => body.offers); | ||
} | ||
//end::getOffers[] | ||
|
||
function notify(offers) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aside from the Just FYI. I guess it's because this is structured as a bottom-up walk-through. If it were reversed (i.e., top-down approach with full script first and then important parts clarified), it would be fine to pick and choose what to cover. |
||
if (!offers.length) { | ||
console.log('No interesting offers found'); | ||
return; | ||
} | ||
|
||
const text = _.map(offers, offer => `${offer.amount / 100000000} BTC (-${_.round(offer.margin * 100, 2)}%)`).join('\n'); | ||
console.info(text); | ||
} | ||
|
||
//tag::filterOffers[] | ||
function filterOffers([offers, marketPrice]) { | ||
return _(offers) | ||
.filter({ | ||
baseCurrencyCode: 'BTC', | ||
counterCurrencyCode: 'EUR', | ||
direction: 'SELL', | ||
paymentMethodId: 'SEPA' | ||
}) | ||
.filter(i => _.includes(i.acceptedCountryCodes, 'PL')) | ||
.filter(getPriceFilter(marketPrice)) | ||
.map(i => _.pick(i, 'baseCurrencyCode', 'counterCurrencyCode', 'direction', 'paymentMethodId', 'id', 'useMarketBasedPrice', 'price', 'marketPriceMargin', 'amount', 'minAmont')) | ||
.map(i => ({amount: i.amount, margin: i.useMarketBasedPrice ? i.marketPriceMargin : marketPrice / i.price})) | ||
.value(); | ||
} | ||
//end::filterOffers[] | ||
|
||
//tag::flow[] | ||
Promise.all([getOffers(), getMarketPrice()]) | ||
.then(filterOffers) | ||
.then(notify) | ||
.catch(e => console.error(e)); | ||
//end::flow[] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's officially 'Node.js'. Just concerned the JS hipster 'ninjas' will get us if we don't get this right...