def __init__(self, args: list): parser = argparse.ArgumentParser(prog='0x-market-maker-chart') parser.add_argument("--rpc-host", help="JSON-RPC host (default: `localhost')", default="localhost", type=str) parser.add_argument("--rpc-port", help="JSON-RPC port (default: `8545')", default=8545, type=int) parser.add_argument("--rpc-timeout", help="JSON-RPC timeout (in seconds, default: 60)", type=int, default=60) parser.add_argument("--exchange-address", help="Ethereum address of the 0x contract", required=True, type=str) parser.add_argument("--buy-token-address", help="Ethereum address of the buy token", required=True, type=str) parser.add_argument("--buy-token-decimals", help="Number of decimals for the buy token", type=int, default=18) parser.add_argument("--sell-token-address", help="Ethereum address of the sell token", required=True, type=str) parser.add_argument("--sell-token-decimals", help="Number of decimals for the sell token", type=int, default=18) parser.add_argument("--old-sell-token-address", help="Ethereum address of the old sell token", required=False, type=str) parser.add_argument("--market-maker-address", help="Ethereum account of the market maker to analyze", required=True, type=str) parser.add_argument("--gdax-price", help="GDAX product (ETH-USD, BTC-USD) to use as the price history source", type=str) parser.add_argument("--price-feed", help="Price endpoint to use as the price history source", type=str) parser.add_argument("--alternative-price-feed", help="Price endpoint to use as the alternative price history source", type=str) parser.add_argument("--order-history", help="Order history endpoint from which to fetch our order history", type=str) parser.add_argument("--past-blocks", help="Number of past blocks to analyze", required=True, type=int) parser.add_argument("-o", "--output", help="Name of the filename to save to chart to." " Will get displayed on-screen if empty", required=False, type=str) self.arguments = parser.parse_args(args) self.web3 = Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={'timeout': self.arguments.rpc_timeout})) self.infura = Web3(HTTPProvider(endpoint_uri=f"https://mainnet.infura.io/", request_kwargs={'timeout': 120})) self.buy_token_address = Address(self.arguments.buy_token_address) self.sell_token_address = Address(self.arguments.sell_token_address) self.old_sell_token_address = Address(self.arguments.old_sell_token_address) if self.arguments.old_sell_token_address else None self.sell_token_addresses = list(filter(lambda address: address is not None, [self.sell_token_address, self.old_sell_token_address])) self.market_maker_address = Address(self.arguments.market_maker_address) self.exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) initialize_charting(self.arguments.output) initialize_logging()
def init_zrx(self): self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) self.zrx_relayer_api = ZrxRelayerApi(exchange=self.zrx_exchange, api_server=self.arguments.relayer_api_server) self.zrx_api = ZrxApi(zrx_exchange=self.zrx_exchange) self.pair = Pair(sell_token_address=Address(self.arguments.sell_token_address), sell_token_decimals=self.arguments.sell_token_decimals, buy_token_address=Address(self.arguments.buy_token_address), buy_token_decimals=self.arguments.buy_token_decimals)
def setup_method(self): py_evm_main.GENESIS_GAS_LIMIT = 10000000 self.web3 = Web3(EthereumTesterProvider(EthereumTester( PyEVMBackend()))) self.web3.eth.defaultAccount = self.web3.eth.accounts[0] self.our_address = Address(self.web3.eth.defaultAccount) self.zrx_token = ERC20Token(web3=self.web3, address=deploy_contract( self.web3, 'ZRXToken')) self.token_transfer_proxy_address = deploy_contract( self.web3, 'TokenTransferProxy') self.exchange = ZrxExchange.deploy(self.web3, self.zrx_token.address, self.token_transfer_proxy_address) self.web3.eth.contract(abi=json.loads( pkg_resources.resource_string( 'pymaker.deployment', f'abi/TokenTransferProxy.abi')))( address=self.token_transfer_proxy_address.address ).functions.addAuthorizedAddress( self.exchange.address.address).transact() self.zrx_api = ZrxApi(self.exchange) self.dgx = DSToken.deploy(self.web3, 'DGX') self.dai = DSToken.deploy(self.web3, 'DAI') self.pair = Pair(self.dgx.address, 9, self.dai.address, 18)
def setup_method(self): # Use Ganache docker container self.web3 = Web3(HTTPProvider("http://0.0.0.0:8555")) self.web3.eth.defaultAccount = self.web3.eth.accounts[0] self.our_address = Address(self.web3.eth.defaultAccount) self.zrx_token = ERC20Token(web3=self.web3, address=deploy_contract( self.web3, 'ZRXToken')) self.token_transfer_proxy_address = deploy_contract( self.web3, 'TokenTransferProxy') self.exchange = ZrxExchange.deploy(self.web3, self.zrx_token.address, self.token_transfer_proxy_address) self.web3.eth.contract(abi=json.loads( pkg_resources.resource_string( 'pymaker.deployment', f'abi/TokenTransferProxy.abi')))( address=self.token_transfer_proxy_address.address ).functions.addAuthorizedAddress( self.exchange.address.address).transact() self.zrx_api = ZrxApi(self.exchange) self.dgx = DSToken.deploy(self.web3, 'DGX') self.dai = DSToken.deploy(self.web3, 'DAI') self.pair = Pair(self.dgx.address, 9, self.dai.address, 18)
def setup_method(self): self.web3 = Web3(EthereumTesterProvider()) self.web3.eth.defaultAccount = self.web3.eth.accounts[0] self.our_address = Address(self.web3.eth.defaultAccount) self.zrx_token = ERC20Token(web3=self.web3, address=deploy_contract(self.web3, 'ZRXToken')) self.token_transfer_proxy_address = deploy_contract(self.web3, 'TokenTransferProxy') self.exchange = ZrxExchange.deploy(self.web3, self.zrx_token.address, self.token_transfer_proxy_address) self.web3.eth.contract(abi=json.loads(pkg_resources.resource_string('pymaker.deployment', f'abi/TokenTransferProxy.abi')))(address=self.token_transfer_proxy_address.address).transact().addAuthorizedAddress(self.exchange.address.address)
def setup_method(self): self.web3 = Web3(EthereumTesterProvider()) self.web3.eth.defaultAccount = self.web3.eth.accounts[0] self.our_address = Address(self.web3.eth.defaultAccount) self.zrx_token = ERC20Token(web3=self.web3, address=deploy_contract( self.web3, 'ZRXToken')) self.token_transfer_proxy_address = deploy_contract( self.web3, 'TokenTransferProxy') self.exchange = ZrxExchange.deploy(self.web3, self.zrx_token.address, self.token_transfer_proxy_address)
class ZrxMarketMakerChart: """Tool to generate a chart displaying the 0x market maker keeper trades.""" def __init__(self, args: list): parser = argparse.ArgumentParser(prog='0x-market-maker-chart') parser.add_argument("--rpc-host", help="JSON-RPC host (default: `localhost')", default="localhost", type=str) parser.add_argument("--rpc-port", help="JSON-RPC port (default: `8545')", default=8545, type=int) parser.add_argument("--rpc-timeout", help="JSON-RPC timeout (in seconds, default: 60)", type=int, default=60) parser.add_argument("--exchange-address", help="Ethereum address of the 0x contract", required=True, type=str) parser.add_argument("--buy-token-address", help="Ethereum address of the buy token", required=True, type=str) parser.add_argument("--buy-token-decimals", help="Number of decimals for the buy token", type=int, default=18) parser.add_argument("--sell-token-address", help="Ethereum address of the sell token", required=True, type=str) parser.add_argument("--sell-token-decimals", help="Number of decimals for the sell token", type=int, default=18) parser.add_argument("--old-sell-token-address", help="Ethereum address of the old sell token", required=False, type=str) parser.add_argument("--market-maker-address", help="Ethereum account of the market maker to analyze", required=True, type=str) parser.add_argument("--gdax-price", help="GDAX product (ETH-USD, BTC-USD) to use as the price history source", type=str) parser.add_argument("--price-feed", help="Price endpoint to use as the price history source", type=str) parser.add_argument("--alternative-price-feed", help="Price endpoint to use as the alternative price history source", type=str) parser.add_argument("--order-history", help="Order history endpoint from which to fetch our order history", type=str) parser.add_argument("--past-blocks", help="Number of past blocks to analyze", required=True, type=int) parser.add_argument("-o", "--output", help="Name of the filename to save to chart to." " Will get displayed on-screen if empty", required=False, type=str) self.arguments = parser.parse_args(args) self.web3 = Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={'timeout': self.arguments.rpc_timeout})) self.infura = Web3(HTTPProvider(endpoint_uri=f"https://mainnet.infura.io/", request_kwargs={'timeout': 120})) self.buy_token_address = Address(self.arguments.buy_token_address) self.sell_token_address = Address(self.arguments.sell_token_address) self.old_sell_token_address = Address(self.arguments.old_sell_token_address) if self.arguments.old_sell_token_address else None self.sell_token_addresses = list(filter(lambda address: address is not None, [self.sell_token_address, self.old_sell_token_address])) self.market_maker_address = Address(self.arguments.market_maker_address) self.exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) initialize_charting(self.arguments.output) initialize_logging() def main(self): start_timestamp = get_block_timestamp(self.infura, self.web3.eth.blockNumber - self.arguments.past_blocks) end_timestamp = int(time.time()) events = self.exchange.past_fill(self.arguments.past_blocks, {'maker': self.market_maker_address.address}) trades = zrx_trades(self.infura, self.market_maker_address, 'DAI', self.buy_token_address, self.arguments.buy_token_decimals, 'WETH', self.sell_token_addresses, self.arguments.sell_token_decimals, events, '-') prices = get_prices(self.arguments.gdax_price, self.arguments.price_feed, None, start_timestamp, end_timestamp) alternative_prices = get_prices(None, self.arguments.alternative_price_feed, None, start_timestamp, end_timestamp) order_history = get_order_history(self.arguments.order_history, start_timestamp, end_timestamp) order_history = prepare_order_history_for_charting(order_history) draw_chart(start_timestamp, end_timestamp, prices, alternative_prices, 180, order_history, trades, [], self.arguments.output)
def __init__(self, args: list): parser = argparse.ArgumentParser(prog='radarrelay-market-maker-chart') parser.add_argument("--rpc-host", help="JSON-RPC host (default: `localhost')", default="localhost", type=str) parser.add_argument("--rpc-port", help="JSON-RPC port (default: `8545')", default=8545, type=int) parser.add_argument("--exchange-address", help="Ethereum address of the 0x contract", required=True, type=str) parser.add_argument("--sai-address", help="Ethereum address of the SAI token", required=True, type=str) parser.add_argument("--weth-address", help="Ethereum address of the WETH token", required=True, type=str) parser.add_argument("--market-maker-address", help="Ethereum account of the market maker to analyze", required=True, type=str) parser.add_argument("--past-blocks", help="Number of past blocks to analyze", required=True, type=int) parser.add_argument("-o", "--output", help="Name of the filename to save to chart to." " Will get displayed on-screen if empty", required=False, type=str) self.arguments = parser.parse_args(args) self.web3 = Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={'timeout': 120})) self.infura = Web3(HTTPProvider(endpoint_uri=f"https://mainnet.infura.io/", request_kwargs={'timeout': 120})) self.sai_address = Address(self.arguments.sai_address) self.weth_address = Address(self.arguments.weth_address) self.market_maker_address = Address(self.arguments.market_maker_address) self.exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) if self.arguments.output: import matplotlib matplotlib.use('Agg')
def setup_method(self): self.web3 = Web3(HTTPProvider("http://localhost:8555")) self.web3.eth.defaultAccount = self.web3.eth.accounts[0] self.our_address = Address(self.web3.eth.defaultAccount) self.zrx_token = ERC20Token(web3=self.web3, address=deploy_contract( self.web3, 'ZRXToken')) self.token_transfer_proxy_address = deploy_contract( self.web3, 'TokenTransferProxy') self.exchange = ZrxExchange.deploy(self.web3, self.zrx_token.address, self.token_transfer_proxy_address) token_proxy_abi = json.loads( pkg_resources.resource_string('pymaker.deployment', f'abi/TokenTransferProxy.abi')) self.web3.eth.contract(abi=token_proxy_abi)\ (address=self.token_transfer_proxy_address.address).functions.addAuthorizedAddress( self.exchange.address.address).transact() self.token1 = DSToken.deploy(self.web3, 'AAA') self.token1.mint(Wad.from_number(100)).transact() self.token2 = DSToken.deploy(self.web3, 'BBB') self.token2.mint(Wad.from_number(100)).transact()
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='radarrelay-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument("--tub-address", type=str, required=True, help="Ethereum address of the Tub contract") parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--weth-address", type=str, required=True, help="Ethereum address of the WETH token") parser.add_argument("--relayer-api-server", type=str, required=True, help="Address of the 0x Relayer API") parser.add_argument("--config", type=str, required=True, help="Buy/sell bands configuration file") parser.add_argument( "--price-feed", type=str, help= "Source of price feed. Tub price feed will be used if not specified" ) parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of non-Tub price feed (in seconds, default: 120)" ) parser.add_argument( "--order-expiry", type=int, required=True, help="Expiration time of created orders (in seconds)") parser.add_argument( "--order-expiry-threshold", type=int, default=0, help= "Order expiration time at which order is considered already expired (in seconds)" ) parser.add_argument( "--min-eth-balance", type=float, default=0, help= "Minimum ETH balance below which keeper with either terminate or not start at all" ) parser.add_argument( '--cancel-on-shutdown', dest='cancel_on_shutdown', action='store_true', help= "Whether should cancel all open orders on RadarRelay on keeper shutdown" ) parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--gas-price-increase", type=int, help="Gas price increase (in Wei) if no confirmation within" " `--gas-price-increase-every` seconds") parser.add_argument( "--gas-price-increase-every", type=int, default=120, help="Gas price increase frequency (in seconds, default: 120)") parser.add_argument("--gas-price-max", type=int, help="Maximum gas price (in Wei)") parser.add_argument("--gas-price-file", type=str, help="Gas price configuration file") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) self.vox = Vox(web3=self.web3, address=self.tub.vox()) self.sai = ERC20Token(web3=self.web3, address=self.tub.sai()) self.ether_token = ERC20Token(web3=self.web3, address=Address( self.arguments.weth_address)) logging.basicConfig( format='%(asctime)-15s %(levelname)-8s %(message)s', level=(logging.DEBUG if self.arguments.debug else logging.INFO)) logging.getLogger('urllib3.connectionpool').setLevel(logging.INFO) logging.getLogger('requests.packages.urllib3.connectionpool').setLevel( logging.INFO) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed( self.arguments.price_feed, self.arguments.price_feed_expiry, self.tub, self.vox) self.radar_relay = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) self.radar_relay_api = ZrxRelayerApi( exchange=self.radar_relay, api_server=self.arguments.relayer_api_server)
web3 = Web3( HTTPProvider("http://localhost:8545", request_kwargs={"timeout": 600})) web3.eth.defaultAccount = sys.argv[1] EXCHANGE_ADDRESS = Address("0xdcdb42c9a256690bd153a7b409751adfc8dd5851") DAI_ADDRESS = Address("0xd9ebebfdab08c643c5f2837632de920c70a56247") ETH_ADDRESS = Address("0xaa7427d8f17d87a28f5e1ba3adbb270badbe1011") FEE_ADDRESS = Address("0x61b9898c9b60a159fc91ae8026563cd226b7a0c1") dai_wrapper = TEthfinexToken(web3, DAI_ADDRESS, "DAI") # print(dai_wrapper.balance_of(Address(web3.eth.defaultAccount))) # print(dai_wrapper.deposit(Wad.from_number(15)).transact()) # print(dai_wrapper.balance_of(Address(web3.eth.defaultAccount))) # ethfinex_trustless = TEthfinexToken(web3, EXCHANGE_ADDRESS) zrx_exchange = ZrxExchange(web3=web3, address=EXCHANGE_ADDRESS) ethfinex_trustless_api = TEthfinexApi(zrx_exchange, 'https://api.ethfinex.com', 15.5) # print(ethfinex_trustless_api.get_symbols()) # print(ethfinex_trustless_api.get_config()) # placed_order = ethfinex_trustless_api.place_order(False, # DAI_ADDRESS, # Wad.from_number(86.4), # ETH_ADDRESS, # Wad.from_number(0.8), # FEE_ADDRESS, # "DAIETH") # print(f"Placed order {placed_order}") # print(ethfinex_trustless_api.get_orders("tDAIUSD")) # print(ethfinex_trustless_api.cancel_order(placed_order))
def __init__(self, args, **kwargs): parser = argparse.ArgumentParser("arbitrage-keeper") parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--eth-key", type=str, nargs='*', help= "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')" ) parser.add_argument("--tub-address", type=str, required=True, help="Ethereum address of the Tub contract") parser.add_argument("--tap-address", type=str, required=True, help="Ethereum address of the Tap contract") parser.add_argument( "--exchange-address", type=str, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--oasis-address", type=str, required=True, help="Ethereum address of the OasisDEX contract") parser.add_argument( "--oasis-support-address", type=str, required=False, help="Ethereum address of the OasisDEX support contract") parser.add_argument("--relayer-api-server", type=str, help="Address of the 0x Relayer API") parser.add_argument( "--relayer-per-page", type=int, default=100, help= "Number of orders to fetch per one page from the 0x Relayer API (default: 100)" ) parser.add_argument( "--tx-manager", type=str, help= "Ethereum address of the TxManager contract to use for multi-step arbitrage" ) parser.add_argument("--gas-price", type=int, default=0, help="Gas price in Wei (default: node default)") parser.add_argument( "--base-token", type=str, required=True, help="The token all arbitrage sequences will start and end with") parser.add_argument( "--min-profit", type=float, required=True, help="Minimum profit (in base token) from one arbitrage operation") parser.add_argument( "--max-engagement", type=float, required=True, help="Maximum engagement (in base token) in one arbitrage operation" ) parser.add_argument( "--max-errors", type=int, default=100, help= "Maximum number of allowed errors before the keeper terminates (default: 100)" ) parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from register_keys(self.web3, self.arguments.eth_key) self.our_address = Address(self.arguments.eth_from) self.tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) self.tap = Tap(web3=self.web3, address=Address(self.arguments.tap_address)) self.gem = ERC20Token(web3=self.web3, address=self.tub.gem()) self.sai = ERC20Token(web3=self.web3, address=self.tub.sai()) self.skr = ERC20Token(web3=self.web3, address=self.tub.skr()) self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) \ if self.arguments.exchange_address is not None else None self.zrx_relayer_api = ZrxRelayerApi(exchange=self.zrx_exchange, api_server=self.arguments.relayer_api_server) \ if self.arguments.relayer_api_server is not None else None self.otc = MatchingMarket( web3=self.web3, address=Address(self.arguments.oasis_address), support_address=Address(self.arguments.oasis_support_address) if self.arguments.oasis_support_address is not None else None) self.base_token = ERC20Token(web3=self.web3, address=Address( self.arguments.base_token)) self.min_profit = Wad.from_number(self.arguments.min_profit) self.max_engagement = Wad.from_number(self.arguments.max_engagement) self.max_errors = self.arguments.max_errors self.errors = 0 if self.arguments.tx_manager: self.tx_manager = TxManager(web3=self.web3, address=Address( self.arguments.tx_manager)) if self.tx_manager.owner() != self.our_address: raise Exception( f"The TxManager has to be owned by the address the keeper is operating from." ) else: self.tx_manager = None logging.basicConfig( format='%(asctime)-15s %(levelname)-8s %(message)s', level=(logging.DEBUG if self.arguments.debug else logging.INFO))
class RadarRelayMarketMakerKeeper: """Keeper acting as a market maker on RadarRelay, on the WETH/SAI pair.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='radarrelay-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument("--tub-address", type=str, required=True, help="Ethereum address of the Tub contract") parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--weth-address", type=str, required=True, help="Ethereum address of the WETH token") parser.add_argument("--relayer-api-server", type=str, required=True, help="Address of the 0x Relayer API") parser.add_argument("--config", type=str, required=True, help="Buy/sell bands configuration file") parser.add_argument( "--price-feed", type=str, help= "Source of price feed. Tub price feed will be used if not specified" ) parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of non-Tub price feed (in seconds, default: 120)" ) parser.add_argument( "--order-expiry", type=int, required=True, help="Expiration time of created orders (in seconds)") parser.add_argument( "--order-expiry-threshold", type=int, default=0, help= "Order expiration time at which order is considered already expired (in seconds)" ) parser.add_argument( "--min-eth-balance", type=float, default=0, help= "Minimum ETH balance below which keeper with either terminate or not start at all" ) parser.add_argument( '--cancel-on-shutdown', dest='cancel_on_shutdown', action='store_true', help= "Whether should cancel all open orders on RadarRelay on keeper shutdown" ) parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--gas-price-increase", type=int, help="Gas price increase (in Wei) if no confirmation within" " `--gas-price-increase-every` seconds") parser.add_argument( "--gas-price-increase-every", type=int, default=120, help="Gas price increase frequency (in seconds, default: 120)") parser.add_argument("--gas-price-max", type=int, help="Maximum gas price (in Wei)") parser.add_argument("--gas-price-file", type=str, help="Gas price configuration file") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) self.vox = Vox(web3=self.web3, address=self.tub.vox()) self.sai = ERC20Token(web3=self.web3, address=self.tub.sai()) self.ether_token = ERC20Token(web3=self.web3, address=Address( self.arguments.weth_address)) logging.basicConfig( format='%(asctime)-15s %(levelname)-8s %(message)s', level=(logging.DEBUG if self.arguments.debug else logging.INFO)) logging.getLogger('urllib3.connectionpool').setLevel(logging.INFO) logging.getLogger('requests.packages.urllib3.connectionpool').setLevel( logging.INFO) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed( self.arguments.price_feed, self.arguments.price_feed_expiry, self.tub, self.vox) self.radar_relay = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) self.radar_relay_api = ZrxRelayerApi( exchange=self.radar_relay, api_server=self.arguments.relayer_api_server) def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(15, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): self.approve() @retry(delay=5, logger=logger) def shutdown(self): if self.arguments.cancel_on_shutdown: self.cancel_orders(self.our_orders()) def approve(self): """Approve 0x to access our tokens, so we can sell it on the exchange.""" self.radar_relay.approve( [self.token_sell(), self.token_buy()], directly(gas_price=self.gas_price)) def price(self) -> Wad: return self.price_feed.get_price() def token_sell(self) -> ERC20Token: return self.ether_token def token_buy(self) -> ERC20Token: return self.sai def our_balance(self, token: ERC20Token) -> Wad: return token.balance_of(self.our_address) def our_orders(self) -> list: our_orders = self.radar_relay_api.get_orders_by_maker(self.our_address) current_timestamp = int(time.time()) our_orders = list( filter( lambda order: order.expiration > current_timestamp - self. arguments.order_expiry_threshold, our_orders)) our_orders = list( filter( lambda order: self.radar_relay.get_unavailable_buy_amount( order) < order.buy_amount, our_orders)) return our_orders def our_sell_orders(self, our_orders: list) -> list: return list( filter( lambda order: order.buy_token == self.token_buy().address and order.pay_token == self.token_sell().address, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list( filter( lambda order: order.buy_token == self.token_sell().address and order.pay_token == self.token_buy().address, our_orders)) def synchronize_orders(self): """Update our positions in the order book to reflect keeper parameters.""" if eth_balance(self.web3, self.our_address) < self.min_eth_balance: self.logger.warning( "Keeper ETH balance below minimum. Cancelling all orders.") self.cancel_orders(self.our_orders()) return bands = Bands(self.bands_config) our_orders = self.our_orders() target_price = self.price() if target_price is None: self.logger.warning( "Cancelling all orders as no price feed available.") self.cancel_orders(our_orders) return # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(our_orders), our_sell_orders=self.our_sell_orders(our_orders), target_price=target_price) if len(cancellable_orders) > 0: self.cancel_orders(cancellable_orders) return # Place new orders self.create_orders( bands.new_orders( our_buy_orders=self.our_buy_orders(our_orders), our_sell_orders=self.our_sell_orders(our_orders), our_buy_balance=self.our_balance(self.token_buy()), our_sell_balance=self.our_balance(self.token_sell()), target_price=target_price)) def cancel_orders(self, orders): """Cancel orders asynchronously.""" synchronize([ self.radar_relay.cancel_order(order).transact_async( gas_price=self.gas_price) for order in orders ]) def create_orders(self, orders): """Create and submit orders synchronously.""" for order in orders: pay_token = self.token_sell() if order.is_sell else self.token_buy( ) buy_token = self.token_buy() if order.is_sell else self.token_sell( ) order = self.radar_relay.create_order(pay_token=pay_token.address, pay_amount=order.pay_amount, buy_token=buy_token.address, buy_amount=order.buy_amount, expiration=int(time.time()) + self.arguments.order_expiry) order = self.radar_relay_api.calculate_fees(order) order = self.radar_relay.sign_order(order) self.radar_relay_api.submit_order(order)
class ZrxMarketMakerKeeper: """Keeper acting as a market maker on any 0x exchange implementing the Standard 0x Relayer API V0.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='0x-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--relayer-api-server", type=str, required=True, help="Address of the 0x Relayer API") parser.add_argument( "--relayer-per-page", type=int, default=100, help= "Number of orders to fetch per one page from the 0x Relayer API (default: 100)" ) parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument( "--order-expiry", type=int, required=True, help="Expiration time of created orders (in seconds)") parser.add_argument( "--order-expiry-threshold", type=int, default=0, help= "How long before order expiration it is considered already expired (in seconds)" ) parser.add_argument( "--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument( '--cancel-on-shutdown', dest='cancel_on_shutdown', action='store_true', help="Whether should cancel all open orders on keeper shutdown") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.token_buy = ERC20Token(web3=self.web3, address=Address( self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address( self.arguments.sell_token_address)) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.history = History() self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) self.zrx_relayer_api = ZrxRelayerApi( exchange=self.zrx_exchange, api_server=self.arguments.relayer_api_server) self.placed_orders = [] def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(15, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): self.approve() @retry(delay=5, logger=logger) def shutdown(self): if self.arguments.cancel_on_shutdown: self.cancel_orders(self.our_orders()) def approve(self): self.zrx_exchange.approve([self.token_sell, self.token_buy], directly(gas_price=self.gas_price)) def our_total_balance(self, token: ERC20Token) -> Wad: return token.balance_of(self.our_address) def our_orders(self) -> list: api_orders = self.zrx_relayer_api.get_orders_by_maker( self.our_address, self.arguments.relayer_per_page) all_orders = list(set(self.placed_orders + api_orders)) return self.remove_old_orders(all_orders) def remove_old_orders(self, orders: list) -> list: current_timestamp = int(time.time()) orders = list( filter( lambda order: order.expiration > current_timestamp - self. arguments.order_expiry_threshold, orders)) orders = list( filter( lambda order: self.zrx_exchange.get_unavailable_buy_amount( order) < order.buy_amount, orders)) return orders def our_sell_orders(self, our_orders: list) -> list: return list( filter( lambda order: order.buy_token == self.token_buy.address and order.pay_token == self.token_sell.address, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list( filter( lambda order: order.buy_token == self.token_sell.address and order.pay_token == self.token_buy.address, our_orders)) def synchronize_orders(self): if eth_balance(self.web3, self.our_address) < self.min_eth_balance: self.logger.warning( "Keeper ETH balance below minimum. Cancelling all orders.") self.cancel_orders(self.our_orders()) return bands = Bands(self.bands_config, self.spread_feed, self.history) our_orders = self.our_orders() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(our_orders), our_sell_orders=self.our_sell_orders(our_orders), target_price=target_price) if len(cancellable_orders) > 0: self.cancel_orders(cancellable_orders) return # Balances returned by `our_total_balance` still contain amounts "locked" # by currently open orders, so we need to explicitly subtract these amounts. our_buy_balance = self.our_total_balance( self.token_buy) - Bands.total_amount( self.our_buy_orders(our_orders)) our_sell_balance = self.our_total_balance( self.token_sell) - Bands.total_amount( self.our_sell_orders(our_orders)) # Place new orders self.place_orders( bands.new_orders(our_buy_orders=self.our_buy_orders(our_orders), our_sell_orders=self.our_sell_orders(our_orders), our_buy_balance=our_buy_balance, our_sell_balance=our_sell_balance, target_price=target_price)[0]) def cancel_orders(self, orders): synchronize([ self.zrx_exchange.cancel_order(order).transact_async( gas_price=self.gas_price) for order in orders ]) def place_orders(self, new_orders): for new_order in new_orders: pay_token = self.token_sell if new_order.is_sell else self.token_buy buy_token = self.token_buy if new_order.is_sell else self.token_sell zrx_order = self.zrx_exchange.create_order( pay_token=pay_token.address, pay_amount=new_order.pay_amount, buy_token=buy_token.address, buy_amount=new_order.buy_amount, expiration=int(time.time()) + self.arguments.order_expiry) zrx_order = self.zrx_relayer_api.calculate_fees(zrx_order) zrx_order = self.zrx_exchange.sign_order(zrx_order) if self.zrx_relayer_api.submit_order(zrx_order): self.placed_orders = self.remove_old_orders(self.placed_orders) self.placed_orders.append(zrx_order)
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='tethfinex-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--eth-key", type=str, nargs='*', help= "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')" ) parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--tub-address", type=str, required=False, help="Ethereum address of the Tub contract") parser.add_argument( "--tethfinex-api-server", type=str, default='https://api.ethfinex.com', help= "Address of the Trustless Ethfinex API server (default: 'https://api.ethfinex.com')" ) parser.add_argument( "--tethfinex-timeout", type=float, default=9.5, help="Timeout for accessing the IDEX API (in seconds, default: 9.5)" ) parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument("--ethgasstation-api-key", type=str, default=None, help="ethgasstation API key") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") parser.set_defaults(cancel_on_shutdown=False, withdraw_on_shutdown=False) self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) register_keys(self.web3, self.arguments.eth_key) tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) \ if self.arguments.tub_address is not None else None self.sai = ERC20Token(web3=self.web3, address=tub.sai()) self.price_feed = PriceFeedFactory().create_price_feed( self.arguments, tub) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price( self.web3, self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.tethfinex_exchange = ZrxExchange( web3=self.web3, address=Address(self.arguments.exchange_address)) self.tethfinex_api = TEthfinexApi( self.tethfinex_exchange, self.arguments.tethfinex_api_server, timeout=self.arguments.tethfinex_timeout) config = self.tethfinex_api.get_config()['0x'] self.fee_address = Address(config['ethfinexAddress']) token_registry = config['tokenRegistry'] token_sell = self.token_sell() token_buy = self.token_buy() self.token_sell_wrapper = TEthfinexToken( self.web3, Address(token_registry[token_sell]['wrapperAddress']), token_sell) self.token_buy_wrapper = TEthfinexToken( self.web3, Address(token_registry[token_buy]['wrapperAddress']), token_buy) pair = self.pair() self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency, max_workers=1) self.order_book_manager.get_orders_with( lambda: self.tethfinex_api.get_orders(pair)) self.order_book_manager.cancel_orders_with( lambda order: self.tethfinex_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='paradex-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--eth-key-file", type=str, required=True, help="File with the private key file for the Ethereum account") parser.add_argument( "--eth-password-file", type=str, required=True, help="File with the private key password for the Ethereum account") parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument( "--paradex-api-server", type=str, default='https://api.paradex.io/consumer', help= "Address of the Paradex API (default: 'https://api.paradex.io/consumer')" ) parser.add_argument("--paradex-api-key", type=str, required=True, help="API key for the Paradex API") parser.add_argument( "--paradex-api-timeout", type=float, default=9.5, help= "Timeout for accessing the Paradex API (in seconds, default: 9.5)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument( "--order-expiry", type=int, required=True, help="Expiration time of created orders (in seconds)") parser.add_argument( "--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed( self.arguments.price_feed, self.arguments.price_feed_expiry) self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) self.paradex_api = ParadexApi( self.zrx_exchange, self.arguments.paradex_api_server, self.arguments.paradex_api_key, self.arguments.paradex_api_timeout, self.arguments.eth_key_file, self.read_password(self.arguments.eth_password_file))
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='paradex-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument("--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument("--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--paradex-api-server", type=str, default='https://api.paradex.io/consumer', help="Address of the Paradex API (default: 'https://api.paradex.io/consumer')") parser.add_argument("--paradex-api-key", type=str, required=True, help="API key for the Paradex API") parser.add_argument("--paradex-api-timeout", type=float, default=9.5, help="Timeout for accessing the Paradex API (in seconds, default: 9.5)") parser.add_argument("--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument("--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument("--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument("--order-history-every", type=int, default=30, help="Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--order-expiry", type=int, required=True, help="Expiration time of created orders (in seconds)") parser.add_argument("--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument("--smart-gas-price", dest='smart_gas_price', action='store_true', help="Use smart gas pricing strategy, based on the ethgasstation.info feed") parser.add_argument("--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.pair = self.arguments.pair.upper() self.token_buy = ERC20Token(web3=self.web3, address=Address(self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address(self.arguments.sell_token_address)) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.price_max_decimals = None self.amount_max_decimals = None self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter(self.arguments) self.history = History() self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) self.paradex_api = ParadexApi(self.zrx_exchange, self.arguments.paradex_api_server, self.arguments.paradex_api_key, self.arguments.paradex_api_timeout)
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='0x-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument("--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument("--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--relayer-api-server", type=str, required=True, help="Address of the 0x Relayer API") parser.add_argument("--relayer-per-page", type=int, default=100, help="Number of orders to fetch per one page from the 0x Relayer API (default: 100)") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument("--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument("--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument("--order-history-every", type=int, default=30, help="Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--order-expiry", type=int, required=True, help="Expiration time of created orders (in seconds)") parser.add_argument("--order-expiry-threshold", type=int, default=0, help="How long before order expiration it is considered already expired (in seconds)") parser.add_argument("--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument('--cancel-on-shutdown', dest='cancel_on_shutdown', action='store_true', help="Whether should cancel all open orders on keeper shutdown") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument("--smart-gas-price", dest='smart_gas_price', action='store_true', help="Use smart gas pricing strategy, based on the ethgasstation.info feed") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.token_buy = ERC20Token(web3=self.web3, address=Address(self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address(self.arguments.sell_token_address)) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter(self.arguments) self.history = History() self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) self.zrx_relayer_api = ZrxRelayerApi(exchange=self.zrx_exchange, api_server=self.arguments.relayer_api_server) self.placed_orders = []
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='ddex-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--eth-key", type=str, nargs='*', help= "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')" ) parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument( "--ddex-api-server", type=str, default='https://api.ddex.io', help="Address of the Ddex API (default: 'https://api.ddex.io')") parser.add_argument( "--ddex-api-timeout", type=float, default=9.5, help="Timeout for accessing the Ddex API (in seconds, default: 9.5)" ) parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) register_keys(self.web3, self.arguments.eth_key) if self.arguments.pair != 'USStocks-DAI': self.pair = self.arguments.pair.upper() else: self.pair = self.arguments.pair self.token_buy = ERC20Token(web3=self.web3, address=Address( self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address( self.arguments.sell_token_address)) self.bands_config = ReloadableConfig(self.arguments.config) self.price_max_decimals = None self.amount_max_decimals = None self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) self.ddex_api = DdexApi(self.web3, self.arguments.ddex_api_server, self.arguments.ddex_api_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency, max_workers=1) self.order_book_manager.get_orders_with( lambda: self.ddex_api.get_orders(self.pair)) self.order_book_manager.cancel_orders_with( lambda order: self.ddex_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
class ZrxMarketMakerKeeper: """Keeper acting as a market maker on any 0x exchange implementing the Standard 0x Relayer API V0.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='0x-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument("--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument("--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--relayer-api-server", type=str, required=True, help="Address of the 0x Relayer API") parser.add_argument("--relayer-per-page", type=int, default=100, help="Number of orders to fetch per one page from the 0x Relayer API (default: 100)") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument("--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument("--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument("--order-history-every", type=int, default=30, help="Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--order-expiry", type=int, required=True, help="Expiration time of created orders (in seconds)") parser.add_argument("--order-expiry-threshold", type=int, default=0, help="How long before order expiration it is considered already expired (in seconds)") parser.add_argument("--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument('--cancel-on-shutdown', dest='cancel_on_shutdown', action='store_true', help="Whether should cancel all open orders on keeper shutdown") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument("--smart-gas-price", dest='smart_gas_price', action='store_true', help="Use smart gas pricing strategy, based on the ethgasstation.info feed") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.token_buy = ERC20Token(web3=self.web3, address=Address(self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address(self.arguments.sell_token_address)) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter(self.arguments) self.history = History() self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) self.zrx_relayer_api = ZrxRelayerApi(exchange=self.zrx_exchange, api_server=self.arguments.relayer_api_server) self.placed_orders = [] def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(15, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): self.approve() @retry(delay=5, logger=logger) def shutdown(self): if self.arguments.cancel_on_shutdown: self.cancel_orders(self.our_orders()) def approve(self): self.zrx_exchange.approve([self.token_sell, self.token_buy], directly(gas_price=self.gas_price)) def our_total_balance(self, token: ERC20Token) -> Wad: return token.balance_of(self.our_address) def our_orders(self) -> list: api_orders = self.zrx_relayer_api.get_orders_by_maker(self.our_address, self.arguments.relayer_per_page) all_orders = list(set(self.placed_orders + api_orders)) return self.remove_old_orders(all_orders) def remove_old_orders(self, orders: list) -> list: current_timestamp = int(time.time()) orders = list(filter(lambda order: order.expiration > current_timestamp - self.arguments.order_expiry_threshold, orders)) orders = list(filter(lambda order: self.zrx_exchange.get_unavailable_buy_amount(order) < order.buy_amount, orders)) return orders def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.buy_token == self.token_buy.address and order.pay_token == self.token_sell.address, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.buy_token == self.token_sell.address and order.pay_token == self.token_buy.address, our_orders)) def synchronize_orders(self): if eth_balance(self.web3, self.our_address) < self.min_eth_balance: self.logger.warning("Keeper ETH balance below minimum. Cancelling all orders.") self.cancel_orders(self.our_orders()) return bands = Bands(self.bands_config, self.spread_feed, self.history) our_orders = self.our_orders() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders(our_buy_orders=self.our_buy_orders(our_orders), our_sell_orders=self.our_sell_orders(our_orders), target_price=target_price) if len(cancellable_orders) > 0: self.cancel_orders(cancellable_orders) return # Balances returned by `our_total_balance` still contain amounts "locked" # by currently open orders, so we need to explicitly subtract these amounts. our_buy_balance = self.our_total_balance(self.token_buy) - Bands.total_amount(self.our_buy_orders(our_orders)) our_sell_balance = self.our_total_balance(self.token_sell) - Bands.total_amount(self.our_sell_orders(our_orders)) # Place new orders self.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(our_orders), our_sell_orders=self.our_sell_orders(our_orders), our_buy_balance=our_buy_balance, our_sell_balance=our_sell_balance, target_price=target_price)[0]) def cancel_orders(self, orders): synchronize([self.zrx_exchange.cancel_order(order).transact_async(gas_price=self.gas_price) for order in orders]) def place_orders(self, new_orders): for new_order in new_orders: pay_token = self.token_sell if new_order.is_sell else self.token_buy buy_token = self.token_buy if new_order.is_sell else self.token_sell zrx_order = self.zrx_exchange.create_order(pay_token=pay_token.address, pay_amount=new_order.pay_amount, buy_token=buy_token.address, buy_amount=new_order.buy_amount, expiration=int(time.time()) + self.arguments.order_expiry) zrx_order = self.zrx_relayer_api.calculate_fees(zrx_order) zrx_order = self.zrx_exchange.sign_order(zrx_order) if self.zrx_relayer_api.submit_order(zrx_order): self.placed_orders = self.remove_old_orders(self.placed_orders) self.placed_orders.append(zrx_order)
def test_should_identify_arbitrage_against_0x_and_bust( self, deployment: Deployment): # given # [0x protocol is in place] zrx_token = ERC20Token(web3=deployment.web3, address=deploy_contract(deployment.web3, 'ZRXToken')) token_transfer_proxy_address = deploy_contract(deployment.web3, 'TokenTransferProxy') exchange = ZrxExchange.deploy(deployment.web3, zrx_token.address, token_transfer_proxy_address) deployment.web3.eth.contract(abi=json.loads( pkg_resources.resource_string( 'pymaker.deployment', f'abi/TokenTransferProxy.abi')))( address=token_transfer_proxy_address.address).transact( ).addAuthorizedAddress(exchange.address.address) # and keeper = ArbitrageKeeper(args=args( f"--eth-from {deployment.our_address.address}" f" --tub-address {deployment.tub.address}" f" --tap-address {deployment.tap.address}" f" --oasis-address {deployment.otc.address}" f" --exchange-address {exchange.address}" f" --relayer-api-server http://127.0.0.1:9999/sra/v0" f" --base-token {deployment.sai.address}" f" --min-profit 950.0 --max-engagement 14250.0"), web3=deployment.web3) # and # [we generate some bad debt available for `bust`] DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int( Wad.from_number(500).value).transact() deployment.tub.mold_cap(Wad.from_number(1000000)).transact() deployment.tub.mold_mat(Ray.from_number(2.0)).transact() deployment.tub.mold_axe(Ray.from_number(2.0)).transact() deployment.gem.mint(Wad.from_number(100)).transact() deployment.tub.join(Wad.from_number(100)).transact() deployment.tub.open().transact() deployment.tub.lock(1, Wad.from_number(100)).transact() deployment.tub.draw(1, Wad.from_number(25000)).transact() DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int( Wad.from_number(400).value).transact() deployment.tub.bite(1).transact() DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int( Wad.from_number(500).value).transact() assert deployment.tap.woe() == Wad.from_number(25000) assert deployment.tap.fog() == Wad.from_number(100) # and # [we add a boom/bust spread to make calculations a bit more difficult] deployment.tap.mold_gap(Wad.from_number(0.95)).transact() assert deployment.tap.ask(Wad.from_number(1)) == Wad.from_number(475.0) assert deployment.tap.bid(Wad.from_number(1)) == Wad.from_number(525.0) # and # [we have some SKR to cover rounding errors] deployment.skr.mint(Wad.from_number(0.000000000000000001)).transact() # and # [we should now have 30 SKR available for 14250 SAI on `bust`] # [now lets pretend we placed an order on 0x offering 15250 SAI for 30 GEM] # [this will be an arbitrage opportunity which can make the bot earn 1000 SAI] deployment.sai.mint(Wad.from_number(15250)).transact() exchange.approve([deployment.sai, deployment.gem], directly()) zrx_order = exchange.sign_order( exchange.create_order(pay_token=deployment.sai.address, pay_amount=Wad.from_number(15250), buy_token=deployment.gem.address, buy_amount=Wad.from_number(30), expiration=int(time.time() + 3600))) keeper.zrx_orders = lambda tokens: [zrx_order] # when keeper.approve() keeper.process_block() # then # [the 0x order has been taken by the keeper] assert exchange.get_unavailable_buy_amount( zrx_order) == Wad.from_number(30) # and # [the amount of bad debt has decreased, so we know the keeper did call bust('14250.0')] # [the inequality below is to cater for rounding errors] assert deployment.tap.woe() < Wad.from_number(10800.0)
class RadarRelayMarketMakerKeeper: """Keeper acting as a market maker on RadarRelay, on the WETH/SAI pair.""" logger = logging.getLogger('radarrelay-market-maker-keeper') def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='radarrelay-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument("--tub-address", type=str, required=True, help="Ethereum address of the Tub contract") parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--weth-address", type=str, required=True, help="Ethereum address of the WETH token") parser.add_argument("--relayer-api-server", type=str, required=True, help="Address of the 0x Relayer API") parser.add_argument("--config", type=str, required=True, help="Buy/sell bands configuration file") parser.add_argument( "--price-feed", type=str, help= "Source of price feed. Tub price feed will be used if not specified" ) parser.add_argument( "--order-expiry", type=int, required=True, help="Expiration time of created orders (in seconds)") parser.add_argument( "--order-expiry-threshold", type=int, default=0, help= "Order expiration time at which order is considered already expired (in seconds)" ) parser.add_argument( "--min-eth-balance", type=float, default=0, help= "Minimum ETH balance below which keeper with either terminate or not start at all" ) parser.add_argument( '--cancel-on-shutdown', dest='cancel_on_shutdown', action='store_true', help= "Whether should cancel all open orders on RadarRelay on keeper shutdown" ) parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}")) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) self.vox = Vox(web3=self.web3, address=self.tub.vox()) self.sai = ERC20Token(web3=self.web3, address=self.tub.sai()) self.ether_token = ERC20Token(web3=self.web3, address=Address( self.arguments.weth_address)) logging.basicConfig( format='%(asctime)-15s %(levelname)-8s %(message)s', level=(logging.DEBUG if self.arguments.debug else logging.INFO)) self.bands_config = ReloadableConfig(self.arguments.config) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.price_feed = PriceFeedFactory().create_price_feed( self.arguments.price_feed, self.tub, self.vox) self.radar_relay = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) self.radar_relay_api = ZrxRelayerApi( exchange=self.radar_relay, api_server=self.arguments.relayer_api_server) def main(self): with Web3Lifecycle(self.web3) as lifecycle: self.lifecycle = lifecycle lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(15, self.synchronize_orders) lifecycle.every(60 * 60, self.print_balances) lifecycle.on_shutdown(self.shutdown) def startup(self): self.approve() def shutdown(self): if self.arguments.cancel_on_shutdown: self.cancel_orders(self.our_orders()) def print_balances(self): sai_owned = self.sai.balance_of(self.our_address) weth_owned = self.ether_token.balance_of(self.our_address) self.logger.info( f"Keeper balances are {sai_owned} SAI, {weth_owned} + 0x-WETH") def approve(self): """Approve 0x to access our 0x-WETH and SAI, so we can sell it on the exchange.""" self.radar_relay.approve([self.ether_token, self.sai], directly()) def our_orders(self) -> list: our_orders = self.radar_relay_api.get_orders_by_maker(self.our_address) current_timestamp = int(time.time()) our_orders = list( filter( lambda order: order.expiration > current_timestamp - self. arguments.order_expiry_threshold, our_orders)) our_orders = list( filter( lambda order: self.radar_relay.get_unavailable_buy_amount( order) < order.buy_amount, our_orders)) return our_orders def our_sell_orders(self, our_orders: list) -> list: return list( filter( lambda order: order.buy_token == self.sai.address and order. pay_token == self.ether_token.address, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list( filter( lambda order: order.buy_token == self.ether_token.address and order.pay_token == self.sai.address, our_orders)) def synchronize_orders(self): """Update our positions in the order book to reflect keeper parameters.""" if eth_balance(self.web3, self.our_address) < self.min_eth_balance: self.lifecycle.terminate( "Keeper balance is below the minimum, terminating.") self.cancel_orders(self.our_orders()) return bands = Bands(self.bands_config) our_orders = self.our_orders() target_price = self.price_feed.get_price() if target_price is None: self.logger.warning( "Cancelling all orders as no price feed available.") self.cancel_orders(our_orders) return self.cancel_orders( itertools.chain( bands.excessive_buy_orders(self.our_buy_orders(our_orders), target_price), bands.excessive_sell_orders(self.our_sell_orders(our_orders), target_price), bands.outside_orders(self.our_buy_orders(our_orders), self.our_sell_orders(our_orders), target_price))) self.top_up_bands(our_orders, bands.buy_bands, bands.sell_bands, target_price) def cancel_orders(self, orders): """Cancel orders asynchronously.""" synchronize([ self.radar_relay.cancel_order(order).transact_async( gas_price=self.gas_price()) for order in orders ]) def excessive_orders_in_band(self, band, orders: list, target_price: Wad): """Return orders which need to be cancelled to bring the total order amount in the band below maximum.""" # if total amount of orders in this band is greater than the maximum, we cancel them all # # if may not be the best solution as cancelling only some of them could bring us below # the maximum, but let's stick to it for now orders_in_band = [ order for order in orders if band.includes(order, target_price) ] return orders_in_band if self.total_amount( orders_in_band) > band.max_amount else [] def top_up_bands(self, our_orders: list, buy_bands: list, sell_bands: list, target_price: Wad): """Create new buy and sell orders in all send and buy bands if necessary.""" self.top_up_buy_bands(our_orders, buy_bands, target_price) self.top_up_sell_bands(our_orders, sell_bands, target_price) def top_up_sell_bands(self, our_orders: list, sell_bands: list, target_price: Wad): """Ensure our WETH engagement is not below minimum in all sell bands. Place new orders if necessary.""" our_balance = self.ether_token.balance_of( self.our_address) #TODO deduct orders / or maybe not...? for band in sell_bands: orders = [ order for order in self.our_sell_orders(our_orders) if band.includes(order, target_price) ] total_amount = self.total_amount(orders) if total_amount < band.min_amount: have_amount = Wad.min(band.avg_amount - total_amount, our_balance) if (have_amount >= band.dust_cutoff) and (have_amount > Wad(0)): our_balance = our_balance - have_amount #TODO I think this line is unnecessary here want_amount = have_amount * round( band.avg_price(target_price)) if want_amount > Wad(0): order = self.radar_relay.create_order( pay_token=self.ether_token.address, pay_amount=have_amount, buy_token=self.sai.address, buy_amount=want_amount, expiration=int(time.time()) + self.arguments.order_expiry) order = self.radar_relay_api.calculate_fees(order) order = self.radar_relay.sign_order(order) self.radar_relay_api.submit_order(order) def top_up_buy_bands(self, our_orders: list, buy_bands: list, target_price: Wad): """Ensure our SAI engagement is not below minimum in all buy bands. Place new orders if necessary.""" our_balance = self.sai.balance_of( self.our_address) #TODO deduct orders / or maybe not...? for band in buy_bands: orders = [ order for order in self.our_buy_orders(our_orders) if band.includes(order, target_price) ] total_amount = self.total_amount(orders) if total_amount < band.min_amount: have_amount = Wad.min(band.avg_amount - total_amount, our_balance) if (have_amount >= band.dust_cutoff) and (have_amount > Wad(0)): our_balance = our_balance - have_amount #TODO I think this line is unnecessary here want_amount = have_amount / round( band.avg_price(target_price)) if want_amount > Wad(0): order = self.radar_relay.create_order( pay_token=self.sai.address, pay_amount=have_amount, buy_token=self.ether_token.address, buy_amount=want_amount, expiration=int(time.time()) + self.arguments.order_expiry) order = self.radar_relay_api.calculate_fees(order) order = self.radar_relay.sign_order(order) self.radar_relay_api.submit_order(order) def total_amount(self, orders): pay_amount_available = lambda order: order.pay_amount - ( self.radar_relay.get_unavailable_buy_amount( order) * order.pay_amount / order.buy_amount) return reduce(operator.add, map(pay_amount_available, orders), Wad(0)) def gas_price(self) -> GasPrice: if self.arguments.gas_price > 0: return FixedGasPrice(self.arguments.gas_price) else: return DefaultGasPrice()
def test_fail_when_no_contract_under_that_address(self): # expect with pytest.raises(Exception): ZrxExchange(web3=self.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='theocean-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument( "--theocean-api-server", type=str, default='https://api.theocean.trade/api', help= "Address of the TheOcean API (default: 'https://api.theocean.trade/api')" ) parser.add_argument("--theocean-api-key", type=str, required=True, help="API key for the TheOcean API") parser.add_argument("--theocean-api-secret", type=str, required=True, help="API secret for the TheOcean API") parser.add_argument( "--theocean-api-timeout", type=float, default=9.5, help= "Timeout for accessing the TheOcean API (in seconds, default: 9.5)" ) parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.token_buy = ERC20Token(web3=self.web3, address=Address( self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address( self.arguments.sell_token_address)) self.pair = Pair(self.token_sell.address, self.token_buy.address) self.bands_config = ReloadableConfig(self.arguments.config) self.price_max_decimals = None self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) self.theocean_api = TheOceanApi(self.zrx_exchange, self.arguments.theocean_api_server, self.arguments.theocean_api_key, self.arguments.theocean_api_secret, self.arguments.theocean_api_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.theocean_api.get_orders(self.pair)) self.order_book_manager.get_balances_with(lambda: self.get_balances()) self.order_book_manager.place_orders_with(self.place_order_function) self.order_book_manager.cancel_orders_with( lambda order: self.theocean_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
class DdexMarketMakerKeeper: """Keeper acting as a market maker on Ddex.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='ddex-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--eth-key", type=str, nargs='*', help= "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')" ) parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument( "--ddex-api-server", type=str, default='https://api.ddex.io', help="Address of the Ddex API (default: 'https://api.ddex.io')") parser.add_argument( "--ddex-api-timeout", type=float, default=9.5, help="Timeout for accessing the Ddex API (in seconds, default: 9.5)" ) parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) register_keys(self.web3, self.arguments.eth_key) if self.arguments.pair != 'USStocks-DAI': self.pair = self.arguments.pair.upper() else: self.pair = self.arguments.pair self.token_buy = ERC20Token(web3=self.web3, address=Address( self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address( self.arguments.sell_token_address)) self.bands_config = ReloadableConfig(self.arguments.config) self.price_max_decimals = None self.amount_max_decimals = None self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) self.ddex_api = DdexApi(self.web3, self.arguments.ddex_api_server, self.arguments.ddex_api_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency, max_workers=1) self.order_book_manager.get_orders_with( lambda: self.ddex_api.get_orders(self.pair)) self.order_book_manager.cancel_orders_with( lambda order: self.ddex_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): self.approve() # Get maximum number of decimals for prices and amounts. # Ddex API enforces it. markets = self.ddex_api.get_markets()['data']['markets'] market = next(filter(lambda item: item['id'] == self.pair, markets)) self.price_max_decimals = market['priceDecimals'] self.amount_max_decimals = market['amountDecimals'] def shutdown(self): self.order_book_manager.cancel_all_orders() def approve(self): self.zrx_exchange.approve([self.token_sell, self.token_buy], directly(gas_price=self.gas_price)) def our_total_balance(self, token: ERC20Token) -> Wad: return token.balance_of(self.our_address) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.control_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # In case of Ddex, balances returned by `our_total_balance` still contain amounts "locked" # by currently open orders, so we need to explicitly subtract these amounts. our_buy_balance = self.our_total_balance( self.token_buy) - Bands.total_amount( self.our_buy_orders(order_book.orders)) our_sell_balance = self.our_total_balance( self.token_sell) - Bands.total_amount( self.our_sell_orders(order_book.orders)) # Place new orders self.place_orders( bands.new_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=our_buy_balance, our_sell_balance=our_sell_balance, target_price=target_price)[0]) def place_orders(self, new_orders): def place_order_function(new_order_to_be_placed): price = round(new_order_to_be_placed.price, self.price_max_decimals) amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount amount = round(amount, self.amount_max_decimals) order_id = self.ddex_api.place_order( pair=self.pair, is_sell=new_order_to_be_placed.is_sell, price=price, amount=amount) return Order(order_id, self.pair, new_order_to_be_placed.is_sell, price, amount, amount) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order))
class RadarRelayMarketMakerChart: """Tool to analyze the RadarRelay Market Maker keeper performance.""" def __init__(self, args: list): parser = argparse.ArgumentParser(prog='radarrelay-market-maker-chart') parser.add_argument("--rpc-host", help="JSON-RPC host (default: `localhost')", default="localhost", type=str) parser.add_argument("--rpc-port", help="JSON-RPC port (default: `8545')", default=8545, type=int) parser.add_argument("--exchange-address", help="Ethereum address of the 0x contract", required=True, type=str) parser.add_argument("--sai-address", help="Ethereum address of the SAI token", required=True, type=str) parser.add_argument("--weth-address", help="Ethereum address of the WETH token", required=True, type=str) parser.add_argument("--market-maker-address", help="Ethereum account of the market maker to analyze", required=True, type=str) parser.add_argument("--past-blocks", help="Number of past blocks to analyze", required=True, type=int) parser.add_argument("-o", "--output", help="Name of the filename to save to chart to." " Will get displayed on-screen if empty", required=False, type=str) self.arguments = parser.parse_args(args) self.web3 = Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={'timeout': 120})) self.infura = Web3(HTTPProvider(endpoint_uri=f"https://mainnet.infura.io/", request_kwargs={'timeout': 120})) self.sai_address = Address(self.arguments.sai_address) self.weth_address = Address(self.arguments.weth_address) self.market_maker_address = Address(self.arguments.market_maker_address) self.exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) if self.arguments.output: import matplotlib matplotlib.use('Agg') def main(self): past_trade = self.exchange.past_fill(self.arguments.past_blocks) def sell_trades() -> List[Trade]: return list(map(lambda log_take: Trade(self.get_event_timestamp(log_take), log_take.filled_buy_amount / log_take.filled_pay_amount, log_take.filled_buy_amount, False, True), filter(lambda log_take: log_take.maker == self.market_maker_address and log_take.buy_token == self.sai_address and log_take.pay_token == self.weth_address, past_trade))) def buy_trades() -> List[Trade]: return list(map(lambda log_take: Trade(self.get_event_timestamp(log_take), log_take.filled_pay_amount / log_take.filled_buy_amount, log_take.filled_pay_amount, True, False), filter(lambda log_take: log_take.maker == self.market_maker_address and log_take.buy_token == self.weth_address and log_take.pay_token == self.sai_address, past_trade))) start_timestamp = self.get_event_timestamp(past_trade[0]) end_timestamp = int(time.time()) prices = get_gdax_prices(start_timestamp, end_timestamp) trades = sell_trades() + buy_trades() self.draw(prices, trades) def get_event_timestamp(self, event): return self.infura.eth.getBlock(event.raw['blockHash']).timestamp def convert_timestamp(self, timestamp): from matplotlib.dates import date2num return date2num(datetime.datetime.fromtimestamp(timestamp)) def to_size(self, trade: Trade): return amount_in_sai_to_size(trade.value_in_sai) def draw(self, prices: List[Price], trades: List[Trade]): import matplotlib.dates as md import matplotlib.pyplot as plt plt.subplots_adjust(bottom=0.2) plt.xticks(rotation=25) ax=plt.gca() ax.xaxis.set_major_formatter(md.DateFormatter('%Y-%m-%d %H:%M:%S')) timestamps = list(map(self.convert_timestamp, map(lambda price: price.timestamp, prices))) market_prices = list(map(lambda price: price.market_price, prices)) plt.plot_date(timestamps, market_prices, 'r-', zorder=1) sell_trades = list(filter(lambda trade: trade.is_sell, trades)) sell_x = list(map(self.convert_timestamp, map(lambda trade: trade.timestamp, sell_trades))) sell_y = list(map(lambda trade: trade.price, sell_trades)) sell_s = list(map(self.to_size, sell_trades)) plt.scatter(x=sell_x, y=sell_y, s=sell_s, c='blue', zorder=2) buy_trades = list(filter(lambda trade: trade.is_buy, trades)) buy_x = list(map(self.convert_timestamp, map(lambda trade: trade.timestamp, buy_trades))) buy_y = list(map(lambda trade: trade.price, buy_trades)) buy_s = list(map(self.to_size, buy_trades)) plt.scatter(x=buy_x, y=buy_y, s=buy_s, c='green', zorder=2) if self.arguments.output: plt.savefig(fname=self.arguments.output, dpi=300, bbox_inches='tight', pad_inches=0) else: plt.show()
class ParadexMarketMakerKeeper: """Keeper acting as a market maker on Paradex.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='paradex-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--eth-key-file", type=str, required=True, help="File with the private key file for the Ethereum account") parser.add_argument( "--eth-password-file", type=str, required=True, help="File with the private key password for the Ethereum account") parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument( "--paradex-api-server", type=str, default='https://api.paradex.io/consumer', help= "Address of the Paradex API (default: 'https://api.paradex.io/consumer')" ) parser.add_argument("--paradex-api-key", type=str, required=True, help="API key for the Paradex API") parser.add_argument( "--paradex-api-timeout", type=float, default=9.5, help= "Timeout for accessing the Paradex API (in seconds, default: 9.5)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument( "--order-expiry", type=int, required=True, help="Expiration time of created orders (in seconds)") parser.add_argument( "--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed( self.arguments.price_feed, self.arguments.price_feed_expiry) self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) self.paradex_api = ParadexApi( self.zrx_exchange, self.arguments.paradex_api_server, self.arguments.paradex_api_key, self.arguments.paradex_api_timeout, self.arguments.eth_key_file, self.read_password(self.arguments.eth_password_file)) @staticmethod def read_password(filename: str): with open(filename) as file: return "".join(line.rstrip() for line in file) def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(3, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): self.approve() @retry(delay=5, logger=logger) def shutdown(self): self.cancel_orders(self.our_orders()) def approve(self): self.zrx_exchange.approve( [self.token_sell(), self.token_buy()], directly(gas_price=self.gas_price)) def price(self) -> Wad: return self.price_feed.get_price() def pair(self): return self.arguments.pair.upper() def token_sell(self) -> ERC20Token: return ERC20Token(web3=self.web3, address=Address(self.arguments.sell_token_address)) def token_buy(self) -> ERC20Token: return ERC20Token(web3=self.web3, address=Address(self.arguments.buy_token_address)) def our_total_balance(self, token: ERC20Token) -> Wad: return token.balance_of(self.our_address) def our_orders(self) -> list: return self.paradex_api.get_orders(self.pair()) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): """Update our positions in the order book to reflect keeper parameters.""" if eth_balance(self.web3, self.our_address) < self.min_eth_balance: self.logger.warning( "Keeper ETH balance below minimum. Cancelling all orders.") self.cancel_orders(self.our_orders()) return bands = Bands(self.bands_config) our_orders = self.our_orders() target_price = self.price() if target_price is None: self.logger.warning( "Cancelling all orders as no price feed available.") self.cancel_orders(our_orders) return # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(our_orders), our_sell_orders=self.our_sell_orders(our_orders), target_price=target_price) if len(cancellable_orders) > 0: self.cancel_orders(cancellable_orders) return # In case of Paradex, balances returned by `our_total_balance` still contain amounts "locked" # by currently open orders, so we need to explicitly subtract these amounts. our_buy_balance = self.our_total_balance( self.token_buy()) - Bands.total_amount( self.our_buy_orders(our_orders)) our_sell_balance = self.our_total_balance( self.token_sell()) - Bands.total_amount( self.our_sell_orders(our_orders)) # Place new orders self.create_orders( bands.new_orders(our_buy_orders=self.our_buy_orders(our_orders), our_sell_orders=self.our_sell_orders(our_orders), our_buy_balance=our_buy_balance, our_sell_balance=our_sell_balance, target_price=target_price)[0]) def cancel_orders(self, orders): for order in orders: self.paradex_api.cancel_order(order.order_id) def create_orders(self, orders): for order in orders: amount = order.pay_amount if order.is_sell else order.buy_amount self.paradex_api.place_order(self.pair(), order.is_sell, order.price, amount, self.arguments.order_expiry) exit(-1)
class ParadexMarketMakerKeeper: """Keeper acting as a market maker on Paradex.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='paradex-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument("--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument("--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--paradex-api-server", type=str, default='https://api.paradex.io/consumer', help="Address of the Paradex API (default: 'https://api.paradex.io/consumer')") parser.add_argument("--paradex-api-key", type=str, required=True, help="API key for the Paradex API") parser.add_argument("--paradex-api-timeout", type=float, default=9.5, help="Timeout for accessing the Paradex API (in seconds, default: 9.5)") parser.add_argument("--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument("--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument("--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument("--order-history-every", type=int, default=30, help="Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--order-expiry", type=int, required=True, help="Expiration time of created orders (in seconds)") parser.add_argument("--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument("--smart-gas-price", dest='smart_gas_price', action='store_true', help="Use smart gas pricing strategy, based on the ethgasstation.info feed") parser.add_argument("--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.pair = self.arguments.pair.upper() self.token_buy = ERC20Token(web3=self.web3, address=Address(self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address(self.arguments.sell_token_address)) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.price_max_decimals = None self.amount_max_decimals = None self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter(self.arguments) self.history = History() self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) self.paradex_api = ParadexApi(self.zrx_exchange, self.arguments.paradex_api_server, self.arguments.paradex_api_key, self.arguments.paradex_api_timeout) def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(self.arguments.refresh_frequency, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): self.approve() # Get maximum number of decimals for prices and amounts. # Paradex API enforces it. markets = self.paradex_api.get_markets() market = next(filter(lambda item: item['symbol'] == self.pair, markets)) self.price_max_decimals = market['priceMaxDecimals'] self.amount_max_decimals = market['amountMaxDecimals'] @retry(delay=5, logger=logger) def shutdown(self): self.cancel_orders(self.our_orders()) def approve(self): self.zrx_exchange.approve([self.token_sell, self.token_buy], directly(gas_price=self.gas_price)) def our_total_balance(self, token: ERC20Token) -> Wad: return token.balance_of(self.our_address) def our_orders(self) -> list: return self.paradex_api.get_orders(self.pair) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): if eth_balance(self.web3, self.our_address) < self.min_eth_balance: self.logger.warning("Keeper ETH balance below minimum. Cancelling all orders.") self.cancel_orders(self.our_orders()) return bands = Bands(self.bands_config, self.spread_feed, self.history) our_orders = self.our_orders() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders(our_buy_orders=self.our_buy_orders(our_orders), our_sell_orders=self.our_sell_orders(our_orders), target_price=target_price) if len(cancellable_orders) > 0: self.cancel_orders(cancellable_orders) return # In case of Paradex, balances returned by `our_total_balance` still contain amounts "locked" # by currently open orders, so we need to explicitly subtract these amounts. our_buy_balance = self.our_total_balance(self.token_buy) - Bands.total_amount(self.our_buy_orders(our_orders)) our_sell_balance = self.our_total_balance(self.token_sell) - Bands.total_amount(self.our_sell_orders(our_orders)) # Place new orders self.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(our_orders), our_sell_orders=self.our_sell_orders(our_orders), our_buy_balance=our_buy_balance, our_sell_balance=our_sell_balance, target_price=target_price)[0]) def cancel_orders(self, orders): for order in orders: self.paradex_api.cancel_order(order.order_id) def place_orders(self, new_orders): for new_order in new_orders: amount = new_order.pay_amount if new_order.is_sell else new_order.buy_amount self.paradex_api.place_order(pair=self.pair, is_sell=new_order.is_sell, price=round(new_order.price, self.price_max_decimals), amount=round(amount, self.amount_max_decimals), expiry=self.arguments.order_expiry)
class TheOceanMarketMakerKeeper: """Keeper acting as a market maker on TheOcean.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='theocean-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument( "--theocean-api-server", type=str, default='https://api.theocean.trade/api', help= "Address of the TheOcean API (default: 'https://api.theocean.trade/api')" ) parser.add_argument("--theocean-api-key", type=str, required=True, help="API key for the TheOcean API") parser.add_argument("--theocean-api-secret", type=str, required=True, help="API secret for the TheOcean API") parser.add_argument( "--theocean-api-timeout", type=float, default=9.5, help= "Timeout for accessing the TheOcean API (in seconds, default: 9.5)" ) parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.token_buy = ERC20Token(web3=self.web3, address=Address( self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address( self.arguments.sell_token_address)) self.pair = Pair(self.token_sell.address, self.token_buy.address) self.bands_config = ReloadableConfig(self.arguments.config) self.price_max_decimals = None self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) self.theocean_api = TheOceanApi(self.zrx_exchange, self.arguments.theocean_api_server, self.arguments.theocean_api_key, self.arguments.theocean_api_secret, self.arguments.theocean_api_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.theocean_api.get_orders(self.pair)) self.order_book_manager.get_balances_with(lambda: self.get_balances()) self.order_book_manager.place_orders_with(self.place_order_function) self.order_book_manager.cancel_orders_with( lambda order: self.theocean_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): self.approve() # Get maximum number of decimals for prices. market = self.theocean_api.get_market(self.pair) assert (int(market['baseToken']['decimals']) == 18) assert (int(market['quoteToken']['decimals']) == 18) assert (int(market['baseToken']['precision']) == int( market['quoteToken']['precision'])) self.price_max_decimals = int(market['baseToken']['precision']) def shutdown(self): self.order_book_manager.cancel_all_orders() def approve(self): self.zrx_exchange.approve([self.token_sell, self.token_buy], directly(gas_price=self.gas_price)) def get_balances(self): return self.theocean_api.get_balance( self.pair.sell_token), self.theocean_api.get_balance( self.pair.buy_token) def our_sell_balance(self, balances) -> Wad: return balances[0] def our_buy_balance(self, balances) -> Wad: return balances[1] def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # Place new orders self.order_book_manager.place_orders( bands.new_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.our_buy_balance(order_book.balances), our_sell_balance=self.our_sell_balance(order_book.balances), target_price=target_price)[0]) def place_order_function(self, new_order: NewOrder): assert (isinstance(new_order, NewOrder)) pair = self.pair is_sell = new_order.is_sell price = round(new_order.price, self.price_max_decimals) amount = new_order.pay_amount if new_order.is_sell else new_order.buy_amount new_order_id = self.theocean_api.place_order(pair=pair, is_sell=is_sell, price=price, amount=amount) if new_order_id is not None: return Order(order_id=new_order_id, pair=pair, is_sell=is_sell, price=price, amount=amount) else: return None
class ZrxMarketMakerKeeper: """Keeper acting as a market maker on any 0x exchange implementing the Standard 0x Relayer API V0.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='0x-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument("--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument("--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--relayer-api-server", type=str, required=True, help="Address of the 0x Relayer API") parser.add_argument("--relayer-per-page", type=int, default=100, help="Number of orders to fetch per one page from the 0x Relayer API (default: 100)") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--buy-token-decimals", type=int, default=18, help="Number of decimals of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--sell-token-decimals", type=int, default=18, help="Number of decimals of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument("--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument("--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument("--order-history-every", type=int, default=30, help="Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--order-expiry", type=int, required=True, help="Expiration time of created orders (in seconds)") parser.add_argument("--order-expiry-threshold", type=int, default=0, help="How long before order expiration it is considered already expired (in seconds)") parser.add_argument("--use-full-balances", dest='use_full_balances', action='store_true', help="Do not subtract the amounts locked by current orders from available balances") parser.add_argument("--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument('--cancel-on-shutdown', dest='cancel_on_shutdown', action='store_true', help="Whether should cancel all open orders on keeper shutdown") parser.add_argument("--remember-own-orders", dest='remember_own_orders', action='store_true', help="Whether should the keeper remember his own submitted orders") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument("--smart-gas-price", dest='smart_gas_price', action='store_true', help="Use smart gas pricing strategy, based on the ethgasstation.info feed") parser.add_argument("--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter(self.arguments) self.history = History() # Delegate 0x specific init to a function to permit overload for 0xv2 self.zrx_exchange = None self.zrx_relayer_api = None self.zrx_api = None self.pair = None self.init_zrx() self.placed_zrx_orders = [] self.placed_zrx_orders_lock = Lock() self.order_book_manager = OrderBookManager(refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with(lambda: self.get_orders()) self.order_book_manager.get_balances_with(lambda: self.get_balances()) self.order_book_manager.place_orders_with(self.place_order_function) self.order_book_manager.cancel_orders_with(self.cancel_order_function) self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def init_zrx(self): self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) self.zrx_relayer_api = ZrxRelayerApi(exchange=self.zrx_exchange, api_server=self.arguments.relayer_api_server) self.zrx_api = ZrxApi(zrx_exchange=self.zrx_exchange) self.pair = Pair(sell_token_address=Address(self.arguments.sell_token_address), sell_token_decimals=self.arguments.sell_token_decimals, buy_token_address=Address(self.arguments.buy_token_address), buy_token_decimals=self.arguments.buy_token_decimals) def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): self.approve() def shutdown(self): self.order_book_manager.cancel_all_orders(final_wait_time=60) def approve(self): token_buy = ERC20Token(web3=self.web3, address=Address(self.pair.buy_token_address)) token_sell = ERC20Token(web3=self.web3, address=Address(self.pair.sell_token_address)) self.zrx_exchange.approve([token_sell, token_buy], directly(gas_price=self.gas_price)) def remove_expired_orders(self, orders: list) -> list: current_timestamp = int(time.time()) return list(filter(lambda order: order.zrx_order.expiration > current_timestamp - self.arguments.order_expiry_threshold, orders)) def remove_expired_zrx_orders(self, zrx_orders: list) -> list: current_timestamp = int(time.time()) return list(filter(lambda order: order.expiration > current_timestamp - self.arguments.order_expiry_threshold, zrx_orders)) def remove_filled_or_cancelled_zrx_orders(self, zrx_orders: list) -> list: return list(filter(lambda order: self.zrx_exchange.get_unavailable_buy_amount(order) < order.buy_amount, zrx_orders)) def get_orders(self) -> list: def remove_old_zrx_orders(zrx_orders: list) -> list: return self.remove_filled_or_cancelled_zrx_orders(self.remove_expired_zrx_orders(zrx_orders)) with self.placed_zrx_orders_lock: self.placed_zrx_orders = remove_old_zrx_orders(self.placed_zrx_orders) api_zrx_orders = remove_old_zrx_orders(self.zrx_relayer_api.get_orders_by_maker(self.our_address, self.arguments.relayer_per_page)) with self.placed_zrx_orders_lock: zrx_orders = list(set(self.placed_zrx_orders + api_zrx_orders)) return self.zrx_api.get_orders(self.pair, zrx_orders) def get_balances(self): balances = self.zrx_api.get_balances(self.pair) return balances[0], balances[1], eth_balance(self.web3, self.our_address) def our_total_sell_balance(self, balances) -> Wad: return balances[0] def our_total_buy_balance(self, balances) -> Wad: return balances[1] def our_eth_balance(self, balances) -> Wad: return balances[2] def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # We filter out expired orders from the order book snapshot. The reason for that is that # it allows us to replace expired orders faster. Without it, we would have to wait # for the next order book refresh in order to realize an order has expired. Unfortunately, # in case of 0x order book refresh can be quite slow as it involves making multiple calls # to the Ethereum node. # # By filtering out expired orders here, we can replace them the next `synchronize_orders` # tick after they expire. Which is ~ 1s delay, instead of avg ~ 5s without this trick. orders = self.remove_expired_orders(order_book.orders) if self.our_eth_balance(order_book.balances) < self.min_eth_balance: self.logger.warning("Keeper ETH balance below minimum. Cancelling all orders.") self.order_book_manager.cancel_all_orders() return # Cancel orders cancellable_orders = bands.cancellable_orders(our_buy_orders=self.our_buy_orders(orders), our_sell_orders=self.our_sell_orders(orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug("Order book is in progress, not placing new orders") return # Balances returned by `our_total_***_balance` still contain amounts "locked" # by currently open orders, so we need to explicitly subtract these amounts. if self.arguments.use_full_balances: our_buy_balance = self.our_total_buy_balance(order_book.balances) our_sell_balance = self.our_total_sell_balance(order_book.balances) else: our_buy_balance = self.our_total_buy_balance(order_book.balances) - Bands.total_amount(self.our_buy_orders(orders)) our_sell_balance = self.our_total_sell_balance(order_book.balances) - Bands.total_amount(self.our_sell_orders(orders)) # Place new orders self.order_book_manager.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(orders), our_sell_orders=self.our_sell_orders(orders), our_buy_balance=our_buy_balance, our_sell_balance=our_sell_balance, target_price=target_price)[0]) def place_order_function(self, new_order: NewOrder): assert(isinstance(new_order, NewOrder)) zrx_order = self.zrx_api.place_order(pair=self.pair, is_sell=new_order.is_sell, price=new_order.price, amount=new_order.amount, expiration=int(time.time()) + self.arguments.order_expiry) zrx_order = self.zrx_relayer_api.calculate_fees(zrx_order) zrx_order = self.zrx_exchange.sign_order(zrx_order) if self.zrx_relayer_api.submit_order(zrx_order): if self.arguments.remember_own_orders: with self.placed_zrx_orders_lock: self.placed_zrx_orders.append(zrx_order) order = self.zrx_api.get_orders(self.pair, [zrx_order])[0] return order else: return None def cancel_order_function(self, order): transact = self.zrx_exchange.cancel_order(order.zrx_order).transact(gas_price=self.gas_price) return transact is not None and transact.successful
def __init__(self, args: list): parser = argparse.ArgumentParser(prog='0x-market-maker-pnl') parser.add_argument("--rpc-host", help="JSON-RPC host (default: `localhost')", default="localhost", type=str) parser.add_argument("--rpc-port", help="JSON-RPC port (default: `8545')", default=8545, type=int) parser.add_argument("--rpc-timeout", help="JSON-RPC timeout (in seconds, default: 60)", type=int, default=60) parser.add_argument("--exchange-address", help="Ethereum address of the 0x contract", required=True, type=str) parser.add_argument( "--market-maker-address", help="Ethereum account of the market maker to analyze", required=True, type=str) parser.add_argument( "--gdax-price", help= "GDAX product (ETH-USD, BTC-USD) to use as the price history source", type=str) parser.add_argument( "--price-feed", help="Price endpoint to use as the price history source", type=str) parser.add_argument("--price-history-file", help="File to use as the price history source", type=str) parser.add_argument("--vwap-minutes", help="Rolling VWAP window size (default: 240)", type=int, default=240) parser.add_argument("--buy-token", help="Name of the buy token", required=True, type=str) parser.add_argument("--buy-token-address", help="Ethereum address of the buy token", required=True, type=str) parser.add_argument("--buy-token-decimals", help="Number of decimals for the buy token", type=int, default=18) parser.add_argument("--sell-token", help="Name of the sell token", required=True, type=str) parser.add_argument("--sell-token-address", help="Ethereum address of the sell token", required=True, type=str) parser.add_argument("--sell-token-decimals", help="Number of decimals for the sell token", type=int, default=18) parser.add_argument("--old-sell-token-address", help="Ethereum address of the old sell token", required=False, type=str) parser.add_argument("--past-blocks", help="Number of past blocks to analyze", required=True, type=int) parser.add_argument("-o", "--output", help="File to save the chart or the table to", required=False, type=str) parser_mode = parser.add_mutually_exclusive_group(required=True) parser_mode.add_argument('--text', help="Show PnL as a text table", dest='text', action='store_true') parser_mode.add_argument('--chart', help="Show PnL on a cumulative graph", dest='chart', action='store_true') self.arguments = parser.parse_args(args) self.web3 = Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={'timeout': self.arguments.rpc_timeout})) self.infura = Web3( HTTPProvider(endpoint_uri=f"https://mainnet.infura.io/", request_kwargs={'timeout': 120})) self.buy_token_address = Address(self.arguments.buy_token_address) self.sell_token_address = Address(self.arguments.sell_token_address) self.old_sell_token_address = Address( self.arguments.old_sell_token_address ) if self.arguments.old_sell_token_address else None self.sell_token_addresses = list( filter(lambda address: address is not None, [self.sell_token_address, self.old_sell_token_address])) self.market_maker_address = Address( self.arguments.market_maker_address) self.exchange = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) if self.arguments.chart and self.arguments.output: import matplotlib matplotlib.use('Agg') logging.basicConfig( format='%(asctime)-15s %(levelname)-8s %(message)s', level=logging.INFO) logging.getLogger("filelock").setLevel(logging.WARNING)
class ArbitrageKeeper: """Keeper to arbitrage on OasisDEX, `join`, `exit`, `boom` and `bust`.""" logger = logging.getLogger('arbitrage-keeper') def __init__(self, args, **kwargs): parser = argparse.ArgumentParser("arbitrage-keeper") parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--eth-key", type=str, nargs='*', help= "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')" ) parser.add_argument("--tub-address", type=str, required=True, help="Ethereum address of the Tub contract") parser.add_argument("--tap-address", type=str, required=True, help="Ethereum address of the Tap contract") parser.add_argument( "--exchange-address", type=str, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--oasis-address", type=str, required=True, help="Ethereum address of the OasisDEX contract") parser.add_argument( "--oasis-support-address", type=str, required=False, help="Ethereum address of the OasisDEX support contract") parser.add_argument("--relayer-api-server", type=str, help="Address of the 0x Relayer API") parser.add_argument( "--relayer-per-page", type=int, default=100, help= "Number of orders to fetch per one page from the 0x Relayer API (default: 100)" ) parser.add_argument( "--tx-manager", type=str, help= "Ethereum address of the TxManager contract to use for multi-step arbitrage" ) parser.add_argument("--gas-price", type=int, default=0, help="Gas price in Wei (default: node default)") parser.add_argument( "--base-token", type=str, required=True, help="The token all arbitrage sequences will start and end with") parser.add_argument( "--min-profit", type=float, required=True, help="Minimum profit (in base token) from one arbitrage operation") parser.add_argument( "--max-engagement", type=float, required=True, help="Maximum engagement (in base token) in one arbitrage operation" ) parser.add_argument( "--max-errors", type=int, default=100, help= "Maximum number of allowed errors before the keeper terminates (default: 100)" ) parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from register_keys(self.web3, self.arguments.eth_key) self.our_address = Address(self.arguments.eth_from) self.tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) self.tap = Tap(web3=self.web3, address=Address(self.arguments.tap_address)) self.gem = ERC20Token(web3=self.web3, address=self.tub.gem()) self.sai = ERC20Token(web3=self.web3, address=self.tub.sai()) self.skr = ERC20Token(web3=self.web3, address=self.tub.skr()) self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) \ if self.arguments.exchange_address is not None else None self.zrx_relayer_api = ZrxRelayerApi(exchange=self.zrx_exchange, api_server=self.arguments.relayer_api_server) \ if self.arguments.relayer_api_server is not None else None self.otc = MatchingMarket( web3=self.web3, address=Address(self.arguments.oasis_address), support_address=Address(self.arguments.oasis_support_address) if self.arguments.oasis_support_address is not None else None) self.base_token = ERC20Token(web3=self.web3, address=Address( self.arguments.base_token)) self.min_profit = Wad.from_number(self.arguments.min_profit) self.max_engagement = Wad.from_number(self.arguments.max_engagement) self.max_errors = self.arguments.max_errors self.errors = 0 if self.arguments.tx_manager: self.tx_manager = TxManager(web3=self.web3, address=Address( self.arguments.tx_manager)) if self.tx_manager.owner() != self.our_address: raise Exception( f"The TxManager has to be owned by the address the keeper is operating from." ) else: self.tx_manager = None logging.basicConfig( format='%(asctime)-15s %(levelname)-8s %(message)s', level=(logging.DEBUG if self.arguments.debug else logging.INFO)) def main(self): with Lifecycle(self.web3) as lifecycle: self.lifecycle = lifecycle lifecycle.on_startup(self.startup) lifecycle.on_block(self.process_block) def startup(self): self.approve() def approve(self): """Approve all components that need to access our balances""" approval_method = via_tx_manager(self.tx_manager, gas_price=self.gas_price()) if self.tx_manager \ else directly(gas_price=self.gas_price()) self.tub.approve(approval_method) self.tap.approve(approval_method) self.otc.approve([self.gem, self.sai, self.skr], approval_method) if self.zrx_exchange: self.zrx_exchange.approve([self.gem, self.sai], approval_method) if self.tx_manager: self.tx_manager.approve([self.gem, self.sai, self.skr], directly(gas_price=self.gas_price())) def token_name(self, address: Address) -> str: if address == self.sai.address: return "DAI" elif address == self.gem.address: return "WETH" elif address == self.skr.address: return "PETH" else: return str(address) def tub_conversions(self) -> List[Conversion]: return [ TubJoinConversion(self.tub), TubExitConversion(self.tub), TubBoomConversion(self.tub, self.tap), TubBustConversion(self.tub, self.tap) ] def otc_orders(self, tokens): orders = [] for token1 in tokens: for token2 in tokens: if token1 != token2: orders = orders + self.otc.get_orders(token1, token2) return orders def otc_conversions(self, tokens) -> List[Conversion]: return list( map(lambda order: OasisTakeConversion(self.otc, order), self.otc_orders(tokens))) def zrx_orders(self, tokens): if self.zrx_exchange is None or self.zrx_relayer_api is None: return [] orders = [] for token1 in tokens: for token2 in tokens: if token1 != token2: orders = orders + self.zrx_relayer_api.get_orders( token1, token2) return list( filter(lambda order: order.expiration <= time.time(), orders)) def zrx_conversions(self, tokens) -> List[Conversion]: return list( map(lambda order: ZrxFillOrderConversion(self.zrx_exchange, order), self.zrx_orders(tokens))) def all_conversions(self): return self.tub_conversions() + \ self.otc_conversions([self.sai.address, self.skr.address, self.gem.address]) + \ self.zrx_conversions([self.sai.address, self.gem.address]) def process_block(self): """Callback called on each new block. If too many errors, terminate the keeper to minimize potential damage.""" if self.errors >= self.max_errors: self.lifecycle.terminate() else: self.execute_best_opportunity_available() def execute_best_opportunity_available(self): """Find the best arbitrage opportunity present and execute it.""" opportunity = self.best_opportunity(self.profitable_opportunities()) if opportunity: self.print_opportunity(opportunity) self.execute_opportunity(opportunity) def profitable_opportunities(self): """Identify all profitable arbitrage opportunities within given limits.""" entry_amount = Wad.min(self.base_token.balance_of(self.our_address), self.max_engagement) opportunity_finder = OpportunityFinder( conversions=self.all_conversions()) opportunities = opportunity_finder.find_opportunities( self.base_token.address, entry_amount) opportunities = filter( lambda op: op.total_rate() > Ray.from_number(1.000001), opportunities) opportunities = filter( lambda op: op.profit(self.base_token.address) > self.min_profit, opportunities) opportunities = sorted( opportunities, key=lambda op: op.profit(self.base_token.address), reverse=True) return opportunities def best_opportunity(self, opportunities: List[Sequence]): """Pick the best opportunity, or return None if no profitable opportunities.""" return opportunities[0] if len(opportunities) > 0 else None def print_opportunity(self, opportunity: Sequence): """Print the details of the opportunity.""" self.logger.info( f"Opportunity with id={opportunity.id()}," f" profit={opportunity.profit(self.base_token.address)} {self.token_name(self.base_token.address)}" ) for index, conversion in enumerate(opportunity.steps, start=1): self.logger.info( f"Step {index}/{len(opportunity.steps)}: {conversion.name()}" f" (from {conversion.source_amount} {self.token_name(conversion.source_token)}" f" to {conversion.target_amount} {self.token_name(conversion.target_token)})" ) def execute_opportunity(self, opportunity: Sequence): """Execute the opportunity either in one Ethereum transaction or step-by-step. Depending on whether `tx_manager` is available.""" if self.tx_manager: self.execute_opportunity_in_one_transaction(opportunity) else: self.execute_opportunity_step_by_step(opportunity) def execute_opportunity_step_by_step(self, opportunity: Sequence): """Execute the opportunity step-by-step.""" def incoming_transfer(our_address: Address): return lambda transfer: transfer.to_address == our_address def outgoing_transfer(our_address: Address): return lambda transfer: transfer.from_address == our_address all_transfers = [] for step in opportunity.steps: receipt = step.transact().transact(gas_price=self.gas_price()) if receipt: all_transfers += receipt.transfers outgoing = TransferFormatter().format( filter(outgoing_transfer(self.our_address), receipt.transfers), self.token_name) incoming = TransferFormatter().format( filter(incoming_transfer(self.our_address), receipt.transfers), self.token_name) self.logger.info(f"Exchanged {outgoing} to {incoming}") else: self.errors += 1 return self.logger.info( f"The profit we made is {TransferFormatter().format_net(all_transfers, self.our_address, self.token_name)}" ) def execute_opportunity_in_one_transaction(self, opportunity: Sequence): """Execute the opportunity in one transaction, using the `tx_manager`.""" tokens = [self.sai.address, self.skr.address, self.gem.address] invocations = list( map(lambda step: step.transact().invocation(), opportunity.steps)) receipt = self.tx_manager.execute( tokens, invocations).transact(gas_price=self.gas_price()) if receipt: self.logger.info( f"The profit we made is {TransferFormatter().format_net(receipt.transfers, self.our_address, self.token_name)}" ) else: self.errors += 1 def gas_price(self): if self.arguments.gas_price > 0: return FixedGasPrice(self.arguments.gas_price) else: return DefaultGasPrice()
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='0x-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--relayer-api-server", type=str, required=True, help="Address of the 0x Relayer API") parser.add_argument( "--relayer-per-page", type=int, default=100, help= "Number of orders to fetch per one page from the 0x Relayer API (default: 100)" ) parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument( "--order-expiry", type=int, required=True, help="Expiration time of created orders (in seconds)") parser.add_argument( "--order-expiry-threshold", type=int, default=0, help= "How long before order expiration it is considered already expired (in seconds)" ) parser.add_argument( "--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument( '--cancel-on-shutdown', dest='cancel_on_shutdown', action='store_true', help="Whether should cancel all open orders on keeper shutdown") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.token_buy = ERC20Token(web3=self.web3, address=Address( self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address( self.arguments.sell_token_address)) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.history = History() self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) self.zrx_relayer_api = ZrxRelayerApi( exchange=self.zrx_exchange, api_server=self.arguments.relayer_api_server) self.placed_orders = []
def __init__(self, args: list): parser = argparse.ArgumentParser(prog='0x-market-maker-trades') parser.add_argument("--rpc-host", help="JSON-RPC host (default: `localhost')", default="localhost", type=str) parser.add_argument("--rpc-port", help="JSON-RPC port (default: `8545')", default=8545, type=int) parser.add_argument("--rpc-timeout", help="JSON-RPC timeout (in seconds, default: 60)", type=int, default=60) parser.add_argument("--exchange-address", help="Ethereum address of the 0x contract", required=True, type=str) parser.add_argument( "--exchange-name", help="Exchange name for including in the JSON file", required=True, type=str) parser.add_argument("--buy-token", help="Name of the buy token", required=True, type=str) parser.add_argument("--buy-token-address", help="Ethereum address of the buy token", required=True, type=str) parser.add_argument("--buy-token-decimals", help="Number of decimals for the buy token", type=int, default=18) parser.add_argument("--sell-token", help="Name of the sell token", required=True, type=str) parser.add_argument("--sell-token-address", help="Ethereum address of the sell token", required=True, type=str) parser.add_argument("--sell-token-decimals", help="Number of decimals for the sell token", type=int, default=18) parser.add_argument("--old-sell-token-address", help="Ethereum address of the old sell token", required=False, type=str) parser.add_argument( "--market-maker-address", help="Ethereum account of the market maker to analyze", required=True, type=str) parser.add_argument("--past-blocks", help="Number of past blocks to analyze", required=True, type=int) parser.add_argument("-o", "--output", help="File to save the table or the JSON to", required=False, type=str) parser_mode = parser.add_mutually_exclusive_group(required=True) parser_mode.add_argument('--text', help="List trades as a text table", dest='text', action='store_true') parser_mode.add_argument('--json', help="List trades as a JSON document", dest='json', action='store_true') self.arguments = parser.parse_args(args) self.web3 = Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={'timeout': self.arguments.rpc_timeout})) self.infura = Web3( HTTPProvider(endpoint_uri=f"https://mainnet.infura.io/", request_kwargs={'timeout': 120})) self.buy_token_address = Address(self.arguments.buy_token_address) self.sell_token_address = Address(self.arguments.sell_token_address) self.old_sell_token_address = Address( self.arguments.old_sell_token_address ) if self.arguments.old_sell_token_address else None self.sell_token_addresses = list( filter(lambda address: address is not None, [self.sell_token_address, self.old_sell_token_address])) self.market_maker_address = Address( self.arguments.market_maker_address) self.exchange = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) logging.basicConfig( format='%(asctime)-15s %(levelname)-8s %(message)s', level=logging.INFO) logging.getLogger("filelock").setLevel(logging.WARNING)