.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LibArbitrage
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | _An extensible library allows to analyze DEX-to-DEX arbitrage oportunities autonomously, besides advanced decentralized exchange operations_
19 |
20 | ## License
21 | This project is licensed under the _GNU General Public License Version 3.0_
22 | See [LICENSE](LICENSE) for more information.
23 |
24 | ## Legal Disclaimer
25 | It is the end user's responsibility to obey all applicable local, state and federal laws. I assume no liability and am not responsible for any misuse or damage caused by this software, documentation and anything in this repository and the packages on the python package index.
26 |
27 | ## This is not an investment advice
28 | Neither the outputs of LibArbitrage nor any information in this repository constitutes professional and/or financial advice.
29 |
30 | ## Table of Content
31 | - [About the Project](#about-the-project)
32 | - [Installation](#installation)
33 | - [Documentation](#documentation)
34 | - [Technical Details](#technical-details)
35 | - [Flowchart Algorithm](#algorithm)
36 |
37 | ## About the Project
38 | This is a Python library developed to calculate and analyze arbitrage opportunities between decentralized exchanges of Ethereum blockchain network. Meanwhile, the library makes it easy to process the data of arbitrage opportunities that belong to different blocks in blockchain as long as the blocks are analyzed.
39 |
40 | ## Installation
41 | Library is available at [PYthon Package Index](https://pypi.org/project/libarbitrage/).
42 | ```console
43 | pip install libarbitrage
44 | ```
45 |
46 | ## Documentation
47 | __Attention:__ You must have an [Infura](https://www.infura.io/) APIKEY that works on Ethereum network in order for program to fetch latest blocks' data while analyzing.
48 |
49 | For now, the number of available exchanges and tokens to use in analyzes are limited. They may be expanded in the upcoming releases.
50 |
51 | #### Available exchanges (DEX):
52 | - `uniswap`: Uniswap Version 3
53 | - `sushiswap`: Sushiswap
54 |
55 | #### Available tokens (ERC-20):
56 | - `ftt`: FTX
57 | - `eth`: WETH
58 | - `btc`: WBTC
59 | - `aave`: AAVE
60 | - `usdt`: Tether
61 | - `cels`: Celsius
62 | - `usdc`: USD Coin
63 | - `link`: Chainlink
64 |
65 | ### Functions
66 | #### arbitrage()
67 | Initializes an arbitrage analyze loop between specified `tokens` on specified `exchanges`.
68 |
69 | ##### Arguments:
70 | - `APIKEY`: Required. A string of valid Infura APIKEY which should be working on Ethereum main network.
71 | - `interactive`: Required. The program outputs data if it's set to `True`; if `False`, it doesn't. In both cases all the arbitrage data will be stored on an internal variable, which can be retrieved through the function [`getArbitrageData()`](#getarbitragedata).
72 | - `exchanges`: Optional. (default: all of the [availables](#available-exchanges-dex)) A list of DEXes that program uses to get price datas of tokens.
73 | - `tokens`: Optional. (default:all of the [availables](#available-tokens-erc-20)) A list of tokens whose price will be analyzed between exchanges during the operation.
74 | - `operation`: Optional. (default: `sell`) A string of trading operation being done while getting price data. (Sell price/Buy price)
75 | - `min_revenue`: Optional. (default: `0`) Minimum profit rate of a unit transaction in percentage.
76 |
77 | #### getArbitrageData()
78 | Returns the results of arbitrage analyze as a dictionary having block numbers as keys and that block's arbitrage data (list) as value. See [this](#reading-the-data) for a real case. Here's the format:
79 | ```
80 | {
81 | block_number(int): [
82 | {'blocknum': block_number(int), 'timestamp': block's_timestamp(int), 'dex1': dex_name(str), 'dex2': dex_name(str), 'sell_token': token_name(str), 'buy_token': token_name(str), 'revenue_per_1usd': (float), 'minimum_amount': min_amount_to_pay_txfee(float)},
83 | {'blocknum': block_number(int), 'timestamp': block's_timestamp(int), 'dex1': dex_name(str), 'dex2': dex_name(str), 'sell_token': token_name(str), 'buy_token': token_name(str), 'revenue_per_1usd': (float), 'minimum_amount': min_amount_to_pay_txfee(float)}
84 | ]
85 | .
86 | .
87 | .
88 | }
89 | ```
90 |
91 | #### printFormattedData()
92 | Prints a whole block of arbitrage data in the form of a table.
93 |
94 | ##### Arguments:
95 | - `liste`: Required. A list of arbitrage datas (dict) which belong to the same block.
96 |
97 | ### Initializing an arbitrage analyze
98 |
99 | #### Interactive (monitor) mode
100 | Because the program acts like a monitor, this mode is named "monitor" mode. It takes the `min_revenue` argument of interactively. Even if you specify a `min_revenue` value to the function, it will be overwritten by the interactively taken input.
101 | An example initialization on interactive mode can be watched below.
102 |
103 | https://user-images.githubusercontent.com/83399767/205143445-285838d4-0177-47c2-a586-cc0a843ab56d.mp4
104 |
105 | Click [this YouTube link](https://youtu.be/AXpGRdGLOpY) if the video doesn't play properly.
106 |
107 | #### Non-interactive (developer) mode
108 | Because all the program works on a seperated thread -hence we can work on the data further-, this mode is named "developer mode".
109 |
110 | Begin by importing the LibArbitrage module:
111 | ```python
112 | >>> import libarbitrage
113 | ```
114 |
115 | Set variables to pass as arguments:
116 | ```python
117 | >>> APIKEY = 'YOUR_INFURA_APIKEY'
118 | >>> tokens = ['eth', 'btc', 'ftt', 'aave', 'usdc', 'usdt'] # Tokens to analyze between
119 | >>> revenue= 1.5 # Minimum amount of revenue in percentage per one USDT
120 | ```
121 |
122 | A single function startes and manages all the process of arbitrage analyze.
123 | ```python
124 | >>> libarbitrage.arbitrage(APIKEY=APIKEY, tokens=tokens, min_revenue=revenue)
125 | ```
126 |
127 | After a while (depending on the number of tokens specified), we are able to retrieve the result of arbitrage analyze respectively:
128 |
129 | ```python
130 | >>> arbitrage_data = libarbitrage.getArbitrageData()
131 | >>> type(arbitrage_data)
132 |
133 | ```
134 |
135 | The data dictionary is updated whenever the arbitrage analyze of a new block in blockchain is done.
136 | ```python
137 | >>> from time import sleep
138 | >>> len(arbitrage_data)
139 | 2
140 | >>> sleep(20)
141 | >>> len(arbitrage_data)
142 | 3
143 | ```
144 |
145 | ##### Reading the data
146 | And now we can process or do whatever we want with it. Let's see which blocks are analyzed so far.
147 | ```python
148 | >>> print(arbitrage_data.keys())
149 | dict_keys([15981551, 15981554, 15981558])
150 | >>> arbitrage_data.get(15981558)
151 | [{'blocknum': 15981558, 'timestamp': 1668589283, 'dex1': 'uniswap', 'dex2': 'sushiswap', 'sell_token': 'eth', 'buy_token': 'ftt', 'revenue_per_1usd': 0.022585200456361365, 'minimum_amount': 190.20624952552157}, {'blocknum': 15981558, 'timestamp': 1668589283, 'dex1': 'sushiswap', 'dex2': 'uniswap', 'sell_token': 'btc', 'buy_token': 'aave', 'revenue_per_1usd': 0.07117425741710715, 'minimum_amount': 60.356741741769994}, {'blocknum': 15981558, 'timestamp': 1668589283, 'dex1': 'uniswap', 'dex2': 'sushiswap', 'sell_token': 'usdc', 'buy_token': 'ftt', 'revenue_per_1usd': 0.44948651720318966, 'minimum_amount': 9.557230549019256}]
152 | ```
153 |
154 | Every single item in above list, is an arbitrage opportunity for that particular block. These opportunities are calculated and filtered by taking the parameters specified in the `arbitrage()` function into account.
155 |
156 | We can also pretty print these data. Let's use the latest analyzed block's data for this. To do that we need to get the last key's value from the `arbitrage_data` dictionary:
157 |
158 | ```python
159 | >>> last_key = list(arbitrage_data)[-1]
160 | >>> print(last_key)
161 | 15981558
162 | >>> data = arbitrage_data.get(last_key)
163 | >>> libarbitrage.printFormattedData(data)
164 |
165 | ┌─────────┬────────────────────┬──────────┬──────────┬─────┬─────┬──────────────────────┬───────────────────┐
166 | │BLOCK NUM│LOCAL TIMESTAMP │DEX1 │DEX2 │SELL │BUY │UNIT REVENUE │THRESHOLD AMOUNT │
167 | ├─────────┼────────────────────┼──────────┼──────────┼─────┼─────┼──────────────────────┼───────────────────┤
168 | │15981558 │2022-11-16 12:01:23 │uniswap │sushiswap │eth │ftt │0.0225852004563613650 │190.20624952552157 │
169 | │15981558 │2022-11-16 12:01:23 │sushiswap │uniswap │btc │aave │0.0711742574171071500 │60.356741741769994 │
170 | │15981558 │2022-11-16 12:01:23 │uniswap │sushiswap │usdc │ftt │0.4494865172031896600 │9.5572305490192560 │
171 | ├─────────┼────────────────────┼──────────┼──────────┼─────┼─────┼──────────────────────┼───────────────────┤
172 | ```
173 |
174 | #### Understanding the table
175 | Here in the table above, there're eight different types of data listed for one arbitrage opportunity in a particular block.
176 |
177 | "BLOCK NUM": Is the number of block that has the arbitrage opportunity
178 | "LOCAL TIMESTAMP": Is the locally formatted time corresponding to block timestamp
179 | "DEX1": Is the first DEX used in arbitrage
180 | "DEX2": Is the second DEX used in arbitrage
181 | "SELL": First token used in arbitrage
182 | "BUY": Second token used in arbitrage
183 | "UNIT REVENUE": The revenue of arbitrage done with 1 USDT
184 | "THRESOLD AMOUNT": The minimum amount of USDT to afford the transaction fees using the revenue comes from arbitrage
185 |
186 | See pink colored analyzer threads on [flowchart algorithm](#algorithm) for more information.
187 |
188 |
189 | ## Technical Details
190 | LibArbitrage adopts an efficient multithread approach for calculating arbitrage opportunities. There're three main __types__ of threads running simultaneously: "_handler_", "_price_fetcher_" and "_analyzer_".
191 |
192 | All these three types of threads are generated at a same number, which is calculated depending on the number of specified tokens. Below formula indicates the total number of threads initialized at the beginning of the program. Some of them may terminate further, in case of any errors occured by decentralized exchange; for example liquidity errors.
193 |
194 | ```
195 | P: Permutation
196 | t: number of specified tokens
197 | n: number of initialized threads
198 |
199 | n = 2 * P(t, 2) * 3
200 | | └→ Number of different thread types (excluding tx_fee_fetcher thread)
201 | └→ Because threads are generated for both DEXes, multiply the number with two.
202 | ```
203 |
204 | Below flowchart shows the working algorithm of LibArbitrage shallowly.
205 | ### Algorithm
206 | 
207 |
208 |
209 |
210 | ___─ Written by f4T1H21 ─___
211 |
--------------------------------------------------------------------------------
/libarbitrage/__init__.py:
--------------------------------------------------------------------------------
1 | from libarbitrage.lib import arbitrage, getArbitrageData, printFormattedData
--------------------------------------------------------------------------------
/libarbitrage/lib.py:
--------------------------------------------------------------------------------
1 | # Copyright [2022-2027] Şefik Efe Altınoluk
2 | #
3 | # This file is a part of project libarbitrage©
4 | # For more details, see https://github.com/f4T1H21/Arbitrage-Bot-Code-Library
5 | #
6 | # Licensed under the GNU GENERAL PUBLIC LICENSE Version 3.0 (the "License")
7 | # You may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.gnu.org/licenses/gpl-3.0.html
11 |
12 |
13 | """An extensible library allows to analyze DEX-to-DEX arbitrage oportunities autonomously, besides advanced decentralized exchange operations"""
14 |
15 | from sys import exit as sysexit
16 | from json import loads as jsonloads, dumps as jsondumps
17 | from math import perm
18 | from threading import Thread
19 |
20 | from urllib.request import urlopen
21 | from urllib.parse import urlencode
22 | from urllib.error import HTTPError, URLError
23 |
24 | from time import sleep, strftime, localtime
25 | from web3 import Web3, HTTPProvider as web3_HTTPProvider, exceptions as web3_exceptions
26 |
27 |
28 | class c:
29 | """
30 | Color class
31 | """
32 | show = '\033[?25h' # Show cursor
33 | hide = '\033[?25l' # Hide cursor
34 | default = '\033[0m'# Default color
35 |
36 | underline = '\033[4;4m'
37 |
38 | # Foreground colors
39 | black ='\033[1;90m'
40 | red ='\033[1;91m'
41 | green ='\033[1;92m'
42 | yellow ='\033[1;93m'
43 | blue ='\033[1;94m'
44 | purple='\033[1;95m'
45 | cyan ='\033[1;96m'
46 | white ='\033[1;97m'
47 |
48 | # Background colors
49 | b_black ='\033[0;100m'
50 | b_red ='\033[0;101m'
51 | b_green ='\033[0;102m'
52 | b_yellow ='\033[0;103m'
53 | b_blue ='\033[0;104m'
54 | b_purple='\033[0;105m'
55 | b_cyan ='\033[0;106m'
56 | b_white ='\033[0;107m'
57 |
58 |
59 | def printHeader(hangisi):
60 | info = f"""
61 | {c.blue}[{c.white}i{c.blue}] Info: {c.white}Currencies are in format of {c.purple}USD{c.default}
62 | {c.red}[{c.white}!{c.red}] Warning: {c.white}This program assumes {c.purple}USDT{c.white} as {c.purple}USD{c.white}!{c.default}
63 | """
64 | headers = """
65 | ┌─────────┬────────────────────┬──────────┬──────────┬─────┬─────┬──────────────────────┬───────────────────┐
66 | │BLOCK NUM│LOCAL TIMESTAMP │DEX1 │DEX2 │SELL │BUY │UNIT REVENUE │THRESHOLD AMOUNT │
67 | ├─────────┼────────────────────┼──────────┼──────────┼─────┼─────┼──────────────────────┼───────────────────┤"""
68 | if hangisi == 'info':
69 | print(info)
70 | elif hangisi == 'headers':
71 | print(headers)
72 | elif hangisi == 'both':
73 | print(info + headers)
74 |
75 |
76 | def writeRow(*tuples): # Create and print a row (to be placed in a table).
77 | row = ""
78 | for item in tuples:
79 | word = str(item[0])
80 | size = 15
81 | add_zeros = False
82 | try:
83 | size = item[1]
84 | add_zeros = eval(item[2])
85 | except:
86 | pass
87 | # if len(item) == 2:
88 | # size = item[1]
89 | if add_zeros:
90 | word += ''.join('0' for i in range(size-1 - len(str(word))))
91 | row += f"│{word}"
92 | fill = 0
93 | fill = size - len(word)
94 | for i in range(fill):
95 | row += " "
96 | row += "│"
97 | print(row)
98 |
99 |
100 | def printFormattedData(liste:list) -> None:
101 | """
102 | Apply 'writeRow' function to every item in a given list consisting of dictionaries.
103 | """
104 |
105 | try:
106 | if INTERACTIVE == False:
107 | printHeader('headers')
108 | except NameError:
109 | printHeader('headers')
110 |
111 | for girdi in liste:
112 | writeRow((girdi.get('blocknum'), 9),\
113 | (strftime('%Y-%m-%d %H:%M:%S', localtime(girdi.get('timestamp'))), 20),\
114 | (girdi.get('dex1'), 10),\
115 | (girdi.get('dex2'),10),\
116 | (girdi.get('sell_token'),5),\
117 | (girdi.get('buy_token'),5),\
118 | (girdi.get('revenue_per_1usd'), 22, 'True'),\
119 | (girdi.get('minimum_amount'), 19, 'True')\
120 | )
121 | print('├─────────┼────────────────────┼──────────┼──────────┼─────┼─────┼──────────────────────┼───────────────────┤')
122 |
123 |
124 | def animate(sentence:str, condition:str, interactive:bool, mode=None) -> None:
125 | """
126 | Till the given condition becomes 'True', delay,
127 | meanwhile print an animation if the program is interactive.
128 |
129 | Not: Cümle olarak verilen yazının karakter sayısının eğer msfconsole modu
130 | seçildiyse 4'ün normal mod (None) seçildiyse 8'in katları olması gerekiyor.
131 | """
132 | signs = ['|', '/', '─', '\\'] # Msfconsole'daki gibi :)
133 | signs_2 = ['└', '├', '┌', '┬', '┐', '┤', '┘', '┴' ]
134 | index = 0
135 | while not eval(condition):
136 | if interactive:
137 | for sign in signs_2:
138 | if mode == 'msfconsole':
139 | if (lstindex:=signs_2.index(sign)) >= len(signs):
140 | lstindex = lstindex - 4
141 | sign = signs[lstindex]
142 | char = sentence[index] # Büyüklük/küçüklük durumu değiştirilecek olan karakter
143 | text = f"{c.purple}[{c.yellow}{sign}{c.purple}] {c.white}{sentence[0:index]}{c.blue}{char.swapcase()}{c.white}{sentence[index+1:]}{c.default} "
144 | print(text, end="\r") # end='\r' ile imleci yazıyı yazdığın satırın başında bırakıyorsun.
145 | sleep(.1)
146 |
147 | index += 1
148 | if index == len(sentence):
149 | if mode == 'msfconsole':
150 | array = signs
151 | else:
152 | array = signs_2
153 | for i in range(len(array)):
154 | print(f"{c.purple}[{c.yellow}{array[i]}{c.purple}] {c.white}{sentence}{'.' * (i-1)}{c.blue}{'•' if i >= 1 else ''}{c.default}", end="\r")
155 | sleep(.1)
156 | for i in range(2 if mode == 'msfconsole' else 1):
157 | for sign in array:
158 | print(f"{c.purple}[{c.yellow}{sign}{c.purple}] {c.white}{sentence}{'.' * 3 if mode == 'msfconsole' else '.' * 7}{c.default}", end="\r")
159 | sleep(.1)
160 | index = 0
161 | else:
162 | pass
163 |
164 | if interactive:
165 | print(f"{c.purple}[👌] {c.white}{sentence}... {c.green}Done{c.default}")
166 |
167 |
168 | class Block: # Get transaction datas of the latest block in the specified blockchain network via Infura API.
169 | def __init__(self, APIKEY:str, network:str=None, w3:object=None):
170 | if type(w3) is not type(None):
171 | self.w3 = w3
172 | elif network == 'ropsten':
173 | self.w3 = Web3(web3_HTTPProvider(f'https://ropsten.infura.io/v3/{APIKEY}')) # Ropsten network node servisi
174 | elif network == 'mainnet':
175 | self.w3 = Web3(web3_HTTPProvider(f'https://mainnet.infura.io/v3/{APIKEY}')) # Main network node servisi
176 | else:
177 | raise Exception("Specify a web3 http node or choose network: 'ropsten', 'mainnet'")
178 |
179 | while True:
180 | try:
181 | block = self.w3.eth.get_block('latest', True)
182 | except web3_exceptions.BlockNotFound:
183 | continue
184 |
185 | self.txs = block['transactions']
186 | self.number = block['number']
187 | self.timestamp = block['timestamp']
188 |
189 | if len(self.txs) != 0:
190 | break
191 |
192 |
193 | def calculateAvgTxFee(self) -> float:
194 | """
195 | Average up the transaction fees of every transaction in the block.
196 | """
197 | transaction_fees = []
198 | for tx in self.txs:
199 | # wei cinsinden değerler alınır | 1 wei = 10^-18 ETH
200 | gas_price = tx['gasPrice']
201 | gas_used = self.w3.eth.waitForTransactionReceipt(tx['hash'])['gasUsed']
202 | # Transaction fee = gas_price * gas_used * 10^-18
203 | transaction_fees.append(float(f"{gas_price * gas_used * (10**-18):.18f}"))
204 |
205 | avg_fee = sum(transaction_fees) / len(transaction_fees)
206 | return avg_fee # Return type: ETH
207 |
208 |
209 | def calculateMyGasPrice(self) -> int:
210 | """
211 | Average up the gas prices of every transaction and return %110 of it.
212 |
213 | In blockchain, there's a high chance for the transaction to be written
214 | at the next block if you give %10 more than the previous block's
215 | average gas price as your gas price to the next block.
216 | """
217 | prices = [tx['gasPrice'] for tx in self.txs]
218 | avg_gp = int(sum(prices) / len(prices))
219 | my_gp = int(avg_gp + (avg_gp/10)) # GasPrice + GasPrice * %10
220 | return my_gp # Return type: wei
221 |
222 |
223 |
224 | class AmountError(Exception): # Custom 'Exception' object
225 | pass
226 |
227 |
228 | class Prices: # Decentralized Exchange Operations
229 | def __init__(self, chosen_dexes:list, chosen_tokens:list, operation:str, interactive:bool) -> None:
230 | self.INTERACTIVE = interactive
231 | available_tokens = {
232 | 'eth': {'address': '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 'decimals': 18}, # WETH
233 | 'btc': {'address': '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 'decimals': 8}, # WBTC
234 | 'ftt': {'address': '0x50D1c9771902476076eCFc8B2A83Ad6b9355a4c9', 'decimals': 18}, # FTX Token
235 | 'aave': {'address': '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', 'decimals': 18},
236 | 'link': {'address': '0x514910771AF9Ca656af840dff83E8264EcF986CA', 'decimals': 18}, # ChainLink
237 |
238 | 'usdt': {'address': '0xdAC17F958D2ee523a2206206994597C13D831ec7', 'decimals': 6}, # Stable
239 | 'usdc': {'address': '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 'decimals': 6}, # Stable
240 | 'cels': {'address': '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 'decimals': 4} #Celsius # Patladı
241 | }
242 |
243 | available_dexes = {
244 | 'uniswap': 'Uniswap_V3',
245 | 'sushiswap': 'SushiSwap'
246 | }
247 |
248 | if chosen_tokens == 'all':
249 | chosen_tokens = available_tokens.keys()
250 |
251 | # string cinsinden 1 tane VEYA listedeki bütün ögelerin available listesinde olmadığı durumda verilecek olan hata
252 | if not all(x in available_tokens.keys() for x in tuple(chosen_tokens)):
253 | raise ValueError(f"Invalid value for 'tokens', can only specify following tokens {c.underline}in list format{c.default}: {', '.join(i for i in available_tokens.keys())} ")
254 | if not all(x in available_dexes.keys() for x in tuple(chosen_dexes)):
255 | raise ValueError(f"Invalid value for 'dexes', can only specify following dexes {c.underline}in list format{c.default}: {', '.join(i for i in available_dexes.keys())} ")
256 |
257 | self.tokens = {key:value for key, value in available_tokens.items() if key in set(chosen_tokens) | {'usdt'}}
258 | self.dexes = {key:value for key, value in available_dexes.items() if key in set(chosen_dexes)}
259 |
260 | # Yeterli sayıda olmadıklarında verilecek olan hata
261 | if len(self.tokens) < 2:
262 | raise AmountError(f"Invalid amount of 'tokens', specify an a number of at least {c.underline}2 different{c.default} ERC-20 tokens!")
263 | if len(self.dexes) != 2:
264 | raise AmountError(f"Invalid amount of 'exchanges', specify an exact number of {c.underline}2 different{c.default} exchanges!")
265 |
266 | # Bütün swap fiyatlarının içerisinde tutulacağı nesne
267 | self.prices_dict = {k:{} for k in self.dexes.keys()}
268 | self.token_pairs = self.generatePermutations(self.tokens.keys())
269 |
270 | self.threads = {
271 | f"{dex[0]}_{pair}":f"Thread(daemon=True, target=self.updatePrices, args=('{dex}', '{operation}', '{pair}', 1))"\
272 | for pair in self.token_pairs\
273 | for dex in self.dexes.keys()
274 | }
275 | self.parent_threads = {
276 | f"parent_{thread_name}":f"{thread_content.replace('updatePrices', 'handleThreads')}"\
277 | for thread_name, thread_content in self.threads.items()
278 | }
279 |
280 | # İlk durumda token sayısının ikili permütasyonundan oluşan fakat fiyat almada hata meydana geldikçe azaltılan
281 | # ve asıl amacı arbitraj nesnesinin kullanımına sunulmak üzere hata vermeyen bütün fiyat çiftlerinin içinde tutulduğu
282 | # nesneye eklenip eklenilmediği koşulunun kontrolü sırasında kullanılmak olan değişken.
283 |
284 | # Çünkü arbitraj hesaplamaları uygun olan bütün çiftlerin fiyatları bir değişkenin içerisinde hazır olmadan başlatılamaz.
285 | self.available_swap_liquidity_number = perm(len(self.tokens.keys()), 2)
286 | # Fiyatını alırken yetersiz likidite hatası gibi hata meydana gelen çiftlerin dex-token_pair:hata_mesajı formatında yer alacağı nesne.
287 | self.failed_pairs = dict()
288 |
289 |
290 | # Return swap price for the given token pair according to;
291 | # - DEX name (uniswap, sushiswap, etc...)
292 | # - Amount of the token to be swapped
293 | # - Buy/Sell price.
294 | def getDexPrice(self, dex:str, op:str, token_pair:str, amount:float) -> float:
295 | # eth_usdt -> eth:buy, usdt:sell | 1 eth satın almak için kaç usdt satmalıyım? -> 1280
296 | # eth_usdt -> eth:sell, usdt:buy | 1 eth satarak kaç usdt alabilirim? -> 1280
297 | # Cevaplar aynı.
298 |
299 | if op == 'buy':
300 | buyToken = token_pair.split('_')[0]
301 | sellToken = token_pair.split('_')[1]
302 | decimals = self.tokens.get(buyToken).get('decimals')
303 | elif op == 'sell':
304 | buyToken = token_pair.split('_')[1]
305 | sellToken = token_pair.split('_')[0]
306 | decimals = self.tokens.get(sellToken).get('decimals')
307 |
308 | amount = amount * (10**decimals)
309 | query = {
310 | 'buyToken': self.tokens.get(buyToken).get('address'),
311 | 'sellToken': self.tokens.get(sellToken).get('address'),
312 | 'includedSources': dex,
313 | f'{op}Amount': amount
314 | }
315 |
316 | url = f"https://api.0x.org/swap/v1/price?{urlencode(query)}"
317 | status = 0
318 | while status != 200:
319 | try:
320 | response = urlopen(url, timeout=10)
321 | status = response.code
322 |
323 | except HTTPError as err:
324 | if err.code == 429: # HTTP 'Too many requests' durum kodu
325 | sleep(.5)
326 | elif err.code == 400: # API'a özel, genelde 'insufficient asset liquiditiy' hataları
327 | http_body = jsonloads(err.read())
328 | error_msg = http_body['validationErrors'][0]['reason'].capitalize().replace('_', ' ')
329 | sleep(10)
330 | if self.INTERACTIVE:
331 | print(f"{c.b_red}[E]{c.default} {c.red}{error_msg}{c.white} for {c.cyan}{token_pair}{c.white} token pair on {c.cyan}{dex}{c.white} exchange!{c.default}")
332 | self.failed_pairs.update({f"{dex}-{token_pair}":error_msg})
333 | exit()
334 | else:
335 | print(err)
336 |
337 | except URLError as err:
338 | if str(err.reason) == '[Errno -3] Temporary failure in name resolution':
339 | print("Error: Failure in name resolution, check your internet connection!")
340 | sleep(5)
341 | else:
342 | print(err)
343 |
344 | response_json = jsonloads(response.read())
345 | #print(jsondumps(response_json, indent=4, sort_keys=True)) # Debug: Print unparsed response as json, directly.
346 | return response_json['price']
347 |
348 |
349 | def updatePrices(self, dex, op, token_pair, amount):
350 | # Handle http/ssl errors
351 | #try:
352 | price = self.getDexPrice(self.dexes.get(dex), op, token_pair, amount)
353 | self.prices_dict.get(dex).update({token_pair:price})
354 | # except Exception as e:
355 | # print('An error occured in updating prices_dict at an instance of Prices class:\n' + str(e))
356 | # sysexit()
357 |
358 |
359 | def handleThreads(self, dex, op, token_pair, amount):
360 | thread_name = f"{dex[0]}_{token_pair}"
361 | # Pairin tersini ve pairi kontrol etmemizin sebebi eğer insufficient vb. bir hata ile karşılaşırsak
362 | # çiftlerin tersleriyle alakalı threadleri sonlandırıyoruz çünkü fiyat verisi onlar için de alınamıyor.
363 | pairin_tersi = f"{''.join(self.dexes.get(i) for i in self.dexes.keys() if i != dex)}-{token_pair.split('_')[1]}_{token_pair.split('_')[0]}"
364 | pair_ve_tersi = [f"{self.dexes.get(dex)}-{token_pair}", pairin_tersi]
365 | # Geçerli pair veya tersinin diğer borsadaki threadinde bir hata çıkmadığı sürece thread döngüsünü çalıştır.
366 | while not any(x in self.failed_pairs.keys() for x in pair_ve_tersi):
367 | exec(f"{thread_name} = {self.threads.get(thread_name)}")
368 | eval(thread_name).start()
369 | eval(thread_name).join() # Wait until current thread ends with either success or http(conn reset)/ssl error.
370 | # Arbitraj hesabı için hem bu paire hem de diğer borsada bu pairin tersine bakmamız gerektiği için 0.5 çıkarıyoruz.
371 | # Çünkü tek bir arbitrage işlemini yapmamızı sağlayan bu fonksiyonun ait olduğu threadin pairi ve o pairin tersinin threadi.
372 | self.available_swap_liquidity_number -= 0.5 # Remove current pair from available liquidities
373 | if token_pair in self.prices_dict.get(dex).keys():
374 | del self.prices_dict.get(dex)[token_pair]
375 |
376 | @classmethod
377 | def generatePermutations(cls, liste:list) -> list:
378 | """
379 | Create a list of given items' binary permutations without theirselves.
380 | """
381 | permutations = []
382 | for item in liste:
383 | other_items = (name for name in liste if name != item)
384 | # Listenin güncellenen bir yapı olmasının sebebi:
385 | # Eğer ikili kombinasyonlarını alsaydık, öge eklenirken hâlihazırda (tersi) var mı diye bakmalıydık.
386 | # Aşağıdaki satırın devamındaki yorumu kaldırırsam eğer permütasyon değil kombinasyon yapmış oluyorum.
387 | permutations = permutations + [f"{item}_{name}" for name in other_items]# if f"{name}_{item}" not in (pair for pair in permutations)]
388 | return permutations
389 |
390 |
391 | def runThreads(self) -> None: # Run price threads asynchronously
392 | for thread_name, thread in self.parent_threads.items():
393 | exec(f"{thread_name} = {thread}")
394 | eval(thread_name).start()
395 | sleep(.3) # Beklememin sebebi http isteklerinin üst üste gitmesini engellemektir.
396 |
397 |
398 | def waitUntilAll(self): # Wait until all the available prices are placed to the dictionary for every token.
399 | # Eleman sayısını kontrol et: Permütasyon
400 | if not (len(self.prices_dict.get('uniswap')) == len(self.prices_dict.get('sushiswap')) == self.available_swap_liquidity_number):
401 | return False
402 | else:
403 | return True
404 |
405 |
406 |
407 | class TxFee: # TxFee processes
408 | def __init__(self, apikey):
409 | self.APIKEY = apikey
410 |
411 | def runThread(self): # Run an independent thread for 'getTxFee' function.
412 | fee_thread = Thread(daemon=True, target=self.getTxFee)
413 | fee_thread.start()
414 |
415 | # Convert txfees to usd via getting the price data from an instance of 'Price' object and using the 'exchange' function of 'Arbitrage' class.
416 | def getTxFee(self):
417 | while True:
418 | block = Block(self.APIKEY, network='mainnet')
419 | self.timestamp = block.timestamp
420 | ethFee = block.calculateAvgTxFee() # In format of ETH
421 | self.usdFee = Arbitrage.exchange(price_obj.prices_dict, 'uniswap', 'eth', 'usdt', ethFee)
422 | self.number = block.number # Bunu en son almak önemli, çünkü waituntilnextblock fonksiyonunda, fee için de bunu kontrol ediyoruz.
423 | sleep(.01)
424 |
425 | # Unless current block's blocknumber is not equal to the given number, return False.
426 | def waitUntilNextBlock(self, eski=None):
427 | if eski == None:
428 | if not 'number' in vars(self).keys():
429 | return False
430 | else:
431 | return True
432 | while eski == self.number: # Preassume that, the previous fee can not be the same as the next fee. Bkz, Blok hatası in GasPrice_Explorer.
433 | pass
434 |
435 |
436 |
437 |
438 | class Arbitrage: # swap, exchange, data output and arbitrage analyze operations
439 | def __init__(self, kar_oranı, interactive):
440 | self.INTERACTIVE = interactive
441 | self.arbitrage_data = dict()
442 | self.temp_values_list = list()
443 | self.key_templates = ("blocknum", "timestamp", "dex1", "dex2", "sell_token", "buy_token", "revenue_per_1usd", "minimum_amount")
444 | if self.INTERACTIVE:
445 | self.kar_oranı = self.getKarOranı()
446 | print(c.hide) # İmleci gizle, hide pointer
447 | else:
448 | self.kar_oranı = kar_oranı
449 |
450 |
451 | def getKarOranı(self):
452 | kar_oranı = input(f'{c.cyan}=> {c.white}Enter minimum earning rate in percentage (blank for all positives): {c.cyan}%')
453 | if kar_oranı == "":
454 | kar_oranı = 0 # Verbose, write all the positive ones
455 | kar_oranı = float(kar_oranı)
456 | return kar_oranı
457 |
458 |
459 | @classmethod
460 | def exchange(cls, kur:dict, exchange_name:str, sell_token:str, buy_token:str, amount:float) -> float: # Exchange tokens
461 | buy_price = float(kur.get(exchange_name).get(f"{sell_token}_{buy_token}"))
462 | summary = buy_price * amount
463 | return summary
464 |
465 |
466 | def swap_tokens(self, kur, dex1, dex2, token1, token2): # Apply the swaps
467 | if token1 == 'usdt': # base_amount ve normal_price işlemlerinde usdt -> usdt olamayacağı için...
468 | exit()
469 | # 1 doların token cinsinden satış fiyatı nedir?
470 | base_amount = self.exchange(kur, dex1, 'usdt', token1, 1)
471 |
472 | # Bir dolara alınan tokenın satış fiyatı, kaç dolar?
473 | normal_price = self.exchange(kur, dex1, token1, 'usdt', base_amount)
474 |
475 | # Swap between DEXes
476 | x = self.exchange(kur, dex1, token1, token2, base_amount)
477 | y = self.exchange(kur, dex2, token2, token1, x)
478 | final_purse = self.exchange(kur, dex1, token1, 'usdt', y)
479 |
480 | return final_purse, normal_price
481 |
482 |
483 | def analyze(self, fee, kur, metadata, dex1, dex2, sell_token, buy_token): # Check if arbitrage exists in compliance with the minimum revenue
484 | final, normal = self.swap_tokens(kur, dex1, dex2, sell_token, buy_token)
485 | if final > normal:
486 | revenue_per_1usd = final - normal # Birim kazanç
487 | minimum_amount = fee / revenue_per_1usd # Fee'yi karşılamak için en az kaç dolarlık işlem yapılmalı?
488 | if revenue_per_1usd >= (self.kar_oranı / 100):
489 | blocknum = metadata[0]
490 | timestamp = metadata[1]
491 | data = (blocknum, timestamp, dex1, dex2, sell_token, buy_token, revenue_per_1usd, minimum_amount)
492 | self.temp_values_list.append(data)
493 |
494 |
495 | def processThreads(self, threads): # Start every arbitrage thread and wait till all to finish.
496 | for thread_name, thread in threads.items():
497 | exec(f"{thread_name} = {thread}")
498 | eval(thread_name).start()
499 | for thread_name, thread in threads.items():
500 | eval(thread_name).join()
501 |
502 |
503 | def loop(self, fee_obj:object, price_obj:object): # The main loop that organizes all the arbitrage process using the functions of of this class.
504 | while True:
505 | current_fee = fee_obj.usdFee
506 | current_prices = price_obj.prices_dict
507 | current_metadata = (fee_obj.number, fee_obj.timestamp)
508 |
509 | threads = {
510 | f"arbitrageOf_{dex[0]}_{pair}":
511 | f"Thread(daemon=True, target=self.analyze, args=({current_fee}, {current_prices}, {current_metadata}, '{dex}', '{[i for i in price_obj.dexes.keys() if i != dex][0]}', '{pair.split('_')[0]}', '{pair.split('_')[1]}'))" \
512 | for pair in price_obj.token_pairs
513 | for dex in price_obj.dexes.keys()
514 | if pair in price_obj.prices_dict.get(dex)
515 | }
516 |
517 |
518 | self.processThreads(threads)
519 | self.arbitrage_data.update({current_metadata[0]:[dict(zip(self.key_templates, values)) for values in self.temp_values_list]})
520 | self.temp_values_list.clear()
521 | if self.INTERACTIVE:
522 | printFormattedData(self.arbitrage_data.get(current_metadata[0]))
523 | fee_obj.waitUntilNextBlock(current_metadata[0])
524 |
525 |
526 |
527 | def getArbitrageData() -> dict:
528 | return arbitrage_obj.arbitrage_data
529 |
530 |
531 | def arbitrage(APIKEY:str, exchanges:list=['uniswap', 'sushiswap'], tokens:list='all', operation:str='sell', min_revenue:float=0, interactive:bool=False) -> None:
532 | """
533 | Initialize objects of the classes and check for arbitrage in compliance with the given parameters.
534 |
535 | ---
536 | :APIKEY: Your Infura APIKEY
537 |
538 | :exchanges: A list including two of the available exchanges
539 |
540 | :tokens: A list including at least one available token apart from "usdt"
541 |
542 | *operation* A string indicating the basic price type while analyzing the exchanges
543 |
544 | *min_revenue* A floating point number indicating the minimum profit rate percent
545 |
546 | *interactive* A boolean, setting this as True will bring animations and a real-time monitor to your screen.
547 | ---
548 |
549 | Available exchanges: ['uniswap', 'sushiswap']
550 | Available tokens: ['eth', 'btc', 'ftt', 'aave', 'link', 'usdt', 'usdc', 'cels']
551 | """
552 |
553 | global arbitrage_obj, price_obj, fee_obj, INTERACTIVE
554 |
555 | try:
556 | INTERACTIVE = interactive
557 | price_obj = Prices(exchanges, tokens, operation=operation, interactive=interactive)
558 | arbitrage_obj = Arbitrage(kar_oranı=min_revenue, interactive=interactive)
559 | fee_obj = TxFee(APIKEY)
560 |
561 | Thread(daemon=True, target=price_obj.runThreads).start()
562 |
563 | # Önce kurun beklenmesi önemli, çünkü fee formatını çevirme için de kur kullanılıyor.
564 | animate("Initializing exchange prices", 'price_obj.waitUntilAll()', mode='msfconsole', interactive=interactive)
565 | fee_obj.runThread()
566 | animate("Getting latest block information", 'fee_obj.waitUntilNextBlock()', interactive=interactive)
567 |
568 | if interactive:
569 | printHeader('both')
570 | arbitrage_obj.loop(fee_obj, price_obj)
571 | else:
572 | Thread(daemon=True, target=arbitrage_obj.loop, args=(fee_obj, price_obj)).start()
573 | sleep(2) # Sleep until the first loop finishes and the dictionary fills up with the initial datas
574 |
575 | except KeyboardInterrupt:
576 | exitmsg = None
577 | if interactive:
578 | exitmsg = f"\n {c.green}Program exited!"
579 | sysexit(exitmsg)
580 | # except Exception as e:
581 | # sysexit(f"\nError: {e}")
582 | finally:
583 | if interactive:
584 | print(c.show)
585 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 | import codecs
3 | import os
4 |
5 | VERSION = "0.0.21"
6 | DESCRIPTION = "Analyzing arbitrage opportunities between DEcentralized eXchanges (dex-to-dex)"
7 | LONG_DESCRIPTION = "An extensible library allows to analyze DEX-to-DEX arbitrage opportunities autonomously, besides advanced decentralized exchange operations"
8 |
9 | urls = {
10 | "Source": "https://github.com/f4T1H21/Arbitrage-Bot-Code-Library/blob/main/libarbitrage/lib.py",
11 | "Homepage": "https://github.com/f4T1H21/Arbitrage-Bot-Code-Library",
12 | "Documentation": "https://github.com/f4T1H21/Arbitrage-Bot-Code-Library#documentation",
13 | "Twitter": "https://twitter.com/f4T1H21",
14 | "Linkedin": "https://www.linkedin.com/in/%C5%9Fefik-efe/"
15 | }
16 |
17 | setup(
18 | name="libarbitrage",
19 | version=VERSION,
20 | author="f4T1H21 (Şefik Efe Altınoluk)",
21 | author_email="",
22 | description=DESCRIPTION,
23 | long_description_content_type="text/markdown",
24 | long_description=LONG_DESCRIPTION,
25 | packages=find_packages(),
26 | install_requires=["web3"],
27 | project_urls=urls,
28 | keywords=["python", "bot", "arbitrage", "analyze", "arbitrage analyze", "dex", "dex arbitrage", "pyarbitrage", "libarbitrage"],
29 | classifiers=[
30 | "Development Status :: 1 - Planning",
31 | "Programming Language :: Python :: 3",
32 | "Operating System :: Unix",
33 | "Operating System :: MacOS :: MacOS X",
34 | "Operating System :: Microsoft :: Windows",
35 | "Topic :: Office/Business :: Financial",
36 | "Topic :: Office/Business :: Financial :: Investment",
37 | "Intended Audience :: Financial and Insurance Industry",
38 | "Intended Audience :: Developers",
39 |
40 | ]
41 | )
--------------------------------------------------------------------------------
/src/algoritma.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/f4T1H21/Arbitrage-Bot-Code-Library/0fd1ac06cae0c298d8662b94f21702a46b19ab62/src/algoritma.png
--------------------------------------------------------------------------------