Skip to content

Commit

Permalink
Merge pull request #17 from geniusyield/11-making-operation-robust
Browse files Browse the repository at this point in the history
Feat #11, #14, #15, #16, #18 & #23
  • Loading branch information
brunjlar authored Jan 5, 2024
2 parents 069c1e8 + 770de21 commit ac5d500
Show file tree
Hide file tree
Showing 11 changed files with 410 additions and 329 deletions.
46 changes: 36 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Market maker bot for [GeniusYield](https://www.geniusyield.co/) DEX which implem
> [!NOTE]
> **Order classification and price**
>
> We call non-ada tokens as _commodity_ and ada as _currency_. Order offering currency in exchange of commodity is called as _buy order_ whereas order offering commodity in exchange of currency is called as _sell order_.
> We call non-ADA tokens as _commodity_ and ADA as _currency_. Order offering currency in exchange of commodity is called as _buy order_ whereas order offering commodity in exchange of currency is called as _sell order_.
>
> _Price_ is described in display unit[^1] of currency token per display unit of commodity token.
Expand Down Expand Up @@ -89,7 +89,8 @@ See [`atlas-config-maestro.json`](./atlas-config-maestro.json) & [`atlas-config-
{
"mbc_user": {
"ur_s_key_path": "path-to-skey",
"ur_coll": "tx-id#tx-ix"
"ur_coll": "tx-id#tx-ix",
"ur_stake_address": "bech32-encoded-stake-address"
},
"mbc_fp_nft_policy": "compiled-scripts/minting-policy",
"mbc_fp_order_validator": "compiled-scripts/partial-order",
Expand All @@ -102,10 +103,10 @@ See [`atlas-config-maestro.json`](./atlas-config-maestro.json) & [`atlas-config-
"mbc_delay": 120000000,
"mbc_price_config": {
"pc_api_key": "<<MAESTRO_TOKEN>>",
"pc_resolution": "15m",
"pc_network_id": "mainnet",
"pc_dex": "genius-yield",
"pc_override": {
"mpo_commodity_token": "c6e65ba7878b2f8ea0ad39287d3e2fd256dc5c4160fc19bdf4c4d87e.7447454e53",
"mpo_pair": "ADA-GENS",
"mpo_commodity_is_first": false
}
Expand All @@ -127,19 +128,23 @@ See [`atlas-config-maestro.json`](./atlas-config-maestro.json) & [`atlas-config-
"sc_cancel_threshold_product": 4
},
"mbc_token": {
"stAc": "c6e65ba7878b2f8ea0ad39287d3e2fd256dc5c4160fc19bdf4c4d87e.7447454e53",
"stPrecision": 6
"ac": "c6e65ba7878b2f8ea0ad39287d3e2fd256dc5c4160fc19bdf4c4d87e.7447454e53",
"precision": 6
}
}
```
* `mbc_user` describes individual bot, it specifies `ur_s_key_path` which is the path to signing key file and `ur_coll` which is the UTxO reserved as collateral. Specifying `ur_coll` is optional but it is advised to set it as then this UTxO would be reserved (i.e., would not be spent) and thus be always available to serve as collateral. It is preferred for `ur_coll` to be pure 5 ada only UTxO (i.e., no other tokens besides ada).
* Fields `mbc_fp_nft_policy`, `mbc_fp_order_validator`, `mbc_po_config_addr` and `mbc_po_refs` relate to DEX smart contracts and can be left as it is.
* `mbc_user` describes bot's wallet.
* `ur_s_key_path` is the path to signing key file.
* `ur_coll` (optional) is the UTxO to be reserved as collateral. Though specifying `ur_coll` is optional but it is advised to set it as then this UTxO would be reserved (i.e., would not be spent) and thus be always available to serve as collateral. It is preferred for `ur_coll` to be pure 5 ADA only UTxO (i.e., no other tokens besides ADA).
* `ur_stake_address` (optional) is the bech32 stake address (`stake_test1...` for testnet and `stake1...` for mainnet). If specified, bot would place orders at the mangled address so that ADA in those orders (both as an offer or as received payment) would be staked. Note that if an order undergoes partial fill, received payment is in the generated order UTxO and is received by the author of order only when order is completely filled or is cancelled.
* Fields `mbc_fp_nft_policy`, `mbc_fp_order_validator`, `mbc_po_config_addr` and `mbc_po_refs` relate to DEX smart contracts and can be left as it is. See sample files corresponding to the network to know for these values.
* `mbc_delay` - Bot in single iteration tries to determine which orders need to be placed and which are needed to be cancelled. Once determined, it tries building the transactions and proceeds with submitting them, completing this single iteration. `mbc_delay` determines time in microseconds that bot must wait before proceeding with next iteration.
* `mbc_price_config` gives the configuration on how to get market price using https://docs.gomaestro.org/DefiMarketAPI/mkt-dex-ohlc Maestro endpoint, for a token.
* `pc_api_key` is the Maestro API key.
* `pc_resolution` is the resolution for the mentioned Maestro endpoint. Please see documentation [here](https://docs.gomaestro.org/DefiMarketAPI/Introduction#prices) on how resolution helps determine price. Possible values of resolution can be seen [here](https://docs.gomaestro.org/DefiMarketAPI/mkt-dex-ohlc).
* `pc_network_id` determines Cardano network which is mentioned for in API calls. It should always be kept `mainnet` as of now.
* `pc_dex` determines DEX from which market price is queried for. Currently `minswap` & `genius-yield` are supported.
* `pc_override` is optional and is needed in case one is not running bot on Mainnet. Since tokens on test network aren't actively traded, their price is not returned for by Maestro endpoint. To still get mainnet price for them, one can override the token given by `mpo_commodity_token` to pair with commodity token as described by `mpo_pair` & `mpo_commodity_is_first` respectively. In the above configuration, we are overriding the testnet GENS asset class `c6e65ba7878b2f8ea0ad39287d3e2fd256dc5c4160fc19bdf4c4d87e.7447454e53`, for the mainnet token pair `ADA-GENS`, and GENS is the second token in the pair so `mpo_commodity_is_first` is set to **false**. If the pair instead was `GENS-ADA` then `mpo_commodity_is_first` should be set to **true**.
* `pc_override` is optional and is needed in case one is not running bot on Mainnet. Since tokens on test network aren't actively traded, their price is not returned for by Maestro endpoint. To still get mainnet price for a corresponding mainnet token, one can specify desired (overriding) pair in `mpo_pair` & mention whether commodity is first token of the given pair or not in `mpo_commodity_is_first` field. In the above configuration, we are overriding the testnet GENS asset class `c6e65ba7878b2f8ea0ad39287d3e2fd256dc5c4160fc19bdf4c4d87e.7447454e53`, for the mainnet token pair `ADA-GENS`, and GENS is the second token in the pair so `mpo_commodity_is_first` is set to **false**. If the pair instead was `GENS-ADA` then `mpo_commodity_is_first` should be set to **true**.
* `mbc_strategy_config` determines parameters for strategy:

* `sc_spread` - Ratio representing `δ` as described before.
Expand All @@ -150,6 +155,7 @@ See [`atlas-config-maestro.json`](./atlas-config-maestro.json) & [`atlas-config-
* `tv_sell_budget` - Total amount of commodity tokens that bot can cumulatively offer in the orders. In every iteration, bot determines the number of commodity tokens locked in the orders and subtracts it from `tv_sell_budget` field, let's call the obtained number `asb` (short for _available sell budget_) then it determines number of sell orders placed to be `⌊asb / tv_sell_min_vol⌋ = ns` where `ns` is short of number of sell orders. Now bot would place `ns` sell orders, each having offer amount as `⌊asb / ns⌋`.
* `tv_buy_budget` - Total amount of currency tokens that bot can cumulatively offer in the orders. It governs bot symmetric to `tv_sell_budget`.
* `tv_sell_vol_threshold` - this is related to `sc_price_check_product`. Bot would build an order book from all the orders for the given pair in GeniusYield DEX. It will sum the offered commodity tokens for sell orders which have price less than `M * (1 + sc_price_check_product * δ)` to get `SV` (short for sell volume) and sum the asked commodity tokens for buy orders which have price greater than `M * (1 + sc_price_check_product * δ)` to get `BV'` (short for buy volume in commodity token). We'll multiply `BV'` with `M` to get `BV` to represent buy volume in currency token. Now, bot would not place a new sell order, if `tv_sell_vol_threshold` is less than or equal to `SV`. Idea is that if there is enough liquidity than bot need not place orders. Symmetrically, bot would not place new buy orders only if `tv_buy_vol_threshold` is less than or equal to `BV`.
* `mbc_token` specifies the commodity token with it's precision. Note that this must not be ADA!

## Canceling all the orders using docker (simple)

Expand Down Expand Up @@ -186,8 +192,28 @@ cabal run geniusyield-market-maker-exe -- Cancel my-atlas-config.json my-maker-b

The output should be similar like in the previous chapter.

## Known Issues
## Operational Costs

* When bot tries to place multiple orders in a single iteration, it might happen that we pick same UTxO against different transaction skeletons (due to a [quirk](https://github.com/geniusyield/dex-contracts-api/blob/cf360d6c1db8185b646a34ed8f6bb330c23774bb/src/GeniusYield/Api/Dex/PartialOrder.hs#L489-L498) where place order operation specifies UTxO to be spent in skeleton itself), leading to successful building of only some of the transaction skeletons and thus only few of the orders might be successfully placed even though bot might very well have the required funds to place all. Now the bot can place the remaining ones in next the iteration but as of now, these next orders are placed starting with initial spread difference from market price leading to a situation where the bot might have multiple orders at the same price.
Here we try to list costs which market maker incurs when interacting with our DEX which would help in better decision for configuration values such as _spread_.

### Order placement

Order placement incurs following fees besides usual transaction fees.

* Flat fees: Every order is charged 1 ADA flat maker fee on creation but order author will get this back only if order underwent no partial filling.
* Percent fees: Every order is charged 0.3% of offered tokens on creation. If an order is cancelled afterwards, 0.3% percent would be charged only on the amount which actually got filled and remaining is refunded. As an example, suppose an order is created - offering 100 GENS. 0.3% of it is 0.3 GENS, which is initially charged. Now if this order is cancelled after only 60 GENS from it was consumed, then order author would get back 0.3% of 40 GENS namely, 0.12 GENS.

### Order cancellation

_tl;dr_ We group up to 6 order cancellations in a single transaction, fees incurred is usual transaction fee plus additional ADA up to 0.5, in worst case.

Order cancellation is slightly complex.

* Order underwent no fills: Only the usual network transaction fee is charged.
* Order underwent some filling: In this case, ADA taker fee might be added to this order or not. If it is added, only the usual network transaction fee is charged. However, if it is not added then as cancelling this order would require a fee output to GeniusYield address be generated, minimum ADA requirement of this fee output must be satisfied which currently stands in worst case at slightly less than 1.5 ADA. Now since maker certainly added 1 ADA due to flat ADA maker fee, it in worst case, would need to put additional 0.5 ADA. Note that we split orders to be cancelled in set of size 6 and then submit cancellation transaction for each of these sets. Thus if there are 6 orders to be cancelled for in a single set, then this additional 0.5 ADA, if needed, is shared across these 6 orders as fee output is to be generated once per transaction and not once per order. As a further illustration, if the bot had 13 orders to cancel, we will generate 3 sets of sizes 6, 6 & 1 and thus submit 3 cancellation transactions.

### Equity monitoring

Bot repeatedly logs for "equity" in terms of ADA where ADA equivalent of commodity token is obtained by using price provider. As an example, if wallet has 500 ADA and 500 GENS and if price of 1 GENS is 2 ADA, then equity of wallet would be 1500 ADA.

[^1]: _Display unit_ is one to which decimals are added as directed under [`cardano-token-registry`](https://github.com/cardano-foundation/cardano-token-registry).
5 changes: 3 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ services:
"mbc_delay": 120000000,
"mbc_price_config": {
"pc_api_key": "${MAESTRO_API_KEY}",
"pc_resolution": "15m",
"pc_network_id": "${NETWORK:-mainnet}",
"pc_dex": "genius-yield"
},
Expand All @@ -60,8 +61,8 @@ services:
"sc_cancel_threshold_product": 4
},
"mbc_token": {
"stAc": "dda5fdb1002f7389b33e036b6afee82a8189becb6cba852e8b79b4fb.0014df1047454e53",
"stPrecision": 6
"ac": "dda5fdb1002f7389b33e036b6afee82a8189becb6cba852e8b79b4fb.0014df1047454e53",
"precision": 6
}
}
restart: always
4 changes: 3 additions & 1 deletion geniusyield-market-maker/geniusyield-market-maker.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,13 @@ executable geniusyield-market-maker-exe
import: common-deps
import: common-ghc-opts
main-is: Main.hs
other-modules: GeniusYield.MarketMaker.MakerBotConfig
other-modules: GeniusYield.MarketMaker.Constants
, GeniusYield.MarketMaker.MakerBotConfig
, GeniusYield.MarketMaker.MakerBot
, GeniusYield.MarketMaker.Orphans
, GeniusYield.MarketMaker.Prices
, GeniusYield.MarketMaker.Strategies
, GeniusYield.MarketMaker.User
, GeniusYield.MarketMaker.Utils
build-depends:
, geniusyield-orderbot-framework:common
Expand Down
18 changes: 18 additions & 0 deletions geniusyield-market-maker/src/GeniusYield/MarketMaker/Constants.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module GeniusYield.MarketMaker.Constants (
awaitTxParams,
logNS,
makerFeeRatio,
) where

import Data.Ratio ((%))
import GeniusYield.Types

awaitTxParams GYAwaitTxParameters
awaitTxParams = GYAwaitTxParameters {maxAttempts = 20, confirmations = 1, checkInterval = 10_000_000}

logNS GYLogNamespace
logNS = "MM"

-- TODO: Get it from blockchain instead. Note that this is only used to determine funds needed by wallet and is not forwarded to dex-contracts-api library.
makerFeeRatio Rational
makerFeeRatio = 3 % 1000 -- Is 0.3%, so ratio should be 0.003 == 3 / 1000.
Loading

0 comments on commit ac5d500

Please sign in to comment.