diff --git a/.env.local.template b/.env.local.template new file mode 100644 index 0000000..fcacf92 --- /dev/null +++ b/.env.local.template @@ -0,0 +1,26 @@ +# works with local chain from `make build-neutron ...` +CHAIN_ID=test-1 +RPC_ADDRESS=http://0.0.0.0:26657 +API_ADDRESS=http://0.0.0.0:1317 +GAS_PRICES=0.0025untrn +GAS_ADJUSTMENT=2 +TRADE_FREQUENCY_SECONDS=60 +# this token config demonstrates several ways to define price and other vars +# use range of token amounts to visualize multiple orders of magnitude in dev UI +TOKEN_CONFIG=' + { + "10000000000000uibcusdc<>10000000000000uibcatom": 10, + "100000000000uibcusdc<>100000000000untrn": { + "price": 2, + "ticks": 50 + }, + "defaults": { + "fees": [0,1,2,3,4,5,10,20,50,100,150,200], + "gas": "1000000000untrn" + } + } +' +# this mnemonic is defined in the default local setup: https://github.com/neutron-org/neutron/blob/v3.0.0/network/init.sh#L19-L21 +FAUCET_MNEMONIC=veteran try aware erosion drink dance decade comic dawn museum release episode original list ability owner size tuition surface ceiling depth seminar capable only +# test the pool withdrawal mechanism to return tokens to the faucet with: +ON_EXIT_WITHDRAW_POOLS=1 diff --git a/.env.testnet.template b/.env.testnet.template new file mode 100644 index 0000000..06c37e8 --- /dev/null +++ b/.env.testnet.template @@ -0,0 +1,18 @@ +# works with testnet, for current information check https://github.com/cosmos/chain-registry/blob/master/neutron/chain.json +CHAIN_ID=pion-1 +RPC_ADDRESS=https://rpc-lb-pion.ntrn.tech:443 +API_ADDRESS=https://rest-lb-pion.ntrn.tech:443 +GAS_PRICES=0.025untrn +GAS_ADJUSTMENT=2 +# it is helpful to see multiple swaps per minute for more realistic minute candles of a price timeseries chart +TRADE_FREQUENCY_SECONDS=20 +# use demo pair amount from demo faucet and untrn amount from pion-1 faucet +TOKEN_CONFIG={"10000000factory/neutron19glux3jzdfyyz6ylmuksgxfj5phdaxfr2uhy86/factoryATOM<>10000000factory/neutron19glux3jzdfyyz6ylmuksgxfj5phdaxfr2uhy86/factoryNTRN":{"price":"coingecko:usd-coin<>neutron-3","ticks":80,"deposit_accuracy":100,"swap_accuracy":5,"gas":"2000000untrn"}} +# add credentials of accounts +COINGECKO_API_TOKEN= +MNEMONIC= + +# note: consider using SKIP_INITIAL_DEPOSIT=1 when starting / restarting a testnet bot +# a testnet bot may need to be restarted often and we don't want to repeat +# the initial deposit. the initial deposit can be made and adjusted at +# any time through the UI at https://app.testnet.duality.xyz/ diff --git a/.gitignore b/.gitignore index 9665ba9..deef8ee 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ keystore.db contracts/artifacts/ contracts/*.wasm +# env +.env + # IDEs *.iml .idea diff --git a/Dockerfile b/Dockerfile index b62339c..a7a8099 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,18 @@ -# Use neutron binary version given through version number or heighliner image -# eg. passing a locally made heighliner image as NEUTRON_IMAGE -ARG NEUTRON_VERSION -# Use Heighliner build by default to get around building for correct platform issue -# as Heighliner build support multiple platforms. More details in commit message -ARG NEUTRON_IMAGE=ghcr.io/strangelove-ventures/heighliner/neutron:${NEUTRON_VERSION} +ARG NEUTRON_VERSION=latest -FROM "$NEUTRON_IMAGE" as neutrond-binary +# the neutron binary image is typically built on amd64 (but may be changed) +# see: https://github.com/neutron-org/neutron/blob/v5.0.0-rc0/Makefile#L107-L122 +ARG BUILDPLATFORM=amd64 + +# optionally specify a difference image to get the binary from +ARG NEUTRON_IMAGE=neutron-${BUILDPLATFORM}:${NEUTRON_VERSION} + +# make build platform consistent across images to avoid docker.io lookup error +# see: https://stackoverflow.com/questions/20481225/how-can-i-use-a-local-image-as-the-base-image-with-a-dockerfile#69798220 +FROM --platform=${BUILDPLATFORM} ${NEUTRON_IMAGE} AS neutrond-binary # allow this container to contact other Docker containers through the docker CLI -FROM docker:24.0.5-cli +FROM --platform=${BUILDPLATFORM} docker:27.3.1-cli # add additional dependencies for the testnet scripts RUN apk add bash curl grep jq; @@ -18,4 +22,4 @@ COPY --from=neutrond-binary /bin/neutrond /usr/bin WORKDIR /workspace/neutron COPY scripts /workspace/neutron/scripts -CMD bash ./scripts/run_trade_bot.sh +CMD ["bash", "./scripts/run_trade_bot.sh"] diff --git a/Makefile b/Makefile index af437da..fcfbafb 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,9 @@ REPOS_DIR ?= ./repos SETUP_DIR ?= $(REPOS_DIR)/neutron-integration-tests/setup +DOCKER ?= docker COMPOSE ?= docker-compose -NEUTRON_VERSION ?= v2.0.2 +NEUTRON_VERSION ?= v5.0.0-rc0 GAIA_VERSION ?= v14.1.0 @@ -18,7 +19,7 @@ init-neutron: ifeq (,$(wildcard $(REPOS_DIR)/neutron)) cd $(REPOS_DIR) && git clone -b $(NEUTRON_VERSION) https://github.com/neutron-org/neutron.git else - cd $(REPOS_DIR)/neutron && git fetch origin $(NEUTRON_VERSION) && git checkout $(NEUTRON_VERSION) + cd $(REPOS_DIR)/neutron && git fetch origin $(NEUTRON_VERSION) && git fetch origin $(NEUTRON_VERSION) --tags && git checkout $(NEUTRON_VERSION) endif init-hermes: @@ -52,6 +53,11 @@ build-gaia: init-dir init-gaia build-neutron: init-dir init-neutron cd $(REPOS_DIR)/neutron && $(MAKE) build-docker-image + docker tag neutron-node:latest neutron-node:$(NEUTRON_VERSION) + +build-neutron-static-linux-amd64: init-dir init-neutron + cd $(REPOS_DIR)/neutron && $(MAKE) build-static-linux-amd64 + docker tag neutron-amd64:latest neutron-amd64:$(NEUTRON_VERSION) build-hermes: init-dir init-hermes cd $(SETUP_DIR) && $(MAKE) build-hermes @@ -83,8 +89,8 @@ clean: # --- new docker compose network commands --- -build-trade-bot: - @$(COMPOSE) build --build-arg NEUTRON_VERSION=$(NEUTRON_VERSION) +build-trade-bot: build-neutron-static-linux-amd64 + @$(DOCKER) build . -t dex-trading-bot:$(NEUTRON_VERSION) --build-arg NEUTRON_VERSION=$(NEUTRON_VERSION) start-trade-bot: build-trade-bot @$(COMPOSE) up diff --git a/README.md b/README.md index ab18c7b..195778f 100644 --- a/README.md +++ b/README.md @@ -11,19 +11,20 @@ To run the bot, you will first need a chain to run the bot against, locally this should be a single dockerized neutron node - `make build-neutron` -To run the default setup of a single neutron-node chain and a single trading bot: -- `make start-trade-bot` +To run the default setup of a single neutron-node chain and a single trading bot with a single wallet: +- `make start-trade-bot MNEMONIC=...` +- the available `MNENOMIC` options are explained in the [Options section](#mnemonic-options) This composed neutron chain and trading bot network will persist until you call: - `make stop-trade-bot` ### Test runs (start+stop) You can test a chain and bot(s) configuration and exit with cleanup in one step using: -- `make test-trade-bot` +- `make test-trade-bot MNEMONIC=...` However the default settings are quite conservative, and won't product many txs. A larger test which should generate approximately ~1000-2000 txs in ~6 minutes with 30 bots could be done with: -- `make test-trade-bot BOTS=30 BOT_RAMPING_DELAY=5 TRADE_FREQUENCY_SECONDS=0 TRADE_DURATION_SECONDS=180` +- `make test-trade-bot BOTS=30 BOT_RAMPING_DELAY=5 TRADE_FREQUENCY_SECONDS=0 TRADE_DURATION_SECONDS=180 MNEMONIC=...` This can be ideal for CI type testing of a service that depends on Dex transactions on a Neutron chain. But if you want the chain to persist after the trades are completed (with a finite `TRADE_DURATION_SECONDS`), @@ -31,6 +32,26 @@ then `make start-trade-bot` should be used instead. ## Available options +### Mnemonic options +A single mnenomic option must be used to run `make start-trade-bot` or `make test-trade-bot` this may be in the form of: +- a list of mnemonics to be used (delimited with any `\r\n,;` characters) where one mnemonic is given to each bot. +An error will be thrown if not enough mnemonics are provided for the number of `BOTS` requested. + - `MNEMONIC` + - `MNEMONICS` + - `BOT_MNEMONIC` + - `BOT_MNEMONICS` +- a single mnenomic which will be used to like a faucet to fund a separate randomly generated wallet for each bot +(so you may easily run more than one bot with one wallet). + - `FAUCET_MNEMONIC` + - on simulation end: the remaining token balance will be refunded to the faucet wallet. + - you should strongly consider using `ON_EXIT_WITHDRAW_POOLS=1` when using this option on a testnet. + If the pools are not withdrawn and you have not saved the randomly generated mnenomics for each bot + then ***you will lose access to these deposited tokens***. +- if you are running a local chain you can use the DEMO_MNEMONICs from the neutron repo +[networks/init.sh](https://github.com/neutron-org/neutron/blob/v3.0.0/network/init.sh#L19-L21) file for these settings. + +### Optional options + All docker-compose env vars are able to be set in both `make start-trade-bot` and `make test-trade-bot` - Chain variables - `CHAIN_ID`: the chain ID @@ -47,18 +68,68 @@ All docker-compose env vars are able to be set in both `make start-trade-bot` an - `ON_EXIT_WITHDRAW_POOLS`: if set, withdraw all user's Dex pools after TRADE_DURATION_SECONDS - `GAS_ADJUSTMENT`: how much more than the base estimated gas price to pay for each tx - `GAS_PRICES`: calculate how many fees to pay from this fraction of gas - - `TOKEN_CONFIG`: a token pairs configuration (JSON) object for eg. token amounts to trade - - see [helpers.sh](https://github.com/neutron-org/dex-trading-bot/blob/131a5f1590483840305cb475f8a867996509333e/scripts/helpers.sh#L41-L63) for more setting details - - mnemonics: - - `FAUCET_MNEMONIC` (optional): the mnemonic of the account that will fund generated bots - - `BOT_MNEMONIC/S` or `MNEMONIC/S` (optional): the mnemonics for self-funded bot account(s) - - at least one of `FAUCET_MNEMONIC` or `BOT_/MNEMONIC/S` should be provided - - with a local chain you can use `DEMO_MNEMONIC`s from the neutron networks/init.sh file + - `TOKEN_CONFIG`: a token pairs configuration (JSON) object to specify trading behavior to use for each token pair + - see the [TOKEN_CONFIG option section](#token_config-option) for more details - `COINGECKO_API_TOKEN`: a Coingecko API token used for live prices fetching. Only used with respective token pair price setting. The token should be a [demo API token](https://www.coingecko.com/en/api/pricing). Pro tokens aren't supported because they use different endpoints. Keep in mind the very limited request rate the demo tokens provide when configuring the bots number and trading intensity. eg. `make start-trade-bot BOTS=30 BOT_RAMPING_DELAY=5 TRADE_FREQUENCY_SECONDS=0 TRADE_DURATION_SECONDS=450 MNEMONIC=...` will start a persistent chain that for the first ~10min (7min+ramping) will generate ~5000txs using 30 bots. +### TOKEN_CONFIG option + +format for TOKEN_CONFIG is: +``` +TOKEN_CONFIG = { + "amountAtokenA<>amountBtokenB": numeric_price_or_PAIR_CONFIG_object, + "defaults": PAIR_CONFIG +} +``` +the object keys are the usable tokens for each pair (to be shared across all bots), +the object values are the price ratio of tokenB/tokenA, a coingecko pair or a config object: (default values are listed) +``` +PAIR_CONFIG = { + "price": 1, # price ratio is of tokenB/tokenA (how many tokenA is required to buy 1 tokenB?), OR + "price": "coingecko:api_idA<>api_idB", # for live price retrieval, use the coingecko API IDs of the tokens (e.g. "coingecko:cosmos<>neutron-3" for atom<>ntrn pair) + "price_decimals": [0, 0] # the display token decimals (exponents) over base token amounts (eg. for ETH=10^18wei, USDC=10^6uUSDC use [18, 6]) + "ticks": 100, # number of ticks for each bot to deposit + "fees": [1, 5, 20, 100] # each LP deposit fee may be (randomly) one of the whitelisted fees here + "gas": "0untrn" # additional gas tokens that bots can use to cover gas fees + "rebalance_factor": 0.5, # fraction of excessive deposits on either pair side to rebalance on each trade + "deposit_factor": 0.5, # fraction of the recommended maximum reserves to use on a single tick deposit + "swap_factor": 0.5, # max fraction of a bot's token reserves to use on a single swap trade (max: 1) + "swap_accuracy": 100, # ~1% of price: swaps will target within ~1% of current price + "deposit_accuracy": 1000, # ~10% of price: deposits will target within ~10% of current price + "amplitude1": 5000, # ~50% of price: current price will vary by ~50% of set price ratio + "period1": 36000, # ten hours: current price will cycle min->max->min every ten hours + "amplitude2": 1000, # ~10% of price: current price will vary by an additional ~10% of price ratio + "period2": 600, # ten minutes: current price will cycle amplitude2 offset every ten minutes +} +``` + +For example the following `TOKEN_CONFIG` option sets each bot to trade on 3 pools +- the first `uibcusdc<>uibcatom` with all default options except price of `1uibcatom = 10uibcusdc` +- the second `untrn<>uibcusdc` with custom GoinGecko pricing and specified 50 deposited ticks +- the third `untrn<>uibcatom` with custom sinusoidal pricing around a price of `1uibcatom = 5untrn` +- all pools will operate with the specified `defaults` values of `fees` and `gas` +```js +TOKEN_CONFIG = { + "10000000000000uibcusdc<>10000000000000uibcatom": 10, + "100000000000untrn<>100000000000uibcusdc":{ + "price": "coingecko:neutron-3<>usd-coin", + "price_decimals": [6, 6], # not required here (no decimal difference) + "ticks": 50 + }, + "1000000000uibcatom<>1000000000untrn":{ + "price": 5, + "amplitude2": 100 + }, + "defaults":{ + "fees": [0,1,2,3,4,5,10,20,50,100,150,200], + "gas": "1000000000untrn" + } +} +``` + # Troubleshooting The chain should be visible at http://localhost:26657 and REST at http://localhost:1317. diff --git a/docker-compose.yml b/docker-compose.yml index 7843cec..d7c7b38 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.8' services: neutron-node: - image: neutron-node + image: neutron-node:${NEUTRON_VERSION:-latest} command: > /bin/bash -c ' #!/bin/bash @@ -22,8 +22,7 @@ services: --log_level "$$LOG_LEVEL" \ --home "$$CHAIN_DIR" \ --pruning=nothing \ - --grpc.address="0.0.0.0:$$GRPCPORT" \ - --grpc-web.address="0.0.0.0:$$GRPCWEB" + --grpc.address="0.0.0.0:$$GRPCPORT" ' container_name: neutron-node volumes: @@ -45,49 +44,26 @@ services: dex-trading-bot: build: context: . + args: + BUILDPLATFORM: ${BUILDPLATFORM:-amd64} + NEUTRON_VERSION: ${NEUTRON_VERSION:-latest} + image: dex-trading-bot:${NEUTRON_VERSION:-latest} + platform: linux/amd64 depends_on: - "neutron-node" entrypoint: ["/bin/bash", "./scripts/setup_entrypoint.sh"] command: ["/bin/bash", "./scripts/run_trade_bot.sh"] deploy: replicas: ${BOTS:-1} + env_file: + - ${ENV_FILE:-.env} environment: - - BOT_RAMPING_DELAY=${BOT_RAMPING_DELAY:-3} # seconds between starting each bot + # override env file with variables that connect to the neutron-node service here - CHAIN_ID=${CHAIN_ID:-test-1} - RPC_ADDRESS=${RPC_ADDRESS:-http://neutron-node:26657} - API_ADDRESS=${API_ADDRESS:-http://neutron-node:1317} + # this var is set when using `make test-trade-bot` - TRADE_DURATION_SECONDS=${TRADE_DURATION_SECONDS:-} - - TRADE_FREQUENCY_SECONDS=${TRADE_FREQUENCY_SECONDS:-60} - - ON_EXIT_WITHDRAW_POOLS=${ON_EXIT_WITHDRAW_POOLS:-} - - GAS_ADJUSTMENT=${GAS_ADJUSTMENT:-2} - - GAS_PRICES=${GAS_PRICES:-0.0025untrn} - # optional faucet mnemonic should be set with specific FAUCET_MNEMONIC: - - FAUCET_MNEMONIC=${FAUCET_MNEMONIC} - # optional bot mnemonics can be set using BOT_MNEMONIC/S or MNEMONIC/S: - # mnemonics may be delimited with: line breaks, tabs, semicolons, commas, and multiple spaces - - MNEMONICS=${MNEMONICS:-$MNEMONIC} - - BOT_MNEMONICS=${BOT_MNEMONICS:-$BOT_MNEMONIC} - # allow passing through a TOKEN_CONFIG object, defaulting to TOKEN_CONFIG_DEFAULT if not found - - TOKEN_CONFIG=${TOKEN_CONFIG} - # use (from default local node test wallets) up to about 10,000,000 (display) tokens in each pool - # use range of token amounts to visualize multiple orders of magnitude in dev UI - - TOKEN_CONFIG_DEFAULT= - { - "10000000000000uibcusdc<>10000000000000uibcatom":10, - "100000000000uibcusdc<>100000000000untrn":{ - "price":2, - "ticks":50 - }, - "1000000000uibcatom<>1000000000untrn":{ - "price":0.2, - "ticks":30 - }, - "defaults":{ - "fees":[0,1,2,3,4,5,10,20,50,100,150,200], - "gas":"1000000000untrn" - } - } - - COINGECKO_API_TOKEN=${COINGECKO_API_TOKEN} volumes: - /var/run/docker.sock:/var/run/docker.sock networks: diff --git a/scripts/check_chain_status.sh b/scripts/check_chain_status.sh index ae519b1..66a5587 100644 --- a/scripts/check_chain_status.sh +++ b/scripts/check_chain_status.sh @@ -28,7 +28,7 @@ abci_info=$( --max-time 3 \ --retry 30 \ --retry-connrefused \ - --retry-delay 1 \ + --retry-delay 2 \ --silent \ $RPC_ADDRESS/abci_info ) diff --git a/scripts/helpers.sh b/scripts/helpers.sh index 1e8cc1d..2f8ef8b 100644 --- a/scripts/helpers.sh +++ b/scripts/helpers.sh @@ -5,11 +5,13 @@ set -e # so you can optionally add a Docker "env" JSON string to run a single bot without Docker getDockerEnv() { # get this Docker container env info - if [ ! -z "$DOCKER_ENV" ] + if [ ! -z "$HOSTNAME" ] && [ -z "$DOCKER_ENV" ] then - echo "$DOCKER_ENV" - else curl -s --unix-socket /run/docker.sock http://docker/containers/$HOSTNAME/json + else + # default will return a setup that works when running image as a single Docker container + # eg. `docker run -it --rm --env-file .env dex-trading-bot:latest` + echo "${DOCKER_ENV:-'{"Config":{"Hostname":"trading-bot","Labels":{"com.docker.compose.container-number":1}}}}'}" fi } getDockerEnvs() { @@ -38,30 +40,11 @@ getBotNumber() { fi } -# format for TOKEN_CONFIG is: -# TOKEN_CONFIG = { -# "amountAtokenA<>amountBtokenB": numeric_price_or_PAIR_CONFIG_object, -# "defaults": PAIR_CONFIG -# } -# the object keys are the usable tokens for each pair (to be shared across all bots), -# the object values are the price ratio of tokenB/tokenA, a coingecko pair or a config object: (default values are listed) -# PAIR_CONFIG = { -# "price": 1, # price ratio is of tokenB/tokenA (how many tokenA is required to buy 1 tokenB?), OR -# "price": "coingecko:api_idA<>api_idB", # for live price retrieval, use the coingecko API IDs of the tokens (e.g. "coingecko:cosmos<>neutron-3" for atom<>ntrn pair) -# "ticks": 100, # number of ticks for each bot to deposit -# "fees": [1, 5, 20, 100] # each LP deposit fee may be (randomly) one of the whitelisted fees here -# "gas": "0untrn" # additional gas tokens that bots can use to cover gas fees -# "rebalance_factor": 0.5, # fraction of excessive deposits on either pair side to rebalance on each trade -# "deposit_factor": 0.5, # fraction of the recommended maximum reserves to use on a single tick deposit -# "swap_factor": 0.5, # max fraction of a bot's token reserves to use on a single swap trade (max: 1) -# "swap_accuracy": 100, # ~1% of price: swaps will target within ~1% of current price -# "deposit_accuracy": 1000, # ~10% of price: deposits will target within ~10% of current price -# "amplitude1": 5000, # ~50% of price: current price will vary by ~50% of set price ratio -# "period1": 36000, # ten hours: current price will cycle min->max->min every ten hours -# "amplitude2": 1000, # ~10% of price: current price will vary by an additional ~10% of price ratio -# "period2": 600, # ten minutes: current price will cycle amplitude2 offset every ten minutes -# } -# which is transformed to format for token_config_array = [ +# getTokenConfigArray transforms a given TOKEN_CONFIG (or TOKEN_CONFIG_DEFAULT) object +# into a new format that is easier to query through `jq`. +# the TOKEN_CONFIG and PAIR_CONFIG object format is listed in the README file +# +# token_config_array = [ # { # "pair": [ # { @@ -88,7 +71,7 @@ getTokenConfigArray() { # this includes extra gas passed in the config or default config object bot_count=$( getBotCount ) # by default shift the period of each token pair slightly so they are not exactly in sync - echo "${TOKEN_CONFIG:-"$TOKEN_CONFIG_DEFAULT"}" | jq -r ' + echo "${TOKEN_CONFIG:-"{}"}" | jq -r ' .defaults as $defaults | del(.defaults) | to_entries @@ -109,6 +92,7 @@ getTokenConfigArray() { pair: .value.pair, config: { price: (.value.price // $defaults.price // 1), + price_decimals: (.value.price_decimals // $defaults.price_decimals // [0, 0]), ticks: (.value.ticks // $defaults.ticks // 100), fees: (.value.fees // $defaults.fees // [1, 5, 20, 100]), rebalance_factor: (.value.rebalance_factor // $defaults.rebalance_factor // 0.5), @@ -211,7 +195,7 @@ getBotStartTime() { fi done echo "waited. found first start time: $first_bot_start_time" > /dev/stderr - echo "$(( ($bot_number - 1) * $BOT_RAMPING_DELAY + $first_bot_start_time ))" + echo "$(( ($bot_number - 1) * "${BOT_RAMPING_DELAY:-0}" + $first_bot_start_time ))" fi } getBotEndTime() { diff --git a/scripts/run_trade_bot.sh b/scripts/run_trade_bot.sh index 2e85efb..6466c69 100644 --- a/scripts/run_trade_bot.sh +++ b/scripts/run_trade_bot.sh @@ -98,6 +98,7 @@ two_pi=$( echo "scale=8; 8*a(1)" | bc -l ) start_epoch=$( bash $SCRIPTPATH/helpers.sh getBotStartTime ) sleep $(( $start_epoch - $EPOCHSECONDS > 0 ? $start_epoch - $EPOCHSECONDS : 0 )) +# enforce a default TRADE_FREQUENCY_SECONDS or the script will fail TRADE_FREQUENCY_SECONDS="${TRADE_FREQUENCY_SECONDS:-60}" # add function to check when the script should finish @@ -115,6 +116,7 @@ function check_duration { } # respond to price changes forever +loop_index="0" while true do # wait a bit, maybe less than a block or enough that we don't touch a block or two @@ -126,8 +128,12 @@ do break fi - echo "... loop will delay for: $delay seconds" - sleep $delay + # delay loops after the first loop + if [ "$loop_index" -gt "0" ] + then + echo "... loop will delay for: $delay seconds" + sleep $delay + fi echo "loop: starting at $EPOCHSECONDS" for (( pair_index=0; pair_index<$token_pair_config_array_length; pair_index++ )) @@ -149,6 +155,7 @@ do deposit_index_accuracy=$( echo "$token_pair_config" | jq -r '.deposit_accuracy' ) swap_index_accuracy=$( echo "$token_pair_config" | jq -r '.swap_accuracy' ) price_config=$( echo "$token_pair_config" | jq -r '.price' ) + price_decimals_diff=$( echo "$token_pair_config" | jq -r '.price_decimals[1] - .price_decimals[0]' ) # if price is a number, i.e. if price is set manually if (( $(echo "$price_config" | grep -c '^[0-9]\+\(\.[0-9]\+\)\?$') == 1 )) @@ -162,15 +169,15 @@ do period2=$( echo "$token_pair_config" | jq -r '.period2' ) # convert price to price index here + pair_display_price=$( echo "$token_pair_config" | jq -r '.price' ) price_index=$( echo "$token_pair_config" | jq -r '((.price | log)/(1.0001 | log) | round)' ) echo "calculated price index before approximation $price_index" # determine the new current price goal # approximate price with sine curves of given amplitude and period # by default: macro curve (1) oscillates over hours / micro curve (2) oscillates over minutes - current_price=$( - rounded_calculation \ - "$price_index + $amplitude1*s($EPOCHSECONDS / $period1 * $two_pi) + $amplitude2*s($EPOCHSECONDS / $period2 * $two_pi)" + pair_price_index_adjustment=$( + bc -l <<< " $amplitude1*s($EPOCHSECONDS / $period1 * $two_pi) + $amplitude2*s($EPOCHSECONDS / $period2 * $two_pi) " ) # if price is configured to be fetched from coingecko @@ -211,18 +218,19 @@ do echo "got prices: $tokenA = $priceA, $tokenB = $priceB" - # convert assets price ratio to price index here - current_price=$( - rounded_calculation \ - "l($priceB/$priceA) / l(1.0001)" - ) - echo "calculated price index $current_price" + pair_display_price=$( bc -l <<< " $priceB/$priceA " ) else echo "error: unexpected $tokenA<>$tokenB price format $price_config: expected a number or a coingecko pair" exit 1 fi + # calculate current price index from display price + display price exponent adjustment + oscillation adjustment: + current_price=$( + rounded_calculation \ + "l($pair_display_price) / l(1.0001) + $price_decimals_diff * l(10) / l(1.0001) + ${pair_price_index_adjustment:-"0"}" + ) + # calculate token amounts we will use in the initial deposit # the amount deposited by all bots should not be more than can be swapped by any one bot # eg. config 300A<>300B with 2 bots: @@ -245,7 +253,7 @@ do echo "pair: $tokenA<>$tokenB current price index is $current_price ($( echo "1.0001^$current_price" | bc -l ) $tokenA per $tokenB)" # if initial ticks do not yet exist, add them so we have some liquidity to swap with - if [ -z "${tokens_available["$pair_index-$tokenA"]}" ] + if [ -z "$SKIP_INITIAL_DEPOSIT" ] && [ -z "${tokens_available["$pair_index-$tokenA"]}" ] then echo "making deposit: initial ticks for $tokenA and $tokenB" # apply half of the available tokens to all tick indexes specified @@ -279,6 +287,8 @@ do "$( get_joined_array $tick_count get_fee "$fees" )" \ `# disable_autoswap` \ "$( repeat_with_comma "true" "$tick_count" )" \ + `# fail_tx_on_BEL` \ + "$( repeat_with_comma "true" "$tick_count" )" \ `# options` \ --from $person --yes --output json --broadcast-mode sync --gas auto --gas-adjustment $GAS_ADJUSTMENT --gas-prices $GAS_PRICES )" @@ -291,7 +301,7 @@ do # add some randomness into price goal (within swap_index_accuracy) deviation=$(( $RANDOM % ( $swap_index_accuracy * 2 ) - $swap_index_accuracy )) - # compute goal price (and inverse gola price for inverted token pair order: tokenB<>tokenA) + # compute goal price (and inverse goal price for inverted token pair order: tokenB<>tokenA) goal_price=$(( $current_price + $deviation )) goal_price_ratio=$( echo "1.0001^$goal_price" | bc -l ) @@ -303,16 +313,24 @@ do echo "making query: of current '$tokenA' ticks" first_tickA_price_ratio=$( neutrond query dex list-tick-liquidity "$tokenA<>$tokenB" "$tokenA" --output json --limit 1 \ - | jq -r ".tick_liquidity[0].pool_reserves.price_taker_to_maker" + | jq -r ".tick_liquidity[0].pool_reserves.price_taker_to_maker // .tick_liquidity[0].limit_order_tranche.price_taker_to_maker" ) # use bc for aribtrary precision math comparison (check for null because non-zero result evals true) echo "check: place-limit-order: tokenA side: is $first_tickA_price_ratio > $goal_price_ratio ?" if [ "$first_tickA_price_ratio" != "null" ] && (( $( bc <<< "$first_tickA_price_ratio > $goal_price_ratio" ) )) then - echo "making place-limit-order: '$tokenB' -> '$tokenA'" - trade_amount="$( neutrond query bank balances $address --denom $tokenB --output json | jq -r "(.amount | tonumber) * $swap_factor | floor" )" - if [ "$trade_amount" -gt "0" ] + balance_amount="$( neutrond query bank balance $address "$tokenB" --output json | jq -r ".balance.amount // 0" )" + trade_amount="$( echo "$balance_amount" | jq -r "(. | tonumber) * $swap_factor | floor" )" + echo "making place-limit-order: '$tokenB' -> '$tokenA' to goal price $goal_price with $trade_amount tokens" + directional_goal_price="$(( $goal_price * -1 ))" + minimum_trade_amount="$( rounded_calculation "1.0001^$directional_goal_price + 1" )" + if [ "$balance_amount" -gt "$minimum_trade_amount" ] then + if [ "$minimum_trade_amount" -gt "$trade_amount" ] + then + trade_amount="$minimum_trade_amount" + echo "changing place-limit-order: increase amount to mininum: $minimum_trade_amount" + fi tx_response="$( neutrond tx dex place-limit-order \ `# receiver` \ @@ -322,7 +340,7 @@ do `# token out` \ $tokenA \ `# tickIndexInToOut (note: this is the limit that we will swap up to, the goal)` \ - "[$(( $goal_price * -1 ))]" \ + "[$directional_goal_price]" \ `# amount in: allow up to a good fraction of the denom balance to be traded, to try to reach the tick limit` \ "$trade_amount" \ `# order type enum see: https://github.com/duality-labs/duality/blob/v0.2.1/proto/duality/dex/tx.proto#L81-L87` \ @@ -333,7 +351,7 @@ do )" tx_result="$( bash $SCRIPTPATH/helpers.sh waitForTxResult "$tx_response" "swapped: ticks toward target tick index of $goal_price" )" else - echo "skipping place-limit-order: '$tokenB' -> '$tokenA': not enough funds" + echo "skipping place-limit-order: '$tokenB' -> '$tokenA': not enough funds for trade (balance: $balance_amount, required: $minimum_trade_amount)" fi else echo "ignore place-limit-order: '$tokenB' -> '$tokenA': no liquidity to arbitrage" @@ -342,15 +360,23 @@ do echo "making query: of current '$tokenB' ticks" first_tickB_price_ratio=$( neutrond query dex list-tick-liquidity "$tokenA<>$tokenB" "$tokenB" --output json --limit 1 \ - | jq -r ".tick_liquidity[0].pool_reserves.price_opposite_taker_to_maker" + | jq -r ".tick_liquidity[0].pool_reserves.price_opposite_taker_to_maker // .tick_liquidity[0].limit_order_tranche.maker_price" ) echo "check: place-limit-order: tokenB side: is $first_tickB_price_ratio < $goal_price_ratio ?" - if [ "$first_tickB_price_ratio" != "null" ] && (( $(bc <<< "$first_tickB_price_ratio < $goal_price_ratio") )) + if [ "$first_tickB_price_ratio" != "null" ] && (( $( bc <<< "$first_tickB_price_ratio < $goal_price_ratio" ) )) then - echo "making place-limit-order: '$tokenA' -> '$tokenB'" - trade_amount="$( neutrond query bank balances $address --denom $tokenA --output json | jq -r "(.amount | tonumber) * $swap_factor | floor" )" - if [ "$trade_amount" -gt "0" ] + balance_amount="$( neutrond query bank balance $address "$tokenA" --output json | jq -r ".balance.amount // 0" )" + trade_amount="$( echo "$balance_amount" | jq -r "(. | tonumber) * $swap_factor | floor" )" + echo "making place-limit-order: '$tokenA' -> '$tokenB' to goal price $goal_price with $trade_amount tokens" + directional_goal_price="$goal_price" + minimum_trade_amount="$( rounded_calculation "1.0001^$directional_goal_price + 1" )" + if [ "$balance_amount" -gt "$minimum_trade_amount" ] then + if [ "$minimum_trade_amount" -gt "$trade_amount" ] + then + trade_amount="$minimum_trade_amount" + echo "changing place-limit-order: increase amount to mininum: $minimum_trade_amount" + fi tx_response="$( neutrond tx dex place-limit-order \ `# receiver` \ @@ -360,7 +386,7 @@ do `# token out` \ $tokenB \ `# tickIndexInToOut (note: this is the limit that we will swap up to, the goal)` \ - "[$goal_price]" \ + "[$directional_goal_price]" \ `# amount in: allow up to a good fraction of the denom balance to be traded, to try to reach the tick limit` \ "$trade_amount" \ `# order type enum see: https://github.com/duality-labs/duality/blob/v0.2.1/proto/duality/dex/tx.proto#L81-L87` \ @@ -371,7 +397,7 @@ do )" tx_result="$( bash $SCRIPTPATH/helpers.sh waitForTxResult "$tx_response" "swapped: ticks toward target tick index of $goal_price" )" else - echo "skipping place-limit-order: '$tokenA' -> '$tokenB': not enough funds" + echo "skipping place-limit-order: '$tokenA' -> '$tokenB': not enough funds for trade (balance: $balance_amount, required: $minimum_trade_amount)" fi else echo "ignore place-limit-order: '$tokenA' -> '$tokenB': no liquidity to arbitrage" @@ -464,6 +490,8 @@ do "$( get_joined_array $excess_user_deposits_count get_fee "$fees" )" \ `# disable_autoswap` \ "$( repeat_with_comma "true" "$excess_user_deposits_count" )" \ + `# fail_tx_on_BEL` \ + "$( repeat_with_comma "false" "$excess_user_deposits_count" )" \ `# options` \ --from $person --yes --output json --broadcast-mode sync --gas auto --gas-adjustment $GAS_ADJUSTMENT --gas-prices $GAS_PRICES )" @@ -523,6 +551,7 @@ do done + loop_index="$(( $loop_index + 1 ))" done echo "TRADE_DURATION_SECONDS has been reached";