def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='idex-market-maker-keeper') self.add_arguments(parser=parser) parser.set_defaults(cancel_on_shutdown=False, withdraw_on_shutdown=False) 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.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.bands_config = ReloadableConfig(self.arguments.config) self.eth_reserve = Wad.from_number(self.arguments.eth_reserve) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.min_eth_deposit = Wad.from_number(self.arguments.min_eth_deposit) self.min_sai_deposit = Wad.from_number(self.arguments.min_sai_deposit) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed( self.arguments, self.tub) 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) if self.eth_reserve <= self.min_eth_balance: raise Exception( "--eth-reserve must be higher than --min-eth-balance") self.history = History() self.idex = IDEX(self.web3, Address(self.arguments.idex_address)) self.idex_api = IDEXApi(self.idex, self.arguments.idex_api_server, self.arguments.idex_timeout)
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.idex = IDEX.deploy(self.web3, self.our_address) self.idex._contract.transact().setInactivityReleasePeriod(0) self.token = DSToken.deploy(self.web3, 'AAA') self.token.mint(Wad.from_number(100)).transact()
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.idex = IDEX.deploy(self.web3, self.our_address) self.idex._contract.functions.setInactivityReleasePeriod(0).transact() self.token = DSToken.deploy(self.web3, 'AAA') self.token.mint(Wad.from_number(100)).transact()
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='idex-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("--idex-address", type=str, required=True, help="Ethereum address of the IDEX contract") parser.add_argument("--idex-api-server", type=str, default='https://api.idex.market', help="Address of the IDEX API server (default: 'https://api.idex.market')") parser.add_argument("--idex-timeout", type=float, default=9.5, help="Timeout for accessing the IDEX API (in seconds, default: 9.5)") 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("--eth-reserve", type=float, required=True, help="Amount of ETH which will never be deposited so the keeper can cover gas") parser.add_argument("--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument("--min-eth-deposit", type=float, required=True, help="Minimum amount of ETH that can be deposited in one transaction") parser.add_argument("--min-sai-deposit", type=float, required=True, help="Minimum amount of SAI that can be deposited in one transaction") 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") 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) 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.bands_config = ReloadableConfig(self.arguments.config) self.eth_reserve = Wad.from_number(self.arguments.eth_reserve) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.min_eth_deposit = Wad.from_number(self.arguments.min_eth_deposit) self.min_sai_deposit = Wad.from_number(self.arguments.min_sai_deposit) 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) if self.eth_reserve <= self.min_eth_balance: raise Exception("--eth-reserve must be higher than --min-eth-balance") self.history = History() self.idex = IDEX(self.web3, Address(self.arguments.idex_address)) self.idex_api = IDEXApi(self.idex, self.arguments.idex_api_server, self.arguments.idex_timeout)
class IdexMarketMakerKeeper: """Keeper acting as a market maker on IDEX, on the ETH/SAI pair.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='idex-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("--idex-address", type=str, required=True, help="Ethereum address of the IDEX contract") parser.add_argument("--idex-api-server", type=str, default='https://api.idex.market', help="Address of the IDEX API server (default: 'https://api.idex.market')") parser.add_argument("--idex-timeout", type=float, default=9.5, help="Timeout for accessing the IDEX API (in seconds, default: 9.5)") 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("--eth-reserve", type=float, required=True, help="Amount of ETH which will never be deposited so the keeper can cover gas") parser.add_argument("--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument("--min-eth-deposit", type=float, required=True, help="Minimum amount of ETH that can be deposited in one transaction") parser.add_argument("--min-sai-deposit", type=float, required=True, help="Minimum amount of SAI that can be deposited in one transaction") 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") 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) 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.bands_config = ReloadableConfig(self.arguments.config) self.eth_reserve = Wad.from_number(self.arguments.eth_reserve) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.min_eth_deposit = Wad.from_number(self.arguments.min_eth_deposit) self.min_sai_deposit = Wad.from_number(self.arguments.min_sai_deposit) 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) if self.eth_reserve <= self.min_eth_balance: raise Exception("--eth-reserve must be higher than --min-eth-balance") self.history = History() self.idex = IDEX(self.web3, Address(self.arguments.idex_address)) self.idex_api = IDEXApi(self.idex, self.arguments.idex_api_server, self.arguments.idex_timeout) def main(self): with Lifecycle(self.web3) as 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() @retry(delay=5, logger=logger) def shutdown(self): self.cancel_all_orders() def approve(self): """Approve IEEX to access our tokens, so we can deposit them with the exchange""" token_addresses = filter(lambda address: address != IDEX.ETH_TOKEN, [self.token_sell(), self.token_buy()]) tokens = list(map(lambda address: ERC20Token(web3=self.web3, address=address), token_addresses)) self.idex.approve(tokens, directly(gas_price=self.gas_price)) def pair(self): # IDEX is inconsistent here. They call the pair `DAI_ETH`, but in reality all prices are # calculated like it was an `ETH/DAI` pair. return 'DAI_ETH' def token_sell(self) -> Address: return IDEX.ETH_TOKEN def token_buy(self) -> Address: return self.sai.address def our_balances(self): return self.idex_api.get_balances() def our_available_balance(self, our_balances, token: Address) -> Wad: if token == EtherDelta.ETH_TOKEN: try: return Wad.from_number(our_balances['ETH']['available']) except KeyError: return Wad(0) elif token == self.sai.address: try: return Wad.from_number(our_balances['DAI']['available']) except KeyError: return Wad(0) else: raise Exception("Unknown token") def our_orders(self) -> list: return self.idex_api.get_orders(self.pair()) def our_sell_orders(self, our_orders: list): return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list): return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): # 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(f"Keeper ETH balance below minimum, cancelling all orders.") self.cancel_all_orders() return bands = Bands(self.bands_config, self.history) our_balances = self.our_balances() 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("Cancelling all orders as no price feed available.") self.cancel_all_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 # If we detect that our total balance reported by the API is not equal to the # total balance reported by the Ethereum contract, it probably means that some # deposits are still pending being credited to our account. In this case # we also do not create any new orders, but at the same time existing orders # can still be cancelled. if not self.balances_match(our_balances): self.logger.info("Balances do not match, probably deposits are in progress, waiting.") return # Evaluate if we need to create new orders, and how much do we need to deposit new_orders, missing_buy_amount, missing_sell_amount = 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_available_balance(our_balances, self.token_buy()), our_sell_balance=self.our_available_balance(our_balances, self.token_sell()), target_price=target_price) # If deposited amount too low for placing buy orders, try to deposit. # If deposited amount too low for placing sell orders, try to deposit. made_deposit = False if missing_buy_amount > Wad(0): if self.deposit_for_buy_order(missing_buy_amount): made_deposit = True if missing_sell_amount > Wad(0): if missing_sell_amount > Wad(0): if self.deposit_for_sell_order(missing_sell_amount): made_deposit = True # If we managed to deposit something, do not do anything so we can reevaluate new orders to be placed. # Otherwise, place new orders. if not made_deposit: self.place_orders(new_orders) def cancel_orders(self, orders: list): for order in orders: self.idex_api.cancel_order(order) def cancel_all_orders(self): self.cancel_orders(self.our_orders()) def place_orders(self, new_orders): for new_order in new_orders: if new_order.is_sell: self.idex_api.place_order(pay_token=self.token_sell(), pay_amount=new_order.pay_amount, buy_token=self.token_buy(), buy_amount=new_order.buy_amount) else: self.idex_api.place_order(pay_token=self.token_buy(), pay_amount=new_order.pay_amount, buy_token=self.token_sell(), buy_amount=new_order.buy_amount) def deposit_for_sell_order(self, missing_sell_amount: Wad): # We always want to deposit at least `min_eth_deposit`. If `missing_sell_amount` is less # than that, we deposit `min_eth_deposit` anyway. if Wad(0) < missing_sell_amount < self.min_eth_deposit: missing_sell_amount = self.min_eth_deposit # We can never deposit more than our available ETH balance minus `eth_reserve` (reserve for gas). depositable_eth = Wad.max(eth_balance(self.web3, self.our_address) - self.eth_reserve, Wad(0)) missing_sell_amount = Wad.min(missing_sell_amount, depositable_eth) # If we still can deposit something, and it's at least `min_eth_deposit`, then we do deposit. if missing_sell_amount > Wad(0) and missing_sell_amount >= self.min_eth_deposit: receipt = self.idex.deposit(missing_sell_amount).transact(gas_price=self.gas_price) return receipt is not None and receipt.successful else: return False def deposit_for_buy_order(self, missing_buy_amount: Wad): # We always want to deposit at least `min_sai_deposit`. If `missing_buy_amount` is less # than that, we deposit `min_sai_deposit` anyway. if Wad(0) < missing_buy_amount < self.min_sai_deposit: missing_buy_amount = self.min_sai_deposit # We can never deposit more than our available SAI balance. depositable_sai = self.sai.balance_of(self.our_address) missing_buy_amount = Wad.min(missing_buy_amount, depositable_sai) # If we still can deposit something, and it's at least `min_sai_deposit`, then we do deposit. if missing_buy_amount > Wad(0) and missing_buy_amount >= self.min_sai_deposit: receipt = self.idex.deposit_token(self.sai.address, missing_buy_amount).transact(gas_price=self.gas_price) return receipt is not None and receipt.successful else: return False def balances_match(self, our_balances) -> bool: try: eth_available = Wad.from_number(our_balances['ETH']['available']) except KeyError: eth_available = Wad(0) try: eth_on_orders = Wad.from_number(our_balances['ETH']['onOrders']) except KeyError: eth_on_orders = Wad(0) try: dai_available = Wad.from_number(our_balances['DAI']['available']) except KeyError: dai_available = Wad(0) try: dai_on_orders = Wad.from_number(our_balances['DAI']['onOrders']) except KeyError: dai_on_orders = Wad(0) return self.idex.balance_of(self.our_address) == eth_available + eth_on_orders and \ self.idex.balance_of_token(self.sai.address, self.our_address) == dai_available + dai_on_orders
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='idex-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("--idex-address", type=str, required=True, help="Ethereum address of the IDEX contract") parser.add_argument("--idex-api-server", type=str, default='https://api.idex.market', help="Address of the IDEX API server (default: 'https://api.idex.market')") parser.add_argument("--idex-timeout", type=float, default=9.5, help="Timeout for accessing the IDEX API (in seconds, default: 9.5)") 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("--eth-reserve", type=float, required=True, help="Amount of ETH which will never be deposited so the keeper can cover gas") parser.add_argument("--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument("--min-eth-deposit", type=float, required=True, help="Minimum amount of ETH that can be deposited in one transaction") parser.add_argument("--min-sai-deposit", type=float, required=True, help="Minimum amount of SAI that can be deposited in one transaction") 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") 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) 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.bands_config = ReloadableConfig(self.arguments.config) self.eth_reserve = Wad.from_number(self.arguments.eth_reserve) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.min_eth_deposit = Wad.from_number(self.arguments.min_eth_deposit) self.min_sai_deposit = Wad.from_number(self.arguments.min_sai_deposit) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments, self.tub) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter(self.arguments) if self.eth_reserve <= self.min_eth_balance: raise Exception("--eth-reserve must be higher than --min-eth-balance") self.history = History() self.idex = IDEX(self.web3, Address(self.arguments.idex_address)) self.idex_api = IDEXApi(self.idex, self.arguments.idex_api_server, self.arguments.idex_timeout)
class IdexMarketMakerKeeper: """Keeper acting as a market maker on IDEX, on the ETH/SAI pair.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='idex-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("--idex-address", type=str, required=True, help="Ethereum address of the IDEX contract") parser.add_argument("--idex-api-server", type=str, default='https://api.idex.market', help="Address of the IDEX API server (default: 'https://api.idex.market')") parser.add_argument("--idex-timeout", type=float, default=9.5, help="Timeout for accessing the IDEX API (in seconds, default: 9.5)") 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("--eth-reserve", type=float, required=True, help="Amount of ETH which will never be deposited so the keeper can cover gas") parser.add_argument("--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument("--min-eth-deposit", type=float, required=True, help="Minimum amount of ETH that can be deposited in one transaction") parser.add_argument("--min-sai-deposit", type=float, required=True, help="Minimum amount of SAI that can be deposited in one transaction") 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") 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) 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.bands_config = ReloadableConfig(self.arguments.config) self.eth_reserve = Wad.from_number(self.arguments.eth_reserve) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.min_eth_deposit = Wad.from_number(self.arguments.min_eth_deposit) self.min_sai_deposit = Wad.from_number(self.arguments.min_sai_deposit) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments, self.tub) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter(self.arguments) if self.eth_reserve <= self.min_eth_balance: raise Exception("--eth-reserve must be higher than --min-eth-balance") self.history = History() self.idex = IDEX(self.web3, Address(self.arguments.idex_address)) self.idex_api = IDEXApi(self.idex, self.arguments.idex_api_server, self.arguments.idex_timeout) def main(self): with Lifecycle(self.web3) as 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() @retry(delay=5, logger=logger) def shutdown(self): self.cancel_all_orders() def approve(self): token_addresses = filter(lambda address: address != IDEX.ETH_TOKEN, [self.token_sell(), self.token_buy()]) tokens = list(map(lambda address: ERC20Token(web3=self.web3, address=address), token_addresses)) self.idex.approve(tokens, directly(gas_price=self.gas_price)) def pair(self): # IDEX is inconsistent here. They call the pair `DAI_ETH`, but in reality all prices are # calculated like it was an `ETH/DAI` pair. return 'DAI_ETH' def token_sell(self) -> Address: return IDEX.ETH_TOKEN def token_buy(self) -> Address: return self.sai.address def our_balances(self): return self.idex_api.get_balances() def our_available_balance(self, our_balances, token: Address) -> Wad: if token == EtherDelta.ETH_TOKEN: try: return Wad.from_number(our_balances['ETH']['available']) except KeyError: return Wad(0) elif token == self.sai.address: try: return Wad.from_number(our_balances['DAI']['available']) except KeyError: return Wad(0) else: raise Exception("Unknown token") def our_orders(self) -> list: return self.idex_api.get_orders(self.pair()) def our_sell_orders(self, our_orders: list): return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list): return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): # 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(f"Keeper ETH balance below minimum, cancelling all orders.") self.cancel_all_orders() return bands = Bands(self.bands_config, self.spread_feed, self.history) our_balances = self.our_balances() 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 # If we detect that our total balance reported by the API is not equal to the # total balance reported by the Ethereum contract, it probably means that some # deposits are still pending being credited to our account. In this case # we also do not create any new orders, but at the same time existing orders # can still be cancelled. if not self.balances_match(our_balances): self.logger.info("Balances do not match, probably deposits are in progress, waiting.") return # Evaluate if we need to create new orders, and how much do we need to deposit new_orders, missing_buy_amount, missing_sell_amount = 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_available_balance(our_balances, self.token_buy()), our_sell_balance=self.our_available_balance(our_balances, self.token_sell()), target_price=target_price) # If deposited amount too low for placing buy orders, try to deposit. # If deposited amount too low for placing sell orders, try to deposit. made_deposit = False if missing_buy_amount > Wad(0): if self.deposit_for_buy_order(missing_buy_amount): made_deposit = True if missing_sell_amount > Wad(0): if missing_sell_amount > Wad(0): if self.deposit_for_sell_order(missing_sell_amount): made_deposit = True # If we managed to deposit something, do not do anything so we can reevaluate new orders to be placed. # Otherwise, place new orders. if not made_deposit: self.place_orders(new_orders) def cancel_orders(self, orders: list): for order in orders: self.idex_api.cancel_order(order) def cancel_all_orders(self): self.cancel_orders(self.our_orders()) def place_orders(self, new_orders): for new_order in new_orders: if new_order.is_sell: self.idex_api.place_order(pay_token=self.token_sell(), pay_amount=new_order.pay_amount, buy_token=self.token_buy(), buy_amount=new_order.buy_amount) else: self.idex_api.place_order(pay_token=self.token_buy(), pay_amount=new_order.pay_amount, buy_token=self.token_sell(), buy_amount=new_order.buy_amount) def deposit_for_sell_order(self, missing_sell_amount: Wad): # We always want to deposit at least `min_eth_deposit`. If `missing_sell_amount` is less # than that, we deposit `min_eth_deposit` anyway. if Wad(0) < missing_sell_amount < self.min_eth_deposit: missing_sell_amount = self.min_eth_deposit # We can never deposit more than our available ETH balance minus `eth_reserve` (reserve for gas). depositable_eth = Wad.max(eth_balance(self.web3, self.our_address) - self.eth_reserve, Wad(0)) missing_sell_amount = Wad.min(missing_sell_amount, depositable_eth) # If we still can deposit something, and it's at least `min_eth_deposit`, then we do deposit. if missing_sell_amount > Wad(0) and missing_sell_amount >= self.min_eth_deposit: receipt = self.idex.deposit(missing_sell_amount).transact(gas_price=self.gas_price) return receipt is not None and receipt.successful else: return False def deposit_for_buy_order(self, missing_buy_amount: Wad): # We always want to deposit at least `min_sai_deposit`. If `missing_buy_amount` is less # than that, we deposit `min_sai_deposit` anyway. if Wad(0) < missing_buy_amount < self.min_sai_deposit: missing_buy_amount = self.min_sai_deposit # We can never deposit more than our available SAI balance. depositable_sai = self.sai.balance_of(self.our_address) missing_buy_amount = Wad.min(missing_buy_amount, depositable_sai) # If we still can deposit something, and it's at least `min_sai_deposit`, then we do deposit. if missing_buy_amount > Wad(0) and missing_buy_amount >= self.min_sai_deposit: receipt = self.idex.deposit_token(self.sai.address, missing_buy_amount).transact(gas_price=self.gas_price) return receipt is not None and receipt.successful else: return False def balances_match(self, our_balances) -> bool: try: eth_available = Wad.from_number(our_balances['ETH']['available']) except KeyError: eth_available = Wad(0) try: eth_on_orders = Wad.from_number(our_balances['ETH']['onOrders']) except KeyError: eth_on_orders = Wad(0) try: dai_available = Wad.from_number(our_balances['DAI']['available']) except KeyError: dai_available = Wad(0) try: dai_on_orders = Wad.from_number(our_balances['DAI']['onOrders']) except KeyError: dai_on_orders = Wad(0) return self.idex.balance_of(self.our_address) == eth_available + eth_on_orders and \ self.idex.balance_of_token(self.sai.address, self.our_address) == dai_available + dai_on_orders
def test_fail_when_no_contract_under_that_address(self): # expect with pytest.raises(Exception): IDEX(web3=self.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))