├── .gitattributes ├── .idea ├── vcs.xml ├── misc.xml ├── modules.xml ├── flow-trading-bot.iml └── workspace.xml ├── waveflow_handler.py ├── .gitignore ├── README.md └── bot.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/flow-trading-bot.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /waveflow_handler.py: -------------------------------------------------------------------------------- 1 | import pywaves as pw 2 | import requests 3 | from json import loads, dumps 4 | 5 | base_url = "https://nodes.wavesnodes.com" 6 | address = "3PNtfqFrJu6Svp7rKYtb3VmZu8hseiownoo" # WAVES/BTC exchange account 7 | # address = "3P6G2qeAsgcxFgSe4qUwN8zLhMCpxS5BUCR" # WAVES/ETH exchange account 8 | tokenA = "WAVES" 9 | tokenB = "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS" # wBTC ID 10 | # tokenB = "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu" # wETH ID 11 | 12 | 13 | def parse_value(text): 14 | return loads(text)["value"] 15 | 16 | def get_amounts(): 17 | amountA = parse_value(requests.get(base_url+"/addresses/data/"+address+"/amountTokenA").text) 18 | amountB = parse_value(requests.get(base_url + "/addresses/data/" + address + "/amountTokenB").text) 19 | return amountA/10**8, amountB/10**8 20 | 21 | 22 | def get_current_wf_price(): 23 | a,b = get_amounts() 24 | return round(b*10**8/a) / 10**8 25 | 26 | 27 | def get_current_dex_price(): 28 | asset1 = pw.Asset(tokenA) 29 | asset2 = pw.Asset(tokenB) 30 | return float(pw.AssetPair(asset1, asset2).last()) 31 | 32 | 33 | def get_optimum_amount(): 34 | def get_delta(a): 35 | A, B = current_amounts 36 | b = A * B / -(A + a) + B 37 | return b / current_dex_price - a 38 | 39 | current_amounts = get_amounts() # amounts of tokens locked in WaveFlow 40 | current_dex_price = get_current_dex_price() # last match price on DEX 41 | 42 | dest = 1 if get_current_wf_price()/get_current_dex_price() > 1 else -1 43 | 44 | a = dest 45 | max_value = False 46 | while True: 47 | step = (1 if dest > 0 else -1) * 10**-4 48 | value = get_delta(a) 49 | a += step 50 | if not max_value or value*10**8 > max_value*10**8: 51 | max_value = value 52 | else: 53 | return round((a-step)*10**8)/10**8 54 | 55 | 56 | if __name__ == "__main__": 57 | get_optimum_amount() 58 | 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crypto trading bot: WaveFlow + DEX 2 | 3 | Smart trading using crypto price difference on two markets: [DEX](https://dex.wavesplatform.com/) and [WaveFlow.xyz](https://waveflow.xyz/). 4 | 5 | ### WaveFlow provides arbitrage 6 | 7 | Oct, 24 a new application was launched on Waves: https://www.dappocean.io/dapps/waveflow. This project allows crypto holders to create and use exsiting crypto pairs for trading. The price is determined in an algorithmic way: the more popular the token is, the higher its price is set. 8 | 9 | 10 | 11 | So, WaveFlow pricing model has no relations to DEX which is the main Waves based exchange client. That means there could be an **arbitrage opportunity**: traders can buy crypto asset on WaveFlow for a price `P1` and sell it on DEX for a price `P2` making `P2 - P1` profit in a few seconds. Learn more about arbitrage at [Investopedia]( https://www.investopedia.com/ask/answers/what-is-arbitrage/) for a better understanding if needed. 12 | 13 | This bot is made to find the arbitrage opportunities and immediately use them doing the exchange operations on [DEX](https://dex.wavesplatform.com/) and [WaveFlow](https://waveflow.xyz/). Right now bot does trading operations for a pair WAVES/BTC only. Welcome any contributions =) 14 | 15 | ### Arbitrage step-by-step 16 | 17 | Bot algorithm for WAVES/BTC pair arbitrage trading is following: 18 | 19 | functionality of _waveflow_handler.get_optimum_amount()_: 20 | 21 | 1. Getting `current_dex_price` for a pair 22 | 2. Determining if BTC is under- or overrated on WaveFlow and storing this value to `dest` 23 | 3. Finding the most profitable `amount` of WAVES to trade using reversed WaveFlow algorithm 24 | 25 | functionality of _bot.trade()_ 26 | 27 | 4. If BTC underrated: buy BTC on WaveFlow spending `amount` WAVES -> sell BTC on DEX 28 | 5. If BTC overrated: buy BTC on DEX spending `amount` WAVES -> sell BTC on WaveFlow 29 | 30 | ### Arbitrage profit counting 31 | 32 | Assume, BTC price on WaveFlow is cheaper than the price on DEX. It will stay cheaper if we spend less than `amount` WAVES for buying BTC on WaveFlow. `amount` is determined following the reversed WaveFlow algorithm. 33 | 34 | We decide to sell `amount` WAVES on WaveFlow and get `amount/wf_price` BTC 35 | 36 | Now we can exchange these `amount/wf_price` BTC back to WAVES on DEX. The price for this will be `dex_price`. We will get `amount*dex_price/wf_price` WAVES from selling our BTC 37 | 38 | The final profit will be `amount*dex_price/wf_price - amount` = `amount*(dex_price/wf_price - 1)` 39 | 40 | ### Usage 41 | 42 | You will need Python 3.4+ to run the bot. 43 | 44 | Please install `requests` and `pywaves` libraries and run `trade()` function in `bot.py`. 45 | 46 | ### Conclusion 47 | 48 | The WAVES/BTC pair is ready for the arbitrage trading. Please feel free to use this bot for "printing" money and contribute for adding new pairs like WAVES/ETH. 49 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import pywaves as pw 2 | import waveflow_handler as wf 3 | from time import sleep 4 | 5 | account = pw.Address(privateKey="27uhZoT9ASj6XZAn7Fidjws5yijyxWsMRpb3hPMQhNDL") # address for a public use :) 6 | pw.setMatcher(pw.MATCHER) 7 | WVS = 10**8 8 | 9 | 10 | def wf_exchange(amount): 11 | """ changes WAVES or tokenB on WaveFlow.xyz """ 12 | amount = int(amount*WVS) 13 | 14 | tx = account.invokeScript("3PNtfqFrJu6Svp7rKYtb3VmZu8hseiownoo", "exchanger", 15 | [{"type": "integer", "value": 0}], 16 | [ 17 | {"assetId": None, "amount": amount} if amount > 0 else 18 | {"assetId": wf.tokenB, "amount": account.balance(wf.tokenB)} 19 | ]) 20 | 21 | for x in range(150): 22 | sleep(0.1) 23 | if "error" not in pw.tx(tx["id"]): 24 | print("WaveFlow exchange completed. Payment: {amount} {assetId}. Transaction ID: {txid}" 25 | .format(amount=int(tx["payment"][0]["amount"])/10**8, 26 | assetId=tx["payment"][0]["assetId"] if tx["payment"][0]["assetId"] else "WAVES", 27 | txid=tx["id"])) 28 | return tx 29 | 30 | 31 | def get_instant_price(book, amount): 32 | """ determines price for the instantly filled order on DEX """ 33 | li = book["asks"] if amount > 0 else book["bids"] 34 | amount = abs(amount) 35 | while amount > 0: 36 | amount -= li[0]["amount"] 37 | price = li[0]["price"] 38 | li.pop(0) 39 | return price/WVS 40 | 41 | 42 | def create_sell_order(amount, pair): 43 | """ creates order to sell WAVES on DEX """ 44 | price = get_instant_price(pair.orderbook(), amount) 45 | tokenA_to_sell = abs(amount) 46 | order = account.sell(assetPair=pair, amount=int(tokenA_to_sell*WVS), price=price) 47 | print("Will sell {} WAVES for a price {} BTC".format(tokenA_to_sell / WVS, price / WVS)) 48 | return order 49 | 50 | 51 | def complete_order(amount=0.001): 52 | """ completes buy/sell orders for the WAVES/BTC pair """ 53 | asset1 = pw.Asset("WAVES") 54 | asset2 = pw.Asset("8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS") 55 | pair = pw.AssetPair(asset1, asset2) 56 | 57 | if amount > 0: 58 | amount = int(amount*WVS) 59 | price = get_instant_price(pair.orderbook(), amount) 60 | tokenA_to_buy = int(account.balance(wf.tokenB)/price) 61 | order = account.buy(assetPair=pair, amount=tokenA_to_buy, price=price) 62 | print("Will buy {} WAVES for a price {} BTC".format(tokenA_to_buy/WVS, price/WVS)) 63 | else: 64 | order = create_sell_order(amount, pair) 65 | 66 | print("Order created. Here is the order ID: {}".format(order.orderId)) 67 | 68 | for x in range(150): 69 | sleep(0.1) 70 | if order.status() == "Filled": 71 | print("Order is filled!") 72 | return order 73 | print("Well, we need wait some more time till order is filled.") 74 | 75 | 76 | def check_arbitrage(): 77 | """ checks the arbitrage opportunity; just FYI """ 78 | amount = wf.get_optimum_amount() 79 | if amount < -1: 80 | return "{token} is overrated on WaveFlow. There is an arbitrage opportunity: " \ 81 | "spend {amount} WAVES for buying {token} on DEX, then resell {token} on WaveFlow".format(amount=-amount, token="BTC") 82 | elif amount > 1: 83 | return "{token} is underrated on WaveFlow. There is an arbitrage opportunity: " \ 84 | "buy {token} on WaveFlow by selling {amount} WAVES, then resell {token} on DEX".format(amount=amount, token="BTC") 85 | else: 86 | return "There is no arbitrage opportunity" 87 | 88 | 89 | def trade(): 90 | """ the main function that finds arbitrage opportunity and uses it to "print" money for you!""" 91 | print("Your account balance is: " + str(account.balance() / WVS)) 92 | print("Looking for a WAVES/BTC arbitrage opportunity") 93 | 94 | amount = wf.get_optimum_amount() 95 | if abs(amount) <= 1: 96 | return "There is no arbitrage opportunity" 97 | 98 | amount = min(amount, account.balance()/WVS) 99 | if abs(amount) <= 0.05: 100 | return "You don't have enough balance. Please fill it up to 0.05 WAVES" 101 | 102 | if amount > 1: 103 | trade_underrate(amount) 104 | else: 105 | trade_overrate(amount) 106 | 107 | 108 | def trade_underrate(amount): 109 | """ 110 | this func is called if tokenB is underrated on waveflow.xyz (i.e. BTC on waveflow is cheaper then on DEX) 111 | first step: exchange WAVES to BTC on WaveFlow 112 | second step: exchange BTC to WAVES on DEX 113 | arbitrage profit: from WAVES to WAVES 114 | profit = WAVES 115 | """ 116 | 117 | print("Arbitrage opportunity found! Going to buy Bitcoins by selling {} WAVES on waveflow.xyz".format(amount)) 118 | 119 | tx = wf_exchange(amount) 120 | 121 | print("Congratulations! {} WAVES were exchanged to BTC on waveflow.xyz".format(amount)) 122 | print("Now you have {} BTC".format(account.balance(wf.tokenB)/WVS)) 123 | print("It's time to use these BTC for buying WAVES back on DEX. Creating order.") 124 | 125 | order = complete_order(amount) 126 | print("Finishing the script...") 127 | print("Final account balance: " + str(account.balance()/WVS)) 128 | 129 | 130 | def trade_overrate(amount): 131 | """ 132 | this func is called if tokenB is overrated on waveflow.xyz (i.e. BTC on waveflow is more expensive then on DEX) 133 | first step: exchange WAVES to BTC on DEX 134 | second step: exchange BTC to WAVES on WaveFlow 135 | arbitrage profit: from WAVES to WAVES 136 | profit = WAVES 137 | """ 138 | 139 | print("Arbitrage opportunity found! Going to buy Bitcoins on DEX") 140 | 141 | order = complete_order(amount) # buying BTC on DEX 142 | 143 | print("Congratulations! {} WAVES were exchanged to BTC on DEX".format(amount)) 144 | print("Now you have {} BTC".format(account.balance(wf.tokenB)/WVS)) 145 | print("It's time to use these BTC for buying WAVES back on WaveFlow. Invoking script.") 146 | 147 | # TODO: wait till tokenB fall on the balance after order completing 148 | tx = wf_exchange(amount) 149 | 150 | print("Exchange completed!") 151 | 152 | 153 | if __name__ == "__main__": 154 | trade() 155 | 156 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 48 | 49 | 50 | 52 | 53 | 60 | 61 | 62 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |