├── requirements.txt ├── config.py.example ├── .gitignore ├── alpaca_crypto.py ├── oneInch.py ├── dex_cex_arb.py ├── usdc_contract_abi.json └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | web3 2 | alpaca_trade_api 3 | requests 4 | logging -------------------------------------------------------------------------------- /config.py.example: -------------------------------------------------------------------------------- 1 | APCA_API_KEY_ID = 'Your Alpaca key ID' 2 | APCA_API_SECRET_KEY = 'Your Alpaca Secret Key' 3 | ALCHEMY_URL = 'Your Alchemy HTTP URL for Polygon' 4 | BASE_ACCOUNT = 'Your Web3 Wallet addres' 5 | PRIVATE_KEY = 'Prive key for the wallet address mentioned above' 6 | -------------------------------------------------------------------------------- /.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 | config.py 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | -------------------------------------------------------------------------------- /alpaca_crypto.py: -------------------------------------------------------------------------------- 1 | from turtle import pos 2 | from webbrowser import get 3 | from wsgiref.headers import Headers 4 | from alpaca_trade_api.rest import REST, TimeFrame 5 | import config 6 | import requests 7 | import logging 8 | from multiprocessing import Process 9 | import time 10 | 11 | # ENABLE LOGGING - options, DEBUG,INFO, WARNING? 12 | logging.basicConfig(level=logging.INFO, 13 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | HEADERS = {'APCA-API-KEY-ID': config.APCA_API_KEY_ID, 18 | 'APCA-API-SECRET-KEY': config.APCA_API_SECRET_KEY} 19 | 20 | BASE_URL = 'https://paper-api.alpaca.markets' 21 | trading_pair = 'MATICUSD' # Checking quotes and trading MATIC against USD 22 | exchange = 'FTXU' # FTXUS 23 | DATA_URL = 'https://data.alpaca.markets' 24 | 25 | alpaca = REST(config.APCA_API_KEY_ID, config.APCA_API_SECRET_KEY, 26 | BASE_URL, DATA_URL) 27 | 28 | 29 | def get_api_quote_data(trading_pair, exchange): 30 | ''' 31 | Get trade quote data from 1Inch API 32 | ''' 33 | try: 34 | quote = requests.get( 35 | '{0}/v1beta1/crypto/{1}/quotes/latest?exchange={2}'.format(DATA_URL, trading_pair, exchange), headers=HEADERS) 36 | logger.info('Alpaca quote reply status code: {0}'.format( 37 | quote.status_code)) 38 | if quote.status_code != 200: 39 | logger.info( 40 | "Undesirable response from Alpaca! {}".format(quote.json())) 41 | return False 42 | logger.info('get_api_quote_data: {0}'.format(quote.json())) 43 | 44 | except Exception as e: 45 | logger.exception( 46 | "There was an issue getting trade quote from Alpaca: {0}".format(e)) 47 | return False 48 | 49 | return quote.json() 50 | 51 | 52 | def get_account_details(): 53 | ''' 54 | Get Alpaca Trading Account Details 55 | ''' 56 | try: 57 | account = requests.get( 58 | '{0}/v2/account'.format(BASE_URL), headers=HEADERS) 59 | logger.info('Alpaca account reply status code: {0}'.format( 60 | account.status_code)) 61 | if account.status_code != 200: 62 | logger.info( 63 | "Undesirable response from Alpaca! {}".format(account.json())) 64 | return False 65 | logger.info('get_account_details: {0}'.format(account.json())) 66 | except Exception as e: 67 | logger.exception( 68 | "There was an issue getting account details from Alpaca: {0}".format(e)) 69 | return False 70 | return account.json() 71 | 72 | 73 | def get_open_orders(): 74 | ''' 75 | Get open orders 76 | ''' 77 | try: 78 | open_orders = requests.get( 79 | '{0}/v2/orders'.format(BASE_URL), headers=HEADERS) 80 | logger.info('Alpaca open orders reply status code: {0}'.format( 81 | open_orders.status_code)) 82 | if open_orders.status_code != 200: 83 | logger.info( 84 | "Undesirable response from Alpaca! {}".format(open_orders.json())) 85 | return False 86 | logger.info('get_open_orders: {0}'.format(open_orders.json())) 87 | except Exception as e: 88 | logger.exception( 89 | "There was an issue getting open orders from Alpaca: {0}".format(e)) 90 | return False 91 | return open_orders.json() 92 | 93 | 94 | def get_positions(): 95 | ''' 96 | Get positions 97 | ''' 98 | try: 99 | positions = requests.get( 100 | '{0}/v2/positions'.format(BASE_URL), headers=HEADERS) 101 | logger.info('Alpaca positions reply status code: {0}'.format( 102 | positions.status_code)) 103 | if positions.status_code != 200: 104 | logger.info( 105 | "Undesirable response from Alpaca! {}".format(positions.json())) 106 | return False 107 | # positions = positions[0] 108 | matic_position = positions.json()[0]['qty'] 109 | logger.info('Matic Position on Alpaca: {0}'.format(matic_position)) 110 | except Exception as e: 111 | logger.exception( 112 | "There was an issue getting positions from Alpaca: {0}".format(e)) 113 | return False 114 | return matic_position 115 | 116 | 117 | def post_order(symbol, qty, side, type, time_in_force): 118 | ''' 119 | Post an order to Alpaca 120 | ''' 121 | try: 122 | order = requests.post( 123 | '{0}/v2/orders'.format(BASE_URL), headers=HEADERS, json={ 124 | 'symbol': symbol, 125 | 'qty': qty, 126 | 'side': side, 127 | 'type': type, 128 | 'time_in_force': time_in_force, 129 | }) 130 | logger.info('Alpaca order reply status code: {0}'.format( 131 | order.status_code)) 132 | if order.status_code != 200: 133 | logger.info( 134 | "Undesirable response from Alpaca! {}".format(order.json())) 135 | return False 136 | logger.info('post_order: {0}'.format(order.json())) 137 | except Exception as e: 138 | logger.exception( 139 | "There was an issue posting order to Alpaca: {0}".format(e)) 140 | return False 141 | return order.json() 142 | 143 | 144 | def main(): 145 | ''' 146 | These are examples of different functions in the script. 147 | Uncomment the command you want to run. 148 | ''' 149 | # get price quote for 1 ETH in DAI right now 150 | # matic_price = one_inch_get_quote( 151 | # ethereum, mcd_contract_address, Web3.toWei(1, 'ether')) 152 | # while True: 153 | matic_price = get_api_quote_data(trading_pair, exchange) 154 | print("matic price is :", matic_price['quote']['ap']) 155 | # print("Account details are: ", get_account_details()) 156 | print("Cash balance is: ", get_account_details()['cash']) 157 | # print("Open orders are: ", get_open_orders()) 158 | if(get_open_orders()): 159 | print("Open orders are: ", get_open_orders()) 160 | else: 161 | print("No open orders") 162 | 163 | get_positions() 164 | print(alpaca.get_latest_crypto_xbbo(trading_pair)) 165 | 166 | # buying_matic = post_order(trading_pair, 10, 'buy', 'market', 'gtc') 167 | # print("Buying matic order response: ", buying_matic) 168 | 169 | # selling_matic = post_order(trading_pair, 20, 'buy', 'market', 'gtc') 170 | # print("Selling matic order response: ", selling_matic) 171 | # time.sleep(2) 172 | 173 | 174 | # in_position_quantity = 0 175 | # pending_orders = {} 176 | # dollar_amount = 1000 177 | # logfile = 'trade.log' 178 | 179 | 180 | # def check_order_status(): 181 | # global in_position_quantity 182 | 183 | # removed_order_ids = [] 184 | 185 | # print("{} - checking order status".format(datetime.now().isoformat())) 186 | 187 | # if len(pending_orders.keys()) > 0: 188 | # print("found pending orders") 189 | # for order_id in pending_orders: 190 | # order = alpaca.get_order(order_id) 191 | 192 | # if order.filled_at is not None: 193 | # filled_message = "order to {} {} {} was filled {} at price {}\n".format( 194 | # order.side, order.qty, order.symbol, order.filled_at, order.filled_avg_price) 195 | # print(filled_message) 196 | # with open(logfile, 'a') as f: 197 | # f.write(str(order)) 198 | # f.write(filled_message) 199 | 200 | # if order.side == 'buy': 201 | # in_position_quantity = float(order.qty) 202 | # else: 203 | # in_position_quantity = 0 204 | 205 | # removed_order_ids.append(order_id) 206 | # else: 207 | # print("order has not been filled yet") 208 | 209 | # for order_id in removed_order_ids: 210 | # del pending_orders[order_id] 211 | 212 | 213 | # def send_order(symbol, quantity, side): 214 | # print("{} - sending {} order".format(datetime.now().isoformat(), side)) 215 | # order = alpaca.submit_order(symbol, quantity, side, 'market') 216 | # print(order) 217 | # pending_orders[order.id] = order 218 | 219 | 220 | # def get_bars(): 221 | # print("{} - getting bars".format(datetime.now().isoformat())) 222 | # data = vbt.CCXTData.download( 223 | # ['SOLUSDT'], start='30 minutes ago', timeframe='1m') 224 | # df = data.get() 225 | # df.ta.stoch(append=True) 226 | # print(df) 227 | 228 | # last_k = df['STOCHk_14_3_3'].iloc[-1] 229 | # last_d = df['STOCHd_14_3_3'].iloc[-1] 230 | # last_close = df['Close'].iloc[-1] 231 | 232 | # print(last_k) 233 | # print(last_d) 234 | # print(last_close) 235 | 236 | # if last_d < 40 and last_k > last_d: 237 | # # min order size for SOL is 0.01 238 | # if in_position_quantity == 0 and (dollar_amount / last_close) >= 0.1: 239 | # # buy 240 | # print("------ Trying to buy -----: ", dollar_amount / last_close) 241 | # send_order('ETHUSD', round(dollar_amount / last_close, 3), 'buy') 242 | # else: 243 | # print("== already in position, nothing to do ==") 244 | 245 | # if last_d > 80 and last_k < last_d: 246 | # if in_position_quantity > 0: 247 | # # sell 248 | # send_order('ETHUSD', in_position_quantity, 'sell') 249 | # else: 250 | # print("== you have nothing to sell ==") 251 | 252 | 253 | # manager = vbt.ScheduleManager() 254 | # manager.every().do(check_order_status) 255 | # manager.every().minute.at(':00').do(get_bars) 256 | # manager.start() 257 | 258 | 259 | # from alpaca_trade_api.stream import Stream 260 | # import config 261 | # import os 262 | 263 | 264 | # async def print_trade(t): 265 | # print('trade', t) 266 | 267 | 268 | # async def print_quote(q): 269 | # print('quote', q) 270 | 271 | 272 | # async def print_trade_update(tu): 273 | # print('trade update', tu) 274 | 275 | 276 | # async def print_crypto_trade(t): 277 | # print('crypto trade', t) 278 | 279 | 280 | # def main(): 281 | 282 | # BASE_URL = "https://paper-api.alpaca.markets" 283 | # CRYPTO_URL = 'https://data.alpaca.markets/v1beta1/crypto' 284 | # ALPACA_API_KEY = config.APCA_API_KEY_ID 285 | # ALPACA_SECRET_KEY = config.APCA_API_SECRET_KEY 286 | 287 | # feed = 'iex' # <- replace to SIP if you have PRO subscription 288 | # stream = Stream(key_id=ALPACA_API_KEY, 289 | # secret_key=ALPACA_SECRET_KEY, base_url=BASE_URL, raw_data=True, data_stream_url=CRYPTO_URL) 290 | # # stream.subscribe_trade_updates(print_trade_update) 291 | # stream.subscribe_trades(print_trade, 'BTCUSD') 292 | # # stream.subscribe_quotes(print_quote, 'IBM') 293 | # # stream.subscribe_crypto_trades(print_crypto_trade, 'BTCUSD') 294 | # # print(stream) 295 | 296 | # @stream.on_bar('MSFT') 297 | # async def _(bar): 298 | # print('bar', bar) 299 | 300 | # # @stream.on_updated_bar('MSFT') 301 | # # async def _(bar): 302 | # # print('updated bar', bar) 303 | 304 | # @stream.on_status("*") 305 | # async def _(status): 306 | # print('status', status) 307 | 308 | # # @stream.on_luld('AAPL', 'MSFT') 309 | # # async def _(luld): 310 | # # print('LULD', luld) 311 | 312 | # stream.run() 313 | if __name__ == "__main__": 314 | main() 315 | -------------------------------------------------------------------------------- /oneInch.py: -------------------------------------------------------------------------------- 1 | from subprocess import call 2 | import config 3 | 4 | #!/usr/bin/env python 5 | # -*- coding: utf-8 -*- 6 | 7 | import requests 8 | import logging 9 | import json 10 | import os 11 | from web3 import Web3 12 | # from abi import usdc_contract_abi 13 | # import '../abi/usdc_contract.json' 14 | # import 'abi/usdc_contract_abi.json' 15 | 16 | 17 | # ENABLE LOGGING - options, DEBUG,INFO, WARNING? 18 | logging.basicConfig(level=logging.INFO, 19 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 20 | logger = logging.getLogger(__name__) 21 | 22 | # Load up MCD and 1 Inch split contract ABIs 23 | # one_inch_split_abi = json.load(open('abi/one_inch_split.json', 'r')) 24 | # beta_one_inch_split_abi = json.load(open('abi/beta_one_inch_split.json', 'r')) 25 | # mcd_abi = json.load(open('abi/mcd_join.json', 'r')) 26 | 27 | production = False # False to prevent any public TX from being sent 28 | slippage = 1 29 | 30 | # if MATIC --> USDC - (enter the amount in units Ether) 31 | amount_to_exchange = Web3.toWei(1, 'ether') 32 | 33 | # if USDC --> MATIC (using base unit, so 1 here = 1 DAI/MCD) 34 | amount_of_usdc = 1000000 35 | 36 | 37 | # one_inch_split_contract = Web3.toChecksumAddress( 38 | # '0xC586BeF4a0992C495Cf22e1aeEE4E446CECDee0E') # 1 inch split contract 39 | 40 | # beta_one_inch_split_contract = Web3.toChecksumAddress( 41 | # '0x50FDA034C0Ce7a8f7EFDAebDA7Aa7cA21CC1267e') # Beta one split contract 42 | 43 | matic_address = Web3.toChecksumAddress( 44 | '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE') # ETHEREUM 45 | 46 | 47 | usdc_address = Web3.toChecksumAddress( 48 | '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174') # USDC Token contract address 49 | 50 | usdc_contract_abi = json.load(open('usdc_contract_abi.json', 'r')) 51 | # print(usdc_contract_abi) 52 | 53 | eth_provider_url = config.ALCHEMY_URL 54 | base_account = Web3.toChecksumAddress(config.BASE_ACCOUNT) 55 | wallet_address = base_account 56 | private_key = config.PRIVATE_KEY 57 | BASE_URL = 'https://api.1inch.io/v4.0/137' 58 | # required - example: export ETH_PROVIDER_URL="https://mainnet.infura.io/v3/yourkeyhere77777" 59 | # if 'ETH_PROVIDER_URL' in os.environ: 60 | # eth_provider_url = os.environ["ETH_PROVIDER_URL"] 61 | # else: 62 | # logger.warning( 63 | # 'No ETH_PROVIDER_URL has been set! Please set that and run the script again.') 64 | # quit() 65 | 66 | # required - The Etheruem account that you will be making the trade/exchange from 67 | # if 'BASE_ACCOUNT' in os.environ: 68 | # base_account = Web3.toChecksumAddress(os.environ["BASE_ACCOUNT"]) 69 | # else: 70 | # logger.warning( 71 | # 'No BASE_ACCOUNT has been set! Please set that and run the script again.') 72 | # quit() 73 | 74 | 75 | # private key for BASE_ACCOUNT 76 | # if 'PRIVATE_KEY' in os.environ: 77 | # private_key = os.environ["PRIVATE_KEY"] 78 | # else: 79 | # logger.warning( 80 | # 'No private key has been set. Script will not be able to send transactions!') 81 | # private_key = False 82 | 83 | # swapParams = { 84 | # fromTokenAddress: matic_address, # matic address 85 | # toTokenAddress: usdc_address, # usdc address 86 | # amount: amount_to_exchange, # amount to exchange 87 | # fromAddress: wallet_address, # wallet address 88 | # slippage: 1, 89 | # disableEstimate: false, 90 | # allowPartialFill: false, 91 | # } 92 | 93 | 94 | def main(): 95 | ''' 96 | These are examples of different functions in the script. 97 | Uncomment the command you want to run. 98 | ''' 99 | # get price quote for 1 ETH in DAI right now 100 | # matic_price = one_inch_get_quote( 101 | # ethereum, mcd_contract_address, Web3.toWei(1, 'ether')) 102 | usdc_token = w3.eth.contract(address=usdc_address, abi=usdc_contract_abi) 103 | usdc_balance = usdc_token.functions.balanceOf(wallet_address).call() 104 | print("USDC balance: {0}".format(usdc_balance)) 105 | 106 | matic_price = get_api_quote_data( 107 | matic_address, usdc_address, amount_to_exchange) 108 | print("matic price is :", float( 109 | matic_price['toTokenAmount'])/10**6, "USDC") 110 | 111 | swap_data = get_api_swap_call_data( 112 | usdc_address, matic_address, amount_of_usdc) 113 | # print("swap data is :", swap_data) 114 | 115 | # allowance = get_allowance(usdc_address) 116 | # print("Allowance for USDC is: ", allowance['allowance']) 117 | 118 | # print("swap data is :", swap_data) 119 | # swap_txn = signAndSendTransaction(swap_data) # swapping USDC for Matic 120 | # print("swap txn response is: ", Web3.toHex(swap_txn)) 121 | 122 | # approval_txn = approve_ERC20(10) 123 | # approval_hash = signAndSendTransaction(approval_txn) 124 | # print("Approval hash is: ", w3.toHex(approval_hash)) 125 | 126 | # allowance = get_allowance(usdc_address) 127 | # print("Allowance for USDC is: ", allowance['allowance']) 128 | 129 | # logger.info("1 ETH = {0} DAI on 1 Inch right now!".format( 130 | # Web3.fromWei(matic_price['toTokenAmount'] / 10**6, 'ether'))) 131 | 132 | # here is a ETH --> DAI exchange using 1 inch split contract (without api) 133 | # one_inch_token_swap(matic_address, usdc_address, amount_to_exchange) 134 | 135 | # Here are the steps for DAI --> ETH exchange using 1 inch split contract (without api) 136 | # We have to take an extra step to make this exchange by approving the 1 Inch contract 137 | # to spend some of our DAI first. You have to make sure the approve tx confirms before 138 | # you make the trade! So, just run this script twice, 1) to approve, wait for confirm 139 | # then 2) run the script again with just trade 140 | 141 | # 1) approve our DAI transfer (run once, first) 142 | # approve_ERC20(amount_of_dai) 143 | 144 | # wait for approve to confrim ^^ 145 | 146 | # 2) then make trade/exchange 147 | # one_inch_token_swap(mcd_contract_address, ethereum, amount_of_dai) 148 | 149 | 150 | def signAndSendTransaction(transaction_data): 151 | txn = w3.eth.account.signTransaction(transaction_data, private_key) 152 | tx_hash = w3.eth.sendRawTransaction(txn.rawTransaction) 153 | return tx_hash 154 | # const {rawTransaction} = await web3.eth.accounts.signTransaction(transaction, privateKey) 155 | 156 | # return await broadCastRawTransaction(rawTransaction) 157 | 158 | 159 | def get_allowance(_token): 160 | ''' 161 | Get allowance for a given token, the 1inch router is allowed to spend 162 | ''' 163 | try: 164 | allowance = requests.get( 165 | '{0}/approve/allowance?tokenAddress={1}&walletAddress={2}'.format(BASE_URL, _token, wallet_address)) 166 | logger.info('1inch allowance reply status code: {0}'.format( 167 | allowance.status_code)) 168 | if allowance.status_code != 200: 169 | logger.info( 170 | "Undesirable response from 1 Inch! This is probably bad.") 171 | return False 172 | logger.info('get_allowance: {0}'.format(allowance.json())) 173 | 174 | except Exception as e: 175 | logger.exception( 176 | "There was an issue getting allowance for the token from 1 Inch: {0}".format(e)) 177 | return False 178 | logger.info("allowance: {0}".format(allowance)) 179 | return allowance.json() 180 | 181 | 182 | def approve_ERC20(_amount_of_ERC): 183 | ''' 184 | Send a transaction to MCD/DAI contract approving 1 Inch join contract to spend _amount_of_ERC worth of base_accounts tokens 185 | ''' 186 | # load our contract 187 | # mcd_contract = web3.eth.contract( 188 | # address=mcd_contract_address, abi=mcd_abi) 189 | 190 | allowance_before = get_allowance(wallet_address) 191 | logger.info("allowance before: {0}".format(allowance_before)) 192 | 193 | try: 194 | approve_txn = requests.get( 195 | '{0}/approve/transaction?tokenAddress={1}&amount={2}'.format(BASE_URL, usdc_address, _amount_of_ERC)) 196 | logger.info('1inch allowance reply status code: {0}'.format( 197 | approve_txn.status_code)) 198 | if approve_txn.status_code != 200: 199 | logger.info( 200 | "Undesirable response from 1 Inch! This is probably bad.") 201 | return False 202 | logger.info('get_allowance: {0}'.format(approve_txn.json())) 203 | 204 | except Exception as e: 205 | logger.exception( 206 | "There was an issue allowing usdc spend from 1 Inch: {0}".format(e)) 207 | return False 208 | 209 | approve_txn = approve_txn.json() 210 | # logger.info("allowance: {0}".format(allowance)) 211 | 212 | # get our nonce 213 | nonce = w3.eth.getTransactionCount(wallet_address) 214 | 215 | # encode our data 216 | # data = mcd_contract.encodeABI(fn_name="approve", args=[ 217 | # one_inch_split_contract, _amount_of_ERC]) 218 | print("gas price is:", w3.fromWei(int(approve_txn['gasPrice']), 'gwei')) 219 | 220 | tx = { 221 | 'nonce': nonce, 222 | 'to': usdc_address, 223 | 'chainId': 137, 224 | # 'value': approve_txn['value'], 225 | 'gasPrice': w3.toWei(70, 'gwei'), 226 | 'from': wallet_address, 227 | 'data': approve_txn['data'] 228 | } 229 | 230 | # get gas estimate 231 | # gas_estimate = w3.eth.estimateGas(tx) 232 | tx['gas'] = 80000 233 | # print("gas estimate: {0}".format(gas_estimate)) 234 | # tx["gas"] = approve_txn['gasPrice'] 235 | 236 | logger.info('transaction data: {0}'.format(tx)) 237 | 238 | return tx 239 | 240 | # # sign and broadcast our trade 241 | # if private_key and production == True: 242 | # try: 243 | # signed_tx = web3.eth.account.signTransaction(tx, private_key) 244 | # except: 245 | # logger.exception("Failed to created signed TX!") 246 | # return False 247 | # try: 248 | # tx_hash = web3.eth.sendRawTransaction(signed_tx.rawTransaction) 249 | # logger.info("TXID from 1 Inch: {0}".format(web3.toHex(tx_hash))) 250 | # except: 251 | # logger.warning("Failed sending TX to 1 inch!") 252 | # return False 253 | # else: 254 | # logger.info('No private key found! Transaction has not been broadcast!') 255 | 256 | 257 | def get_api_swap_call_data(_from_coin, _to_coin, _amount_to_exchange): 258 | ''' 259 | Get call data from 1Inch API 260 | ''' 261 | try: 262 | call_data = requests.get( 263 | '{0}/swap?fromTokenAddress={1}&toTokenAddress={2}&amount={3}&fromAddress={4}&slippage={5}'.format(BASE_URL, _from_coin, _to_coin, _amount_to_exchange, wallet_address, slippage)) 264 | logger.info('response from 1 inch generic call_data request - status code: {0}'.format( 265 | call_data.status_code)) 266 | if call_data.status_code != 200: 267 | logger.info(call_data.json()['description']) 268 | return False 269 | call_data = call_data.json() 270 | nonce = w3.eth.getTransactionCount(wallet_address) 271 | tx = { 272 | 'from': call_data['tx']['from'], 273 | 'nonce': nonce, 274 | 'to': Web3.toChecksumAddress(call_data['tx']['to']), 275 | 'chainId': 137, 276 | 'value': int(call_data['tx']['value']), 277 | 'gasPrice': w3.toWei(call_data['tx']['gasPrice'], 'wei'), 278 | 'data': call_data['tx']['data'], 279 | 'gas': call_data['tx']['gas'] 280 | } 281 | # tx = call_data['tx'] 282 | # tx['nonce'] = nonce # Adding nonce to tx data 283 | 284 | logger.info('get_api_call_data: {0}'.format(call_data)) 285 | 286 | except Exception as e: 287 | logger.warning( 288 | "There was a issue getting get contract call data from 1 inch: {0}".format(e)) 289 | return False 290 | 291 | return tx 292 | 293 | 294 | def get_api_quote_data(_from_coin, _to_coin, _amount_to_exchange): 295 | ''' 296 | Get trade quote data from 1Inch API 297 | ''' 298 | try: 299 | quote = requests.get( 300 | '{0}/quote?fromTokenAddress={1}&toTokenAddress={2}&amount={3}'.format(BASE_URL, _from_coin, _to_coin, _amount_to_exchange)) 301 | logger.info('1inch quote reply status code: {0}'.format( 302 | quote.status_code)) 303 | if quote.status_code != 200: 304 | logger.info( 305 | "Undesirable response from 1 Inch! This is probably bad.") 306 | return False 307 | logger.info('get_api_quote_data: {0}'.format(quote.json())) 308 | 309 | except Exception as e: 310 | logger.exception( 311 | "There was an issue getting trade quote from 1 Inch: {0}".format(e)) 312 | return False 313 | 314 | return quote.json() 315 | 316 | 317 | def one_inch_token_swap(_from_token, _to_token, _amount): 318 | ''' 319 | Used to swap tokens on 1Inch directly through the one inch split contract 320 | ''' 321 | # get quote for trade, 322 | # quote = one_inch_get_quote(_from_token, _to_token, _amount) 323 | quote = get_api_quote_data(_from_token, _to_token, _amount) 324 | 325 | # min coins to accept, taken from our quote 326 | min_return = quote['toTokenAmount']/(10**6) 327 | 328 | # list of dist across exchanges like: [99, 0, 1, 0, 0, 0, 0, 0, 0, 0] 329 | # distribution = quote[1] 330 | 331 | # use all available exchanges 332 | disable_flags = 0 333 | 334 | # load our contract 335 | one_inch_join = web3.eth.contract( 336 | address=one_inch_split_contract, abi=one_inch_split_abi) 337 | 338 | # get our nonce 339 | nonce = web3.eth.getTransactionCount(wallet_address) 340 | 341 | # craft transaction call data 342 | data = one_inch_join.encodeABI(fn_name="swap", args=[ 343 | _from_token, _to_token, _amount, min_return, distribution, disable_flags]) 344 | 345 | # if ETH --> DAI then value is exchange _amount, if DAI-->ETH then value != _amount 346 | if _from_token == mcd_contract_address: 347 | value = 0 348 | else: 349 | value = _amount 350 | 351 | tx = { 352 | 'nonce': nonce, 353 | 'to': one_inch_split_contract, 354 | 'value': value, 355 | 'gasPrice': web3.eth.gasPrice, 356 | 'from': wallet_address, 357 | 'data': data 358 | } 359 | 360 | # get gas estimate 361 | gas = web3.eth.estimateGas(tx) 362 | tx["gas"] = gas 363 | 364 | logger.info('transaction data: {0}'.format(tx)) 365 | 366 | # sign and broadcast our trade 367 | if private_key and production == True: 368 | try: 369 | signed_tx = web3.eth.account.signTransaction(tx, private_key) 370 | except: 371 | logger.exception("Failed to created signed TX!") 372 | return False 373 | try: 374 | tx_hash = web3.eth.sendRawTransaction(signed_tx.rawTransaction) 375 | logger.info("TXID: {0}".format(web3.toHex(tx_hash))) 376 | except: 377 | logger.warning("Failed sending TX to 1 inch!") 378 | return False 379 | else: 380 | logger.info('No private key found! Transaction has not been broadcast!') 381 | 382 | 383 | def one_inch_get_quote(_from_token, _to_token, _amount): 384 | ''' 385 | Get quote data from one inch join contract using on-chain call 386 | ''' 387 | # load our contract 388 | one_inch_join = web3.eth.contract( 389 | address=config.polygon_oracle, abi=config.oracli_abi) 390 | 391 | # # load beta contract 392 | # beta_one_inch_join = web3.eth.contract( 393 | # address=beta_one_inch_split_contract, abi=beta_one_inch_split_abi) 394 | 395 | # make call request to contract on the Ethereum blockchain 396 | contract_response = one_inch_join.functions.getExpectedReturn( 397 | _from_token, _to_token, _amount, 100, 0).call({'from': wallet_address}) 398 | 399 | ''' 400 | work in progress. I'm not sure that it's safe to get quotes onchain yet though 401 | https://github.com/CryptoManiacsZone/1inchProtocol 402 | The sequence of number of pieces source volume could be splitted (Works like granularity, 403 | higly affects gas usage. Should be called offchain, but could be called onchain if user swaps not his own funds, 404 | but this is still considered as not safe) 405 | ''' 406 | parts = 10 # still not 100% what parts means here. I _think_ maybe it maps to total number of exchanges to use 407 | # static for now, might be better if it was dynamic 408 | gas_price = web3.toWei('100', 'gwei') 409 | 410 | beta_contract_response = beta_one_inch_join.functions.getExpectedReturnWithGas( 411 | _from_token, _to_token, _amount, parts, 0, gas_price * 412 | contract_response[0] 413 | ).call({'from': wallet_address}) 414 | 415 | logger.info("contract response: {0}".format(contract_response)) 416 | logger.info("beta contract response: {0}".format(beta_contract_response)) 417 | return contract_response 418 | 419 | 420 | def connect_to_ETH_provider(): 421 | try: 422 | web3 = Web3(Web3.HTTPProvider(eth_provider_url)) 423 | except Exception as e: 424 | logger.warning( 425 | "There is an issue with your initial connection to Ethereum Provider: {0}".format(e)) 426 | quit() 427 | return web3 428 | 429 | 430 | # establish web3 connection 431 | w3 = connect_to_ETH_provider() 432 | 433 | # run it! 434 | if __name__ == '__main__': 435 | main() 436 | 437 | 438 | #-----------------# 439 | # Here are some examples of single methods/functions being executed 440 | #-----------------# 441 | 442 | # Get a trade quote directly from the blockchain 443 | # response is a list like: [1533867641279495750, [0, 95, 5, 0, 0, 0, 0, 0, 0, 0]] 444 | # where first item is amount, second is a list of how your order will be distributed across exchanges 445 | # logger.info(one_inch_get_quote(ethereum, mcd_contract_address, amount_to_exchange)) 446 | 447 | #--- Making an Approval ---# 448 | # check if MCD contract has allowance for provided account to spend tokens 449 | # get_allowance(base_account) 450 | 451 | # This will approve the one inch split contract to spend to spend amount_of_dai worth of base_account's tokens 452 | # you will need to call this before trading your MCD/DAI on 1 inch. Will cost a small bit of ETH/gas 453 | # approve_ERC20(amount_of_dai) 454 | 455 | # check MCD again to confirm approval worked 456 | # get_allowance(base_account) 457 | 458 | #--- Using API to get data and make trades ---# 459 | # get_api_quote_data("DAI", "ETH", amount_to_exchange) 460 | # get_api_call_data("DAI", "ETH", amount_to_exchange) 461 | -------------------------------------------------------------------------------- /dex_cex_arb.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import logging 3 | import json 4 | from web3 import Web3 5 | import config 6 | import logging 7 | import asyncio 8 | 9 | 10 | # ENABLE LOGGING - options, DEBUG,INFO, WARNING? 11 | logging.basicConfig(level=logging.INFO, 12 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 13 | logger = logging.getLogger(__name__) 14 | 15 | # Flag if set to True, will execute live trades 16 | production = False 17 | 18 | # Permitable slippage 19 | slippage = 1 20 | 21 | # Seconds to wait between each quote request 22 | waitTime = 5 23 | 24 | # Minimum percentage between prices to trigger arbitrage 25 | min_arb_percent = 0.5 26 | 27 | # Percentage difference between the prices to trigger rebalancing 28 | rebalance_percent = 0.4 29 | 30 | # OneInch API 31 | BASE_URL = 'https://api.1inch.io/v4.0/137' 32 | 33 | # if MATIC --> USDC - (enter the amount in units Ether) 34 | trade_size = 10 35 | amount_to_exchange = Web3.toWei(trade_size, 'ether') 36 | amount_of_usdc_to_trade = trade_size * 10**6 37 | 38 | matic_address = Web3.toChecksumAddress( 39 | '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE') # MATIC Token Contract address on Polygon Network 40 | 41 | 42 | usdc_address = Web3.toChecksumAddress( 43 | '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174') # USDC Token contract address on Polygon Network 44 | 45 | # Contract abi for usdc contract on poolygon 46 | usdc_contract_abi = json.load(open('usdc_contract_abi.json', 'r')) 47 | 48 | 49 | eth_provider_url = config.ALCHEMY_URL 50 | base_account = Web3.toChecksumAddress(config.BASE_ACCOUNT) 51 | wallet_address = base_account 52 | private_key = config.PRIVATE_KEY 53 | 54 | 55 | # Alpaca API 56 | BASE_ALPACA_URL = 'https://paper-api.alpaca.markets' 57 | DATA_URL = 'https://data.alpaca.markets' 58 | HEADERS = {'APCA-API-KEY-ID': config.APCA_API_KEY_ID, 59 | 'APCA-API-SECRET-KEY': config.APCA_API_SECRET_KEY} 60 | 61 | trading_pair = 'MATICUSD' # Checking quotes and trading MATIC against USD 62 | exchange = 'FTXU' # FTXUS 63 | 64 | last_alpaca_ask_price = 0 65 | last_oneInch_market_price = 0 66 | alpaca_trade_counter = 0 67 | oneInch_trade_counter = 0 68 | 69 | 70 | async def main(): 71 | ''' 72 | These are examples of different functions in the script. 73 | Uncomment the command you want to run. 74 | ''' 75 | # Accessing the usdc contract on polygon using Web3 Library 76 | usdc_token = w3.eth.contract(address=usdc_address, abi=usdc_contract_abi) 77 | # Log the current balance of the usdc token for our wallet_address 78 | usdc_balance = usdc_token.functions.balanceOf(wallet_address).call() 79 | 80 | # Log the current balance of the MATIC token in our Alpaca account 81 | logger.info('Matic Position on Alpaca: {0}'.format(get_positions())) 82 | # Log the current Cash Balance (USD) in our Alpaca account 83 | logger.info("USD position on Alpaca: {0}".format( 84 | get_account_details()['cash'])) 85 | # Log the current balance of MATIC token in our wallet_address 86 | logger.info('Matic Position on 1 Inch: {0}'.format( 87 | Web3.fromWei(w3.eth.getBalance(wallet_address), 'ether'))) 88 | # Log the current balance of USDC token in our wallet_address. We 89 | logger.info('USD Position on 1 Inch: {0}'.format(usdc_balance/10**6)) 90 | 91 | while True: 92 | l1 = loop.create_task(get_oneInch_quote_data( 93 | matic_address, usdc_address, amount_to_exchange)) 94 | l2 = loop.create_task(get_alpaca_quote_data(trading_pair, exchange)) 95 | # Wait for the tasks to finish 96 | await asyncio.wait([l1, l2]) 97 | await check_arbitrage() 98 | # Wait for the a certain amount of time between each quote request 99 | await asyncio.sleep(waitTime) 100 | 101 | 102 | async def get_oneInch_quote_data(_from_coin, _to_coin, _amount_to_exchange): 103 | ''' 104 | Get trade quote data from 1Inch API 105 | ''' 106 | # Try to get a quote from 1Inch 107 | try: 108 | # Get the current quote response for the trading pair (MATIC/USDC) 109 | quote = requests.get( 110 | '{0}/quote?fromTokenAddress={1}&toTokenAddress={2}&amount={3}'.format(BASE_URL, _from_coin, _to_coin, _amount_to_exchange)) 111 | # Status code 200 means the request was successful 112 | if quote.status_code != 200: 113 | logger.info( 114 | "Undesirable response from 1 Inch! This is probably bad.") 115 | return False 116 | # Refer to the global variable we initialized earlier 117 | global last_oneInch_market_price 118 | # Get the current quoted price from the quote response in terms USDC (US Dollar) 119 | last_oneInch_market_price = int(quote.json()['toTokenAmount'])/10**6 120 | # Log the current quote of MATIC/USDC 121 | logger.info('OneInch Price for 10 MATIC: {0}'.format( 122 | last_oneInch_market_price)) 123 | # If there is an error, log it 124 | except Exception as e: 125 | logger.exception( 126 | "There was an issue getting trade quote from 1 Inch: {0}".format(e)) 127 | return False 128 | 129 | return last_oneInch_market_price 130 | 131 | 132 | async def get_alpaca_quote_data(trading_pair, exchange): 133 | ''' 134 | Get trade quote data from Alpaca API 135 | ''' 136 | # Try to get a quote from 1Inch 137 | try: 138 | # Get the current quote response for the trading pair (MATIC/USDC) 139 | quote = requests.get( 140 | '{0}/v1beta1/crypto/{1}/quotes/latest?exchange={2}'.format(DATA_URL, trading_pair, exchange), headers=HEADERS) 141 | # Status code 200 means the request was successful 142 | if quote.status_code != 200: 143 | logger.info( 144 | "Undesirable response from Alpaca! {}".format(quote.json())) 145 | return False 146 | # Refer to the global variable we initialized earlier 147 | global last_alpaca_ask_price 148 | # Get the latest quoted asking price from the quote response in terms US Dollar 149 | last_alpaca_ask_price = quote.json( 150 | )['quote']['ap'] * 10 # for 10 MATIC 151 | # Log the latest quote of MATICUSD 152 | logger.info('Alpaca Price for 10 MATIC: {0}'.format( 153 | last_alpaca_ask_price)) 154 | # If there is an error, log it 155 | except Exception as e: 156 | logger.exception( 157 | "There was an issue getting trade quote from Alpaca: {0}".format(e)) 158 | return False 159 | 160 | return last_alpaca_ask_price 161 | 162 | 163 | def get_oneInch_swap_data(_from_coin, _to_coin, _amount_to_exchange): 164 | ''' 165 | Get call data from 1Inch API 166 | ''' 167 | try: 168 | call_data = requests.get( 169 | '{0}/swap?fromTokenAddress={1}&toTokenAddress={2}&amount={3}&fromAddress={4}&slippage={5}'.format(BASE_URL, _from_coin, _to_coin, _amount_to_exchange, wallet_address, slippage)) 170 | logger.info('response from 1 inch generic call_data request - status code: {0}'.format( 171 | call_data.status_code)) 172 | if call_data.status_code != 200: 173 | logger.info(call_data.json()['description']) 174 | return False 175 | call_data = call_data.json() 176 | nonce = w3.eth.getTransactionCount(wallet_address) 177 | tx = { 178 | 'from': call_data['tx']['from'], 179 | 'nonce': nonce, 180 | 'to': Web3.toChecksumAddress(call_data['tx']['to']), 181 | 'chainId': 137, 182 | 'value': int(call_data['tx']['value']), 183 | 'gasPrice': w3.toWei(call_data['tx']['gasPrice'], 'wei'), 184 | 'data': call_data['tx']['data'], 185 | 'gas': call_data['tx']['gas'] 186 | } 187 | # tx = call_data['tx'] 188 | # tx['nonce'] = nonce # Adding nonce to tx data 189 | 190 | # logger.info('get_api_call_data: {0}'.format(call_data)) 191 | 192 | except Exception as e: 193 | logger.warning( 194 | "There was a issue getting get contract call data from 1 inch: {0}".format(e)) 195 | return False 196 | 197 | return tx 198 | 199 | 200 | def get_account_details(): 201 | ''' 202 | Get Alpaca Trading Account Details 203 | ''' 204 | try: 205 | account = requests.get( 206 | '{0}/v2/account'.format(BASE_ALPACA_URL), headers=HEADERS) 207 | if account.status_code != 200: 208 | logger.info( 209 | "Undesirable response from Alpaca! {}".format(account.json())) 210 | return False 211 | except Exception as e: 212 | logger.exception( 213 | "There was an issue getting account details from Alpaca: {0}".format(e)) 214 | return False 215 | return account.json() 216 | 217 | 218 | def get_open_orders(): 219 | ''' 220 | Get open orders 221 | ''' 222 | try: 223 | open_orders = requests.get( 224 | '{0}/v2/orders'.format(BASE_URL), headers=HEADERS) 225 | logger.info('Alpaca open orders reply status code: {0}'.format( 226 | open_orders.status_code)) 227 | if open_orders.status_code != 200: 228 | logger.info( 229 | "Undesirable response from Alpaca! {}".format(open_orders.json())) 230 | return False 231 | logger.info('get_open_orders: {0}'.format(open_orders.json())) 232 | except Exception as e: 233 | logger.exception( 234 | "There was an issue getting open orders from Alpaca: {0}".format(e)) 235 | return False 236 | return open_orders.json() 237 | 238 | # Get current MATIC position on Alpaca 239 | 240 | 241 | def get_positions(): 242 | ''' 243 | Get positions 244 | ''' 245 | try: 246 | positions = requests.get( 247 | '{0}/v2/positions'.format(BASE_ALPACA_URL), headers=HEADERS) 248 | # logger.info('Alpaca positions reply status code: {0}'.format( 249 | # positions.status_code)) 250 | if positions.status_code != 200: 251 | logger.info( 252 | "Undesirable response from Alpaca! {}".format(positions.json())) 253 | return False 254 | # positions = positions[0] 255 | matic_position = positions.json()[0]['qty'] 256 | # logger.info('Matic Position on Alpaca: {0}'.format(matic_position)) 257 | except Exception as e: 258 | logger.exception( 259 | "There was an issue getting positions from Alpaca: {0}".format(e)) 260 | return False 261 | return matic_position 262 | 263 | 264 | # Post and Order to Alpaca 265 | def post_Alpaca_order(symbol, qty, side, type, time_in_force): 266 | ''' 267 | Post an order to Alpaca 268 | ''' 269 | try: 270 | order = requests.post( 271 | '{0}/v2/orders'.format(BASE_ALPACA_URL), headers=HEADERS, json={ 272 | 'symbol': symbol, 273 | 'qty': qty, 274 | 'side': side, 275 | 'type': type, 276 | 'time_in_force': time_in_force, 277 | }) 278 | logger.info('Alpaca order reply status code: {0}'.format( 279 | order.status_code)) 280 | if order.status_code != 200: 281 | logger.info( 282 | "Undesirable response from Alpaca! {}".format(order.json())) 283 | return False 284 | except Exception as e: 285 | logger.exception( 286 | "There was an issue posting order to Alpaca: {0}".format(e)) 287 | return False 288 | return order.json() 289 | 290 | # Establish connection to the WEB3 provider 291 | 292 | 293 | def connect_to_ETH_provider(): 294 | try: 295 | web3 = Web3(Web3.HTTPProvider(eth_provider_url)) 296 | except Exception as e: 297 | logger.warning( 298 | "There is an issue with your initial connection to Ethereum Provider: {0}".format(e)) 299 | quit() 300 | return web3 301 | 302 | 303 | # Sign and send txns to the blockchain 304 | async def signAndSendTransaction(transaction_data): 305 | try: 306 | txn = w3.eth.account.signTransaction(transaction_data, private_key) 307 | tx_hash = w3.eth.sendRawTransaction(txn.rawTransaction) 308 | logger.info( 309 | '1Inch Txn can be found at https://polygonscan.com/tx/{0}'.format(Web3.toHex(tx_hash))) 310 | tx_receipt = await w3.eth.wait_for_transaction_receipt(Web3.toHex(tx_hash)) 311 | if tx_receipt.json()['status'] == 1: 312 | logger.info('1Inch Transaction went through!') 313 | return True 314 | else: 315 | logger.info("1Inch Transaction failed!") 316 | return False 317 | except Exception as e: 318 | logger.warning( 319 | "There is an issue sending transaction to the blockchain: {0}".format(e)) 320 | return False 321 | 322 | 323 | # Check for Arbitrage opportunities 324 | async def check_arbitrage(): 325 | logger.info('Checking for arbitrage opportunities') 326 | rebalance = needs_rebalancing() 327 | global alpaca_trade_counter 328 | global oneInch_trade_counter 329 | # if the current price at alpaca is greater than the current price at 1inch by a given arb % and we do not need a rebalnce 330 | # then we have an arbitrage opportunity. In this case we will buy on 1Inch and sell on Alpaca 331 | if (last_alpaca_ask_price > last_oneInch_market_price * (1 + min_arb_percent/100) and rebalance != True): 332 | logger.info('Selling on ALPACA, Buying on 1Inch') 333 | if production: 334 | sell_order = post_Alpaca_order( 335 | trading_pair, trade_size, 'sell', 'market', 'gtc') 336 | # if the above sell order goes through we will subtract 1 from alpaca trade counter 337 | if sell_order['status'] == 'accepted': 338 | alpaca_trade_counter -= 1 339 | # Only buy on oneInch if our sell txn on alpaca goes through 340 | # To buy 10 MATIC, we multiply its price by 10 (amount to exchnage) and then futher multiply it by 10^6 to get USDC value 341 | buy_order_data = get_oneInch_swap_data( 342 | usdc_address, matic_address, last_oneInch_market_price*amount_of_usdc_to_trade) 343 | buy_order = await signAndSendTransaction(buy_order_data) 344 | if buy_order == True: 345 | oneInch_trade_counter += 1 346 | # If the current price at alpaca is less than the current price at 1inch by a given arb % and we do not need a rebalnce 347 | # then we have an arbitrage opportunity. In this case we will buy on Alpaca and sell on 1Inch 348 | elif (last_alpaca_ask_price < last_oneInch_market_price * (1 - min_arb_percent/100) and rebalance != True): 349 | logger.info('Buying on ALPACA, Selling on 1Inch') 350 | if production: 351 | buy_order = post_Alpaca_order( 352 | trading_pair, 10, 'buy', 'market', 'gtc') 353 | # if the above buy order goes through we will add 1 to alpaca trade counter 354 | if buy_order['status'] == 'accepted': 355 | alpaca_trade_counter += 1 356 | # Only sell on oneInch if our buy txn on alpaca goes through 357 | # To sell 10 MATIC, we pass it amount to exchnage 358 | sell_order_data = get_oneInch_swap_data( 359 | matic_address, usdc_address, amount_to_exchange) 360 | sell_order = await signAndSendTransaction(sell_order_data) 361 | if sell_order == True: 362 | oneInch_trade_counter -= 1 363 | # If neither of the above conditions are met then either there is no arbitrage opportunity found and/or we need to rebalance 364 | else: 365 | if rebalance: 366 | await rebalancing() 367 | else: 368 | logger.info('No arbitrage opportunity available') 369 | pass 370 | 371 | # Function to check if we need to rebalance -> might need to add more conditions here 372 | 373 | 374 | def needs_rebalancing(): 375 | # Get current MATIC positions on both exchanges 376 | current_matic_alpaca = int(get_positions()) 377 | current_matic_1Inch = int(Web3.fromWei( 378 | w3.eth.getBalance(wallet_address), 'ether')) 379 | # If the current amount of MATIC on Alpaca minus the trade size (10) is greater than 0 then we are good enough to trade on Alpaca 380 | if current_matic_alpaca - 10 < 0 or current_matic_1Inch - 10 < 0: 381 | logger.info( 382 | 'We will have less than 10 MATIC on one of the exchanges if we trade. We need to rebalance.') 383 | return True 384 | # If the current trade counter on Alpaca and 1Inch is not 0 then we need to rebalance 385 | if alpaca_trade_counter != 0 or oneInch_trade_counter != 0: 386 | logger.info("We need to rebalance our positions") 387 | return True 388 | return False 389 | 390 | 391 | # Rebalance Portfolio 392 | async def rebalancing(): 393 | logger.info('Rebalancing') 394 | global alpaca_trade_counter 395 | global oneInch_trade_counter 396 | 397 | # Get current MATIC positions on both exchanges 398 | current_matic_alpaca = get_positions() 399 | current_matic_1Inch = Web3.fromWei( 400 | w3.eth.getBalance(wallet_address), 'ether') 401 | # Only execute rebalancing trades if production is true (we are live) 402 | 403 | if (current_matic_alpaca - 10 > 0 and alpaca_trade_counter != 0): 404 | if (alpaca_trade_counter > 0 and last_alpaca_ask_price < last_oneInch_market_price * (1 + rebalance_percent/100)): 405 | logger.info('Rebalancing Alpaca side by selling on Alpaca') 406 | if production: 407 | sell_order = post_Alpaca_order( 408 | trading_pair, 10, 'sell', 'market', 'gtc') 409 | if sell_order['status'] == 'accepted': 410 | alpaca_trade_counter -= 1 411 | elif(alpaca_trade_counter < 0 and last_alpaca_ask_price > last_oneInch_market_price * (1 - rebalance_percent/100)): 412 | logger.info('Rebalancing Alpaca side by buying on Alpaca') 413 | if production: 414 | buy_order = post_Alpaca_order( 415 | trading_pair, 10, 'buy', 'market', 'gtc') 416 | if buy_order['status'] == 'accepted': 417 | alpaca_trade_counter += 1 418 | 419 | if current_matic_alpaca - 10 < 0 and alpaca_trade_counter != 0: 420 | logger.info( 421 | 'Unable to rebalance Alpaca side due to insufficient funds') 422 | 423 | if current_matic_1Inch - 10 > 0 and oneInch_trade_counter != 0: 424 | if (oneInch_trade_counter > 0 and last_oneInch_market_price < last_alpaca_ask_price * (1 + rebalance_percent/100)): 425 | logger.info('Rebalancing oneInch side by selling on oneInch') 426 | if production: 427 | sell_order_data = get_oneInch_swap_data( 428 | matic_address, usdc_address, amount_to_exchange) 429 | sell_order = await signAndSendTransaction(sell_order_data) 430 | if sell_order == True: 431 | oneInch_trade_counter -= 1 432 | elif(oneInch_trade_counter < 0 and last_oneInch_market_price > last_alpaca_ask_price * (1 - rebalance_percent/100)): 433 | logger.info('Rebalancing oneInch side by buying on oneInch') 434 | if production: 435 | buy_order_data = get_oneInch_swap_data( 436 | usdc_address, matic_address, amount_to_exchange) 437 | buy_order = await signAndSendTransaction(buy_order_data) 438 | if sell_order == True: 439 | oneInch_trade_counter += 1 440 | if current_matic_1Inch - 10 < 0 and oneInch_trade_counter != 0: 441 | logger.info( 442 | 'Unable to rebalance oneInch side due to insufficient funds') 443 | 444 | pass 445 | 446 | 447 | def get_allowance(_token): 448 | ''' 449 | Get allowance for a given token, the 1inch router is allowed to spend 450 | ''' 451 | try: 452 | allowance = requests.get( 453 | '{0}/approve/allowance?tokenAddress={1}&walletAddress={2}'.format(BASE_URL, _token, wallet_address)) 454 | logger.info('1inch allowance reply status code: {0}'.format( 455 | allowance.status_code)) 456 | if allowance.status_code != 200: 457 | logger.info( 458 | "Undesirable response from 1 Inch! This is probably bad.") 459 | return False 460 | logger.info('get_allowance: {0}'.format(allowance.json())) 461 | 462 | except Exception as e: 463 | logger.exception( 464 | "There was an issue getting allowance for the token from 1 Inch: {0}".format(e)) 465 | return False 466 | logger.info("allowance: {0}".format(allowance)) 467 | return allowance.json() 468 | 469 | 470 | def approve_ERC20(_amount_of_ERC): 471 | ''' 472 | Creating transaction data to approve 1 Inch router to spend _amount_of_ERC worth of our USDC tokens 473 | ''' 474 | allowance_before = get_allowance(wallet_address) 475 | logger.info("allowance before: {0}".format(allowance_before)) 476 | 477 | try: 478 | approve_txn = requests.get( 479 | '{0}/approve/transaction?tokenAddress={1}&amount={2}'.format(BASE_URL, usdc_address, _amount_of_ERC)) 480 | logger.info('1inch allowance reply status code: {0}'.format( 481 | approve_txn.status_code)) 482 | if approve_txn.status_code != 200: 483 | logger.info( 484 | "Undesirable response from 1 Inch! This is probably bad.") 485 | return False 486 | logger.info('get_allowance: {0}'.format(approve_txn.json())) 487 | 488 | except Exception as e: 489 | logger.exception( 490 | "There was an issue allowing usdc spend from 1 Inch: {0}".format(e)) 491 | return False 492 | 493 | approve_txn = approve_txn.json() 494 | nonce = w3.eth.getTransactionCount(wallet_address) 495 | 496 | print("gas price is:", w3.fromWei(int(approve_txn['gasPrice']), 'gwei')) 497 | 498 | tx = { 499 | 'nonce': nonce, 500 | 'to': usdc_address, 501 | 'chainId': 137, 502 | # 'value': approve_txn['value'], 503 | 'gasPrice': w3.toWei(70, 'gwei'), 504 | 'from': wallet_address, 505 | 'data': approve_txn['data'] 506 | } 507 | 508 | tx['gas'] = 80000 509 | return tx 510 | 511 | 512 | # establish web3 connection 513 | w3 = connect_to_ETH_provider() 514 | 515 | # Initial Matic Balance on Alpaca and 1Inch 516 | initial_matic_alpaca = get_positions() 517 | initial_matic_1inch = Web3.fromWei(w3.eth.getBalance(wallet_address), 'ether') 518 | 519 | 520 | loop = asyncio.get_event_loop() 521 | loop.run_until_complete(main()) 522 | loop.close() 523 | # run it! 524 | # if __name__ == '__main__': 525 | # main() 526 | -------------------------------------------------------------------------------- /usdc_contract_abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "address", 8 | "name": "owner", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": true, 13 | "internalType": "address", 14 | "name": "spender", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": false, 19 | "internalType": "uint256", 20 | "name": "value", 21 | "type": "uint256" 22 | } 23 | ], 24 | "name": "Approval", 25 | "type": "event" 26 | }, 27 | { 28 | "anonymous": false, 29 | "inputs": [ 30 | { 31 | "indexed": true, 32 | "internalType": "address", 33 | "name": "authorizer", 34 | "type": "address" 35 | }, 36 | { 37 | "indexed": true, 38 | "internalType": "bytes32", 39 | "name": "nonce", 40 | "type": "bytes32" 41 | } 42 | ], 43 | "name": "AuthorizationCanceled", 44 | "type": "event" 45 | }, 46 | { 47 | "anonymous": false, 48 | "inputs": [ 49 | { 50 | "indexed": true, 51 | "internalType": "address", 52 | "name": "authorizer", 53 | "type": "address" 54 | }, 55 | { 56 | "indexed": true, 57 | "internalType": "bytes32", 58 | "name": "nonce", 59 | "type": "bytes32" 60 | } 61 | ], 62 | "name": "AuthorizationUsed", 63 | "type": "event" 64 | }, 65 | { 66 | "anonymous": false, 67 | "inputs": [ 68 | { 69 | "indexed": true, 70 | "internalType": "address", 71 | "name": "account", 72 | "type": "address" 73 | } 74 | ], 75 | "name": "Blacklisted", 76 | "type": "event" 77 | }, 78 | { 79 | "anonymous": false, 80 | "inputs": [ 81 | { 82 | "indexed": false, 83 | "internalType": "address", 84 | "name": "userAddress", 85 | "type": "address" 86 | }, 87 | { 88 | "indexed": false, 89 | "internalType": "address payable", 90 | "name": "relayerAddress", 91 | "type": "address" 92 | }, 93 | { 94 | "indexed": false, 95 | "internalType": "bytes", 96 | "name": "functionSignature", 97 | "type": "bytes" 98 | } 99 | ], 100 | "name": "MetaTransactionExecuted", 101 | "type": "event" 102 | }, 103 | { "anonymous": false, "inputs": [], "name": "Pause", "type": "event" }, 104 | { 105 | "anonymous": false, 106 | "inputs": [ 107 | { 108 | "indexed": true, 109 | "internalType": "address", 110 | "name": "newRescuer", 111 | "type": "address" 112 | } 113 | ], 114 | "name": "RescuerChanged", 115 | "type": "event" 116 | }, 117 | { 118 | "anonymous": false, 119 | "inputs": [ 120 | { 121 | "indexed": true, 122 | "internalType": "bytes32", 123 | "name": "role", 124 | "type": "bytes32" 125 | }, 126 | { 127 | "indexed": true, 128 | "internalType": "bytes32", 129 | "name": "previousAdminRole", 130 | "type": "bytes32" 131 | }, 132 | { 133 | "indexed": true, 134 | "internalType": "bytes32", 135 | "name": "newAdminRole", 136 | "type": "bytes32" 137 | } 138 | ], 139 | "name": "RoleAdminChanged", 140 | "type": "event" 141 | }, 142 | { 143 | "anonymous": false, 144 | "inputs": [ 145 | { 146 | "indexed": true, 147 | "internalType": "bytes32", 148 | "name": "role", 149 | "type": "bytes32" 150 | }, 151 | { 152 | "indexed": true, 153 | "internalType": "address", 154 | "name": "account", 155 | "type": "address" 156 | }, 157 | { 158 | "indexed": true, 159 | "internalType": "address", 160 | "name": "sender", 161 | "type": "address" 162 | } 163 | ], 164 | "name": "RoleGranted", 165 | "type": "event" 166 | }, 167 | { 168 | "anonymous": false, 169 | "inputs": [ 170 | { 171 | "indexed": true, 172 | "internalType": "bytes32", 173 | "name": "role", 174 | "type": "bytes32" 175 | }, 176 | { 177 | "indexed": true, 178 | "internalType": "address", 179 | "name": "account", 180 | "type": "address" 181 | }, 182 | { 183 | "indexed": true, 184 | "internalType": "address", 185 | "name": "sender", 186 | "type": "address" 187 | } 188 | ], 189 | "name": "RoleRevoked", 190 | "type": "event" 191 | }, 192 | { 193 | "anonymous": false, 194 | "inputs": [ 195 | { 196 | "indexed": true, 197 | "internalType": "address", 198 | "name": "from", 199 | "type": "address" 200 | }, 201 | { 202 | "indexed": true, 203 | "internalType": "address", 204 | "name": "to", 205 | "type": "address" 206 | }, 207 | { 208 | "indexed": false, 209 | "internalType": "uint256", 210 | "name": "value", 211 | "type": "uint256" 212 | } 213 | ], 214 | "name": "Transfer", 215 | "type": "event" 216 | }, 217 | { 218 | "anonymous": false, 219 | "inputs": [ 220 | { 221 | "indexed": true, 222 | "internalType": "address", 223 | "name": "account", 224 | "type": "address" 225 | } 226 | ], 227 | "name": "UnBlacklisted", 228 | "type": "event" 229 | }, 230 | { "anonymous": false, "inputs": [], "name": "Unpause", "type": "event" }, 231 | { 232 | "inputs": [], 233 | "name": "APPROVE_WITH_AUTHORIZATION_TYPEHASH", 234 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 235 | "stateMutability": "view", 236 | "type": "function" 237 | }, 238 | { 239 | "inputs": [], 240 | "name": "BLACKLISTER_ROLE", 241 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 242 | "stateMutability": "view", 243 | "type": "function" 244 | }, 245 | { 246 | "inputs": [], 247 | "name": "CANCEL_AUTHORIZATION_TYPEHASH", 248 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 249 | "stateMutability": "view", 250 | "type": "function" 251 | }, 252 | { 253 | "inputs": [], 254 | "name": "DECREASE_ALLOWANCE_WITH_AUTHORIZATION_TYPEHASH", 255 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 256 | "stateMutability": "view", 257 | "type": "function" 258 | }, 259 | { 260 | "inputs": [], 261 | "name": "DEFAULT_ADMIN_ROLE", 262 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 263 | "stateMutability": "view", 264 | "type": "function" 265 | }, 266 | { 267 | "inputs": [], 268 | "name": "DEPOSITOR_ROLE", 269 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 270 | "stateMutability": "view", 271 | "type": "function" 272 | }, 273 | { 274 | "inputs": [], 275 | "name": "DOMAIN_SEPARATOR", 276 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 277 | "stateMutability": "view", 278 | "type": "function" 279 | }, 280 | { 281 | "inputs": [], 282 | "name": "EIP712_VERSION", 283 | "outputs": [{ "internalType": "string", "name": "", "type": "string" }], 284 | "stateMutability": "view", 285 | "type": "function" 286 | }, 287 | { 288 | "inputs": [], 289 | "name": "INCREASE_ALLOWANCE_WITH_AUTHORIZATION_TYPEHASH", 290 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 291 | "stateMutability": "view", 292 | "type": "function" 293 | }, 294 | { 295 | "inputs": [], 296 | "name": "META_TRANSACTION_TYPEHASH", 297 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 298 | "stateMutability": "view", 299 | "type": "function" 300 | }, 301 | { 302 | "inputs": [], 303 | "name": "PAUSER_ROLE", 304 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 305 | "stateMutability": "view", 306 | "type": "function" 307 | }, 308 | { 309 | "inputs": [], 310 | "name": "PERMIT_TYPEHASH", 311 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 312 | "stateMutability": "view", 313 | "type": "function" 314 | }, 315 | { 316 | "inputs": [], 317 | "name": "RESCUER_ROLE", 318 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 319 | "stateMutability": "view", 320 | "type": "function" 321 | }, 322 | { 323 | "inputs": [], 324 | "name": "TRANSFER_WITH_AUTHORIZATION_TYPEHASH", 325 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 326 | "stateMutability": "view", 327 | "type": "function" 328 | }, 329 | { 330 | "inputs": [], 331 | "name": "WITHDRAW_WITH_AUTHORIZATION_TYPEHASH", 332 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 333 | "stateMutability": "view", 334 | "type": "function" 335 | }, 336 | { 337 | "inputs": [ 338 | { "internalType": "address", "name": "owner", "type": "address" }, 339 | { "internalType": "address", "name": "spender", "type": "address" } 340 | ], 341 | "name": "allowance", 342 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 343 | "stateMutability": "view", 344 | "type": "function" 345 | }, 346 | { 347 | "inputs": [ 348 | { "internalType": "address", "name": "spender", "type": "address" }, 349 | { "internalType": "uint256", "name": "amount", "type": "uint256" } 350 | ], 351 | "name": "approve", 352 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 353 | "stateMutability": "nonpayable", 354 | "type": "function" 355 | }, 356 | { 357 | "inputs": [ 358 | { "internalType": "address", "name": "owner", "type": "address" }, 359 | { "internalType": "address", "name": "spender", "type": "address" }, 360 | { "internalType": "uint256", "name": "value", "type": "uint256" }, 361 | { "internalType": "uint256", "name": "validAfter", "type": "uint256" }, 362 | { "internalType": "uint256", "name": "validBefore", "type": "uint256" }, 363 | { "internalType": "bytes32", "name": "nonce", "type": "bytes32" }, 364 | { "internalType": "uint8", "name": "v", "type": "uint8" }, 365 | { "internalType": "bytes32", "name": "r", "type": "bytes32" }, 366 | { "internalType": "bytes32", "name": "s", "type": "bytes32" } 367 | ], 368 | "name": "approveWithAuthorization", 369 | "outputs": [], 370 | "stateMutability": "nonpayable", 371 | "type": "function" 372 | }, 373 | { 374 | "inputs": [ 375 | { "internalType": "address", "name": "authorizer", "type": "address" }, 376 | { "internalType": "bytes32", "name": "nonce", "type": "bytes32" } 377 | ], 378 | "name": "authorizationState", 379 | "outputs": [ 380 | { 381 | "internalType": "enum GasAbstraction.AuthorizationState", 382 | "name": "", 383 | "type": "uint8" 384 | } 385 | ], 386 | "stateMutability": "view", 387 | "type": "function" 388 | }, 389 | { 390 | "inputs": [ 391 | { "internalType": "address", "name": "account", "type": "address" } 392 | ], 393 | "name": "balanceOf", 394 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 395 | "stateMutability": "view", 396 | "type": "function" 397 | }, 398 | { 399 | "inputs": [ 400 | { "internalType": "address", "name": "account", "type": "address" } 401 | ], 402 | "name": "blacklist", 403 | "outputs": [], 404 | "stateMutability": "nonpayable", 405 | "type": "function" 406 | }, 407 | { 408 | "inputs": [], 409 | "name": "blacklisters", 410 | "outputs": [ 411 | { "internalType": "address[]", "name": "", "type": "address[]" } 412 | ], 413 | "stateMutability": "view", 414 | "type": "function" 415 | }, 416 | { 417 | "inputs": [ 418 | { "internalType": "address", "name": "authorizer", "type": "address" }, 419 | { "internalType": "bytes32", "name": "nonce", "type": "bytes32" }, 420 | { "internalType": "uint8", "name": "v", "type": "uint8" }, 421 | { "internalType": "bytes32", "name": "r", "type": "bytes32" }, 422 | { "internalType": "bytes32", "name": "s", "type": "bytes32" } 423 | ], 424 | "name": "cancelAuthorization", 425 | "outputs": [], 426 | "stateMutability": "nonpayable", 427 | "type": "function" 428 | }, 429 | { 430 | "inputs": [], 431 | "name": "decimals", 432 | "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], 433 | "stateMutability": "view", 434 | "type": "function" 435 | }, 436 | { 437 | "inputs": [ 438 | { "internalType": "address", "name": "spender", "type": "address" }, 439 | { 440 | "internalType": "uint256", 441 | "name": "subtractedValue", 442 | "type": "uint256" 443 | } 444 | ], 445 | "name": "decreaseAllowance", 446 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 447 | "stateMutability": "nonpayable", 448 | "type": "function" 449 | }, 450 | { 451 | "inputs": [ 452 | { "internalType": "address", "name": "owner", "type": "address" }, 453 | { "internalType": "address", "name": "spender", "type": "address" }, 454 | { "internalType": "uint256", "name": "decrement", "type": "uint256" }, 455 | { "internalType": "uint256", "name": "validAfter", "type": "uint256" }, 456 | { "internalType": "uint256", "name": "validBefore", "type": "uint256" }, 457 | { "internalType": "bytes32", "name": "nonce", "type": "bytes32" }, 458 | { "internalType": "uint8", "name": "v", "type": "uint8" }, 459 | { "internalType": "bytes32", "name": "r", "type": "bytes32" }, 460 | { "internalType": "bytes32", "name": "s", "type": "bytes32" } 461 | ], 462 | "name": "decreaseAllowanceWithAuthorization", 463 | "outputs": [], 464 | "stateMutability": "nonpayable", 465 | "type": "function" 466 | }, 467 | { 468 | "inputs": [ 469 | { "internalType": "address", "name": "user", "type": "address" }, 470 | { "internalType": "bytes", "name": "depositData", "type": "bytes" } 471 | ], 472 | "name": "deposit", 473 | "outputs": [], 474 | "stateMutability": "nonpayable", 475 | "type": "function" 476 | }, 477 | { 478 | "inputs": [ 479 | { "internalType": "address", "name": "userAddress", "type": "address" }, 480 | { "internalType": "bytes", "name": "functionSignature", "type": "bytes" }, 481 | { "internalType": "bytes32", "name": "sigR", "type": "bytes32" }, 482 | { "internalType": "bytes32", "name": "sigS", "type": "bytes32" }, 483 | { "internalType": "uint8", "name": "sigV", "type": "uint8" } 484 | ], 485 | "name": "executeMetaTransaction", 486 | "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], 487 | "stateMutability": "payable", 488 | "type": "function" 489 | }, 490 | { 491 | "inputs": [ 492 | { "internalType": "bytes32", "name": "role", "type": "bytes32" } 493 | ], 494 | "name": "getRoleAdmin", 495 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 496 | "stateMutability": "view", 497 | "type": "function" 498 | }, 499 | { 500 | "inputs": [ 501 | { "internalType": "bytes32", "name": "role", "type": "bytes32" }, 502 | { "internalType": "uint256", "name": "index", "type": "uint256" } 503 | ], 504 | "name": "getRoleMember", 505 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }], 506 | "stateMutability": "view", 507 | "type": "function" 508 | }, 509 | { 510 | "inputs": [ 511 | { "internalType": "bytes32", "name": "role", "type": "bytes32" } 512 | ], 513 | "name": "getRoleMemberCount", 514 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 515 | "stateMutability": "view", 516 | "type": "function" 517 | }, 518 | { 519 | "inputs": [ 520 | { "internalType": "bytes32", "name": "role", "type": "bytes32" }, 521 | { "internalType": "address", "name": "account", "type": "address" } 522 | ], 523 | "name": "grantRole", 524 | "outputs": [], 525 | "stateMutability": "nonpayable", 526 | "type": "function" 527 | }, 528 | { 529 | "inputs": [ 530 | { "internalType": "bytes32", "name": "role", "type": "bytes32" }, 531 | { "internalType": "address", "name": "account", "type": "address" } 532 | ], 533 | "name": "hasRole", 534 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 535 | "stateMutability": "view", 536 | "type": "function" 537 | }, 538 | { 539 | "inputs": [ 540 | { "internalType": "address", "name": "spender", "type": "address" }, 541 | { "internalType": "uint256", "name": "addedValue", "type": "uint256" } 542 | ], 543 | "name": "increaseAllowance", 544 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 545 | "stateMutability": "nonpayable", 546 | "type": "function" 547 | }, 548 | { 549 | "inputs": [ 550 | { "internalType": "address", "name": "owner", "type": "address" }, 551 | { "internalType": "address", "name": "spender", "type": "address" }, 552 | { "internalType": "uint256", "name": "increment", "type": "uint256" }, 553 | { "internalType": "uint256", "name": "validAfter", "type": "uint256" }, 554 | { "internalType": "uint256", "name": "validBefore", "type": "uint256" }, 555 | { "internalType": "bytes32", "name": "nonce", "type": "bytes32" }, 556 | { "internalType": "uint8", "name": "v", "type": "uint8" }, 557 | { "internalType": "bytes32", "name": "r", "type": "bytes32" }, 558 | { "internalType": "bytes32", "name": "s", "type": "bytes32" } 559 | ], 560 | "name": "increaseAllowanceWithAuthorization", 561 | "outputs": [], 562 | "stateMutability": "nonpayable", 563 | "type": "function" 564 | }, 565 | { 566 | "inputs": [ 567 | { "internalType": "string", "name": "newName", "type": "string" }, 568 | { "internalType": "string", "name": "newSymbol", "type": "string" }, 569 | { "internalType": "uint8", "name": "newDecimals", "type": "uint8" }, 570 | { 571 | "internalType": "address", 572 | "name": "childChainManager", 573 | "type": "address" 574 | } 575 | ], 576 | "name": "initialize", 577 | "outputs": [], 578 | "stateMutability": "nonpayable", 579 | "type": "function" 580 | }, 581 | { 582 | "inputs": [], 583 | "name": "initialized", 584 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 585 | "stateMutability": "view", 586 | "type": "function" 587 | }, 588 | { 589 | "inputs": [ 590 | { "internalType": "address", "name": "account", "type": "address" } 591 | ], 592 | "name": "isBlacklisted", 593 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 594 | "stateMutability": "view", 595 | "type": "function" 596 | }, 597 | { 598 | "inputs": [], 599 | "name": "name", 600 | "outputs": [{ "internalType": "string", "name": "", "type": "string" }], 601 | "stateMutability": "view", 602 | "type": "function" 603 | }, 604 | { 605 | "inputs": [ 606 | { "internalType": "address", "name": "owner", "type": "address" } 607 | ], 608 | "name": "nonces", 609 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 610 | "stateMutability": "view", 611 | "type": "function" 612 | }, 613 | { 614 | "inputs": [], 615 | "name": "pause", 616 | "outputs": [], 617 | "stateMutability": "nonpayable", 618 | "type": "function" 619 | }, 620 | { 621 | "inputs": [], 622 | "name": "paused", 623 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 624 | "stateMutability": "view", 625 | "type": "function" 626 | }, 627 | { 628 | "inputs": [], 629 | "name": "pausers", 630 | "outputs": [ 631 | { "internalType": "address[]", "name": "", "type": "address[]" } 632 | ], 633 | "stateMutability": "view", 634 | "type": "function" 635 | }, 636 | { 637 | "inputs": [ 638 | { "internalType": "address", "name": "owner", "type": "address" }, 639 | { "internalType": "address", "name": "spender", "type": "address" }, 640 | { "internalType": "uint256", "name": "value", "type": "uint256" }, 641 | { "internalType": "uint256", "name": "deadline", "type": "uint256" }, 642 | { "internalType": "uint8", "name": "v", "type": "uint8" }, 643 | { "internalType": "bytes32", "name": "r", "type": "bytes32" }, 644 | { "internalType": "bytes32", "name": "s", "type": "bytes32" } 645 | ], 646 | "name": "permit", 647 | "outputs": [], 648 | "stateMutability": "nonpayable", 649 | "type": "function" 650 | }, 651 | { 652 | "inputs": [ 653 | { "internalType": "bytes32", "name": "role", "type": "bytes32" }, 654 | { "internalType": "address", "name": "account", "type": "address" } 655 | ], 656 | "name": "renounceRole", 657 | "outputs": [], 658 | "stateMutability": "nonpayable", 659 | "type": "function" 660 | }, 661 | { 662 | "inputs": [ 663 | { 664 | "internalType": "contract IERC20", 665 | "name": "tokenContract", 666 | "type": "address" 667 | }, 668 | { "internalType": "address", "name": "to", "type": "address" }, 669 | { "internalType": "uint256", "name": "amount", "type": "uint256" } 670 | ], 671 | "name": "rescueERC20", 672 | "outputs": [], 673 | "stateMutability": "nonpayable", 674 | "type": "function" 675 | }, 676 | { 677 | "inputs": [], 678 | "name": "rescuers", 679 | "outputs": [ 680 | { "internalType": "address[]", "name": "", "type": "address[]" } 681 | ], 682 | "stateMutability": "view", 683 | "type": "function" 684 | }, 685 | { 686 | "inputs": [ 687 | { "internalType": "bytes32", "name": "role", "type": "bytes32" }, 688 | { "internalType": "address", "name": "account", "type": "address" } 689 | ], 690 | "name": "revokeRole", 691 | "outputs": [], 692 | "stateMutability": "nonpayable", 693 | "type": "function" 694 | }, 695 | { 696 | "inputs": [], 697 | "name": "symbol", 698 | "outputs": [{ "internalType": "string", "name": "", "type": "string" }], 699 | "stateMutability": "view", 700 | "type": "function" 701 | }, 702 | { 703 | "inputs": [], 704 | "name": "totalSupply", 705 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 706 | "stateMutability": "view", 707 | "type": "function" 708 | }, 709 | { 710 | "inputs": [ 711 | { "internalType": "address", "name": "recipient", "type": "address" }, 712 | { "internalType": "uint256", "name": "amount", "type": "uint256" } 713 | ], 714 | "name": "transfer", 715 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 716 | "stateMutability": "nonpayable", 717 | "type": "function" 718 | }, 719 | { 720 | "inputs": [ 721 | { "internalType": "address", "name": "sender", "type": "address" }, 722 | { "internalType": "address", "name": "recipient", "type": "address" }, 723 | { "internalType": "uint256", "name": "amount", "type": "uint256" } 724 | ], 725 | "name": "transferFrom", 726 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 727 | "stateMutability": "nonpayable", 728 | "type": "function" 729 | }, 730 | { 731 | "inputs": [ 732 | { "internalType": "address", "name": "from", "type": "address" }, 733 | { "internalType": "address", "name": "to", "type": "address" }, 734 | { "internalType": "uint256", "name": "value", "type": "uint256" }, 735 | { "internalType": "uint256", "name": "validAfter", "type": "uint256" }, 736 | { "internalType": "uint256", "name": "validBefore", "type": "uint256" }, 737 | { "internalType": "bytes32", "name": "nonce", "type": "bytes32" }, 738 | { "internalType": "uint8", "name": "v", "type": "uint8" }, 739 | { "internalType": "bytes32", "name": "r", "type": "bytes32" }, 740 | { "internalType": "bytes32", "name": "s", "type": "bytes32" } 741 | ], 742 | "name": "transferWithAuthorization", 743 | "outputs": [], 744 | "stateMutability": "nonpayable", 745 | "type": "function" 746 | }, 747 | { 748 | "inputs": [ 749 | { "internalType": "address", "name": "account", "type": "address" } 750 | ], 751 | "name": "unBlacklist", 752 | "outputs": [], 753 | "stateMutability": "nonpayable", 754 | "type": "function" 755 | }, 756 | { 757 | "inputs": [], 758 | "name": "unpause", 759 | "outputs": [], 760 | "stateMutability": "nonpayable", 761 | "type": "function" 762 | }, 763 | { 764 | "inputs": [ 765 | { "internalType": "string", "name": "newName", "type": "string" }, 766 | { "internalType": "string", "name": "newSymbol", "type": "string" } 767 | ], 768 | "name": "updateMetadata", 769 | "outputs": [], 770 | "stateMutability": "nonpayable", 771 | "type": "function" 772 | }, 773 | { 774 | "inputs": [ 775 | { "internalType": "uint256", "name": "amount", "type": "uint256" } 776 | ], 777 | "name": "withdraw", 778 | "outputs": [], 779 | "stateMutability": "nonpayable", 780 | "type": "function" 781 | }, 782 | { 783 | "inputs": [ 784 | { "internalType": "address", "name": "owner", "type": "address" }, 785 | { "internalType": "uint256", "name": "value", "type": "uint256" }, 786 | { "internalType": "uint256", "name": "validAfter", "type": "uint256" }, 787 | { "internalType": "uint256", "name": "validBefore", "type": "uint256" }, 788 | { "internalType": "bytes32", "name": "nonce", "type": "bytes32" }, 789 | { "internalType": "uint8", "name": "v", "type": "uint8" }, 790 | { "internalType": "bytes32", "name": "r", "type": "bytes32" }, 791 | { "internalType": "bytes32", "name": "s", "type": "bytes32" } 792 | ], 793 | "name": "withdrawWithAuthorization", 794 | "outputs": [], 795 | "stateMutability": "nonpayable", 796 | "type": "function" 797 | } 798 | ] 799 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## What is a DEX? 2 | 3 | A Decentralized exchange or a DEX is a peer-to-peer marketplace that facilitates transactions in a permission-less manner. DEX's use "Automated Market Maker" protocols that orchestrate trade without the need for a centralized body. 4 | 5 | ## What is a DEX Aggregator? 6 | 7 | A DEX Aggregator (eg: 1Inch) aims to provide the best prices for swaps across multiple liquidity sources (eg: Uniswap, Sushiswap, etc). Using a DEX aggregator like 1Inch helps us avoid relying on a single source for price data. This is important since 1 liquidity source alone might not be able to route the entire transaction without experiencing significant slippage. 8 | 9 | ## What is a CEX? 10 | 11 | A Centralized exchange or CEX is operated in a centralized manner by a company. Orders on a CEX are maintained in an order book where buyers and sellers place their bids and a trade executes when the bids match. 12 | 13 | ## What are we building? 14 | 15 | We are building an arbitrage bot that trades when the price of an asset is different on a Centralized exchange compared to that on a Decentralized exchange by a given percentage. You can find the source code of the bot [here.](https://github.com/akshay-rakheja/arbitrage-alpaca/blob/master/dex_cex_arb.py) 16 | 17 | This will involve: 18 | 1. Receiving quotes from a CEX and DEX asynchronously every 5 seconds 19 | 2. Checking if those prices meet the arbitrage condition 20 | 3. Executing trades if the condition is met 21 | 4. Rebalancing position on both sides if needed 22 | 23 | We will be using [Alpaca's Crypto API's](https://alpaca.markets/docs/api-references/) to get quotes and execute trades on Centralized exchanges such as FTXUS and Coinbase. On the other hand, 1Inch's API will help us get quotes and execute trades on a Decentralized exchanges like Quickswap and Uniswap. 24 | 25 | We will be using Polygon network (formerly Matic Network) to execute our trades on a decentralized exchange. The two main reasons for this are its transaction costs and speed. It costs a few cents (single digits) to execute a swap trade on Polygon while it can cost tens of dollars on Ethereum to do the same task. This will help us maximize our profits by keeping transaction costs minimal. (Note: the bot is not keeping into account the transaction costs accrued by trading on Polygon since they are quite minimal). 26 | 27 | Since we will be executing the trades on the Polygon network, it makes sense to trade one of the most liquid assets on it, 'MATIC'. 'MATIC' is the base currency for the network and is required to pay for all the transaction costs on the network. Similarly, for the Centralized exchanges, we will use Alpaca's Market Data and Trading API to execute trade on the 'MATICUSD' pair. 28 | 29 | ### Rough idea of our Arbitrage strategy 30 | 31 | We will try to execute a version of Convergence Arbitrage strategy. This strategy involves a long/short trade. Here, the bot buys the crypto on the exchange where it is underpriced ("long") and sells it on the exchange where it is overpriced ("short"). When the two prices are not deviating far enough anymore we can reverse the trades we did earlier and sell on the exchange where we went long and vice versa. 32 | 33 | 34 | ## Let's BUIDL 35 | 36 | Since the code is a little lengthy I will break it into snippets and explain them along the way. So, Let's get started! 37 | 38 | ``` 39 | import requests 40 | import logging 41 | import json 42 | from web3 import Web3 43 | import config 44 | import logging 45 | import asyncio 46 | 47 | 48 | # ENABLE LOGGING - options, DEBUG,INFO, WARNING? 49 | logging.basicConfig(level=logging.INFO, 50 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 51 | logger = logging.getLogger(__name__) 52 | 53 | ``` 54 | 55 | In the snippet above, we are importing the necessary libraries and enabling logging. Logging will keep us informed on the prices and the arbitrage condition. 56 | Then, we define a few variables that will control our arbitrage logic and quotes. 57 | 58 | ``` 59 | # Flag if set to True, will execute live trades 60 | production = False 61 | 62 | # Permissible slippage 63 | slippage = 1 64 | 65 | # Seconds to wait between each quote request 66 | waitTime = 5 67 | 68 | # Minimum percentage between prices to trigger arbitrage 69 | min_arb_percent = 0.5 70 | ``` 71 | `production` is like a safety flag that should be set to True only if you are ready to send transactions with your bot and False otherwise. `slippage` is the maximum amount of slippage that we would like when executing a trade on the DEX aggregator. `waitTime` as the name suggests is the amount of seconds we would like to wait before requesting quotes from our CEX and DEX sources. `min_arb_percent` is the minimum percentage difference we would like between the quotes to consider an arbitrage condition. Essentially, if the quotes from two sources are not at least as far apart as this percentage, then there is no arbitrage. Keep in mind, keeping `min_arb_percent` value high might lead to fewer chances of the arbitrage condition being triggered. While keeping it too low may lead to more frequent trades and a net loss due to transaction costs and slippage on the decentralized side of things. 72 | 73 | Now let's define the API parameters for both the sources (CEX and DEX). 74 | 75 | ``` 76 | # OneInch API 77 | BASE_URL = 'https://api.1inch.io/v4.0/137' 78 | 79 | # if MATIC --> USDC - (enter the amount in units Ether) 80 | trade_size = 10 81 | amount_to_exchange = Web3.toWei(trade_size, 'ether') 82 | 83 | matic_address = Web3.toChecksumAddress( 84 | '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE') # MATIC Token Contract address on Polygon Network 85 | 86 | usdc_address = Web3.toChecksumAddress( 87 | '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174') # USDC Token contract address on Polygon Network 88 | 89 | # Contract abi for usdc contract on polygon 90 | usdc_contract_abi = json.load(open('usdc_contract_abi.json', 'r')) 91 | 92 | 93 | eth_provider_url = 94 | base_account = Web3.toChecksumAddress() 95 | wallet_address = base_account 96 | private_key = 97 | ``` 98 | First, let's go over the 1Inch API parameters and relevant Web3 variables defined above. We define a `BASE_URL` for the API that stays constant for all the requests to 1Inch. 99 | `trade_size` is the amount of `MATIC` token we would like to trade. This should be at least 10 and should increment by 10. This is because Alpaca only lets you trade MATIC in multiples of 10. Then, we take the trade_size and convert it into 10 MATIC. We do this using the Web3 library that we imported earlier. `ether` is used merely as a unit in `Web3.toWei(trade_size, 'ether')` to represent MATIC as a token with 18 decimal places. You can read more on that [here.](https://ethereum.stackexchange.com/questions/38704/why-most-erc-20-tokens-have-18-decimals) 100 | Next, we initialize the contract addresses for the tokens we intend on trading, MATIC and USDC. You can find the contract addresses for these tokens on [polygonscan.com](https://polygonscan.com/tokens). Keep in mind that these addresses will be different on different chains/networks. 101 | You will see that we are importing something called 'usdc_contract_abi'. This is the contract ABI for the USDC token. Later, we will be using this to check our USDC balance in our wallet. You can find the ABI [here.](https://polygonscan.com/address/0x2791bca1f2de4661ed88a30c99a7a9449aa84174#code) 102 | Next, we will initialize our `eth_provider_url`, `base_account` and `private_key`. Provider URL is an HTTP address that you can get from a node api provider like [Alchemy](https://alchemy.com/?r=fbf1a4db9748e301). Base Account is your Wallet Address (eg: Metamask) which should start something like (0X...) and the private key as the name suggests is the corresponding private key to your wallet. Your private key should be kept secret as anyone with access to your private key has access to all your assets in the wallet. 103 | 104 | Now that we have discussed the API parameters and variables relevant to the decentralized side of things, let's take a look at the centralized side. 105 | ``` 106 | # Alpaca API 107 | BASE_ALPACA_URL = 'https://paper-api.alpaca.markets' 108 | DATA_URL = 'https://data.alpaca.markets' 109 | HEADERS = {'APCA-API-KEY-ID': , 110 | 'APCA-API-SECRET-KEY': } 111 | 112 | trading_pair = 'MATICUSD' # Checking quotes and trading MATIC against USD 113 | exchange = 'FTXU' # FTXUS 114 | 115 | last_alpaca_ask_price = 0 116 | last_oneInch_market_price = 0 117 | alpaca_trade_counter = 0 118 | oneInch_trade_counter = 0 119 | ``` 120 | The above snippet initializes the key parameters we will be using to make API calls through Alpaca. `BASE_ALPACA_URL` is used to access the trading api's that Alpaca provides. You might notice that this url has its value set to `https://paper-api.alpaca.markets`. This gets you access to a paper trading account once you register with Alpaca. It is always a good idea to try a new strategy using a paper trading account first. Once you are confident enough to trade with real money, this url can be changed to `https://api.alpaca.markets`. 121 | We will be using `DATA_URL` to get the latest quote for our trading pair. To complete a request to Alpaca we need to define its headers in a JSON format (dictionary in python). This information needs to be kept secret since anyone with access to your KeyID and Secret Key can access your Alpaca account. 122 | As we did earlier, we need to define what token we are trading. In Alpaca's case it is `MATICUSD`. This is token MATIC trading against US dollars. Then, we initialize the exchange we would like the quotes from. Here it is initialized to FTX US `FTXU`. You can change this based on the asset you are trading and its liquidity on that exchange. Alpaca makes our life better by making their [API docs](https://alpaca.markets/docs/api-references/market-data-api/) super easy to follow. 123 | Finally, we initialize the variables `last_alpaca_ask_price`, `last_oneInch_market_price`, `alpaca_trade_counter` and `oneInch_trade_counter` to 0. The last two are used to check if we need to rebalance our positions. 124 | 125 | Now that we have initialized `eth_provider_url` and imported the Web3 library, we are ready to establish a connection to blockchain. 126 | 127 | ``` 128 | def connect_to_ETH_provider(): 129 | try: 130 | web3 = Web3(Web3.HTTPProvider(eth_provider_url)) 131 | except Exception as e: 132 | logger.warning( 133 | "There is an issue with your initial connection to Ethereum Provider: {0}".format(e)) 134 | quit() 135 | return web3 136 | 137 | # establish web3 connection 138 | w3 = connect_to_ETH_provider() 139 | ``` 140 | 141 | `connect_to_ETH_provider()` uses the method `HTTPProvider()` from `web3` with `eth_provider_url` as a parameter to establish a connection with the blockchain. `w3` is an instance of the Polygon node that is returned when we establish this connection. 142 | Remember to use the provider url for Polygon chain since transaction costs are minimal. 143 | 144 | ``` 145 | async def main(): 146 | ''' 147 | These are examples of different functions in the script. 148 | Uncomment the command you want to run. 149 | ''' 150 | # Accessing the usdc contract on polygon using Web3 Library 151 | usdc_token = w3.eth.contract(address=usdc_address, abi=usdc_contract_abi) 152 | # Log the current balance of the usdc token for our wallet_address 153 | usdc_balance = usdc_token.functions.balanceOf(wallet_address).call() 154 | 155 | # Log the current balance of the MATIC token in our Alpaca account 156 | logger.info('Matic Position on Alpaca: {0}'.format(get_positions())) 157 | # Log the current Cash Balance (USD) in our Alpaca account 158 | logger.info("USD position on Alpaca: {0}".format( 159 | get_account_details()['cash'])) 160 | # Log the current balance of MATIC token in our wallet_address 161 | logger.info('Matic Position on 1 Inch: {0}'.format( 162 | Web3.fromWei(w3.eth.getBalance(wallet_address), 'ether'))) 163 | # Log the current balance of USDC token in our wallet_address. 164 | logger.info('USD Position on 1 Inch: 165 | {0}'.format(usdc_balance/10**6)) 166 | 167 | while True: 168 | l1 = loop.create_task(get_oneInch_quote_data( 169 | matic_address, usdc_address, amount_to_exchange)) 170 | l2 = loop.create_task(get_Alpaca_quote_data(trading_pair, exchange)) 171 | # Wait for the tasks to finish 172 | await asyncio.wait([l1, l2]) 173 | check_arbitrage() 174 | # Wait for the a certain amount of time between each quote request 175 | await asyncio.sleep(waitTime) 176 | ``` 177 | 178 | The above snippet is our main function. It runs asynchronously (starts with async) and essentially logs a bunch of stuff. The comments above the `logger` statements explain what we are trying to log. So, Let's start with the `while True` loop. 179 | We are trying to create two asynchronous tasks (`l1` and `l2`) that fetch quotes from 1Inch and Alpaca respectively for our asset MATIC. In the case of 1Inch, we pass in the contract addresses of both the tokens MATIC and USDC along with the amount we intend to swap in case we would like to make a trade if an arbitrage situation arrives. Then, we use `asyncio.wait()` to wait for both the tasks to finish. This is important because we would like to receive the quotes from both the sources before we decide what to do next. Our next step is to check if we meet any arbitrage condition and wait a certain amount of time (we set it as 5 seconds, remember?) using `asyncio.sleep(waitTime)`. Then, we begin logging the price quotes from 1inch and Alpaca and repeat the process every 5 seconds. 180 | 181 | Now let's look at some of the functions we called in main. 182 | 183 | ``` 184 | def get_positions(): 185 | ''' 186 | Get positions 187 | ''' 188 | try: 189 | positions = requests.get( 190 | '{0}/v2/positions'.format(BASE_ALPACA_URL), headers=HEADERS) 191 | # logger.info('Alpaca positions reply status code: {0}'.format( 192 | # positions.status_code)) 193 | if positions.status_code != 200: 194 | logger.info( 195 | "Undesirable response from Alpaca! {}".format(positions.json())) 196 | return False 197 | # positions = positions[0] 198 | matic_position = positions.json()[0]['qty'] 199 | # logger.info('Matic Position on Alpaca: {0}'.format(matic_position)) 200 | except Exception as e: 201 | logger.exception( 202 | "There was an issue getting positions from Alpaca: {0}".format(e)) 203 | return False 204 | return matic_position 205 | 206 | ``` 207 | We use `get_positions()` to get our current `MATIC` position on Alpaca. We use a `GET` request with `/v2/positions` endpoint to retrieve our position. The `qty` attribute of the JSON response gives you your MATIC position. 208 | 209 | ``` 210 | async def get_oneInch_quote_data(_from_coin, _to_coin, _amount_to_exchange): 211 | ''' 212 | Get trade quote data from 1Inch API 213 | ''' 214 | # Try to get a quote from 1Inch 215 | try: 216 | # Get the current quote response for the trading pair (MATIC/USDC) 217 | quote = requests.get( 218 | '{0}/quote?fromTokenAddress={1}&toTokenAddress={2}&amount={3}'.format(BASE_URL, _from_coin, _to_coin, _amount_to_exchange)) 219 | # Status code 200 means the request was successful 220 | if quote.status_code != 200: 221 | logger.info( 222 | "Undesirable response from 1 Inch! This is probably bad.") 223 | return False 224 | # Refer to the global variable we initialized earlier 225 | global last_oneInch_market_price 226 | # Get the current quoted price from the quote response in terms USDC (US Dollar) 227 | last_oneInch_market_price = int(quote.json()['toTokenAmount'])/10**6 228 | # Log the current quote of MATIC/USDC 229 | logger.info('OneInch Price for 10 MATIC: {0}'.format( 230 | last_oneInch_market_price)) 231 | # If there is an error, log it 232 | except Exception as e: 233 | logger.exception( 234 | "There was an issue getting trade quote from 1 Inch: {0}".format(e)) 235 | return False 236 | 237 | return last_oneInch_market_price 238 | ``` 239 | The function in the above snippet is responsible for getting quotes from our decentralized exchange aggregator 1Inch. You can read more about their API endpoints [here](https://docs.1inch.io/docs/aggregation-protocol/api/swagger). 240 | I hope the comments in the snippet above try to be as clear as possible but let's try to understand what's happening here. Basically, we are trying to get the latest quote using 1Inch API by passing it the required query params. Along the way, we try to log our response for better understanding of the quotes and if any error arises during runtime. Then, we convert the query response to a dollar denominated value by dividing it by 10^6. This is because unlike other ERC-20 tokens, USDC only has 6 decimals of precision. 241 | Fun fact: Another famous stable coin, USDT also has 6 decimals of precision. 242 | 243 | Next, we will look at the function that gets us quotes using Alpaca's API. 244 | 245 | ``` 246 | async def get_Alpaca_quote_data(trading_pair, exchange): 247 | ''' 248 | Get trade quote data from Alpaca API 249 | ''' 250 | # Try to get a quote from 1Inch 251 | try: 252 | # Get the current quote response for the trading pair (MATIC/USDC) 253 | quote = requests.get( 254 | '{0}/v1beta1/crypto/{1}/quotes/latest?exchange={2}'.format(DATA_URL, trading_pair, exchange), headers=HEADERS) 255 | # Status code 200 means the request was successful 256 | if quote.status_code != 200: 257 | logger.info( 258 | "Undesirable response from Alpaca! {}".format(quote.json())) 259 | return False 260 | # Refer to the global variable we initialized earlier 261 | global last_alpaca_ask_price 262 | # Get the latest quoted asking price from the quote response in terms US Dollar 263 | last_alpaca_ask_price = quote.json( 264 | )['quote']['ap'] * 10 # for 10 MATIC 265 | # Log the latest quote of MATICUSD 266 | logger.info('Alpaca Price for 10 MATIC: {0}'.format( 267 | last_alpaca_ask_price)) 268 | # If there is an error, log it 269 | except Exception as e: 270 | logger.exception( 271 | "There was an issue getting trade quote from Alpaca: {0}".format(e)) 272 | return False 273 | 274 | return last_alpaca_ask_price 275 | ``` 276 | The function in the above snippet helps us in getting quotes from our centralized exchange provider Alpaca. You can read more about their API endpoints [here](https://alpaca.markets/docs/api-references/market-data-api/crypto-pricing-data/historical/). 277 | The comments in the snippet above are pretty self explanatory so I won't go in detail. Basically, we are trying to get the latest asking price of MATIC in terms of US dollars from FTX US using Alpaca's Crypto API. We pass it the required query params. Then, we access the dollar denominated value and multiply it by 10 to get the dollar denominated asking price for 10 MATIC tokens since 10 is our default trading size. 278 | Next, we will look at the function that checks if any arbitrage opportunity is present. 279 | 280 | ``` 281 | def check_arbitrage(): 282 | logger.info('Checking for arbitrage opportunities') 283 | rebalance = needs_rebalancing() 284 | # if the current price at alpaca is greater than the current price at 1inch by a given arb % and we do not need a rebalance 285 | # then we have an arbitrage opportunity. In this case we will buy on 1Inch and sell on Alpaca 286 | if (last_alpaca_ask_price > last_oneInch_market_price * (1 + min_arb_percent/100) and rebalance != True): 287 | logger.info('Selling on ALPACA, Buying on 1Inch') 288 | if production: 289 | sell_order = post_Alpaca_order( 290 | trading_pair, trade_size, 'sell', 'market', 'gtc') 291 | # if the above sell order goes through we will subtract 1 from alpaca trade counter 292 | if sell_order['status'] == 'accepted': 293 | global alpaca_trade_counter 294 | alpaca_trade_counter -= 1 295 | # Only buy on oneInch if our sell txn on alpaca goes through 296 | # To buy 10 MATIC, we multiply its price by 10 (amount to exchange) and then further multiply it by 10^6 to get USDC value 297 | buy_order_data = get_oneInch_swap_data( 298 | usdc_address, matic_address, last_oneInch_market_price*amount_of_usdc_to_trade) 299 | buy_order = signAndSendTransaction(buy_order_data) 300 | if buy_order == True: 301 | global oneInch_trade_counter 302 | oneInch_trade_counter += 1 303 | # If the current price at alpaca is less than the current price at 1inch by a given arb % and we do not need a rebalance 304 | # then we have an arbitrage opportunity. In this case we will buy on Alpaca and sell on 1Inch 305 | elif (last_alpaca_ask_price < last_oneInch_market_price * (1 - min_arb_percent/100) and rebalance != True): 306 | logger.info('Buying on ALPACA, Selling on 1Inch') 307 | if production: 308 | buy_order = post_Alpaca_order( 309 | trading_pair, 10, 'buy', 'market', 'gtc') 310 | # if the above buy order goes through we will add 1 to alpaca trade counter 311 | if buy_order['status'] == 'accepted': 312 | global alpaca_trade_counter 313 | alpaca_trade_counter += 1 314 | # Only sell on oneInch if our buy txn on alpaca goes through 315 | # To sell 10 MATIC, we pass it amount to exchnage 316 | sell_order_data = get_oneInch_swap_data( 317 | matic_address, usdc_address, amount_to_exchange) 318 | sell_order = signAndSendTransaction(sell_order_data) 319 | if sell_order == True: 320 | global oneInch_trade_counter 321 | oneInch_trade_counter -= 1 322 | # If neither of the above conditions are met then either there is no arbitrage opportunity found and/or we need to rebalance 323 | else: 324 | if rebalance: 325 | rebalancing() 326 | else: 327 | logger.info('No arbitrage opportunity available') 328 | ``` 329 | The code above seems a bit daunting at first but as we walk through it you will realize it's executing very simple operations. There are a few variables that we need to understand first. We went over both the trade counters (`alpaca_trade_counter` and `oneInch_trade_counter`) earlier in the post. They are initialized to 0 when the bot starts and increment or decrement by 1 for Alpaca and 1Inch based on the trade executed. A "sell" trade decrements the counter by 1 while a "buy" trade increments it by 1. `rebalance` is a variable that takes the value returned by `needs_rebalancing`. It essentially checks if our positions need rebalancing before we proceed with our next trade and even consider an arbitrage opportunity. We will go over the `needs_rebalancing` function later in the post. 330 | `production` as we discussed earlier is a safety flag. 331 | In this function, we look at 2 conditions to consider an arbitrage, the price difference between the two sources (Alpaca and 1Inch) and whether our positions need rebalancing. Based on these conditions we decide whether we would like to buy/sell or rebalance our portfolio. 332 | 333 | Next, let's take a look at the `needs_rebalancing()` function before we understand `rebalancing()`. 334 | 335 | ``` 336 | def needs_rebalancing(): 337 | # Get current MATIC positions on both exchanges 338 | current_matic_alpaca = int(get_positions()) 339 | current_matic_1Inch = int(Web3.fromWei( 340 | w3.eth.getBalance(wallet_address), 'ether')) 341 | # If the current amount of MATIC on either exchange minus the trade size (10) is greater than 0 then we are good enough to trade 342 | if current_matic_alpaca - 10 < 0 or current_matic_1Inch - 10 < 0: 343 | logger.info( 344 | 'We will have less than 10 MATIC on one of the exchanges if we trade. We need to rebalance.') 345 | return True 346 | # If the current trade counter on Alpaca or 1Inch is not 0 then we need to rebalance 347 | if alpaca_trade_counter != 0 or oneInch_trade_counter != 0: 348 | logger.info("We need to rebalance our positions") 349 | return True 350 | return False 351 | ``` 352 | 353 | This function involves a couple of checks that return `True` if we need to rebalance our positions and `False` otherwise. 354 | Condition 1: If we have less than 10 MATIC on either of the exchanges that means we either have an active open position or we have less funds. 355 | Condition 2: If the trade counter for either of the exchanges is not 0 then we need to rebalance. Since we are going long/short at the same time on the exchanges, we need to reverse our positions before we execute on the next opportunity. 356 | 357 | ``` 358 | def rebalancing(): 359 | logger.info('Rebalancing') 360 | global alpaca_trade_counter 361 | global oneInch_trade_counter 362 | 363 | # Get current MATIC positions on both exchanges 364 | current_matic_alpaca = get_positions() 365 | current_matic_1Inch = Web3.fromWei( 366 | w3.eth.getBalance(wallet_address), 'ether') 367 | # Only execute rebalancing trades if production is true (we are live) 368 | if production: 369 | if (current_matic_alpaca - 10 > 0 and alpaca_trade_counter != 0): 370 | if alpaca_trade_counter > 0: 371 | logger.info('Rebalancing Alpaca side by selling on Alpaca') 372 | sell_order = post_Alpaca_order( 373 | trading_pair, 10, 'sell', 'market', 'gtc') 374 | if sell_order['status'] == 'accepted': 375 | alpaca_trade_counter -= 1 376 | else: 377 | logger.info('Rebalancing Alpaca side by buying on Alpaca') 378 | buy_order = post_Alpaca_order( 379 | trading_pair, 10, 'buy', 'market', 'gtc') 380 | if buy_order['status'] == 'accepted': 381 | alpaca_trade_counter += 1 382 | 383 | if current_matic_alpaca - 10 < 0 and alpaca_trade_counter != 0: 384 | logger.info( 385 | 'Unable to rebalance Alpaca side due to insufficient funds') 386 | 387 | if current_matic_1Inch - 10 > 0 and oneInch_trade_counter != 0: 388 | if oneInch_trade_counter > 0: 389 | logger.info('Rebalancing oneInch side by selling on oneInch') 390 | sell_order_data = get_oneInch_swap_data( 391 | matic_address, usdc_address, amount_to_exchange) 392 | sell_order = signAndSendTransaction(sell_order_data) 393 | if sell_order == True: 394 | oneInch_trade_counter -= 1 395 | else: 396 | logger.info('Rebalancing oneInch side by buying on oneInch') 397 | buy_order_data = get_oneInch_swap_data( 398 | matic_address, usdc_address, amount_to_exchange) 399 | buy_order = signAndSendTransaction(buy_order_data) 400 | if buy_order == True: 401 | oneInch_trade_counter += 1 402 | if current_matic_1Inch - 10 < 0 and oneInch_trade_counter != 0: 403 | logger.info( 404 | 'Unable to rebalance oneInch side due to insufficient funds') 405 | 406 | ``` 407 | 408 | The `rebalancing` function uses the amount of MATIC we hold on each exchange and their respective trade counters to determine if there needs to be a sell order or a buy order for that exchange. The trade counters for both the exchanges should either be `-1`,`0` or `1`. `-1` representing a short position on the exchange (Sell MATIC), `0` represents no active position and `1` represents a long position (Buy MATIC). If the trade counter on alpaca is greater than 0 that means a trade to buy alpaca had been executed earlier. We will need to sell this before we can make another trade again. Likewise, we will buy if the counter is less than 0. 409 | Functions `rebalancing()` , `needs_rebalancing()` and `check_arbitrage()` can be further optimized to maximize profits in my opinion, but they also serve as a good starting point for someone to looking to start trading on both the DeX's and CeX's simultaneously. 410 | 411 | Now that the core functions of the bot have been defined we can call our main function. 412 | ``` 413 | loop = asyncio.get_event_loop() 414 | loop.run_until_complete(main()) 415 | loop.close() 416 | ``` 417 | 418 | Using asyncio library, we create an event loop and make it run until it finishes. In the loop we call our main function. Do remember though we defined it to work such that it runs indefinitely. So you will probably need to exit it by using `Ctrl+C`. Again, this can be improved upon to take inputs and exit a little more elegantly. 419 | 420 | Apart from the functions mentioned above, I have included some functions in the file that might help you interact with both the exchanges a little better. Let's briefly go over them. 421 | `get_account_details()` uses a `GET` request along with `/v2/account` endpoint to access your Alpaca account information. In this code we are just using it to access our cash balance. Apart from cash balance, you can use this endpoint to get a lot more important information about your account, such as buying power, portfolio value, account status, etc. That's great because 1 API call can get you so much information. 422 | 423 | `get_allowance()` method uses 1Inch's API endpoints to check how many tokens for a given token, 1Inch is allowed to spend from our address. By default, on Polygon network, 1Inch is allowed to spend an infinite amount of MATIC (since this is the native currency of the chain) but this is not the same for USDC tokens and it should most likely be 0 if you have not used 1Inch before. To approve 1Inch to spend your USDC tokens, we use the method `approve_ERC20()`. This method generates the necessary transaction data that approves 1Inch to spend a said token (USDC here) on your behalf. We need this because we would like 1Inch to find the best quotes for our trading pair `MATIC/USDC` and we need it to execute trade at those prices. 424 | Once this transaction data has been created using `approve_ERC20()`, we can call `signAndSendTransaction()` to execute this approval. 425 | 426 | ## Few takeaways: 427 | 428 | 1. Use Alpaca's APIs to access market data and instant trading capabilities if you are looking to trade crypto on centralized exchanges. They have most of the high volume popular coins and provide a super easy way to access information and execute trades. 429 | 2. Use 1Inch's APIs to receive quotes and trade in the cheapest way on the most popular EVM compatible blockchains. 430 | 3. Logic for `check_arbitrage()`, `rebalancing()` and `needs_rebalancing()` is very naive in its approach and probably won't be profitable. But that shouldn't matter and discourage you. You can customize the logic as you wish. At the very least, it should give you a good starting point to trading using API's. 431 | 432 | 433 | 434 | 435 | 436 | --------------------------------------------------------------------------------