├── .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 |
12 |
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 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 | 1572274414431
177 |
178 |
179 | 1572274414431
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
--------------------------------------------------------------------------------