class OasisMarketMakerCancel: """Tool to cancel all our open orders on OasisDEX.""" def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='oasis-market-maker-cancel') 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: 10)", default=10, type=int) parser.add_argument("--eth-from", help="Ethereum account from which to send transactions", required=True, type=str) parser.add_argument("--oasis-address", help="Ethereum address of the OasisDEX contract", required=True, type=str) parser.add_argument("--gas-price", help="Gas price in Wei (default: node default)", default=0, type=int) 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.otc = MatchingMarket(web3=self.web3, address=Address(self.arguments.oasis_address)) logging.basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s', level=logging.INFO) def main(self): self.cancel_orders(self.our_orders(self.otc.get_orders())) def our_orders(self, orders: list): return list(filter(lambda order: order.maker == self.our_address, orders)) def cancel_orders(self, orders: list): synchronize([self.otc.kill(order.order_id).transact_async(gas_price=self.gas_price()) for order in orders]) def gas_price(self): if self.arguments.gas_price > 0: return FixedGasPrice(self.arguments.gas_price) else: return DefaultGasPrice()
class OasisMarketMakerCancel: """Tool to cancel all our open orders on OasisDEX.""" def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='oasis-market-maker-cancel') parser.add_argument("--endpoint-uri", type=str, help="JSON-RPC uri (example: `http://localhost:8545`)") parser.add_argument("--rpc-host", default="localhost", type=str, help="[DEPRECATED] JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", default=8545, type=int, help="[DEPRECATED] JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", help="JSON-RPC timeout (in seconds, default: 10)", default=10, type=int) parser.add_argument("--eth-from", help="Ethereum account from which to send transactions", required=True, type=str) parser.add_argument("--eth-key", type=str, nargs='*', help="Ethereum private key(s) to use") parser.add_argument("--oasis-address", help="Ethereum address of the OasisDEX contract", required=True, type=str) parser.add_argument("--gas-price", help="Gas price in Wei (default: node default)", default=0, type=int) self.arguments = parser.parse_args(args) if 'web3' in kwargs: self.web3 = kwargs['web3'] elif self.arguments.endpoint_uri: self.web3: Web3 = web3_via_http(self.arguments.endpoint_uri, self.arguments.rpc_timeout) else: self.web3 = 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) self.otc = MatchingMarket(web3=self.web3, address=Address(self.arguments.oasis_address)) logging.basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s', level=logging.INFO) def main(self): self.cancel_orders(self.our_orders(self.otc.get_orders())) def our_orders(self, orders: list): return list(filter(lambda order: order.maker == self.our_address, orders)) def cancel_orders(self, orders: list): synchronize([self.otc.kill(order.order_id).transact_async(gas_price=self.gas_price()) for order in orders]) def gas_price(self): if self.arguments.gas_price > 0: return FixedGasPrice(self.arguments.gas_price) else: return DefaultGasPrice()
class OasisMarketMakerKeeper: """Keeper acting as a market maker on OasisDEX, on the W-ETH/SAI pair.""" 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=True, 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("--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( "--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("--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)) self.tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) self.sai = ERC20Token(web3=self.web3, address=self.tub.sai()) self.gem = ERC20Token(web3=self.web3, address=self.tub.gem()) 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.order_book_manager = OrderBookManager(refresh_frequency=3) self.order_book_manager.get_orders_with(lambda: self.our_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.on_block(self.on_block) 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_all_orders() def on_block(self): # This method is present only so the lifecycle binds the new block listener, which makes # it then terminate the keeper if no new blocks have been arriving for 300 seconds. pass 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 price(self) -> Wad: return self.price_feed.get_price() def token_sell(self) -> ERC20Token: return self.gem def token_buy(self) -> ERC20Token: return self.sai 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.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.cancel_all_orders() return bands = Bands(self.bands_config) order_book = self.order_book_manager.get_order_book() target_price = self.price() # If the is no target price feed, cancel all orders but do not terminate the keeper. # The moment the price feed comes back, the keeper will resume placing orders. if target_price is None: self.logger.warning( "No price feed available. Cancelling all orders.") self.cancel_all_orders() return # If there are any orders to be cancelled, cancel them. It is deliberate that we wait with topping-up # bands until the next block. This way we would create new orders based on the most recent price and # order book state. We could theoretically retrieve both (`target_price` and `our_orders`) again here, # but it just seems cleaner to do it in one place instead of in two. 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.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.create_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 cancel_all_orders(self): # Wait for the order book to stabilize while True: order_book = self.order_book_manager.get_order_book() if not order_book.orders_being_cancelled and not order_book.orders_being_placed: break # Cancel all open orders self.cancel_orders(self.order_book_manager.get_order_book().orders) self.order_book_manager.wait_for_order_cancellation() def cancel_orders(self, orders): for order in orders: self.order_book_manager.cancel_order( order.order_id, lambda: self.otc.kill(order.order_id).transact( gas_price=self.gas_price).successful) def create_orders(self, new_orders): def place_order_function(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 for new_order in new_orders: self.order_book_manager.place_order( lambda: place_order_function(new_order))
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 OasisMarketMakerKeeper: """Keeper acting as a market maker on OasisDEX, on the W-ETH/SAI pair.""" 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=True, 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("--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( "--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 with either terminate or not start at all" ) 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.otc = MatchingMarket(web3=self.web3, address=Address( self.arguments.oasis_address)) 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.gem = ERC20Token(web3=self.web3, address=self.tub.gem()) 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) def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.on_block(self.on_block) 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_all_orders() def on_block(self): # This method is present only so the lifecycle binds the new block listener, which makes # it then terminate the keeper if no new blocks have been arriving for 300 seconds. pass 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 price(self) -> Wad: return self.price_feed.get_price() def token_sell(self) -> ERC20Token: return self.gem 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): return self.otc.get_orders_by_maker(self.our_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.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.cancel_all_orders() return bands = Bands(self.bands_config) our_orders = self.our_orders() target_price = self.price() # If the is no target price feed, cancel all orders but do not terminate the keeper. # The moment the price feed comes back, the keeper will resume placing orders. if target_price is None: self.logger.warning( "No price feed available. Cancelling all orders.") self.cancel_all_orders() return # If there are any orders to be cancelled, cancel them. It is deliberate that we wait with topping-up # bands until the next block. This way we would create new orders based on the most recent price and # order book state. We could theoretically retrieve both (`target_price` and `our_orders`) again here, # but it just seems cleaner to do it in one place instead of in two. 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 # If there are any new orders to be created, create them. new_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) if len(new_orders) > 0: self.create_orders(new_orders) # We do wait some time after the orders have been created. The reason for that is sometimes # orders that have been just placed were not picked up by the next `our_orders()` call # (one can presume the block hasn't been fully imported into the node yet), which made # the keeper try to place same order(s) again. Of course the second transaction did fail, but it # resulted in wasted gas and significant delay in keeper operation. # # There is no specific reason behind choosing to wait exactly 3s. time.sleep(3) def cancel_all_orders(self): """Cancel all orders owned by the keeper.""" self.cancel_orders(self.our_orders()) def cancel_orders(self, orders): """Cancel orders asynchronously.""" synchronize([ self.otc.kill( order.order_id).transact_async(gas_price=self.gas_price) for order in orders ]) def create_orders(self, new_orders): """Create orders asynchronously.""" def to_transaction(new_order: NewOrder): assert (isinstance(new_order, NewOrder)) if new_order.is_sell: return self.otc.make(pay_token=self.token_sell().address, pay_amount=new_order.pay_amount, buy_token=self.token_buy().address, buy_amount=new_order.buy_amount) else: return self.otc.make(pay_token=self.token_buy().address, pay_amount=new_order.pay_amount, buy_token=self.token_sell().address, buy_amount=new_order.buy_amount) synchronize([ transaction.transact_async(gas_price=self.gas_price) for transaction in map(to_transaction, new_orders) ])
class OasisMarketMakerKeeper: """Keeper acting as a market maker on OasisDEX, on the W-ETH/SAI pair.""" logger = logging.getLogger('oasis-market-maker-keeper') 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( "--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("--oasis-address", type=str, required=True, help="Ethereum address of the OasisDEX contract") 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( "--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 with either terminate or not start at all" ) 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}")) 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)) 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.gem = ERC20Token(web3=self.web3, address=self.tub.gem()) logging.basicConfig( format='%(asctime)-15s %(levelname)-8s %(message)s', level=(logging.DEBUG if self.arguments.debug else 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.tub, self.vox) def main(self): with Web3Lifecycle(self.web3) as lifecycle: self.lifecycle = lifecycle lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.on_block(self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): self.approve() def shutdown(self): self.cancel_all_orders() def approve(self): """Approve OasisDEX to access our balances, so we can place orders.""" self.otc.approve([self.gem, self.sai], directly(gas_price=self.gas_price)) def our_orders(self): return list( filter(lambda order: order.maker == self.our_address, self.otc.get_orders())) def our_sell_orders(self, our_orders: list): return list( filter( lambda order: order.buy_token == self.sai.address and order. pay_token == self.gem.address, our_orders)) def our_buy_orders(self, our_orders: list): return list( filter( lambda order: order.buy_token == self.gem.address and order. pay_token == self.sai.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("Marked is closed. Cancelling all orders.") self.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.cancel_all_orders() return bands = Bands(self.bands_config) our_orders = self.our_orders() target_price = self.price_feed.get_price() # If the is no target price feed, cancel all orders but do not terminate the keeper. # The moment the price feed comes back, the keeper will resume placing orders. if target_price is None: self.logger.warning( "No price feed available. Cancelling all orders.") self.cancel_all_orders() return # If there are any orders to be cancelled, cancel them. It is deliberate that we wait with topping-up # bands until the next block. This way we would create new orders based on the most recent price and # order book state. We could theoretically retrieve both (`target_price` and `our_orders`) again here, # but it just seems cleaner to do it in one place instead of in two. orders_to_cancel = list( 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))) if len(orders_to_cancel) > 0: self.cancel_orders(orders_to_cancel) else: self.top_up_bands(our_orders, bands.buy_bands, bands.sell_bands, target_price) # We do wait some time after the orders have been created. The reason for that is sometimes # orders that have been just placed were not picked up by the next `our_orders()` call # (one can presume the block hasn't been fully imported into the node yet), which made # the keeper try to place same order(s) again. Of course the second transaction did fail, but it # resulted in wasted gas and significant delay in keeper operation. # # There is no specific reason behind choosing to wait exactly 7s. time.sleep(7) def cancel_all_orders(self): """Cancel all orders owned by the keeper.""" self.cancel_orders(self.our_orders()) def cancel_orders(self, orders): """Cancel orders asynchronously.""" synchronize([ self.otc.kill( order.order_id).transact_async(gas_price=self.gas_price) for order in orders ]) def top_up_bands(self, our_orders: list, buy_bands: list, sell_bands: list, target_price: Wad): """Asynchronously create new buy and sell orders in all send and buy bands if necessary.""" synchronize([ transact.transact_async(gas_price=self.gas_price) for transact in itertools.chain( 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. Yield new orders if necessary.""" our_balance = self.gem.balance_of(self.our_address) 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) want_amount = have_amount * round(band.avg_price(target_price), self.arguments.round_places) if (have_amount >= band.dust_cutoff) and ( have_amount > Wad(0)) and (want_amount > Wad(0)): our_balance = our_balance - have_amount yield self.otc.make(pay_token=self.gem.address, pay_amount=have_amount, buy_token=self.sai.address, buy_amount=want_amount) 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. Yield new orders if necessary.""" our_balance = self.sai.balance_of(self.our_address) 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) want_amount = have_amount / round(band.avg_price(target_price), self.arguments.round_places) if (have_amount >= band.dust_cutoff) and ( have_amount > Wad(0)) and (want_amount > Wad(0)): our_balance = our_balance - have_amount yield self.otc.make(pay_token=self.sai.address, pay_amount=have_amount, buy_token=self.gem.address, buy_amount=want_amount) @staticmethod def total_amount(orders: List[Order]): return reduce(operator.add, map(lambda order: order.pay_amount, orders), Wad(0))
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("--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("--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)) 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=3) self.order_book_manager.get_orders_with(lambda: self.our_orders()) 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.on_block(self.on_block) 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_all_orders() def on_block(self): # This method is present only so the lifecycle binds the new block listener, which makes # it then terminate the keeper if no new blocks have been arriving for 300 seconds. pass 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.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.cancel_all_orders() return 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() # If there are any orders to be cancelled, cancel them. It is deliberate that we wait with topping-up # bands until the next block. This way we would create new orders based on the most recent price and # order book state. We could theoretically retrieve both (`target_price` and `our_orders`) again here, # but it just seems cleaner to do it in one place instead of in two. 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.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.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 cancel_all_orders(self): # Wait for the order book to stabilize while True: order_book = self.order_book_manager.get_order_book() if not order_book.orders_being_cancelled and not order_book.orders_being_placed: break # Cancel all open orders self.cancel_orders(self.order_book_manager.get_order_book().orders) self.order_book_manager.wait_for_order_cancellation() def cancel_orders(self, orders): for order in orders: self.order_book_manager.cancel_order(order.order_id, lambda order=order: self.otc.kill(order.order_id).transact(gas_price=self.gas_price).successful) def place_orders(self, new_orders): def place_order_function(new_order_to_be_placed: NewOrder): assert(isinstance(new_order_to_be_placed, NewOrder)) if new_order_to_be_placed.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_to_be_placed.pay_amount, buy_token=buy_token, buy_amount=new_order_to_be_placed.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_to_be_placed.pay_amount, buy_token=buy_token, buy_amount=new_order_to_be_placed.buy_amount, timestamp=0) else: return None for new_order in new_orders: self.order_book_manager.place_order(lambda new_order=new_order: place_order_function(new_order))
class OasisMarketMakerCancel: """Tool to cancel all our open orders on OasisDEX.""" def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='oasis-market-maker-cancel') 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( "--eth-from", help="Ethereum account from which to send transactions", required=True, type=str) parser.add_argument("--oasis-address", help="Ethereum address of the OasisDEX contract", required=True, type=str) parser.add_argument("--gas-price", help="Gas price in Wei (default: node default)", default=0, type=int) 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.otc = MatchingMarket(web3=self.web3, address=Address( self.arguments.oasis_address)) logging.basicConfig( format='%(asctime)-15s %(levelname)-8s %(message)s', level=logging.INFO) def main(self): self.cancel_orders(self.our_orders(self.otc.get_orders())) def our_orders(self, orders: list): """Return list of orders owned by us.""" return list( filter(lambda order: order.maker == self.our_address, orders)) def cancel_orders(self, orders: list): """Cancel orders asynchronously.""" synchronize([ self.otc.kill( order.order_id).transact_async(gas_price=self.gas_price()) for order in orders ]) def gas_price(self): if self.arguments.gas_price > 0: return FixedGasPrice(self.arguments.gas_price) else: return DefaultGasPrice()