Beispiel #1
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='etherdelta-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")

        assert (self.arguments.order_expiry_threshold >= 0)
        assert (self.arguments.order_no_cancel_threshold >=
                self.arguments.order_expiry_threshold)

        self.history = History()
        self.etherdelta = EtherDelta(web3=self.web3,
                                     address=Address(
                                         self.arguments.etherdelta_address))
        self.etherdelta_api = EtherDeltaApi(
            client_tool_directory="lib/pymaker/utils/etherdelta-client",
            client_tool_command="node main.js",
            api_server=self.arguments.etherdelta_socket,
            number_of_attempts=self.arguments.etherdelta_number_of_attempts,
            retry_interval=self.arguments.etherdelta_retry_interval,
            timeout=self.arguments.etherdelta_timeout)

        self.our_orders = list()
Beispiel #2
0
 def __init__(self, web3, dai_tub = '0x448a5065aeBB8E423F0896E6c5D525C040f59af3'):
     self.web3 = web3
     self.tub = Tub(web3=web3, address=Address(dai_tub))
     self.tap = Tap(web3=web3, address=self.tub.tap())
     self.tokens = {
             'MKR': ERC20Token(web3, self.tub.gov()),
             'PETH': ERC20Token(web3, self.tub.skr()),
             'WETH': ERC20Token(web3, self.tub.gem()),
             'DAI': ERC20Token(web3, self.tub.sai()),
     }
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='bite-keeper')
        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(
            "--eth-key",
            type=str,
            nargs='*',
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=/path/to/keystore.json,pass_file=/path/to/passphrase.txt')"
        )
        parser.add_argument("--tub-address",
                            help="Ethereum address of the Tub contract",
                            required=True,
                            type=str)
        parser.add_argument("--debug",
                            help="Enable debug output",
                            dest='debug',
                            action='store_true')
        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"https://{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.tub = Tub(web3=self.web3,
                       address=Address(self.arguments.tub_address))
        self.vox = Vox(web3=self.web3, address=self.tub.vox())

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))
    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)
Beispiel #5
0
 def __init__(self, tub: Tub):
     self.tub = tub
     super().__init__(
         source_token=self.tub.skr(),
         target_token=self.tub.gem(),
         rate=Ray(tub.bid(Wad.from_number(1))),
         max_source_amount=Wad.from_number(
             1000000),  #1 mio SKR = infinity ;)
         method="tub.exit()")
Beispiel #6
0
 def __init__(self, tub: Tub):
     self.tub = tub
     super().__init__(
         source_token=self.tub.gem(),
         target_token=self.tub.skr(),
         rate=(Ray.from_number(1) / Ray(tub.ask(Wad.from_number(1)))),
         max_source_amount=Wad.from_number(
             1000000),  #1 mio ETH = infinity ;)
         method="tub.join()")
Beispiel #7
0
class BiteKeeper:
    """Keeper to bite undercollateralized cups."""

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='bite-keeper')
        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("--tub-address", help="Ethereum address of the Tub contract", required=True, type=str)
        parser.add_argument("--gas-price", help="Gas price in Wei (default: node default)", type=int)
        parser.add_argument("--debug", help="Enable debug output", dest='debug', action='store_true')
        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                                                                              request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        self.our_address = Address(self.arguments.eth_from)
        self.tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address))

        logging.basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s',
                            level=(logging.DEBUG if self.arguments.debug else logging.INFO))

    def main(self):
        with Lifecycle(self.web3) as lifecycle:
            lifecycle.on_block(self.check_all_cups)

    def check_all_cups(self):
        for cup_id in range(self.tub.cupi()):
            self.check_cup(cup_id+1)

    def check_cup(self, cup_id):
        if not self.tub.safe(cup_id):
            self.tub.bite(cup_id).transact(gas_price=self.gas_price())

    def gas_price(self):
        if self.arguments.gas_price:
            return FixedGasPrice(self.arguments.gas_price)
        else:
            return DefaultGasPrice()
Beispiel #8
0
    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()
class RadarRelayMarketMakerKeeper:
    """Keeper acting as a market maker on RadarRelay, on the WETH/SAI pair."""

    logger = logging.getLogger()

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='radarrelay-market-maker-keeper')

        parser.add_argument("--rpc-host",
                            type=str,
                            default="localhost",
                            help="JSON-RPC host (default: `localhost')")

        parser.add_argument("--rpc-port",
                            type=int,
                            default=8545,
                            help="JSON-RPC port (default: `8545')")

        parser.add_argument("--rpc-timeout",
                            type=int,
                            default=10,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help="Ethereum account from which to send transactions")

        parser.add_argument("--tub-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Tub contract")

        parser.add_argument(
            "--exchange-address",
            type=str,
            required=True,
            help="Ethereum address of the 0x Exchange contract")

        parser.add_argument("--weth-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the WETH token")

        parser.add_argument("--relayer-api-server",
                            type=str,
                            required=True,
                            help="Address of the 0x Relayer API")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Buy/sell bands configuration file")

        parser.add_argument(
            "--price-feed",
            type=str,
            help=
            "Source of price feed. Tub price feed will be used if not specified"
        )

        parser.add_argument(
            "--price-feed-expiry",
            type=int,
            default=120,
            help="Maximum age of non-Tub price feed (in seconds, default: 120)"
        )

        parser.add_argument(
            "--order-expiry",
            type=int,
            required=True,
            help="Expiration time of created orders (in seconds)")

        parser.add_argument(
            "--order-expiry-threshold",
            type=int,
            default=0,
            help=
            "Order expiration time at which order is considered already expired (in seconds)"
        )

        parser.add_argument(
            "--min-eth-balance",
            type=float,
            default=0,
            help=
            "Minimum ETH balance below which keeper with either terminate or not start at all"
        )

        parser.add_argument(
            '--cancel-on-shutdown',
            dest='cancel_on_shutdown',
            action='store_true',
            help=
            "Whether should cancel all open orders on RadarRelay on keeper shutdown"
        )

        parser.add_argument("--gas-price",
                            type=int,
                            default=0,
                            help="Gas price (in Wei)")

        parser.add_argument(
            "--gas-price-increase",
            type=int,
            help="Gas price increase (in Wei) if no confirmation within"
            " `--gas-price-increase-every` seconds")

        parser.add_argument(
            "--gas-price-increase-every",
            type=int,
            default=120,
            help="Gas price increase frequency (in seconds, default: 120)")

        parser.add_argument("--gas-price-max",
                            type=int,
                            help="Maximum gas price (in Wei)")

        parser.add_argument("--gas-price-file",
                            type=str,
                            help="Gas price configuration file")

        parser.add_argument(
            "--smart-gas-price",
            dest='smart_gas_price',
            action='store_true',
            help=
            "Use smart gas pricing strategy, based on the ethgasstation.info feed"
        )

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        self.our_address = Address(self.arguments.eth_from)
        self.tub = Tub(web3=self.web3,
                       address=Address(self.arguments.tub_address))
        self.vox = Vox(web3=self.web3, address=self.tub.vox())
        self.sai = ERC20Token(web3=self.web3, address=self.tub.sai())
        self.ether_token = ERC20Token(web3=self.web3,
                                      address=Address(
                                          self.arguments.weth_address))

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))
        logging.getLogger('urllib3.connectionpool').setLevel(logging.INFO)
        logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(
            logging.INFO)

        self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance)
        self.bands_config = ReloadableConfig(self.arguments.config)
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(
            self.arguments.price_feed, self.arguments.price_feed_expiry,
            self.tub, self.vox)

        self.radar_relay = ZrxExchange(web3=self.web3,
                                       address=Address(
                                           self.arguments.exchange_address))
        self.radar_relay_api = ZrxRelayerApi(
            exchange=self.radar_relay,
            api_server=self.arguments.relayer_api_server)

    def main(self):
        with Lifecycle(self.web3) as lifecycle:
            lifecycle.initial_delay(10)
            lifecycle.on_startup(self.startup)
            lifecycle.every(15, self.synchronize_orders)
            lifecycle.on_shutdown(self.shutdown)

    def startup(self):
        self.approve()

    @retry(delay=5, logger=logger)
    def shutdown(self):
        if self.arguments.cancel_on_shutdown:
            self.cancel_orders(self.our_orders())

    def approve(self):
        """Approve 0x to access our tokens, so we can sell it on the exchange."""
        self.radar_relay.approve(
            [self.token_sell(), self.token_buy()],
            directly(gas_price=self.gas_price))

    def price(self) -> Wad:
        return self.price_feed.get_price()

    def token_sell(self) -> ERC20Token:
        return self.ether_token

    def token_buy(self) -> ERC20Token:
        return self.sai

    def our_balance(self, token: ERC20Token) -> Wad:
        return token.balance_of(self.our_address)

    def our_orders(self) -> list:
        our_orders = self.radar_relay_api.get_orders_by_maker(self.our_address)
        current_timestamp = int(time.time())

        our_orders = list(
            filter(
                lambda order: order.expiration > current_timestamp - self.
                arguments.order_expiry_threshold, our_orders))
        our_orders = list(
            filter(
                lambda order: self.radar_relay.get_unavailable_buy_amount(
                    order) < order.buy_amount, our_orders))
        return our_orders

    def our_sell_orders(self, our_orders: list) -> list:
        return list(
            filter(
                lambda order: order.buy_token == self.token_buy().address and
                order.pay_token == self.token_sell().address, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(
            filter(
                lambda order: order.buy_token == self.token_sell().address and
                order.pay_token == self.token_buy().address, our_orders))

    def synchronize_orders(self):
        """Update our positions in the order book to reflect keeper parameters."""
        if eth_balance(self.web3, self.our_address) < self.min_eth_balance:
            self.logger.warning(
                "Keeper ETH balance below minimum. Cancelling all orders.")
            self.cancel_orders(self.our_orders())
            return

        bands = Bands(self.bands_config)
        our_orders = self.our_orders()
        target_price = self.price()

        if target_price is None:
            self.logger.warning(
                "Cancelling all orders as no price feed available.")
            self.cancel_orders(our_orders)
            return

        # Cancel orders
        cancellable_orders = bands.cancellable_orders(
            our_buy_orders=self.our_buy_orders(our_orders),
            our_sell_orders=self.our_sell_orders(our_orders),
            target_price=target_price)
        if len(cancellable_orders) > 0:
            self.cancel_orders(cancellable_orders)
            return

        # Place new orders
        self.create_orders(
            bands.new_orders(
                our_buy_orders=self.our_buy_orders(our_orders),
                our_sell_orders=self.our_sell_orders(our_orders),
                our_buy_balance=self.our_balance(self.token_buy()),
                our_sell_balance=self.our_balance(self.token_sell()),
                target_price=target_price))

    def cancel_orders(self, orders):
        """Cancel orders asynchronously."""
        synchronize([
            self.radar_relay.cancel_order(order).transact_async(
                gas_price=self.gas_price) for order in orders
        ])

    def create_orders(self, orders):
        """Create and submit orders synchronously."""
        for order in orders:
            pay_token = self.token_sell() if order.is_sell else self.token_buy(
            )
            buy_token = self.token_buy() if order.is_sell else self.token_sell(
            )

            order = self.radar_relay.create_order(pay_token=pay_token.address,
                                                  pay_amount=order.pay_amount,
                                                  buy_token=buy_token.address,
                                                  buy_amount=order.buy_amount,
                                                  expiration=int(time.time()) +
                                                  self.arguments.order_expiry)

            order = self.radar_relay_api.calculate_fees(order)
            order = self.radar_relay.sign_order(order)
            self.radar_relay_api.submit_order(order)
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='radarrelay-market-maker-keeper')

        parser.add_argument("--rpc-host",
                            type=str,
                            default="localhost",
                            help="JSON-RPC host (default: `localhost')")

        parser.add_argument("--rpc-port",
                            type=int,
                            default=8545,
                            help="JSON-RPC port (default: `8545')")

        parser.add_argument("--rpc-timeout",
                            type=int,
                            default=10,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help="Ethereum account from which to send transactions")

        parser.add_argument("--tub-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Tub contract")

        parser.add_argument(
            "--exchange-address",
            type=str,
            required=True,
            help="Ethereum address of the 0x Exchange contract")

        parser.add_argument("--weth-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the WETH token")

        parser.add_argument("--relayer-api-server",
                            type=str,
                            required=True,
                            help="Address of the 0x Relayer API")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Buy/sell bands configuration file")

        parser.add_argument(
            "--price-feed",
            type=str,
            help=
            "Source of price feed. Tub price feed will be used if not specified"
        )

        parser.add_argument(
            "--price-feed-expiry",
            type=int,
            default=120,
            help="Maximum age of non-Tub price feed (in seconds, default: 120)"
        )

        parser.add_argument(
            "--order-expiry",
            type=int,
            required=True,
            help="Expiration time of created orders (in seconds)")

        parser.add_argument(
            "--order-expiry-threshold",
            type=int,
            default=0,
            help=
            "Order expiration time at which order is considered already expired (in seconds)"
        )

        parser.add_argument(
            "--min-eth-balance",
            type=float,
            default=0,
            help=
            "Minimum ETH balance below which keeper with either terminate or not start at all"
        )

        parser.add_argument(
            '--cancel-on-shutdown',
            dest='cancel_on_shutdown',
            action='store_true',
            help=
            "Whether should cancel all open orders on RadarRelay on keeper shutdown"
        )

        parser.add_argument("--gas-price",
                            type=int,
                            default=0,
                            help="Gas price (in Wei)")

        parser.add_argument(
            "--gas-price-increase",
            type=int,
            help="Gas price increase (in Wei) if no confirmation within"
            " `--gas-price-increase-every` seconds")

        parser.add_argument(
            "--gas-price-increase-every",
            type=int,
            default=120,
            help="Gas price increase frequency (in seconds, default: 120)")

        parser.add_argument("--gas-price-max",
                            type=int,
                            help="Maximum gas price (in Wei)")

        parser.add_argument("--gas-price-file",
                            type=str,
                            help="Gas price configuration file")

        parser.add_argument(
            "--smart-gas-price",
            dest='smart_gas_price',
            action='store_true',
            help=
            "Use smart gas pricing strategy, based on the ethgasstation.info feed"
        )

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        self.our_address = Address(self.arguments.eth_from)
        self.tub = Tub(web3=self.web3,
                       address=Address(self.arguments.tub_address))
        self.vox = Vox(web3=self.web3, address=self.tub.vox())
        self.sai = ERC20Token(web3=self.web3, address=self.tub.sai())
        self.ether_token = ERC20Token(web3=self.web3,
                                      address=Address(
                                          self.arguments.weth_address))

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))
        logging.getLogger('urllib3.connectionpool').setLevel(logging.INFO)
        logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(
            logging.INFO)

        self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance)
        self.bands_config = ReloadableConfig(self.arguments.config)
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(
            self.arguments.price_feed, self.arguments.price_feed_expiry,
            self.tub, self.vox)

        self.radar_relay = ZrxExchange(web3=self.web3,
                                       address=Address(
                                           self.arguments.exchange_address))
        self.radar_relay_api = ZrxRelayerApi(
            exchange=self.radar_relay,
            api_server=self.arguments.relayer_api_server)
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='etherdelta-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("--etherdelta-address", type=str, required=True,
                            help="Ethereum address of the EtherDelta contract")

        parser.add_argument("--etherdelta-socket", type=str, required=True,
                            help="Ethereum address of the EtherDelta API socket")

        parser.add_argument("--etherdelta-number-of-attempts", type=int, default=3,
                            help="Number of attempts of running the tool to talk to the EtherDelta API socket")

        parser.add_argument("--etherdelta-retry-interval", type=int, default=10,
                            help="Retry interval for sending orders over the EtherDelta API socket")

        parser.add_argument("--etherdelta-timeout", type=int, default=120,
                            help="Timeout for sending orders over the EtherDelta API socket")

        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-age", type=int, required=True,
                            help="Age of created orders (in blocks)")

        parser.add_argument("--order-expiry-threshold", type=int, default=0,
                            help="Remaining order age (in blocks) at which order is considered already expired, which"
                                 " means the keeper will send a new replacement order slightly ahead")

        parser.add_argument("--order-no-cancel-threshold", type=int, default=0,
                            help="Remaining order age (in blocks) below which keeper does not try to cancel orders,"
                                 " assuming that they will probably expire before the cancel transaction gets mined")

        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('--cancel-on-shutdown', dest='cancel_on_shutdown', action='store_true',
                            help="Whether should cancel all open orders on EtherDelta on keeper shutdown")

        parser.add_argument('--withdraw-on-shutdown', dest='withdraw_on_shutdown', action='store_true',
                            help="Whether should withdraw all tokens from EtherDelta on keeper shutdown")

        parser.add_argument("--gas-price", type=int, default=0,
                            help="Gas price (in Wei)")

        parser.add_argument("--smart-gas-price", dest='smart_gas_price', action='store_true',
                            help="Use smart gas pricing strategy, based on the ethgasstation.info feed")

        parser.add_argument("--debug", dest='debug', action='store_true',
                            help="Enable debug output")

        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")

        assert(self.arguments.order_expiry_threshold >= 0)
        assert(self.arguments.order_no_cancel_threshold >= self.arguments.order_expiry_threshold)

        self.history = History()
        self.etherdelta = EtherDelta(web3=self.web3, address=Address(self.arguments.etherdelta_address))
        self.etherdelta_api = EtherDeltaApi(client_tool_directory="lib/pymaker/utils/etherdelta-client",
                                            client_tool_command="node main.js",
                                            api_server=self.arguments.etherdelta_socket,
                                            number_of_attempts=self.arguments.etherdelta_number_of_attempts,
                                            retry_interval=self.arguments.etherdelta_retry_interval,
                                            timeout=self.arguments.etherdelta_timeout)

        self.our_orders = list()
    def __init__(self, args, **kwargs):
        parser = argparse.ArgumentParser("arbitrage-keeper")

        parser.add_argument("--rpc-host",
                            type=str,
                            default="localhost",
                            help="JSON-RPC host (default: `localhost')")

        parser.add_argument("--rpc-port",
                            type=int,
                            default=8545,
                            help="JSON-RPC port (default: `8545')")

        parser.add_argument("--rpc-timeout",
                            type=int,
                            default=10,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help="Ethereum account from which to send transactions")

        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')"
        )

        parser.add_argument("--tub-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Tub contract")

        parser.add_argument("--tap-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Tap contract")

        parser.add_argument(
            "--exchange-address",
            type=str,
            help="Ethereum address of the 0x Exchange contract")

        parser.add_argument("--oasis-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the OasisDEX contract")

        parser.add_argument(
            "--oasis-support-address",
            type=str,
            required=False,
            help="Ethereum address of the OasisDEX support contract")

        parser.add_argument("--relayer-api-server",
                            type=str,
                            help="Address of the 0x Relayer API")

        parser.add_argument(
            "--relayer-per-page",
            type=int,
            default=100,
            help=
            "Number of orders to fetch per one page from the 0x Relayer API (default: 100)"
        )

        parser.add_argument(
            "--tx-manager",
            type=str,
            help=
            "Ethereum address of the TxManager contract to use for multi-step arbitrage"
        )

        parser.add_argument("--gas-price",
                            type=int,
                            default=0,
                            help="Gas price in Wei (default: node default)")

        parser.add_argument(
            "--base-token",
            type=str,
            required=True,
            help="The token all arbitrage sequences will start and end with")

        parser.add_argument(
            "--min-profit",
            type=float,
            required=True,
            help="Minimum profit (in base token) from one arbitrage operation")

        parser.add_argument(
            "--max-engagement",
            type=float,
            required=True,
            help="Maximum engagement (in base token) in one arbitrage operation"
        )

        parser.add_argument(
            "--max-errors",
            type=int,
            default=100,
            help=
            "Maximum number of allowed errors before the keeper terminates (default: 100)"
        )

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        self.tub = Tub(web3=self.web3,
                       address=Address(self.arguments.tub_address))
        self.tap = Tap(web3=self.web3,
                       address=Address(self.arguments.tap_address))
        self.gem = ERC20Token(web3=self.web3, address=self.tub.gem())
        self.sai = ERC20Token(web3=self.web3, address=self.tub.sai())
        self.skr = ERC20Token(web3=self.web3, address=self.tub.skr())

        self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) \
            if self.arguments.exchange_address is not None else None
        self.zrx_relayer_api = ZrxRelayerApi(exchange=self.zrx_exchange, api_server=self.arguments.relayer_api_server) \
            if self.arguments.relayer_api_server is not None else None

        self.otc = MatchingMarket(
            web3=self.web3,
            address=Address(self.arguments.oasis_address),
            support_address=Address(self.arguments.oasis_support_address)
            if self.arguments.oasis_support_address is not None else None)

        self.base_token = ERC20Token(web3=self.web3,
                                     address=Address(
                                         self.arguments.base_token))
        self.min_profit = Wad.from_number(self.arguments.min_profit)
        self.max_engagement = Wad.from_number(self.arguments.max_engagement)
        self.max_errors = self.arguments.max_errors
        self.errors = 0

        if self.arguments.tx_manager:
            self.tx_manager = TxManager(web3=self.web3,
                                        address=Address(
                                            self.arguments.tx_manager))
            if self.tx_manager.owner() != self.our_address:
                raise Exception(
                    f"The TxManager has to be owned by the address the keeper is operating from."
                )
        else:
            self.tx_manager = None

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))
    def __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)
Beispiel #14
0
from pymaker.token import ERC20Token
from pymaker.numeric import Ray,Wad
from pymaker.sai import Tub

parser = argparse.ArgumentParser()
parser.add_argument("-w", "--warnratio", help="minimum ratio in percent that gives a warning",
                    type=int, default="180")
parser.add_argument("-q", "--quiet", help="only output warnings",
                    action='store_true')
parser.add_argument('cdps', metavar='CDP', type=int, nargs='+',
                    help='CDP id(s) to check')
args = parser.parse_args()

web3 = Web3(HTTPProvider(endpoint_uri="https://mainnet.infura.io/metamask"))

tub = Tub(web3=web3, address=Address('0x448a5065aebb8e423f0896e6c5d525c040f59af3'))
minimum_ratio=Ray.from_number(args.warnratio / 100)
requirements_satisfied=True

for cup_id in args.cdps:
    cup = tub.cups(cup_id)
    pro = tub.ink(cup_id)*tub.tag()
    tab = tub.tab(cup_id)
    if not args.quiet:
        print(f'CDP #{cup_id}')
        print(f'  Owner {cup.lad}')
        print(f'  Deposited {float(cup.ink):.8} PETH')
        print(f'  Debt {float(tab):.8} DAI')
    if tab > Wad(0):
        current_ratio = Ray(pro / tab)
        if not args.quiet:
Beispiel #15
0
 def test_fail_when_no_contract_under_that_address(self, deployment: Deployment):
     # expect
     with pytest.raises(Exception):
         Tub(web3=deployment.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
Beispiel #16
0
    def __init__(self):
        web3 = Web3(EthereumTesterProvider())
        web3.eth.defaultAccount = web3.eth.accounts[0]
        our_address = Address(web3.eth.defaultAccount)
        sai = DSToken.deploy(web3, 'DAI')
        sin = DSToken.deploy(web3, 'SIN')
        skr = DSToken.deploy(web3, 'PETH')
        gem = DSToken.deploy(web3, 'WETH')
        gov = DSToken.deploy(web3, 'MKR')
        pip = DSValue.deploy(web3)
        pep = DSValue.deploy(web3)
        pit = DSVault.deploy(web3)

        vox = Vox.deploy(web3, per=Ray.from_number(1))
        tub = Tub.deploy(web3, sai=sai.address, sin=sin.address, skr=skr.address, gem=gem.address, gov=gov.address,
                         pip=pip.address, pep=pep.address, vox=vox.address, pit=pit.address)
        tap = Tap.deploy(web3, tub.address)
        top = Top.deploy(web3, tub.address, tap.address)

        tub._contract.transact().turn(tap.address.address)

        otc = MatchingMarket.deploy(web3, 2600000000)
        etherdelta = EtherDelta.deploy(web3,
                                       admin=Address('0x1111100000999998888877777666665555544444'),
                                       fee_account=Address('0x8888877777666665555544444111110000099999'),
                                       account_levels_addr=Address('0x0000000000000000000000000000000000000000'),
                                       fee_make=Wad.from_number(0.01),
                                       fee_take=Wad.from_number(0.02),
                                       fee_rebate=Wad.from_number(0.03))

        # set permissions
        dad = DSGuard.deploy(web3)
        dad.permit(DSGuard.ANY, DSGuard.ANY, DSGuard.ANY).transact()
        tub.set_authority(dad.address).transact()
        for auth in [sai, sin, skr, gem, gov, pit, tap, top]:
            auth.set_authority(dad.address).transact()

        # whitelist pairs
        otc.add_token_pair_whitelist(sai.address, gem.address).transact()

        # approve
        tub.approve(directly())
        tap.approve(directly())

        # mint some GEMs
        gem.mint(Wad.from_number(1000000)).transact()

        web3.providers[0].rpc_methods.evm_snapshot()

        self.web3 = web3
        self.our_address = our_address
        self.sai = sai
        self.sin = sin
        self.skr = skr
        self.gem = gem
        self.gov = gov
        self.vox = vox
        self.tub = tub
        self.tap = tap
        self.top = top
        self.otc = otc
        self.etherdelta = etherdelta
Beispiel #17
0
class CdpKeeper:
    """Keeper to actively manage open CDPs."""

    logger = logging.getLogger('cdp-keeper')

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='cdp-keeper')
        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("--tub-address",
                            help="Ethereum address of the Tub contract",
                            required=True,
                            type=str)
        parser.add_argument(
            "--min-margin",
            help=
            "Margin between the liquidation ratio and the top-up threshold",
            type=float,
            required=True)
        parser.add_argument(
            "--top-up-margin",
            help="Margin between the liquidation ratio and the top-up target",
            type=float,
            required=True)
        parser.add_argument("--max-sai", type=float, required=True)
        parser.add_argument("--avg-sai", type=float, required=True)
        parser.add_argument("--gas-price",
                            help="Gas price in Wei (default: node default)",
                            default=0,
                            type=int)
        parser.add_argument("--debug",
                            help="Enable debug output",
                            dest='debug',
                            action='store_true')
        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        self.our_address = Address(self.arguments.eth_from)
        self.tub = Tub(web3=self.web3,
                       address=Address(self.arguments.tub_address))
        self.sai = ERC20Token(web3=self.web3, address=self.tub.sai())

        self.liquidation_ratio = self.tub.mat()
        self.minimum_ratio = self.liquidation_ratio + Ray.from_number(
            self.arguments.min_margin)
        self.target_ratio = self.liquidation_ratio + Ray.from_number(
            self.arguments.top_up_margin)
        self.max_sai = Wad.from_number(self.arguments.max_sai)
        self.avg_sai = Wad.from_number(self.arguments.avg_sai)

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))

    def main(self):
        with Lifecycle(self.web3) as lifecycle:
            lifecycle.on_startup(self.startup)
            lifecycle.on_block(self.check_all_cups)

    def startup(self):
        self.approve()

    def approve(self):
        self.tub.approve(directly(gas_price=self.gas_price()))

    def check_all_cups(self):
        for cup in self.our_cups():
            self.check_cup(cup.cup_id)

    def check_cup(self, cup_id: int):
        assert (isinstance(cup_id, int))

        # If cup is undercollateralized and the amount of SAI we are holding is more than `--max-sai`
        # then we wipe some debt first so our balance reaches `--avg-sai`. Bear in mind that it is
        # possible that we pay all out debt this way and our SAI balance will still be higher
        # than `--max-sai`.
        if self.is_undercollateralized(cup_id) and self.sai.balance_of(
                self.our_address) > self.max_sai:
            amount_of_sai_to_wipe = self.calculate_sai_wipe()
            if amount_of_sai_to_wipe > Wad(0):
                self.tub.wipe(
                    cup_id,
                    amount_of_sai_to_wipe).transact(gas_price=self.gas_price())

        # If cup is still undercollateralized, calculate the amount of SKR needed to top it up so
        # the collateralization level reaches `--top-up-margin`. If we have enough ETH, exchange
        # in to SKR and then top-up the cup.
        if self.is_undercollateralized(cup_id):
            top_up_amount = self.calculate_skr_top_up(cup_id)
            if top_up_amount <= eth_balance(self.web3, self.our_address):
                # TODO we do not always join with the same amount as the one we lock!
                self.tub.join(top_up_amount).transact(
                    gas_price=self.gas_price())
                self.tub.lock(
                    cup_id, top_up_amount).transact(gas_price=self.gas_price())
            else:
                self.logger.info(
                    f"Cannot top-up as our balance is less than {top_up_amount} ETH."
                )

    def our_cups(self):
        for cup_id in range(1, self.tub.cupi() + 1):
            cup = self.tub.cups(cup_id)
            if cup.lad == self.our_address:
                yield cup

    def is_undercollateralized(self, cup_id) -> bool:
        pro = self.tub.ink(cup_id) * self.tub.tag()
        tab = self.tub.tab(cup_id)
        if tab > Wad(0):
            current_ratio = Ray(pro / tab)
            # Prints the Current CDP Ratio and the Minimum Ratio specified under --min-margin
            print(f'Current Ratio {current_ratio}')
            print(f'Minimum Ratio {self.minimum_ratio}')
            return current_ratio < self.minimum_ratio
        else:
            return False

    def calculate_sai_wipe(self) -> Wad:
        """Calculates the amount of SAI that can be wiped.

        Calculates the amount of SAI than can be wiped in order to bring the SAI holdings
        to `--avg-sai`.
        """
        return Wad.max(
            self.sai.balance_of(self.our_address) - self.avg_sai, Wad(0))

    def calculate_skr_top_up(self, cup_id) -> Wad:
        """Calculates the required top-up in SKR.

        Calculates the required top-up in SKR in order to bring the collateralization level
        of the cup to `--target-ratio`.
        """
        pro = self.tub.ink(cup_id) * self.tub.tag()
        tab = self.tub.tab(cup_id)
        if tab > Wad(0):
            current_ratio = Ray(pro / tab)
            return Wad.max(
                tab * (Wad(self.target_ratio - current_ratio) /
                       Wad.from_number(self.tub.tag())), Wad(0))
        else:
            return Wad(0)

    def gas_price(self):
        if self.arguments.gas_price > 0:
            return FixedGasPrice(self.arguments.gas_price)
        else:
            return DefaultGasPrice()
Beispiel #18
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='cdp-keeper')
        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("--tub-address",
                            help="Ethereum address of the Tub contract",
                            required=True,
                            type=str)
        parser.add_argument(
            "--min-margin",
            help=
            "Margin between the liquidation ratio and the top-up threshold",
            type=float,
            required=True)
        parser.add_argument(
            "--top-up-margin",
            help="Margin between the liquidation ratio and the top-up target",
            type=float,
            required=True)
        parser.add_argument("--max-sai", type=float, required=True)
        parser.add_argument("--avg-sai", type=float, required=True)
        parser.add_argument("--gas-price",
                            help="Gas price in Wei (default: node default)",
                            default=0,
                            type=int)
        parser.add_argument("--debug",
                            help="Enable debug output",
                            dest='debug',
                            action='store_true')
        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        self.our_address = Address(self.arguments.eth_from)
        self.tub = Tub(web3=self.web3,
                       address=Address(self.arguments.tub_address))
        self.sai = ERC20Token(web3=self.web3, address=self.tub.sai())

        self.liquidation_ratio = self.tub.mat()
        self.minimum_ratio = self.liquidation_ratio + Ray.from_number(
            self.arguments.min_margin)
        self.target_ratio = self.liquidation_ratio + Ray.from_number(
            self.arguments.top_up_margin)
        self.max_sai = Wad.from_number(self.arguments.max_sai)
        self.avg_sai = Wad.from_number(self.arguments.avg_sai)

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))
    def __init__(self, tub: Tub):
        assert(isinstance(tub, Tub))

        self.ds_value = DSValue(web3=tub.web3, address=tub.pip())
Beispiel #20
0
class DAIv1(Market):

    def __init__(self, web3, dai_tub = '0x448a5065aeBB8E423F0896E6c5D525C040f59af3'):
        self.web3 = web3
        self.tub = Tub(web3=web3, address=Address(dai_tub))
        self.tap = Tap(web3=web3, address=self.tub.tap())
        self.tokens = {
                'MKR': ERC20Token(web3, self.tub.gov()),
                'PETH': ERC20Token(web3, self.tub.skr()),
                'WETH': ERC20Token(web3, self.tub.gem()),
                'DAI': ERC20Token(web3, self.tub.sai()),
        }

    def get_cup(self, cup_id):
        cup = self.tub.cups(cup_id)
        return { 'id': cup.cup_id,
                'lad': cup.lad.address,
                'art': float(cup.art),
                'ink': float(cup.ink),
                'safe': self.tub.safe(cup_id)
               }

    def get_cups(self):
        last_cup_id = self.tub.cupi()
        cups = map(self.get_cup, range(1, last_cup_id+1))
        not_empty_cups = filter(lambda cup: cup['lad'] != "0x0000000000000000000000000000000000000000", cups)
        return list(not_empty_cups)

    def get_pairs(self):
        pairs = ['PETH/DAI', 'PETH/WETH']
        return pairs

    def get_orders(self, base, quote):
        depth = {'bids': [], 'asks': []}

        # PETH/DAI order book
        if base == 'PETH' and quote == 'DAI':
            # boom is a taker using a bid side from maker tap
            # a taker convert PETH to DAI using tap.bid(1) as price
            # maker side offer DAI in exchange for PETH (flap)
            # DAI qty offered by is min(joy - woe, 0)
            order = { 'price': float(self.tap.bid(Wad.from_number(1))),
                      'amount': float(min(self.tap.joy() - self.tap.woe(), Wad.from_number(0))),
                      'id': 'take:tap.boom()',
                    }
            if order['amount'] > 0:
                depth['bids'].append(order)

            # bust is a taker using ask side from maker tap
            # a taker convert DAI to PETH using tap.ask(1) as price
            # maker side offer PETH from fog (flip) and PETH minted to cover woe (flop)
            # PETH qty offered by maker is fog+min(woe-joy, 0)/ask
            order = { 'price': float(self.tap.ask(Wad.from_number(1))),
                      'amount': float(self.tap.fog() + min(self.tap.woe() - self.tap.joy(), Wad.from_number(0)) / self.tap.ask(Wad.from_number(1))),
                      'id': 'take:tap.bust()',
                    }
            if order['amount'] > 0:
                depth['asks'].append(order)

        # PETH/WETH order book
        if base == 'PETH' and quote == 'WETH':
            # exit is a taker using a bid side from maker tub
            # a taker PETH to WETH using tub.bid(1) as price
            # maker side offer WETH in exchange for PETH
            # WETH qty offered by maker is infinity (2**32 as a large number for infinity ...)
            order = { 'price': float(self.tub.bid(Wad.from_number(1))),
                      'amount': float(2**32),
                      'id': 'take:tub.exit()',
                    }
            depth['bids'].append(order)

            # join is a taker using ask side from maker tub
            # a taker convert WETH to PETH usgin tub.ask(1) as price
            # maker side offer PETH in exchange for WETH
            # PETH qty offered by maker is infinity (2**32 as a large number for infinity ...)
            order = { 'price': float(self.tub.ask(Wad.from_number(1))),
                      'amount': float(2**32),
                      'id': 'take:tub.join()',
                    }
            depth['asks'].append(order)

        return depth

    def get_accounts(self, manager_url):
        accounts = {}
        for addr in requests.get(manager_url).json():
            accounts[addr] = {
                'balance' : self.web3.eth.getBalance(addr),
            }
            accounts[addr]['tokens'] = {}
            for name, token in self.tokens.items():
                balance = token.balance_of(Address(addr))
                allowance = token.allowance_of(Address(addr), self.tub.address)
                #TODO check tap allowance ...
                if float(allowance) or float(balance):
                    accounts[addr]['tokens'][name] = {
                            'allowance': float(allowance),
                            'balance': float(balance),
                    }

        return accounts
    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 __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)
Beispiel #24
0
 def test_comparison(self, deployment: Deployment):
     # expect
     assert deployment.tub == deployment.tub
     assert deployment.tub == Tub(web3=deployment.web3, address=deployment.tub.address)
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='etherdelta-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("--etherdelta-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the EtherDelta contract")

        parser.add_argument(
            "--etherdelta-socket",
            type=str,
            required=True,
            help="Ethereum address of the EtherDelta API socket")

        parser.add_argument(
            "--etherdelta-number-of-attempts",
            type=int,
            default=3,
            help=
            "Number of attempts of running the tool to talk to the EtherDelta API socket"
        )

        parser.add_argument(
            "--etherdelta-retry-interval",
            type=int,
            default=10,
            help=
            "Retry interval for sending orders over the EtherDelta API socket")

        parser.add_argument(
            "--etherdelta-timeout",
            type=int,
            default=120,
            help="Timeout for sending orders over the EtherDelta API socket")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Buy/sell bands configuration file")

        parser.add_argument(
            "--price-feed",
            type=str,
            help=
            "Source of price feed. Tub price feed will be used if not specified"
        )

        parser.add_argument(
            "--price-feed-expiry",
            type=int,
            default=120,
            help="Maximum age of non-Tub price feed (in seconds, default: 120)"
        )

        parser.add_argument("--order-age",
                            type=int,
                            required=True,
                            help="Age of created orders (in blocks)")

        parser.add_argument(
            "--order-expiry-threshold",
            type=int,
            default=0,
            help=
            "Remaining order age (in blocks) at which order is considered already expired, which"
            " means the keeper will send a new replacement order slightly ahead"
        )

        parser.add_argument(
            "--order-no-cancel-threshold",
            type=int,
            default=0,
            help=
            "Remaining order age (in blocks) below which keeper does not try to cancel orders,"
            " assuming that they will probably expire before the cancel transaction gets mined"
        )

        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 with either terminate or not start at all"
        )

        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(
            '--cancel-on-shutdown',
            dest='cancel_on_shutdown',
            action='store_true',
            help=
            "Whether should cancel all open orders on EtherDelta on keeper shutdown"
        )

        parser.add_argument(
            '--withdraw-on-shutdown',
            dest='withdraw_on_shutdown',
            action='store_true',
            help=
            "Whether should withdraw all tokens from EtherDelta on keeper shutdown"
        )

        parser.add_argument("--gas-price",
                            type=int,
                            default=0,
                            help="Gas price (in Wei)")

        parser.add_argument(
            "--gas-price-increase",
            type=int,
            help="Gas price increase (in Wei) if no confirmation within"
            " `--gas-price-increase-every` seconds")

        parser.add_argument(
            "--gas-price-increase-every",
            type=int,
            default=120,
            help="Gas price increase frequency (in seconds, default: 120)")

        parser.add_argument("--gas-price-max",
                            type=int,
                            help="Maximum gas price (in Wei)")

        parser.add_argument("--gas-price-file",
                            type=str,
                            help="Gas price configuration file")

        parser.add_argument(
            "--smart-gas-price",
            dest='smart_gas_price',
            action='store_true',
            help=
            "Use smart gas pricing strategy, based on the ethgasstation.info feed"
        )

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        parser.set_defaults(cancel_on_shutdown=False,
                            withdraw_on_shutdown=False)

        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        self.our_address = Address(self.arguments.eth_from)
        self.tub = Tub(web3=self.web3,
                       address=Address(self.arguments.tub_address))
        self.vox = Vox(web3=self.web3, address=self.tub.vox())
        self.sai = ERC20Token(web3=self.web3, address=self.tub.sai())
        self.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.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, self.vox)

        if self.eth_reserve <= self.min_eth_balance:
            raise Exception(
                "--eth-reserve must be higher than --min-eth-balance")

        assert (self.arguments.order_expiry_threshold >= 0)
        assert (self.arguments.order_no_cancel_threshold >=
                self.arguments.order_expiry_threshold)

        self.etherdelta = EtherDelta(web3=self.web3,
                                     address=Address(
                                         self.arguments.etherdelta_address))
        self.etherdelta_api = EtherDeltaApi(
            client_tool_directory="lib/pymaker/utils/etherdelta-client",
            client_tool_command="node main.js",
            api_server=self.arguments.etherdelta_socket,
            number_of_attempts=self.arguments.etherdelta_number_of_attempts,
            retry_interval=self.arguments.etherdelta_retry_interval,
            timeout=self.arguments.etherdelta_timeout)

        self.our_orders = list()
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
Beispiel #27
0
    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()
Beispiel #28
0
    def __init__(self):
        web3 = Web3(HTTPProvider("http://localhost:8555"))
        web3.eth.defaultAccount = web3.eth.accounts[0]
        our_address = Address(web3.eth.defaultAccount)
        sai = DSToken.deploy(web3, 'DAI')
        sin = DSToken.deploy(web3, 'SIN')
        skr = DSToken.deploy(web3, 'PETH')
        gem = DSToken.deploy(web3, 'WETH')
        gov = DSToken.deploy(web3, 'MKR')
        pip = DSValue.deploy(web3)
        pep = DSValue.deploy(web3)
        pit = DSVault.deploy(web3)

        vox = Vox.deploy(web3, per=Ray.from_number(1))
        tub = Tub.deploy(web3,
                         sai=sai.address,
                         sin=sin.address,
                         skr=skr.address,
                         gem=gem.address,
                         gov=gov.address,
                         pip=pip.address,
                         pep=pep.address,
                         vox=vox.address,
                         pit=pit.address)
        tap = Tap.deploy(web3, tub.address)
        top = Top.deploy(web3, tub.address, tap.address)

        tub._contract.functions.turn(tap.address.address).transact()

        etherdelta = EtherDelta.deploy(
            web3,
            admin=Address('0x1111100000999998888877777666665555544444'),
            fee_account=Address('0x8888877777666665555544444111110000099999'),
            account_levels_addr=Address(
                '0x0000000000000000000000000000000000000000'),
            fee_make=Wad.from_number(0.01),
            fee_take=Wad.from_number(0.02),
            fee_rebate=Wad.from_number(0.03))

        # set permissions
        dad = DSGuard.deploy(web3)
        dad.permit(DSGuard.ANY, DSGuard.ANY, DSGuard.ANY).transact()
        tub.set_authority(dad.address).transact()
        for auth in [sai, sin, skr, gem, gov, pit, tap, top]:
            auth.set_authority(dad.address).transact()

        # approve
        tub.approve(directly())
        tap.approve(directly())

        # mint some GEMs
        gem.mint(Wad.from_number(1000000)).transact()

        self.snapshot_id = web3.manager.request_blocking("evm_snapshot", [])

        self.web3 = web3
        self.our_address = our_address
        self.sai = sai
        self.sin = sin
        self.skr = skr
        self.gem = gem
        self.gov = gov
        self.vox = vox
        self.tub = tub
        self.tap = tap
        self.top = top
        self.etherdelta = etherdelta
class ArbitrageKeeper:
    """Keeper to arbitrage on OasisDEX, `join`, `exit`, `boom` and `bust`."""

    logger = logging.getLogger('arbitrage-keeper')

    def __init__(self, args, **kwargs):
        parser = argparse.ArgumentParser("arbitrage-keeper")

        parser.add_argument("--rpc-host",
                            type=str,
                            default="localhost",
                            help="JSON-RPC host (default: `localhost')")

        parser.add_argument("--rpc-port",
                            type=int,
                            default=8545,
                            help="JSON-RPC port (default: `8545')")

        parser.add_argument("--rpc-timeout",
                            type=int,
                            default=10,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help="Ethereum account from which to send transactions")

        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')"
        )

        parser.add_argument("--tub-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Tub contract")

        parser.add_argument("--tap-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Tap contract")

        parser.add_argument(
            "--exchange-address",
            type=str,
            help="Ethereum address of the 0x Exchange contract")

        parser.add_argument("--oasis-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the OasisDEX contract")

        parser.add_argument(
            "--oasis-support-address",
            type=str,
            required=False,
            help="Ethereum address of the OasisDEX support contract")

        parser.add_argument("--relayer-api-server",
                            type=str,
                            help="Address of the 0x Relayer API")

        parser.add_argument(
            "--relayer-per-page",
            type=int,
            default=100,
            help=
            "Number of orders to fetch per one page from the 0x Relayer API (default: 100)"
        )

        parser.add_argument(
            "--tx-manager",
            type=str,
            help=
            "Ethereum address of the TxManager contract to use for multi-step arbitrage"
        )

        parser.add_argument("--gas-price",
                            type=int,
                            default=0,
                            help="Gas price in Wei (default: node default)")

        parser.add_argument(
            "--base-token",
            type=str,
            required=True,
            help="The token all arbitrage sequences will start and end with")

        parser.add_argument(
            "--min-profit",
            type=float,
            required=True,
            help="Minimum profit (in base token) from one arbitrage operation")

        parser.add_argument(
            "--max-engagement",
            type=float,
            required=True,
            help="Maximum engagement (in base token) in one arbitrage operation"
        )

        parser.add_argument(
            "--max-errors",
            type=int,
            default=100,
            help=
            "Maximum number of allowed errors before the keeper terminates (default: 100)"
        )

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        self.tub = Tub(web3=self.web3,
                       address=Address(self.arguments.tub_address))
        self.tap = Tap(web3=self.web3,
                       address=Address(self.arguments.tap_address))
        self.gem = ERC20Token(web3=self.web3, address=self.tub.gem())
        self.sai = ERC20Token(web3=self.web3, address=self.tub.sai())
        self.skr = ERC20Token(web3=self.web3, address=self.tub.skr())

        self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) \
            if self.arguments.exchange_address is not None else None
        self.zrx_relayer_api = ZrxRelayerApi(exchange=self.zrx_exchange, api_server=self.arguments.relayer_api_server) \
            if self.arguments.relayer_api_server is not None else None

        self.otc = MatchingMarket(
            web3=self.web3,
            address=Address(self.arguments.oasis_address),
            support_address=Address(self.arguments.oasis_support_address)
            if self.arguments.oasis_support_address is not None else None)

        self.base_token = ERC20Token(web3=self.web3,
                                     address=Address(
                                         self.arguments.base_token))
        self.min_profit = Wad.from_number(self.arguments.min_profit)
        self.max_engagement = Wad.from_number(self.arguments.max_engagement)
        self.max_errors = self.arguments.max_errors
        self.errors = 0

        if self.arguments.tx_manager:
            self.tx_manager = TxManager(web3=self.web3,
                                        address=Address(
                                            self.arguments.tx_manager))
            if self.tx_manager.owner() != self.our_address:
                raise Exception(
                    f"The TxManager has to be owned by the address the keeper is operating from."
                )
        else:
            self.tx_manager = None

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))

    def main(self):
        with Lifecycle(self.web3) as lifecycle:
            self.lifecycle = lifecycle
            lifecycle.on_startup(self.startup)
            lifecycle.on_block(self.process_block)

    def startup(self):
        self.approve()

    def approve(self):
        """Approve all components that need to access our balances"""
        approval_method = via_tx_manager(self.tx_manager, gas_price=self.gas_price()) if self.tx_manager \
            else directly(gas_price=self.gas_price())
        self.tub.approve(approval_method)
        self.tap.approve(approval_method)
        self.otc.approve([self.gem, self.sai, self.skr], approval_method)
        if self.zrx_exchange:
            self.zrx_exchange.approve([self.gem, self.sai], approval_method)
        if self.tx_manager:
            self.tx_manager.approve([self.gem, self.sai, self.skr],
                                    directly(gas_price=self.gas_price()))

    def token_name(self, address: Address) -> str:
        if address == self.sai.address:
            return "DAI"

        elif address == self.gem.address:
            return "WETH"

        elif address == self.skr.address:
            return "PETH"

        else:
            return str(address)

    def tub_conversions(self) -> List[Conversion]:
        return [
            TubJoinConversion(self.tub),
            TubExitConversion(self.tub),
            TubBoomConversion(self.tub, self.tap),
            TubBustConversion(self.tub, self.tap)
        ]

    def otc_orders(self, tokens):
        orders = []

        for token1 in tokens:
            for token2 in tokens:
                if token1 != token2:
                    orders = orders + self.otc.get_orders(token1, token2)

        return orders

    def otc_conversions(self, tokens) -> List[Conversion]:
        return list(
            map(lambda order: OasisTakeConversion(self.otc, order),
                self.otc_orders(tokens)))

    def zrx_orders(self, tokens):
        if self.zrx_exchange is None or self.zrx_relayer_api is None:
            return []

        orders = []

        for token1 in tokens:
            for token2 in tokens:
                if token1 != token2:
                    orders = orders + self.zrx_relayer_api.get_orders(
                        token1, token2)

        return list(
            filter(lambda order: order.expiration <= time.time(), orders))

    def zrx_conversions(self, tokens) -> List[Conversion]:
        return list(
            map(lambda order: ZrxFillOrderConversion(self.zrx_exchange, order),
                self.zrx_orders(tokens)))

    def all_conversions(self):
        return self.tub_conversions() + \
               self.otc_conversions([self.sai.address, self.skr.address, self.gem.address]) + \
               self.zrx_conversions([self.sai.address, self.gem.address])

    def process_block(self):
        """Callback called on each new block.
        If too many errors, terminate the keeper to minimize potential damage."""
        if self.errors >= self.max_errors:
            self.lifecycle.terminate()
        else:
            self.execute_best_opportunity_available()

    def execute_best_opportunity_available(self):
        """Find the best arbitrage opportunity present and execute it."""
        opportunity = self.best_opportunity(self.profitable_opportunities())
        if opportunity:
            self.print_opportunity(opportunity)
            self.execute_opportunity(opportunity)

    def profitable_opportunities(self):
        """Identify all profitable arbitrage opportunities within given limits."""
        entry_amount = Wad.min(self.base_token.balance_of(self.our_address),
                               self.max_engagement)
        opportunity_finder = OpportunityFinder(
            conversions=self.all_conversions())
        opportunities = opportunity_finder.find_opportunities(
            self.base_token.address, entry_amount)
        opportunities = filter(
            lambda op: op.total_rate() > Ray.from_number(1.000001),
            opportunities)
        opportunities = filter(
            lambda op: op.profit(self.base_token.address) > self.min_profit,
            opportunities)
        opportunities = sorted(
            opportunities,
            key=lambda op: op.profit(self.base_token.address),
            reverse=True)
        return opportunities

    def best_opportunity(self, opportunities: List[Sequence]):
        """Pick the best opportunity, or return None if no profitable opportunities."""
        return opportunities[0] if len(opportunities) > 0 else None

    def print_opportunity(self, opportunity: Sequence):
        """Print the details of the opportunity."""
        self.logger.info(
            f"Opportunity with id={opportunity.id()},"
            f" profit={opportunity.profit(self.base_token.address)} {self.token_name(self.base_token.address)}"
        )

        for index, conversion in enumerate(opportunity.steps, start=1):
            self.logger.info(
                f"Step {index}/{len(opportunity.steps)}: {conversion.name()}"
                f" (from {conversion.source_amount} {self.token_name(conversion.source_token)}"
                f" to {conversion.target_amount} {self.token_name(conversion.target_token)})"
            )

    def execute_opportunity(self, opportunity: Sequence):
        """Execute the opportunity either in one Ethereum transaction or step-by-step.
        Depending on whether `tx_manager` is available."""
        if self.tx_manager:
            self.execute_opportunity_in_one_transaction(opportunity)
        else:
            self.execute_opportunity_step_by_step(opportunity)

    def execute_opportunity_step_by_step(self, opportunity: Sequence):
        """Execute the opportunity step-by-step."""
        def incoming_transfer(our_address: Address):
            return lambda transfer: transfer.to_address == our_address

        def outgoing_transfer(our_address: Address):
            return lambda transfer: transfer.from_address == our_address

        all_transfers = []
        for step in opportunity.steps:
            receipt = step.transact().transact(gas_price=self.gas_price())
            if receipt:
                all_transfers += receipt.transfers
                outgoing = TransferFormatter().format(
                    filter(outgoing_transfer(self.our_address),
                           receipt.transfers), self.token_name)
                incoming = TransferFormatter().format(
                    filter(incoming_transfer(self.our_address),
                           receipt.transfers), self.token_name)
                self.logger.info(f"Exchanged {outgoing} to {incoming}")
            else:
                self.errors += 1
                return
        self.logger.info(
            f"The profit we made is {TransferFormatter().format_net(all_transfers, self.our_address, self.token_name)}"
        )

    def execute_opportunity_in_one_transaction(self, opportunity: Sequence):
        """Execute the opportunity in one transaction, using the `tx_manager`."""
        tokens = [self.sai.address, self.skr.address, self.gem.address]
        invocations = list(
            map(lambda step: step.transact().invocation(), opportunity.steps))
        receipt = self.tx_manager.execute(
            tokens, invocations).transact(gas_price=self.gas_price())
        if receipt:
            self.logger.info(
                f"The profit we made is {TransferFormatter().format_net(receipt.transfers, self.our_address, self.token_name)}"
            )
        else:
            self.errors += 1

    def gas_price(self):
        if self.arguments.gas_price > 0:
            return FixedGasPrice(self.arguments.gas_price)
        else:
            return DefaultGasPrice()
class EtherDeltaMarketMakerKeeper:
    """Keeper acting as a market maker on EtherDelta, on the ETH/SAI pair."""

    logger = logging.getLogger()

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='etherdelta-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("--etherdelta-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the EtherDelta contract")

        parser.add_argument(
            "--etherdelta-socket",
            type=str,
            required=True,
            help="Ethereum address of the EtherDelta API socket")

        parser.add_argument(
            "--etherdelta-number-of-attempts",
            type=int,
            default=3,
            help=
            "Number of attempts of running the tool to talk to the EtherDelta API socket"
        )

        parser.add_argument(
            "--etherdelta-retry-interval",
            type=int,
            default=10,
            help=
            "Retry interval for sending orders over the EtherDelta API socket")

        parser.add_argument(
            "--etherdelta-timeout",
            type=int,
            default=120,
            help="Timeout for sending orders over the EtherDelta API socket")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Buy/sell bands configuration file")

        parser.add_argument(
            "--price-feed",
            type=str,
            help=
            "Source of price feed. Tub price feed will be used if not specified"
        )

        parser.add_argument(
            "--price-feed-expiry",
            type=int,
            default=120,
            help="Maximum age of non-Tub price feed (in seconds, default: 120)"
        )

        parser.add_argument("--order-age",
                            type=int,
                            required=True,
                            help="Age of created orders (in blocks)")

        parser.add_argument(
            "--order-expiry-threshold",
            type=int,
            default=0,
            help=
            "Remaining order age (in blocks) at which order is considered already expired, which"
            " means the keeper will send a new replacement order slightly ahead"
        )

        parser.add_argument(
            "--order-no-cancel-threshold",
            type=int,
            default=0,
            help=
            "Remaining order age (in blocks) below which keeper does not try to cancel orders,"
            " assuming that they will probably expire before the cancel transaction gets mined"
        )

        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 with either terminate or not start at all"
        )

        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(
            '--cancel-on-shutdown',
            dest='cancel_on_shutdown',
            action='store_true',
            help=
            "Whether should cancel all open orders on EtherDelta on keeper shutdown"
        )

        parser.add_argument(
            '--withdraw-on-shutdown',
            dest='withdraw_on_shutdown',
            action='store_true',
            help=
            "Whether should withdraw all tokens from EtherDelta on keeper shutdown"
        )

        parser.add_argument("--gas-price",
                            type=int,
                            default=0,
                            help="Gas price (in Wei)")

        parser.add_argument(
            "--gas-price-increase",
            type=int,
            help="Gas price increase (in Wei) if no confirmation within"
            " `--gas-price-increase-every` seconds")

        parser.add_argument(
            "--gas-price-increase-every",
            type=int,
            default=120,
            help="Gas price increase frequency (in seconds, default: 120)")

        parser.add_argument("--gas-price-max",
                            type=int,
                            help="Maximum gas price (in Wei)")

        parser.add_argument("--gas-price-file",
                            type=str,
                            help="Gas price configuration file")

        parser.add_argument(
            "--smart-gas-price",
            dest='smart_gas_price',
            action='store_true',
            help=
            "Use smart gas pricing strategy, based on the ethgasstation.info feed"
        )

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        parser.set_defaults(cancel_on_shutdown=False,
                            withdraw_on_shutdown=False)

        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        self.our_address = Address(self.arguments.eth_from)
        self.tub = Tub(web3=self.web3,
                       address=Address(self.arguments.tub_address))
        self.vox = Vox(web3=self.web3, address=self.tub.vox())
        self.sai = ERC20Token(web3=self.web3, address=self.tub.sai())
        self.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.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, self.vox)

        if self.eth_reserve <= self.min_eth_balance:
            raise Exception(
                "--eth-reserve must be higher than --min-eth-balance")

        assert (self.arguments.order_expiry_threshold >= 0)
        assert (self.arguments.order_no_cancel_threshold >=
                self.arguments.order_expiry_threshold)

        self.etherdelta = EtherDelta(web3=self.web3,
                                     address=Address(
                                         self.arguments.etherdelta_address))
        self.etherdelta_api = EtherDeltaApi(
            client_tool_directory="lib/pymaker/utils/etherdelta-client",
            client_tool_command="node main.js",
            api_server=self.arguments.etherdelta_socket,
            number_of_attempts=self.arguments.etherdelta_number_of_attempts,
            retry_interval=self.arguments.etherdelta_retry_interval,
            timeout=self.arguments.etherdelta_timeout)

        self.our_orders = list()

    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):
        if self.arguments.cancel_on_shutdown:
            self.cancel_all_orders()

        if self.arguments.withdraw_on_shutdown:
            self.withdraw_everything()

    def approve(self):
        """Approve EtherDelta to access our tokens, so we can deposit them with the exchange"""
        token_addresses = filter(
            lambda address: address != EtherDelta.ETH_TOKEN,
            [self.token_sell(), self.token_buy()])
        tokens = list(
            map(lambda address: ERC20Token(web3=self.web3, address=address),
                token_addresses))

        self.etherdelta.approve(tokens, directly(gas_price=self.gas_price))

    def place_order(self, order: Order):
        self.our_orders.append(order)
        self.etherdelta_api.publish_order(order)

    def price(self) -> Wad:
        return self.price_feed.get_price()

    def token_sell(self) -> Address:
        return EtherDelta.ETH_TOKEN

    def token_buy(self) -> Address:
        return self.sai.address

    def our_balance(self, token: Address) -> Wad:
        if token == EtherDelta.ETH_TOKEN:
            return self.etherdelta.balance_of(self.our_address)
        else:
            return self.etherdelta.balance_of_token(token, self.our_address)

    def our_sell_orders(self):
        return list(
            filter(
                lambda order: order.buy_token == self.token_buy() and order.
                pay_token == self.token_sell(), self.our_orders))

    def our_buy_orders(self):
        return list(
            filter(
                lambda order: order.buy_token == self.token_sell() and order.
                pay_token == self.token_buy(), self.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(
                "Keeper ETH balance below minimum. Cancelling all orders.")
            self.cancel_all_orders()
            return

        bands = Bands(self.bands_config)
        block_number = self.web3.eth.blockNumber
        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(
                "Cancelling all orders as no price feed available.")
            self.cancel_all_orders()
            return

        # Remove expired orders from the local order list
        self.remove_expired_orders(block_number)

        # Cancel orders
        cancellable_orders = bands.cancellable_orders(self.our_buy_orders(),
                                                      self.our_sell_orders(),
                                                      target_price)
        if len(cancellable_orders) > 0:
            self.cancel_orders(cancellable_orders, block_number)
            return

        # Place new orders
        self.top_up_bands(bands.buy_bands, bands.sell_bands, target_price)

    @staticmethod
    def is_order_age_above_threshold(order: Order, block_number: int,
                                     threshold: int):
        return block_number >= order.expires - threshold  # we do >= 0, which makes us effectively detect an order
        # as expired one block earlier than the contract, but
        # this is desirable from the keeper point of view

    def is_expired(self, order: Order, block_number: int):
        return self.is_order_age_above_threshold(
            order, block_number, self.arguments.order_expiry_threshold)

    def is_non_cancellable(self, order: Order, block_number: int):
        return self.is_order_age_above_threshold(
            order, block_number, self.arguments.order_no_cancel_threshold)

    def remove_expired_orders(self, block_number: int):
        self.our_orders = list(
            filter(lambda order: not self.is_expired(order, block_number),
                   self.our_orders))

    def cancel_orders(self, orders: Iterable, block_number: int):
        """Cancel orders asynchronously."""
        cancellable_orders = list(
            filter(
                lambda order: not self.is_non_cancellable(order, block_number),
                orders))
        synchronize([
            self.etherdelta.cancel_order(order).transact_async(
                gas_price=self.gas_price) for order in cancellable_orders
        ])
        self.our_orders = list(set(self.our_orders) - set(cancellable_orders))

    def cancel_all_orders(self):
        """Cancel all our orders."""
        self.cancel_orders(self.our_orders, self.web3.eth.blockNumber)

    def withdraw_everything(self):
        eth_balance = self.etherdelta.balance_of(self.our_address)
        if eth_balance > Wad(0):
            self.etherdelta.withdraw(eth_balance).transact(
                gas_price=self.gas_price)

        sai_balance = self.etherdelta.balance_of_token(self.sai.address,
                                                       self.our_address)
        if sai_balance > Wad(0):
            self.etherdelta.withdraw_token(self.sai.address,
                                           sai_balance).transact()

    def top_up_bands(self, buy_bands: list, sell_bands: list,
                     target_price: Wad):
        """Create new buy and sell orders in all send and buy bands if necessary."""
        self.top_up_buy_bands(buy_bands, target_price)
        self.top_up_sell_bands(sell_bands, target_price)

    def top_up_sell_bands(self, sell_bands: list, target_price: Wad):
        """Ensure our sell engagement is not below minimum in all sell bands. Place new orders if necessary."""
        our_balance = self.our_balance(self.token_sell())
        for band in sell_bands:
            orders = [
                order for order in self.our_sell_orders()
                if band.includes(order, target_price)
            ]
            total_amount = self.total_amount(orders)
            if total_amount < band.min_amount:
                if self.deposit_for_sell_order_if_needed(band.avg_amount -
                                                         total_amount):
                    return

                price = band.avg_price(target_price)
                pay_amount = self.fix_amount(
                    Wad.min(
                        band.avg_amount - total_amount, our_balance -
                        self.total_amount(self.our_sell_orders())))
                buy_amount = self.fix_amount(pay_amount * price)
                if (pay_amount >= band.dust_cutoff) and (
                        pay_amount > Wad(0)) and (buy_amount > Wad(0)):
                    self.logger.debug(
                        f"Using price {price} for new sell order")

                    order = self.etherdelta.create_order(
                        pay_token=self.token_sell(),
                        pay_amount=pay_amount,
                        buy_token=self.token_buy(),
                        buy_amount=buy_amount,
                        expires=self.web3.eth.blockNumber +
                        self.arguments.order_age)
                    self.place_order(order)

    def top_up_buy_bands(self, buy_bands: list, target_price: Wad):
        """Ensure our buy engagement is not below minimum in all buy bands. Place new orders if necessary."""
        our_balance = self.our_balance(self.token_buy())
        for band in buy_bands:
            orders = [
                order for order in self.our_buy_orders()
                if band.includes(order, target_price)
            ]
            total_amount = self.total_amount(orders)
            if total_amount < band.min_amount:
                if self.deposit_for_buy_order_if_needed(band.avg_amount -
                                                        total_amount):
                    return

                price = band.avg_price(target_price)
                pay_amount = self.fix_amount(
                    Wad.min(
                        band.avg_amount - total_amount, our_balance -
                        self.total_amount(self.our_buy_orders())))
                buy_amount = self.fix_amount(pay_amount / price)
                if (pay_amount >= band.dust_cutoff) and (
                        pay_amount > Wad(0)) and (buy_amount > Wad(0)):
                    self.logger.debug(f"Using price {price} for new buy order")

                    order = self.etherdelta.create_order(
                        pay_token=self.token_buy(),
                        pay_amount=pay_amount,
                        buy_token=self.token_sell(),
                        buy_amount=buy_amount,
                        expires=self.web3.eth.blockNumber +
                        self.arguments.order_age)
                    self.place_order(order)

    def depositable_balance(self, token: Address) -> Wad:
        if token == EtherDelta.ETH_TOKEN:
            return Wad.max(
                eth_balance(self.web3, self.our_address) - self.eth_reserve,
                Wad(0))
        else:
            return ERC20Token(web3=self.web3,
                              address=token).balance_of(self.our_address)

    def deposit_for_sell_order_if_needed(self, desired_order_pay_amount: Wad):
        if self.our_balance(self.token_sell()) < desired_order_pay_amount:
            return self.deposit_for_sell_order()
        else:
            return False

    def deposit_for_sell_order(self):
        depositable_eth = self.depositable_balance(self.token_sell())
        if depositable_eth > self.min_eth_deposit:
            return self.etherdelta.deposit(depositable_eth).transact(
                gas_price=self.gas_price).successful
        else:
            return False

    def deposit_for_buy_order_if_needed(self, desired_order_pay_amount: Wad):
        if self.our_balance(self.token_buy()) < desired_order_pay_amount:
            return self.deposit_for_buy_order()
        else:
            return False

    def deposit_for_buy_order(self):
        depositable_sai = self.depositable_balance(self.token_buy())
        if depositable_sai > self.min_sai_deposit:
            return self.etherdelta.deposit_token(
                self.sai.address,
                depositable_sai).transact(gas_price=self.gas_price).successful
        else:
            return False

    def total_amount(self, orders):
        return reduce(operator.add,
                      map(lambda order: order.remaining_sell_amount, orders),
                      Wad(0))

    @staticmethod
    def fix_amount(amount: Wad) -> Wad:
        # for some reason, the EtherDelta backend rejects offchain orders with some amounts
        # for example, the following order:
        #       self.etherdelta.place_order_offchain(self.sai.address, Wad(93033469375510291122),
        #                                                 EtherDelta.ETH_TOKEN, Wad(400000000000000000),
        #                                                 self.web3.eth.blockNumber + 50)
        # will get placed correctly, but if we substitute 93033469375510291122 for 93033469375510237227
        # the backend will not accept it. this is 100% reproductible with above amounts,
        # although I wasn't able to figure out the actual reason
        #
        # what I have noticed is that rounding the amount seems to help,
        # so this is what this particular method does
        return Wad(int(amount.value / 10**9) * 10**9)
class EtherDeltaMarketMakerKeeper:
    """Keeper acting as a market maker on EtherDelta, on the ETH/SAI pair."""

    logger = logging.getLogger()

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='etherdelta-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("--etherdelta-address", type=str, required=True,
                            help="Ethereum address of the EtherDelta contract")

        parser.add_argument("--etherdelta-socket", type=str, required=True,
                            help="Ethereum address of the EtherDelta API socket")

        parser.add_argument("--etherdelta-number-of-attempts", type=int, default=3,
                            help="Number of attempts of running the tool to talk to the EtherDelta API socket")

        parser.add_argument("--etherdelta-retry-interval", type=int, default=10,
                            help="Retry interval for sending orders over the EtherDelta API socket")

        parser.add_argument("--etherdelta-timeout", type=int, default=120,
                            help="Timeout for sending orders over the EtherDelta API socket")

        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-age", type=int, required=True,
                            help="Age of created orders (in blocks)")

        parser.add_argument("--order-expiry-threshold", type=int, default=0,
                            help="Remaining order age (in blocks) at which order is considered already expired, which"
                                 " means the keeper will send a new replacement order slightly ahead")

        parser.add_argument("--order-no-cancel-threshold", type=int, default=0,
                            help="Remaining order age (in blocks) below which keeper does not try to cancel orders,"
                                 " assuming that they will probably expire before the cancel transaction gets mined")

        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('--cancel-on-shutdown', dest='cancel_on_shutdown', action='store_true',
                            help="Whether should cancel all open orders on EtherDelta on keeper shutdown")

        parser.add_argument('--withdraw-on-shutdown', dest='withdraw_on_shutdown', action='store_true',
                            help="Whether should withdraw all tokens from EtherDelta on keeper shutdown")

        parser.add_argument("--gas-price", type=int, default=0,
                            help="Gas price (in Wei)")

        parser.add_argument("--smart-gas-price", dest='smart_gas_price', action='store_true',
                            help="Use smart gas pricing strategy, based on the ethgasstation.info feed")

        parser.add_argument("--debug", dest='debug', action='store_true',
                            help="Enable debug output")

        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")

        assert(self.arguments.order_expiry_threshold >= 0)
        assert(self.arguments.order_no_cancel_threshold >= self.arguments.order_expiry_threshold)

        self.history = History()
        self.etherdelta = EtherDelta(web3=self.web3, address=Address(self.arguments.etherdelta_address))
        self.etherdelta_api = EtherDeltaApi(client_tool_directory="lib/pymaker/utils/etherdelta-client",
                                            client_tool_command="node main.js",
                                            api_server=self.arguments.etherdelta_socket,
                                            number_of_attempts=self.arguments.etherdelta_number_of_attempts,
                                            retry_interval=self.arguments.etherdelta_retry_interval,
                                            timeout=self.arguments.etherdelta_timeout)

        self.our_orders = list()

    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):
        if self.arguments.cancel_on_shutdown:
            self.cancel_all_orders()

        if self.arguments.withdraw_on_shutdown:
            self.withdraw_everything()

    def approve(self):
        token_addresses = filter(lambda address: address != EtherDelta.ETH_TOKEN, [self.token_sell(), self.token_buy()])
        tokens = list(map(lambda address: ERC20Token(web3=self.web3, address=address), token_addresses))

        self.etherdelta.approve(tokens, directly(gas_price=self.gas_price))

    def place_order(self, order: Order):
        self.our_orders.append(order)
        self.etherdelta_api.publish_order(order)

    def token_sell(self) -> Address:
        return EtherDelta.ETH_TOKEN

    def token_buy(self) -> Address:
        return self.sai.address

    def our_total_balance(self, token: Address) -> Wad:
        if token == EtherDelta.ETH_TOKEN:
            return self.etherdelta.balance_of(self.our_address)
        else:
            return self.etherdelta.balance_of_token(token, self.our_address)

    def our_sell_orders(self):
        return list(filter(lambda order: order.buy_token == self.token_buy() and
                                         order.pay_token == self.token_sell(), self.our_orders))

    def our_buy_orders(self):
        return list(filter(lambda order: order.buy_token == self.token_sell() and
                                         order.pay_token == self.token_buy(), self.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.
        #
        # The exception is when we can withdraw some ETH from EtherDelta. Then we do it and carry on.
        if eth_balance(self.web3, self.our_address) < self.min_eth_balance:
            if self.etherdelta.balance_of(self.our_address) > self.eth_reserve:
                self.logger.warning(f"Keeper ETH balance below minimum, withdrawing {self.eth_reserve}.")
                self.etherdelta.withdraw(self.eth_reserve).transact()
            else:
                self.logger.warning(f"Keeper ETH balance below minimum, cannot withdraw. Cancelling all orders.")
                self.cancel_all_orders()

            return

        bands = Bands(self.bands_config, self.spread_feed, self.history)
        block_number = self.web3.eth.blockNumber
        target_price = self.price_feed.get_price()

        # Remove expired orders from the local order list
        self.remove_expired_orders(block_number)

        # Cancel orders
        cancellable_orders = bands.cancellable_orders(self.our_buy_orders(), self.our_sell_orders(), target_price)
        if len(cancellable_orders) > 0:
            self.cancel_orders(cancellable_orders, block_number)
            return

        # In case of EtherDelta, balances returned by `our_total_balance` still contain amounts "locked"
        # by currently open orders, so we need to explicitly subtract these amounts.
        our_buy_balance = self.our_total_balance(self.token_buy()) - Bands.total_amount(self.our_buy_orders())
        our_sell_balance = self.our_total_balance(self.token_sell()) - Bands.total_amount(self.our_sell_orders())

        # 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_sell_orders=self.our_sell_orders(),
                                                                               our_buy_balance=our_buy_balance,
                                                                               our_sell_balance=our_sell_balance,
                                                                               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():
                made_deposit = True

        if missing_sell_amount > Wad(0):
            if self.deposit_for_sell_order():
                made_deposit = True

        # If we managed to deposit something, do not do anything so we can reevaluate new orders to be created.
        # Otherwise, create new orders.
        if not made_deposit:
            self.place_orders(new_orders)

    @staticmethod
    def is_order_age_above_threshold(order: Order, block_number: int, threshold: int):
        return block_number >= order.expires-threshold  # we do >= 0, which makes us effectively detect an order
                                                        # as expired one block earlier than the contract, but
                                                        # this is desirable from the keeper point of view

    def is_expired(self, order: Order, block_number: int):
        return self.is_order_age_above_threshold(order, block_number, self.arguments.order_expiry_threshold)

    def is_non_cancellable(self, order: Order, block_number: int):
        return self.is_order_age_above_threshold(order, block_number, self.arguments.order_no_cancel_threshold)

    def remove_expired_orders(self, block_number: int):
        self.our_orders = list(filter(lambda order: not self.is_expired(order, block_number), self.our_orders))

    def cancel_orders(self, orders: Iterable, block_number: int):
        cancellable_orders = list(filter(lambda order: not self.is_non_cancellable(order, block_number), orders))
        synchronize([self.etherdelta.cancel_order(order).transact_async(gas_price=self.gas_price) for order in cancellable_orders])
        self.our_orders = list(set(self.our_orders) - set(cancellable_orders))

    def cancel_all_orders(self):
        self.cancel_orders(self.our_orders, self.web3.eth.blockNumber)

    def place_orders(self, new_orders):
        # EtherDelta sometimes rejects orders when the amounts are not rounded. Choice of choosing
        # rounding to 9 decimal digits is completely arbitrary as it's not documented anywhere.
        for new_order in new_orders:
            if new_order.is_sell:
                order = self.etherdelta.create_order(pay_token=self.token_sell(),
                                                     pay_amount=round(new_order.pay_amount, 9),
                                                     buy_token=self.token_buy(),
                                                     buy_amount=round(new_order.buy_amount, 9),
                                                     expires=self.web3.eth.blockNumber + self.arguments.order_age)
            else:
                order = self.etherdelta.create_order(pay_token=self.token_buy(),
                                                     pay_amount=round(new_order.pay_amount, 9),
                                                     buy_token=self.token_sell(),
                                                     buy_amount=round(new_order.buy_amount, 9),
                                                     expires=self.web3.eth.blockNumber + self.arguments.order_age)

            self.place_order(order)

            new_order.confirm()

    def withdraw_everything(self):
        eth_balance = self.etherdelta.balance_of(self.our_address)
        if eth_balance > Wad(0):
            self.etherdelta.withdraw(eth_balance).transact(gas_price=self.gas_price)

        sai_balance = self.etherdelta.balance_of_token(self.sai.address, self.our_address)
        if sai_balance > Wad(0):
            self.etherdelta.withdraw_token(self.sai.address, sai_balance).transact()

    def depositable_balance(self, token: Address) -> Wad:
        if token == EtherDelta.ETH_TOKEN:
            return Wad.max(eth_balance(self.web3, self.our_address) - self.eth_reserve, Wad(0))
        else:
            return ERC20Token(web3=self.web3, address=token).balance_of(self.our_address)

    def deposit_for_sell_order(self):
        depositable_eth = self.depositable_balance(self.token_sell())
        if depositable_eth > self.min_eth_deposit:
            return self.etherdelta.deposit(depositable_eth).transact(gas_price=self.gas_price).successful
        else:
            return False

    def deposit_for_buy_order(self):
        depositable_sai = self.depositable_balance(self.token_buy())
        if depositable_sai > self.min_sai_deposit:
            return self.etherdelta.deposit_token(self.token_buy(), depositable_sai).transact(gas_price=self.gas_price).successful
        else:
            return False
Beispiel #32
0
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))
Beispiel #33
0
class EtherDeltaMarketMakerKeeper:
    """Keeper acting as a market maker on EtherDelta, on the ETH/SAI pair."""

    logger = logging.getLogger()

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='etherdelta-market-maker-keeper')

        parser.add_argument("--rpc-host",
                            type=str,
                            default="localhost",
                            help="JSON-RPC host (default: `localhost')")

        parser.add_argument("--rpc-port",
                            type=int,
                            default=8545,
                            help="JSON-RPC port (default: `8545')")

        parser.add_argument("--rpc-timeout",
                            type=int,
                            default=10,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help="Ethereum account from which to send transactions")

        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')"
        )

        parser.add_argument("--tub-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Tub contract")

        parser.add_argument("--etherdelta-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the EtherDelta contract")

        parser.add_argument(
            "--etherdelta-socket",
            type=str,
            required=True,
            help="Ethereum address of the EtherDelta API socket")

        parser.add_argument(
            "--etherdelta-number-of-attempts",
            type=int,
            default=3,
            help=
            "Number of attempts of running the tool to talk to the EtherDelta API socket"
        )

        parser.add_argument(
            "--etherdelta-retry-interval",
            type=int,
            default=10,
            help=
            "Retry interval for sending orders over the EtherDelta API socket")

        parser.add_argument(
            "--etherdelta-timeout",
            type=int,
            default=120,
            help="Timeout for sending orders over the EtherDelta API socket")

        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("--order-age",
                            type=int,
                            required=True,
                            help="Age of created orders (in blocks)")

        parser.add_argument(
            "--order-expiry-threshold",
            type=int,
            default=0,
            help=
            "Remaining order age (in blocks) at which order is considered already expired, which"
            " means the keeper will send a new replacement order slightly ahead"
        )

        parser.add_argument(
            "--order-no-cancel-threshold",
            type=int,
            default=0,
            help=
            "Remaining order age (in blocks) below which keeper does not try to cancel orders,"
            " assuming that they will probably expire before the cancel transaction gets mined"
        )

        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(
            '--cancel-on-shutdown',
            dest='cancel_on_shutdown',
            action='store_true',
            help=
            "Whether should cancel all open orders on EtherDelta on keeper shutdown"
        )

        parser.add_argument(
            '--withdraw-on-shutdown',
            dest='withdraw_on_shutdown',
            action='store_true',
            help=
            "Whether should withdraw all tokens from EtherDelta on keeper shutdown"
        )

        parser.add_argument("--gas-price",
                            type=int,
                            default=0,
                            help="Gas price (in Wei)")

        parser.add_argument(
            "--smart-gas-price",
            dest='smart_gas_price',
            action='store_true',
            help=
            "Use smart gas pricing strategy, based on the ethgasstation.info feed"
        )

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        parser.set_defaults(cancel_on_shutdown=False,
                            withdraw_on_shutdown=False)

        self.arguments = parser.parse_args(args)
        setup_logging(self.arguments)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        self.our_address = Address(self.arguments.eth_from)
        register_keys(self.web3, self.arguments.eth_key)

        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")

        assert (self.arguments.order_expiry_threshold >= 0)
        assert (self.arguments.order_no_cancel_threshold >=
                self.arguments.order_expiry_threshold)

        self.history = History()
        self.etherdelta = EtherDelta(web3=self.web3,
                                     address=Address(
                                         self.arguments.etherdelta_address))
        self.etherdelta_api = EtherDeltaApi(
            client_tool_directory="lib/pymaker/utils/etherdelta-client",
            client_tool_command="node main.js",
            api_server=self.arguments.etherdelta_socket,
            number_of_attempts=self.arguments.etherdelta_number_of_attempts,
            retry_interval=self.arguments.etherdelta_retry_interval,
            timeout=self.arguments.etherdelta_timeout)

        self.our_orders = list()

    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):
        if self.arguments.cancel_on_shutdown:
            self.cancel_all_orders()

        if self.arguments.withdraw_on_shutdown:
            self.withdraw_everything()

    def approve(self):
        token_addresses = filter(
            lambda address: address != EtherDelta.ETH_TOKEN,
            [self.token_sell(), self.token_buy()])
        tokens = list(
            map(lambda address: ERC20Token(web3=self.web3, address=address),
                token_addresses))

        self.etherdelta.approve(tokens, directly(gas_price=self.gas_price))

    def place_order(self, order: Order):
        self.our_orders.append(order)
        self.etherdelta_api.publish_order(order)

    def token_sell(self) -> Address:
        return EtherDelta.ETH_TOKEN

    def token_buy(self) -> Address:
        return self.sai.address

    def our_total_balance(self, token: Address) -> Wad:
        if token == EtherDelta.ETH_TOKEN:
            return self.etherdelta.balance_of(self.our_address)
        else:
            return self.etherdelta.balance_of_token(token, self.our_address)

    def our_sell_orders(self):
        return list(
            filter(
                lambda order: order.buy_token == self.token_buy() and order.
                pay_token == self.token_sell(), self.our_orders))

    def our_buy_orders(self):
        return list(
            filter(
                lambda order: order.buy_token == self.token_sell() and order.
                pay_token == self.token_buy(), self.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.
        #
        # The exception is when we can withdraw some ETH from EtherDelta. Then we do it and carry on.
        if eth_balance(self.web3, self.our_address) < self.min_eth_balance:
            if self.etherdelta.balance_of(self.our_address) > self.eth_reserve:
                self.logger.warning(
                    f"Keeper ETH balance below minimum, withdrawing {self.eth_reserve}."
                )
                self.etherdelta.withdraw(self.eth_reserve).transact()
            else:
                self.logger.warning(
                    f"Keeper ETH balance below minimum, cannot withdraw. Cancelling all orders."
                )
                self.cancel_all_orders()

            return

        bands = Bands.read(self.bands_config, self.spread_feed,
                           self.control_feed, self.history)
        block_number = self.web3.eth.blockNumber
        target_price = self.price_feed.get_price()

        # Remove expired orders from the local order list
        self.remove_expired_orders(block_number)

        # Cancel orders
        cancellable_orders = bands.cancellable_orders(self.our_buy_orders(),
                                                      self.our_sell_orders(),
                                                      target_price)
        if len(cancellable_orders) > 0:
            self.cancel_orders(cancellable_orders, block_number)
            return

        # In case of EtherDelta, balances returned by `our_total_balance` still contain amounts "locked"
        # by currently open orders, so we need to explicitly subtract these amounts.
        our_buy_balance = self.our_total_balance(
            self.token_buy()) - Bands.total_amount(self.our_buy_orders())
        our_sell_balance = self.our_total_balance(
            self.token_sell()) - Bands.total_amount(self.our_sell_orders())

        # 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_sell_orders=self.our_sell_orders(),
            our_buy_balance=our_buy_balance,
            our_sell_balance=our_sell_balance,
            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():
                made_deposit = True

        if missing_sell_amount > Wad(0):
            if self.deposit_for_sell_order():
                made_deposit = True

        # If we managed to deposit something, do not do anything so we can reevaluate new orders to be created.
        # Otherwise, create new orders.
        if not made_deposit:
            self.place_orders(new_orders)

    @staticmethod
    def is_order_age_above_threshold(order: Order, block_number: int,
                                     threshold: int):
        return block_number >= order.expires - threshold  # we do >= 0, which makes us effectively detect an order
        # as expired one block earlier than the contract, but
        # this is desirable from the keeper point of view

    def is_expired(self, order: Order, block_number: int):
        return self.is_order_age_above_threshold(
            order, block_number, self.arguments.order_expiry_threshold)

    def is_non_cancellable(self, order: Order, block_number: int):
        return self.is_order_age_above_threshold(
            order, block_number, self.arguments.order_no_cancel_threshold)

    def remove_expired_orders(self, block_number: int):
        self.our_orders = list(
            filter(lambda order: not self.is_expired(order, block_number),
                   self.our_orders))

    def cancel_orders(self, orders: Iterable, block_number: int):
        cancellable_orders = list(
            filter(
                lambda order: not self.is_non_cancellable(order, block_number),
                orders))
        synchronize([
            self.etherdelta.cancel_order(order).transact_async(
                gas_price=self.gas_price) for order in cancellable_orders
        ])
        self.our_orders = list(set(self.our_orders) - set(cancellable_orders))

    def cancel_all_orders(self):
        self.cancel_orders(self.our_orders, self.web3.eth.blockNumber)

    def place_orders(self, new_orders):
        # EtherDelta sometimes rejects orders when the amounts are not rounded. Choice of choosing
        # rounding to 9 decimal digits is completely arbitrary as it's not documented anywhere.
        for new_order in new_orders:
            if new_order.is_sell:
                order = self.etherdelta.create_order(
                    pay_token=self.token_sell(),
                    pay_amount=round(new_order.pay_amount, 9),
                    buy_token=self.token_buy(),
                    buy_amount=round(new_order.buy_amount, 9),
                    expires=self.web3.eth.blockNumber +
                    self.arguments.order_age)
            else:
                order = self.etherdelta.create_order(
                    pay_token=self.token_buy(),
                    pay_amount=round(new_order.pay_amount, 9),
                    buy_token=self.token_sell(),
                    buy_amount=round(new_order.buy_amount, 9),
                    expires=self.web3.eth.blockNumber +
                    self.arguments.order_age)

            self.place_order(order)

            new_order.confirm()

    def withdraw_everything(self):
        eth_balance = self.etherdelta.balance_of(self.our_address)
        if eth_balance > Wad(0):
            self.etherdelta.withdraw(eth_balance).transact(
                gas_price=self.gas_price)

        sai_balance = self.etherdelta.balance_of_token(self.sai.address,
                                                       self.our_address)
        if sai_balance > Wad(0):
            self.etherdelta.withdraw_token(self.sai.address,
                                           sai_balance).transact()

    def depositable_balance(self, token: Address) -> Wad:
        if token == EtherDelta.ETH_TOKEN:
            return Wad.max(
                eth_balance(self.web3, self.our_address) - self.eth_reserve,
                Wad(0))
        else:
            return ERC20Token(web3=self.web3,
                              address=token).balance_of(self.our_address)

    def deposit_for_sell_order(self):
        depositable_eth = self.depositable_balance(self.token_sell())
        if depositable_eth > self.min_eth_deposit:
            return self.etherdelta.deposit(depositable_eth).transact(
                gas_price=self.gas_price).successful
        else:
            return False

    def deposit_for_buy_order(self):
        depositable_sai = self.depositable_balance(self.token_buy())
        if depositable_sai > self.min_sai_deposit:
            return self.etherdelta.deposit_token(
                self.token_buy(),
                depositable_sai).transact(gas_price=self.gas_price).successful
        else:
            return False
Beispiel #34
0
    def __init__(self, tub: Tub):
        assert(isinstance(tub, Tub))

        self.ds_value = DSValue(web3=tub.web3, address=tub.pip())
Beispiel #35
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='etherdelta-market-maker-keeper')

        parser.add_argument("--rpc-host",
                            type=str,
                            default="localhost",
                            help="JSON-RPC host (default: `localhost')")

        parser.add_argument("--rpc-port",
                            type=int,
                            default=8545,
                            help="JSON-RPC port (default: `8545')")

        parser.add_argument("--rpc-timeout",
                            type=int,
                            default=10,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help="Ethereum account from which to send transactions")

        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')"
        )

        parser.add_argument("--tub-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Tub contract")

        parser.add_argument("--etherdelta-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the EtherDelta contract")

        parser.add_argument(
            "--etherdelta-socket",
            type=str,
            required=True,
            help="Ethereum address of the EtherDelta API socket")

        parser.add_argument(
            "--etherdelta-number-of-attempts",
            type=int,
            default=3,
            help=
            "Number of attempts of running the tool to talk to the EtherDelta API socket"
        )

        parser.add_argument(
            "--etherdelta-retry-interval",
            type=int,
            default=10,
            help=
            "Retry interval for sending orders over the EtherDelta API socket")

        parser.add_argument(
            "--etherdelta-timeout",
            type=int,
            default=120,
            help="Timeout for sending orders over the EtherDelta API socket")

        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("--order-age",
                            type=int,
                            required=True,
                            help="Age of created orders (in blocks)")

        parser.add_argument(
            "--order-expiry-threshold",
            type=int,
            default=0,
            help=
            "Remaining order age (in blocks) at which order is considered already expired, which"
            " means the keeper will send a new replacement order slightly ahead"
        )

        parser.add_argument(
            "--order-no-cancel-threshold",
            type=int,
            default=0,
            help=
            "Remaining order age (in blocks) below which keeper does not try to cancel orders,"
            " assuming that they will probably expire before the cancel transaction gets mined"
        )

        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(
            '--cancel-on-shutdown',
            dest='cancel_on_shutdown',
            action='store_true',
            help=
            "Whether should cancel all open orders on EtherDelta on keeper shutdown"
        )

        parser.add_argument(
            '--withdraw-on-shutdown',
            dest='withdraw_on_shutdown',
            action='store_true',
            help=
            "Whether should withdraw all tokens from EtherDelta on keeper shutdown"
        )

        parser.add_argument("--gas-price",
                            type=int,
                            default=0,
                            help="Gas price (in Wei)")

        parser.add_argument(
            "--smart-gas-price",
            dest='smart_gas_price',
            action='store_true',
            help=
            "Use smart gas pricing strategy, based on the ethgasstation.info feed"
        )

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        parser.set_defaults(cancel_on_shutdown=False,
                            withdraw_on_shutdown=False)

        self.arguments = parser.parse_args(args)
        setup_logging(self.arguments)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        self.our_address = Address(self.arguments.eth_from)
        register_keys(self.web3, self.arguments.eth_key)

        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")

        assert (self.arguments.order_expiry_threshold >= 0)
        assert (self.arguments.order_no_cancel_threshold >=
                self.arguments.order_expiry_threshold)

        self.history = History()
        self.etherdelta = EtherDelta(web3=self.web3,
                                     address=Address(
                                         self.arguments.etherdelta_address))
        self.etherdelta_api = EtherDeltaApi(
            client_tool_directory="lib/pymaker/utils/etherdelta-client",
            client_tool_command="node main.js",
            api_server=self.arguments.etherdelta_socket,
            number_of_attempts=self.arguments.etherdelta_number_of_attempts,
            retry_interval=self.arguments.etherdelta_retry_interval,
            timeout=self.arguments.etherdelta_timeout)

        self.our_orders = list()