class EthfinexMarketMakerKeeper: logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='ethfinex-market-maker-keeper') parser.add_argument( "--ethfinex-api-server", type=str, default="https://api.ethfinex.com", help= "Address of the Ethfinex API server (default: 'https://api.ethfinex.com')" ) parser.add_argument("--ethfinex-api-key", type=str, required=True, help="API key for the Ethfinex API") parser.add_argument("--ethfinex-api-secret", type=str, required=True, help="API secret for the Ethfinex API") parser.add_argument( "--ethfinex-timeout", type=float, default=9.5, help= "Timeout for accessing the Ethfinex 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("--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( "--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.history = History() self.ethfinex_api = EthfinexApi( api_server=self.arguments.ethfinex_api_server, api_key=self.arguments.ethfinex_api_key, api_secret=self.arguments.ethfinex_api_secret, timeout=self.arguments.ethfinex_timeout) self.bands_config = ReloadableConfig(self.arguments.config) 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.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency, max_workers=1) self.order_book_manager.get_orders_with( lambda: self.ethfinex_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.ethfinex_api.get_balances()) self.order_book_manager.place_orders_with(self.place_order_function) self.order_book_manager.cancel_orders_with( lambda order: self.ethfinex_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() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def shutdown(self): self.order_book_manager.cancel_all_orders() def pair(self): return self.arguments.pair def token_sell(self) -> str: return self.arguments.pair[:3] def token_buy(self) -> str: return self.arguments.pair[3:] def our_available_balance(self, our_balances: list, token: str) -> Wad: try: return Wad.from_number( next( filter(lambda coin: coin['currency'].upper() == token, our_balances))['available']) except: return Wad(0) 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(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_available_balance( order_book.balances, self.token_buy()), our_sell_balance=self.our_available_balance( order_book.balances, self.token_sell()), target_price=target_price)[0]) def place_order_function(self, new_order): pair = self.pair() is_sell = new_order.is_sell price = new_order.price amount = new_order.pay_amount if new_order.is_sell else new_order.buy_amount new_order_id = self.ethfinex_api.place_order(pair=pair, is_sell=is_sell, price=price, amount=amount) return Order(order_id=new_order_id, pair=pair, is_sell=is_sell, price=price, amount=amount)
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 OasisMarketMakerKeeper: """Keeper acting as a market maker on OasisDEX.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='oasis-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=False, help="Ethereum address of the Tub 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("--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( "--round-places", type=int, default=2, help="Number of decimal places to round order prices to (default=2)" ) 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=10, help="Order book refresh frequency (in seconds, default: 10)") 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.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 else None) tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) \ if self.arguments.tub_address is not None else None 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, tub) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with(lambda: self.our_orders()) 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 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): """Approve OasisDEX to access our balances, so we can place orders.""" self.otc.approve([self.token_sell, self.token_buy], directly(gas_price=self.gas_price)) def our_available_balance(self, token: ERC20Token) -> Wad: return token.balance_of(self.our_address) def our_orders(self): return list( filter( lambda order: order.maker == self.our_address, self.otc.get_orders(self.token_sell.address, self.token_buy.address) + self.otc.get_orders(self.token_buy.address, self.token_sell.address))) def our_sell_orders(self, our_orders: 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): 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 market is closed, cancel all orders but do not terminate the keeper. if self.otc.is_closed(): self.logger.warning("Market is closed. Cancelling all orders.") self.order_book_manager.cancel_all_orders() return # If keeper balance is below `--min-eth-balance`, cancel all orders but do not terminate # the keeper, keep processing blocks as the moment the keeper gets a top-up it should # resume activity straight away, without the need to restart it. if eth_balance(self.web3, self.our_address) < self.min_eth_balance: self.logger.warning( "Keeper ETH balance below minimum. Cancelling all orders.") self.order_book_manager.cancel_all_orders() return 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 other new orders are being placed. In contrary to other keepers, # we allow placing new orders when other orders are being cancelled. This is because Ethereum # transactions are ordered so we are sure that the order placement will not 'overtake' # order cancellation. if order_book.orders_being_placed: self.logger.debug( "Other orders are being placed, 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_available_balance(self.token_buy), our_sell_balance=self.our_available_balance(self.token_sell), target_price=target_price)[0]) def place_order_function(self, new_order: NewOrder): assert (isinstance(new_order, NewOrder)) if new_order.is_sell: pay_token = self.token_sell.address buy_token = self.token_buy.address else: pay_token = self.token_buy.address buy_token = self.token_sell.address transact = self.otc.make( pay_token=pay_token, pay_amount=new_order.pay_amount, buy_token=buy_token, buy_amount=new_order.buy_amount).transact(gas_price=self.gas_price) if transact is not None and transact.successful and transact.result is not None: return Order(market=self.otc, order_id=transact.result, maker=self.our_address, pay_token=pay_token, pay_amount=new_order.pay_amount, buy_token=buy_token, buy_amount=new_order.buy_amount, timestamp=0) else: return None def cancel_order_function(self, order): transact = self.otc.kill( order.order_id).transact(gas_price=self.gas_price) return transact is not None and transact.successful
class MpxMarketMakerKeeper: """Keeper acting as a market maker on MPExchange.""" logger = logging.getLogger() def add_arguments(self, parser): parser.add_argument( "--rpc-host", type=str, default="http://localhost:8545", help="JSON-RPC host (default: `http://localhost: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( "--mpx-api-server", type=str, default='https://api.mpexchange.io', help= "Address of the MPX API server (default: 'https://api.mpexchange.io')" ) parser.add_argument( "--mpx-api-timeout", type=float, default=9.5, help="Timeout for accessing the MPX API (in seconds, default: 9.5)" ) parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the Mpx Exchange contract") parser.add_argument("--fee-address", type=str, required=True, help="Ethereum address of the Mpx Fee contract") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the Sell Token") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the Buy Token") 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("--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( "--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") pparser.add_argument( "--telegram-log-config-file", type=str, required=False, help= "config file for send logs to telegram chat (e.g. 'telegram_conf.json')", default=None) parser.add_argument( "--keeper-name", type=str, required=False, help="market maker keeper name (e.g. 'Uniswap_V2_MDTETH')", default="mpx") def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='mpx-market-maker-keeper') self.add_arguments(parser=parser) self.arguments = parser.parse_args(args) setup_logging(self.arguments) provider = HTTPProvider( endpoint_uri=self.arguments.rpc_host, request_kwargs={'timeout': self.arguments.rpc_timeout}) self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3( provider) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) register_keys(self.web3, self.arguments.eth_key) 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.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 = ZrxExchangeV2(web3=self.web3, address=Address( self.arguments.exchange_address)) self.mpx_api = MpxApi(api_server=self.arguments.mpx_api_server, zrx_exchange=self.zrx_exchange, fee_recipient=Address( self.arguments.fee_address), timeout=self.arguments.mpx_api_timeout, our_address=self.arguments.eth_from) self.zrx_relayer_api = ZrxRelayerApiV2( exchange=self.zrx_exchange, api_server=self.arguments.mpx_api_server) self.zrx_api = ZrxApiV2(zrx_exchange=self.zrx_exchange, zrx_api=self.zrx_relayer_api) markets = self.mpx_api.get_markets()['data'] market = next( filter( lambda item: item['attributes']['pair-name'] == self.arguments. pair, markets)) self.pair = MpxPair(self.arguments.pair, self.token_buy.address, int(market['attributes']['base-token-decimals']), self.token_sell.address, int(market['attributes']['quote-token-decimals'])) 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.cancel_orders_with(self.cancel_order_function) self.order_book_manager.place_orders_with(self.place_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 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 approve(self): self.zrx_exchange.approve([self.token_sell, self.token_buy], directly(gas_price=self.gas_price)) def shutdown(self): self.order_book_manager.cancel_all_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 get_orders(self) -> list: orders = self.mpx_api.get_orders(self.pair) return self.zrx_api.get_orders(self.pair, orders) 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 MPX, 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.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=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)) price = round(new_order.price, self.price_max_decimals) amount = new_order.pay_amount if new_order.is_sell else new_order.buy_amount zrx_order = self.mpx_api.place_order(pair=self.pair, is_sell=new_order.is_sell, price=price, amount=amount) if zrx_order is not None: return self.zrx_api.get_orders(self.pair, [zrx_order])[0] else: return None def cancel_order_function(self, order): self.logger.info(f"Canceling order {order.zrx_order.order_hash}") if self.mpx_api.cancel_order(order.zrx_order.order_hash): transact = self.zrx_exchange.cancel_order( order.zrx_order).transact(gas_price=self.gas_price) return transact is not None and transact.successful return False
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