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

        parser.add_argument("--gateio-api-server", type=str, default="https://data.gate.io",
                            help="Address of the Gate.io API server (default: 'https://data.gate.io')")

        parser.add_argument("--gateio-api-key", type=str, required=True,
                            help="API key for the Gate.io API")

        parser.add_argument("--gateio-secret-key", type=str, required=True,
                            help="Secret key for the Gate.io API")

        parser.add_argument("--gateio-timeout", type=float, default=9.5,
                            help="Timeout for accessing the Gate.io API (in seconds, default: 9.5)")

        parser.add_argument("--pair", type=str, required=True,
                            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--config", type=str, required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed", type=str, required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed", type=str,
                            help="Source of spread feed")

        parser.add_argument("--spread-feed-expiry", type=int, default=3600,
                            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history", type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument("--order-history-every", type=int, default=30,
                            help="Frequency of reporting active orders (in seconds, default: 30)")

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

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

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(self.arguments)

        self.history = History()
        self.gateio_api = GateIOApi(api_server=self.arguments.gateio_api_server,
                                    api_key=self.arguments.gateio_api_key,
                                    secret_key=self.arguments.gateio_secret_key,
                                    timeout=self.arguments.gateio_timeout)

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

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

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

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

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

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

        parser.add_argument("--paradex-api-server", type=str, default='https://api.paradex.io/consumer',
                            help="Address of the Paradex API (default: 'https://api.paradex.io/consumer')")

        parser.add_argument("--paradex-api-key", type=str, required=True,
                            help="API key for the Paradex API")

        parser.add_argument("--paradex-api-timeout", type=float, default=9.5,
                            help="Timeout for accessing the Paradex API (in seconds, default: 9.5)")

        parser.add_argument("--pair", type=str, required=True,
                            help="Token pair (sell/buy) on which the keeper will operate")

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

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

        parser.add_argument("--config", type=str, required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed", type=str, required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed", type=str,
                            help="Source of spread feed")

        parser.add_argument("--spread-feed-expiry", type=int, default=3600,
                            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history", type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument("--order-history-every", type=int, default=30,
                            help="Frequency of reporting active orders (in seconds, default: 30)")

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

        parser.add_argument("--min-eth-balance", type=float, default=0,
                            help="Minimum ETH balance below which keeper will cease operation")

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

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

        parser.add_argument("--refresh-frequency", type=int, default=3,
                            help="Order book refresh frequency (in seconds, default: 3)")

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

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

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

        self.pair = self.arguments.pair.upper()
        self.token_buy = ERC20Token(web3=self.web3, address=Address(self.arguments.buy_token_address))
        self.token_sell = ERC20Token(web3=self.web3, address=Address(self.arguments.sell_token_address))
        self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance)
        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_max_decimals = None
        self.amount_max_decimals = None
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(self.arguments)

        self.history = History()
        self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address))
        self.paradex_api = ParadexApi(self.zrx_exchange,
                                      self.arguments.paradex_api_server,
                                      self.arguments.paradex_api_key,
                                      self.arguments.paradex_api_timeout)
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='oasis-market-maker-keeper')

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

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

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

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

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

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

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

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

        parser.add_argument("--config", type=str, required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed", type=str, required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed", type=str,
                            help="Source of spread feed")

        parser.add_argument("--spread-feed-expiry", type=int, default=3600,
                            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history", type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument("--order-history-every", type=int, default=30,
                            help="Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument("--round-places", type=int, default=2,
                            help="Number of decimal places to round order prices to (default=2)")

        parser.add_argument("--min-eth-balance", type=float, default=0,
                            help="Minimum ETH balance below which keeper will cease operation")

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

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

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

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

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

        tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) \
            if self.arguments.tub_address is not None else None

        self.token_buy = ERC20Token(web3=self.web3, address=Address(self.arguments.buy_token_address))
        self.token_sell = ERC20Token(web3=self.web3, address=Address(self.arguments.sell_token_address))
        self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance)
        self.bands_config = ReloadableConfig(self.arguments.config)
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments, tub)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(self.arguments)

        self.history = History()
        self.order_book_manager = OrderBookManager(refresh_frequency=3)
        self.order_book_manager.get_orders_with(lambda: self.our_orders())
        self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders)
        self.order_book_manager.start()
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
Exemple #5
0
class ZrxMarketMakerKeeper:
    """Keeper acting as a market maker on any 0x exchange implementing the Standard 0x Relayer API V0."""

    logger = logging.getLogger()

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

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

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

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

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

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

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

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

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

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

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

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

        parser.add_argument(
            "--order-expiry-threshold",
            type=int,
            default=0,
            help=
            "How long before order expiration it is considered already expired (in seconds)"
        )

        parser.add_argument(
            "--min-eth-balance",
            type=float,
            default=0,
            help="Minimum ETH balance below which keeper will cease operation")

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

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

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

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

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

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

        self.token_buy = ERC20Token(web3=self.web3,
                                    address=Address(
                                        self.arguments.buy_token_address))
        self.token_sell = ERC20Token(web3=self.web3,
                                     address=Address(
                                         self.arguments.sell_token_address))
        self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance)
        self.bands_config = ReloadableConfig(self.arguments.config)
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()
        self.zrx_exchange = ZrxExchange(web3=self.web3,
                                        address=Address(
                                            self.arguments.exchange_address))
        self.zrx_relayer_api = ZrxRelayerApi(
            exchange=self.zrx_exchange,
            api_server=self.arguments.relayer_api_server)

        self.placed_orders = []
        self.placed_orders_lock = Lock()

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(lambda: self.get_orders())
        self.order_book_manager.get_balances_with(lambda: self.get_balances())
        self.order_book_manager.place_orders_with(self.place_order_function)
        self.order_book_manager.cancel_orders_with(self.cancel_order_function)
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()

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

    def startup(self):
        self.approve()

    def shutdown(self):
        self.order_book_manager.cancel_all_orders(final_wait_time=60)

    def approve(self):
        self.zrx_exchange.approve([self.token_sell, self.token_buy],
                                  directly(gas_price=self.gas_price))

    def remove_expired_orders(self, orders: list) -> list:
        current_timestamp = int(time.time())
        return list(
            filter(
                lambda order: order.expiration > current_timestamp - self.
                arguments.order_expiry_threshold, orders))

    def remove_filled_or_cancelled_orders(self, orders: list) -> list:
        return list(
            filter(
                lambda order: self.zrx_exchange.get_unavailable_buy_amount(
                    order) < order.buy_amount, orders))

    def get_orders(self) -> list:
        def remove_old_orders(orders: list) -> list:
            return self.remove_filled_or_cancelled_orders(
                self.remove_expired_orders(orders))

        with self.placed_orders_lock:
            self.placed_orders = remove_old_orders(self.placed_orders)

        api_orders = remove_old_orders(
            self.zrx_relayer_api.get_orders_by_maker(
                self.our_address, self.arguments.relayer_per_page))

        with self.placed_orders_lock:
            return list(set(self.placed_orders + api_orders))

    def get_balances(self):
        return self.token_sell.balance_of(self.our_address), \
               self.token_buy.balance_of(self.our_address), \
               eth_balance(self.web3, self.our_address)

    def our_total_sell_balance(self, balances) -> Wad:
        return balances[0]

    def our_total_buy_balance(self, balances) -> Wad:
        return balances[1]

    def our_eth_balance(self, balances) -> Wad:
        return balances[2]

    def our_sell_orders(self, our_orders: list) -> list:
        return list(
            filter(
                lambda order: order.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):
        bands = Bands.read(self.bands_config, self.spread_feed, self.history)
        order_book = self.order_book_manager.get_order_book()
        target_price = self.price_feed.get_price()

        # We filter out expired orders from the order book snapshot. The reason for that is that
        # it allows us to replace expired orders faster. Without it, we would have to wait
        # for the next order book refresh in order to realize an order has expired. Unfortunately,
        # in case of 0x order book refresh can be quite slow as it involves making multiple calls
        # to the Ethereum node.
        #
        # By filtering out expired orders here, we can replace them the next `synchronize_orders`
        # tick after they expire. Which is ~ 1s delay, instead of avg ~ 5s without this trick.
        orders = self.remove_expired_orders(order_book.orders)

        if self.our_eth_balance(order_book.balances) < self.min_eth_balance:
            self.logger.warning(
                "Keeper ETH balance below minimum. Cancelling all orders.")
            self.order_book_manager.cancel_all_orders()
            return

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

        # Do not place new orders if order book state is not confirmed
        if order_book.orders_being_placed or order_book.orders_being_cancelled:
            self.logger.debug(
                "Order book is in progress, not placing new orders")
            return

        # Balances returned by `our_total_***_balance` still contain amounts "locked"
        # by currently open orders, so we need to explicitly subtract these amounts.
        our_buy_balance = self.our_total_buy_balance(
            order_book.balances) - Bands.total_amount(
                self.our_buy_orders(orders))
        our_sell_balance = self.our_total_sell_balance(
            order_book.balances) - Bands.total_amount(
                self.our_sell_orders(orders))

        # Place new orders
        self.order_book_manager.place_orders(
            bands.new_orders(our_buy_orders=self.our_buy_orders(orders),
                             our_sell_orders=self.our_sell_orders(orders),
                             our_buy_balance=our_buy_balance,
                             our_sell_balance=our_sell_balance,
                             target_price=target_price)[0])

    def place_order_function(self, new_order: NewOrder):
        assert (isinstance(new_order, NewOrder))

        pay_token = self.token_sell if new_order.is_sell else self.token_buy
        buy_token = self.token_buy if new_order.is_sell else self.token_sell

        zrx_order = self.zrx_exchange.create_order(
            pay_token=pay_token.address,
            pay_amount=new_order.pay_amount,
            buy_token=buy_token.address,
            buy_amount=new_order.buy_amount,
            expiration=int(time.time()) + self.arguments.order_expiry)

        zrx_order = self.zrx_relayer_api.calculate_fees(zrx_order)
        zrx_order = self.zrx_exchange.sign_order(zrx_order)

        if self.zrx_relayer_api.submit_order(zrx_order):
            with self.placed_orders_lock:
                self.placed_orders.append(zrx_order)

            return zrx_order

        else:
            return None

    def cancel_order_function(self, order):
        transact = self.zrx_exchange.cancel_order(order).transact(
            gas_price=self.gas_price)
        return transact is not None and transact.successful
Exemple #6
0
class TethfinexMarketMakerKeeper:
    """Keeper acting as a market maker on Trustless Ethfinex."""

    logger = logging.getLogger()

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

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

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

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

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

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

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

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

        parser.add_argument(
            "--tethfinex-api-server",
            type=str,
            default='https://api.ethfinex.com',
            help=
            "Address of the Trustless Ethfinex API server (default: 'https://api.ethfinex.com')"
        )

        parser.add_argument(
            "--tethfinex-timeout",
            type=float,
            default=9.5,
            help="Timeout for accessing the IDEX API (in seconds, default: 9.5)"
        )

        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed",
                            type=str,
                            help="Source of control feed")

        parser.add_argument(
            "--control-feed-expiry",
            type=int,
            default=86400,
            help="Maximum age of the control feed (in seconds, default: 86400)"
        )

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

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

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

        parser.add_argument("--ethgasstation-api-key",
                            type=str,
                            default=None,
                            help="ethgasstation API key")

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

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

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

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

        tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) \
            if self.arguments.tub_address is not None else None
        self.sai = ERC20Token(web3=self.web3, address=tub.sai())
        self.price_feed = PriceFeedFactory().create_price_feed(
            self.arguments, tub)

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()
        self.tethfinex_exchange = ZrxExchange(
            web3=self.web3, address=Address(self.arguments.exchange_address))
        self.tethfinex_api = TEthfinexApi(
            self.tethfinex_exchange,
            self.arguments.tethfinex_api_server,
            timeout=self.arguments.tethfinex_timeout)

        config = self.tethfinex_api.get_config()['0x']
        self.fee_address = Address(config['ethfinexAddress'])

        token_registry = config['tokenRegistry']
        token_sell = self.token_sell()
        token_buy = self.token_buy()
        self.token_sell_wrapper = TEthfinexToken(
            self.web3, Address(token_registry[token_sell]['wrapperAddress']),
            token_sell)
        self.token_buy_wrapper = TEthfinexToken(
            self.web3, Address(token_registry[token_buy]['wrapperAddress']),
            token_buy)

        pair = self.pair()

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency, max_workers=1)
        self.order_book_manager.get_orders_with(
            lambda: self.tethfinex_api.get_orders(pair))
        self.order_book_manager.cancel_orders_with(
            lambda order: self.tethfinex_api.cancel_order(order.order_id))
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()

    def main(self):
        with Lifecycle(self.web3) as lifecycle:
            lifecycle.initial_delay(10)
            lifecycle.on_block(self.synchronize_orders)
            lifecycle.on_shutdown(self.shutdown)

    def pair(self):
        # Trustless Ethfinex is inconsistent here. They call the pair `DAIETH`, but in reality all prices are
        # calculated like it was an `ETH/DAI` pair.
        return 'DAIETH'

    def token_sell(self) -> str:
        return self.arguments.pair[:3]

    def token_buy(self) -> str:
        return self.arguments.pair[3:]

    @retry(delay=5, logger=logger)
    def shutdown(self):
        self.order_book_manager.cancel_all_orders()

    def our_available_balance(self, token: TEthfinexToken) -> Wad:
        return Wad.from_number(token.balance_of(self.our_address))

    def our_sell_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: order.is_sell, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: not order.is_sell, our_orders))

    def synchronize_orders(self):
        bands = Bands.read(self.bands_config, self.spread_feed,
                           self.control_feed, self.history)
        order_book = self.order_book_manager.get_order_book()
        target_price = self.price_feed.get_price()

        # Cancel orders
        cancellable_orders = bands.cancellable_orders(
            our_buy_orders=self.our_buy_orders(order_book.orders),
            our_sell_orders=self.our_sell_orders(order_book.orders),
            target_price=target_price)
        if len(cancellable_orders) > 0:
            self.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

        # 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(order_book.orders),
            our_sell_orders=self.our_sell_orders(order_book.orders),
            our_buy_balance=self.our_available_balance(self.token_buy_wrapper),
            our_sell_balance=self.our_available_balance(
                self.token_sell_wrapper),
            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.tethfinex_api.cancel_order(order.order_id)

    def place_orders(self, new_orders):
        for new_order in new_orders:
            if new_order.is_sell:
                self.logger.info(
                    f"Sell amount {float(new_order.pay_amount)} of ETH with {float(new_order.buy_amount)} DAI"
                )
                self.tethfinex_api.place_order(
                    True,
                    pay_token=self.token_sell_wrapper.address,
                    pay_amount=new_order.pay_amount,
                    buy_token=self.token_buy_wrapper.address,
                    buy_amount=new_order.buy_amount,
                    fee_address=self.fee_address,
                    pair=self.pair())
            else:
                self.logger.info(
                    f"Buy amount {float(new_order.buy_amount)} of ETH with {float(new_order.pay_amount)} DAI"
                )
                self.tethfinex_api.place_order(
                    False,
                    pay_token=self.token_buy_wrapper.address,
                    pay_amount=new_order.pay_amount,
                    buy_token=self.token_sell_wrapper.address,
                    buy_amount=new_order.buy_amount,
                    fee_address=self.fee_address,
                    pair=self.pair())

    def deposit_for_sell_order(self, missing_sell_amount: Wad):

        # We can never lock more than our available ETH balance.
        depositable_eth = eth_balance(self.web3, self.our_address)
        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):
            receipt = self.token_sell_wrapper.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 can never lock 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):
            receipt = self.token_buy_wrapper.deposit(
                missing_buy_amount).transact(gas_price=self.gas_price)
            return receipt is not None and receipt.successful
        else:
            return False
class ZrxMarketMakerKeeper:
    """Keeper acting as a market maker on any 0x exchange implementing the Standard 0x Relayer API V0."""

    logger = logging.getLogger()

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

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

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

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

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

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

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

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

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

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

        parser.add_argument("--config", type=str, required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed", type=str, required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed", type=str,
                            help="Source of spread feed")

        parser.add_argument("--spread-feed-expiry", type=int, default=3600,
                            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history", type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument("--order-history-every", type=int, default=30,
                            help="Frequency of reporting active orders (in seconds, default: 30)")

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

        parser.add_argument("--order-expiry-threshold", type=int, default=0,
                            help="How long before order expiration it is considered already expired (in seconds)")

        parser.add_argument("--min-eth-balance", type=float, default=0,
                            help="Minimum ETH balance below which keeper will cease operation")

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

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

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

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

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

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

        self.token_buy = ERC20Token(web3=self.web3, address=Address(self.arguments.buy_token_address))
        self.token_sell = ERC20Token(web3=self.web3, address=Address(self.arguments.sell_token_address))
        self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance)
        self.bands_config = ReloadableConfig(self.arguments.config)
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(self.arguments)

        self.history = History()
        self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address))
        self.zrx_relayer_api = ZrxRelayerApi(exchange=self.zrx_exchange, api_server=self.arguments.relayer_api_server)
        self.placed_orders = []

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

    def startup(self):
        self.approve()

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

    def approve(self):
        self.zrx_exchange.approve([self.token_sell, self.token_buy], directly(gas_price=self.gas_price))

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

    def our_orders(self) -> list:
        api_orders = self.zrx_relayer_api.get_orders_by_maker(self.our_address, self.arguments.relayer_per_page)
        all_orders = list(set(self.placed_orders + api_orders))
        return self.remove_old_orders(all_orders)

    def remove_old_orders(self, orders: list) -> list:
        current_timestamp = int(time.time())
        orders = list(filter(lambda order: order.expiration > current_timestamp - self.arguments.order_expiry_threshold, orders))
        orders = list(filter(lambda order: self.zrx_exchange.get_unavailable_buy_amount(order) < order.buy_amount, orders))
        return orders

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

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

    def synchronize_orders(self):
        if eth_balance(self.web3, self.our_address) < self.min_eth_balance:
            self.logger.warning("Keeper ETH balance below minimum. Cancelling all orders.")
            self.cancel_orders(self.our_orders())
            return

        bands = Bands(self.bands_config, self.spread_feed, self.history)
        our_orders = self.our_orders()
        target_price = self.price_feed.get_price()

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

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

        # Place new orders
        self.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(our_orders),
                                           our_sell_orders=self.our_sell_orders(our_orders),
                                           our_buy_balance=our_buy_balance,
                                           our_sell_balance=our_sell_balance,
                                           target_price=target_price)[0])

    def cancel_orders(self, orders):
        synchronize([self.zrx_exchange.cancel_order(order).transact_async(gas_price=self.gas_price) for order in orders])

    def place_orders(self, new_orders):
        for new_order in new_orders:
            pay_token = self.token_sell if new_order.is_sell else self.token_buy
            buy_token = self.token_buy if new_order.is_sell else self.token_sell

            zrx_order = self.zrx_exchange.create_order(pay_token=pay_token.address, pay_amount=new_order.pay_amount,
                                                       buy_token=buy_token.address, buy_amount=new_order.buy_amount,
                                                       expiration=int(time.time()) + self.arguments.order_expiry)

            zrx_order = self.zrx_relayer_api.calculate_fees(zrx_order)
            zrx_order = self.zrx_exchange.sign_order(zrx_order)

            if self.zrx_relayer_api.submit_order(zrx_order):
                self.placed_orders = self.remove_old_orders(self.placed_orders)
                self.placed_orders.append(zrx_order)
class GateIOMarketMakerKeeper:
    """Keeper acting as a market maker on Gate.io."""

    logger = logging.getLogger()

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

        parser.add_argument("--gateio-api-server", type=str, default="https://data.gate.io",
                            help="Address of the Gate.io API server (default: 'https://data.gate.io')")

        parser.add_argument("--gateio-api-key", type=str, required=True,
                            help="API key for the Gate.io API")

        parser.add_argument("--gateio-secret-key", type=str, required=True,
                            help="Secret key for the Gate.io API")

        parser.add_argument("--gateio-timeout", type=float, default=9.5,
                            help="Timeout for accessing the Gate.io API (in seconds, default: 9.5)")

        parser.add_argument("--pair", type=str, required=True,
                            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--config", type=str, required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed", type=str, required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed", type=str,
                            help="Source of spread feed")

        parser.add_argument("--spread-feed-expiry", type=int, default=3600,
                            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history", type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument("--order-history-every", type=int, default=30,
                            help="Frequency of reporting active orders (in seconds, default: 30)")

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

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

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(self.arguments)

        self.history = History()
        self.gateio_api = GateIOApi(api_server=self.arguments.gateio_api_server,
                                    api_key=self.arguments.gateio_api_key,
                                    secret_key=self.arguments.gateio_secret_key,
                                    timeout=self.arguments.gateio_timeout)

        self._last_order_creation = 0

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

    def startup(self):
        self.our_orders()
        self.our_balances()
        self.logger.info(f"Gate.io API key seems to be valid")
        self.logger.info(f"Keeper configured to work on the '{self.pair()}' pair")

    @retry(delay=5, logger=logger)
    def shutdown(self):
        self.gateio_api.cancel_all_orders(self.pair())

    def pair(self):
        return self.arguments.pair.lower()

    def token_sell(self) -> str:
        return self.arguments.pair.split('_')[0].upper()

    def token_buy(self) -> str:
        return self.arguments.pair.split('_')[1].upper()

    def our_balances(self) -> dict:
        return self.gateio_api.get_balances()

    def our_available_balance(self, our_balances: dict, token: str) -> Wad:
        try:
            return Wad.from_number(our_balances['available'][token])
        except KeyError:
            return Wad(0)

    def our_orders(self) -> list:
        return self.gateio_api.get_orders(self.pair())

    def our_sell_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: order.is_sell, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: not order.is_sell, our_orders))

    def synchronize_orders(self):
        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

        # Place new orders
        new_orders = bands.new_orders(our_buy_orders=self.our_buy_orders(our_orders),
                                      our_sell_orders=self.our_sell_orders(our_orders),
                                      our_buy_balance=self.our_available_balance(our_balances, self.token_buy()),
                                      our_sell_balance=self.our_available_balance(our_balances, self.token_sell()),
                                      target_price=target_price)[0]

        if len(new_orders) > 0:
            if self.can_create_orders():
                self.place_orders(new_orders)
                self.register_order_creation()
            else:
                self.logger.info("Too little time elapsed from last order creation, waiting...")

    # Unfortunately the gate.io API does not immediately reflect the fact that our orders have
    # been placed. In order to avoid placing orders twice we explicitly wait some time here.
    def can_create_orders(self) -> bool:
        return time.time() - self._last_order_creation > 15

    def register_order_creation(self):
        self._last_order_creation = time.time()

    def cancel_orders(self, orders: List[Order]):
        for order in orders:
            self.gateio_api.cancel_order(self.pair(), order.order_id)

    def place_orders(self, new_orders: List[NewOrder]):
        for new_order in new_orders:
            amount = new_order.pay_amount if new_order.is_sell else new_order.buy_amount
            self.gateio_api.place_order(self.pair(), new_order.is_sell, new_order.price, amount)
Exemple #9
0
class AirswapMarketMakerKeeper:
    """Keeper acting as a market maker on Airswap."""

    logger = logging.getLogger()

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='airswap-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("--orderserver-port",
                            type=str,
                            default='5004',
                            help="Port of the order server (default: '5004')")

        parser.add_argument(
            "--orderserver-host",
            type=str,
            default='127.0.0.1',
            help="host of the order server (default: '127.0.0.1')")

        parser.add_argument(
            "--airswap-api-server",
            type=str,
            default='http://localhost:5005',
            help="Address of the Airswap API (default: 'http://localhost:5005')"
        )

        parser.add_argument(
            "--airswap-api-timeout",
            type=float,
            default=9.5,
            help=
            "Timeout for accessing the Airswap API (in seconds, default: 9.5)")

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

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

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

        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

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

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

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

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed",
                            type=str,
                            help="Source of control feed")

        parser.add_argument(
            "--control-feed-expiry",
            type=int,
            default=86400,
            help="Maximum age of the control feed (in seconds, default: 86400)"
        )

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

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

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

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

        self.airswap_api = AirswapApi(self.arguments.airswap_api_server,
                                      self.arguments.airswap_api_timeout)

        if self.arguments.buy_token_address == '0x0000000000000000000000000000000000000000':
            self.token_buy = EthToken(web3=self.web3,
                                      address=Address(
                                          self.arguments.buy_token_address))
        else:
            self.token_buy = ERC20Token(web3=self.web3,
                                        address=Address(
                                            self.arguments.buy_token_address))

        self.eth_token_sell = EthToken(
            web3=self.web3,
            address=Address(self.arguments.eth_sell_token_address))
        self.weth_token_sell = ERC20Token(
            web3=self.web3,
            address=Address(self.arguments.weth_sell_token_address))

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()

    def main(self):
        self.startup()
        app.run(host=self.arguments.orderserver_host,
                port=self.arguments.orderserver_port)

    def startup(self):
        #approvals are a bit tricky as the call below is made to the airswap API but the actual approval takes place on the blockchain. They only need to be run once so double check on etherscan that this has been executed for all token pairs.

        bands = AirswapBands.read(self.bands_config, self.spread_feed,
                                  self.control_feed, self.history)

        self.airswap_api.approve(self.token_buy.address,
                                 self.eth_token_sell.address)
        self.airswap_api.approve(self.token_buy.address,
                                 self.weth_token_sell.address)

        self.airswap_api.set_intents(self.token_buy.address,
                                     self.eth_token_sell.address,
                                     self.weth_token_sell.address)

        self.logger.info(
            f"intents to buy/sell set successfully: {self.token_buy.address.address}, {self.eth_token_sell.address.address}, {self.weth_token_sell.address.address}"
        )

# def shutdown(self):
# not implemented, will be added when cancel order is finished

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

    def _error_handler(self, err):
        return err.dont_respond()

    def r_get_order(self):
        req = request.get_json()
        logging.info(f"receiving getOrder: {req}")
        order = self._order_handler(req)

        # build & sign order with our private key
        signed_order = self.airswap_api.sign_order(order['maker_address'],
                                                   order['maker_token'],
                                                   order['maker_amount'],
                                                   order['taker_address'],
                                                   order['taker_token'],
                                                   order['taker_amount'])

        # send signed order back to the taker
        logging.info(f"Sending signed order: {signed_order}")
        return signed_order, 200

    def r_get_quote(self):
        req = request.get_json()
        logging.info(f"receiving quoteOrder: {req}")
        order = self._order_handler(req)

        # send quote order back to the taker
        logging.info(f"Sending quote order: {order}")
        return json.dumps(order)

    def _order_handler(self, req):
        bands = AirswapBands.read(self.bands_config, self.spread_feed,
                                  self.control_feed, self.history)

        assert ('makerAddress' in req)
        assert ('takerAddress' in req)
        assert ('makerToken' in req)
        assert ('takerToken' in req)

        maker_address = Address(req["makerAddress"])
        taker_address = Address(req["takerAddress"])
        maker_token = Address(req["makerToken"])
        taker_token = Address(req["takerToken"])

        # Only makerAmount or takerAmount should be sent in the request
        # Takers will usually request a makerAmount, however they can request takerAmount
        if req.get('makerAmount') != None:
            maker_amount = Wad(int(req["makerAmount"]))
            taker_amount = Wad(0)

        elif req.get('takerAmount') != None:
            taker_amount = Wad(int(req["takerAmount"]))
            maker_amount = Wad(0)

        else:
            raise CustomException(
                'Neither takerAmount or makerAmount was specified in the request',
                status_code=400)

        # V2 should adjust for signed orders we already have out there (essentially create an orderbook)?
        # still debating...

        if (maker_token != self.token_buy.address) and (
                maker_token != self.eth_token_sell.address) and (
                    maker_token != self.weth_token_sell.address):
            raise CustomException("Not set to trade this token pair",
                                  self.logger)

        if maker_token == self.token_buy.address:
            amount_side = 'buy'
            our_buy_balance = self.our_total_balance(self.token_buy)
            our_sell_balance = self.our_total_balance(self.eth_token_sell) \
                               if taker_token == self.eth_token_sell.address \
                               else self.our_total_balance(self.weth_token_sell)

        else:
            amount_side = 'sell'
            our_buy_balance = self.our_total_balance(self.token_buy)
            our_sell_balance = self.our_total_balance(self.eth_token_sell) \
                               if maker_token == self.eth_token_sell.address \
                               else self.our_total_balance(self.weth_token_sell)

        target_price = self.price_feed.get_price()

        token_amnts = bands.new_orders(amount_side, maker_amount, taker_amount,
                                       our_buy_balance, our_sell_balance,
                                       target_price)
        if not token_amnts:
            raise CustomException("bands.new_orders did not return orders",
                                  self.logger)

        # return successfully built order
        return {
            "maker_address": str(maker_address.address).lower(),
            "maker_token": str(maker_token.address).lower(),
            "maker_amount": str(token_amnts["maker_amount"].value),
            "taker_address": str(taker_address.address).lower(),
            "taker_token": str(taker_token.address).lower(),
            "taker_amount": str(token_amnts["taker_amount"].value)
        }
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='ddex-market-maker-keeper')

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

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

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

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

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

        parser.add_argument(
            "--ddex-api-server",
            type=str,
            default='https://api.ddex.io',
            help="Address of the Ddex API (default: 'https://api.ddex.io')")

        parser.add_argument(
            "--ddex-api-timeout",
            type=float,
            default=9.5,
            help="Timeout for accessing the Ddex API (in seconds, default: 9.5)"
        )

        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

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

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

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

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

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

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

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

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

        self.pair = self.arguments.pair.upper()
        self.token_buy = ERC20Token(web3=self.web3,
                                    address=Address(
                                        self.arguments.buy_token_address))
        self.token_sell = ERC20Token(web3=self.web3,
                                     address=Address(
                                         self.arguments.sell_token_address))
        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_max_decimals = None
        self.amount_max_decimals = None
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()
        self.zrx_exchange = ZrxExchange(web3=self.web3,
                                        address=Address(
                                            self.arguments.exchange_address))
        self.ddex_api = DdexApi(self.web3, self.arguments.ddex_api_server,
                                self.arguments.ddex_api_timeout)

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency, max_workers=1)
        self.order_book_manager.get_orders_with(
            lambda: self.ddex_api.get_orders(self.pair))
        self.order_book_manager.cancel_orders_with(
            lambda order: self.ddex_api.cancel_order(order.order_id))
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()
class DdexMarketMakerKeeper:
    """Keeper acting as a market maker on Ddex."""

    logger = logging.getLogger()

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

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

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

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

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

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

        parser.add_argument(
            "--ddex-api-server",
            type=str,
            default='https://api.ddex.io',
            help="Address of the Ddex API (default: 'https://api.ddex.io')")

        parser.add_argument(
            "--ddex-api-timeout",
            type=float,
            default=9.5,
            help="Timeout for accessing the Ddex API (in seconds, default: 9.5)"
        )

        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

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

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

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

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

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

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

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

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

        self.pair = self.arguments.pair.upper()
        self.token_buy = ERC20Token(web3=self.web3,
                                    address=Address(
                                        self.arguments.buy_token_address))
        self.token_sell = ERC20Token(web3=self.web3,
                                     address=Address(
                                         self.arguments.sell_token_address))
        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_max_decimals = None
        self.amount_max_decimals = None
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()
        self.zrx_exchange = ZrxExchange(web3=self.web3,
                                        address=Address(
                                            self.arguments.exchange_address))
        self.ddex_api = DdexApi(self.web3, self.arguments.ddex_api_server,
                                self.arguments.ddex_api_timeout)

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency, max_workers=1)
        self.order_book_manager.get_orders_with(
            lambda: self.ddex_api.get_orders(self.pair))
        self.order_book_manager.cancel_orders_with(
            lambda order: self.ddex_api.cancel_order(order.order_id))
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()

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

    def startup(self):
        self.approve()

        # Get maximum number of decimals for prices and amounts.
        # Ddex API enforces it.
        markets = self.ddex_api.get_markets()['data']['markets']
        market = next(filter(lambda item: item['id'] == self.pair, markets))

        self.price_max_decimals = market['pricePrecision']
        self.amount_max_decimals = market['amountDecimals']

        # For some reason, DDEX returns `pricePrecision == 5` for the `DAI-ETH` pair, although they allow
        # prices with precision of `7` both in the UI and in the API. So I did temporarily set it to `7`
        # so we can start testing the keeper, as precision of `5` gives us a 'step' of ~ 3.40 USD,
        # which is definitely too much!
        self.price_max_decimals = 7

    def shutdown(self):
        self.order_book_manager.cancel_all_orders()

    def approve(self):
        self.zrx_exchange.approve([self.token_sell, self.token_buy],
                                  directly(gas_price=self.gas_price))

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

    def our_sell_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: order.is_sell, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: not order.is_sell, our_orders))

    def synchronize_orders(self):
        bands = Bands.read(self.bands_config, self.spread_feed, self.history)
        order_book = self.order_book_manager.get_order_book()
        target_price = self.price_feed.get_price()

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

        # Do not place new orders if order book state is not confirmed
        if order_book.orders_being_placed or order_book.orders_being_cancelled:
            self.logger.debug(
                "Order book is in progress, not placing new orders")
            return

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

        # Place new orders
        self.place_orders(
            bands.new_orders(
                our_buy_orders=self.our_buy_orders(order_book.orders),
                our_sell_orders=self.our_sell_orders(order_book.orders),
                our_buy_balance=our_buy_balance,
                our_sell_balance=our_sell_balance,
                target_price=target_price)[0])

    def place_orders(self, new_orders):
        def place_order_function(new_order_to_be_placed):
            price = round(new_order_to_be_placed.price,
                          self.price_max_decimals)
            amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount
            amount = round(amount, self.amount_max_decimals)
            order_id = self.ddex_api.place_order(
                pair=self.pair,
                is_sell=new_order_to_be_placed.is_sell,
                price=price,
                amount=amount)

            return Order(order_id, self.pair, new_order_to_be_placed.is_sell,
                         price, amount, amount)

        for new_order in new_orders:
            self.order_book_manager.place_order(
                lambda new_order=new_order: place_order_function(new_order))
Exemple #12
0
    def __init__(self, args: list):
        parser = argparse.ArgumentParser(prog='leverj-market-maker-keeper')

        parser.add_argument(
            "--leverj-api-server",
            type=str,
            default="https://test.leverj.io",
            help=
            "Address of the leverj API server (default: 'https://test.leverj.io')"
        )

        parser.add_argument("--account-id",
                            type=str,
                            default="",
                            help="Address of leverj api account id")

        parser.add_argument("--api-key",
                            type=str,
                            default="",
                            help="Address of leverj api key")

        parser.add_argument("--api-secret",
                            type=str,
                            default="",
                            help="Address of leverj api secret")

        parser.add_argument(
            "--leverj-timeout",
            type=float,
            default=9.5,
            help=
            "Timeout for accessing the Leverj API (in seconds, default: 9.5)")

        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 watch our trades")

        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("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed",
                            type=str,
                            help="Source of control feed")

        parser.add_argument(
            "--control-feed-expiry",
            type=int,
            default=86400,
            help="Maximum age of the control feed (in seconds, default: 86400)"
        )

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

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

        self.arguments = parser.parse_args(args)

        self.web3 = Web3(
            HTTPProvider(
                endpoint_uri=
                f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))

        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)

        setup_logging(self.arguments)

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)
        self.target_price_lean = Wad(0)

        self.history = History()

        self.leverj_api = LeverjFuturesAPI(
            web3=self.web3,
            api_server=self.arguments.leverj_api_server,
            account_id=self.arguments.account_id,
            api_key=self.arguments.api_key,
            api_secret=self.arguments.api_secret,
            timeout=self.arguments.leverj_timeout)

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(
            lambda: self.leverj_api.get_orders(self.pair()))
        self.order_book_manager.get_balances_with(
            lambda: self.leverj_api.get_balances())
        self.order_book_manager.cancel_orders_with(
            lambda order: self.leverj_api.cancel_order(order.order_id))
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()
Exemple #13
0
class LeverjMarketMakerKeeper:
    """Keeper acting as a market maker on leverj."""

    logger = logging.getLogger()

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

        parser.add_argument(
            "--leverj-api-server",
            type=str,
            default="https://test.leverj.io",
            help=
            "Address of the leverj API server (default: 'https://test.leverj.io')"
        )

        parser.add_argument("--account-id",
                            type=str,
                            default="",
                            help="Address of leverj api account id")

        parser.add_argument("--api-key",
                            type=str,
                            default="",
                            help="Address of leverj api key")

        parser.add_argument("--api-secret",
                            type=str,
                            default="",
                            help="Address of leverj api secret")

        parser.add_argument(
            "--leverj-timeout",
            type=float,
            default=9.5,
            help=
            "Timeout for accessing the Leverj API (in seconds, default: 9.5)")

        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 watch our trades")

        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("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed",
                            type=str,
                            help="Source of control feed")

        parser.add_argument(
            "--control-feed-expiry",
            type=int,
            default=86400,
            help="Maximum age of the control feed (in seconds, default: 86400)"
        )

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

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

        self.arguments = parser.parse_args(args)

        self.web3 = Web3(
            HTTPProvider(
                endpoint_uri=
                f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))

        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)

        setup_logging(self.arguments)

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)
        self.target_price_lean = Wad(0)

        self.history = History()

        self.leverj_api = LeverjFuturesAPI(
            web3=self.web3,
            api_server=self.arguments.leverj_api_server,
            account_id=self.arguments.account_id,
            api_key=self.arguments.api_key,
            api_secret=self.arguments.api_secret,
            timeout=self.arguments.leverj_timeout)

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(
            lambda: self.leverj_api.get_orders(self.pair()))
        self.order_book_manager.get_balances_with(
            lambda: self.leverj_api.get_balances())
        self.order_book_manager.cancel_orders_with(
            lambda order: self.leverj_api.cancel_order(order.order_id))
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()

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

    def startup(self):
        quote_increment = self.leverj_api.get_tickSize(self.pair())
        self.precision = -(int(log10(float(quote_increment))) + 1)

    def shutdown(self):
        self.order_book_manager.cancel_all_orders()

    def pair(self):
        name_to_id_map = {'BTCDAI': '1', 'ETHDAI': '2'}
        return name_to_id_map[self.arguments.pair.upper()]

    def token_sell(self) -> str:
        return self.arguments.pair.upper()[:3]

    def token_buy(self) -> str:
        return self.arguments.pair.upper()[3:]

    def allocated_balance(self, token: str) -> Wad:
        quote_asset_address = self.leverj_api.get_product(
            self.pair())["quote"]["address"]
        # for perpetual contracts, the quote balance is allocated across instruments and sides to enter into trades
        total_available = self.leverj_api.get_quote_balance(
            quote_asset_address)
        self.logger.debug(f'total_available: {total_available}')
        return self._allocate_to_pair(total_available).get(token)

    def _allocate_to_pair(self, total_available):
        # total number of instruments across which the total_available balance is distributed
        # total_available is denominated in quote units
        total_number_of_instruments = 1

        # there are 2 partitions for allocation per instrument
        # the dai amount is divided in 2, one for the buy side and another for the sell side
        number_of_partitions_for_allocation = Wad.from_number(
            total_number_of_instruments * 2)

        # buffer_adjustment_factor is a small intentional buffer to avoid allocating the maximum possible.
        # the allocated amount is a little smaller than the maximum possible allocation
        # and that is determined by the buffer_adjustment_factor
        buffer_adjustment_factor = Wad.from_number(1.2)

        base = self.arguments.pair.upper()[:3]
        quote = self.arguments.pair.upper()[3:]
        target_price = self.price_feed.get_price()
        product = self.leverj_api.get_product(self.pair())
        minimum_order_quantity = self.leverj_api.get_minimum_order_quantity(
            self.pair())
        minimum_quantity_wad = Wad.from_number(minimum_order_quantity)

        if ((base == product['baseSymbol'])
                and (quote == product['quoteSymbol'])):
            if ((target_price is None) or (target_price.buy_price is None)
                    or (target_price.sell_price is None)):
                base_allocation = Wad(0)
                quote_allocation = Wad(0)
                self.logger.debug(
                    f'target_price not available to calculate allocations')
            else:
                average_price = (target_price.buy_price +
                                 target_price.sell_price) / Wad.from_number(2)
                # at 1x average_price * minimum_quantity_wad is the minimum_required_balance
                # multiplying this minimum_required_balance by 2 to avoid sending very small orders to the exchange
                minimum_required_balance = average_price * minimum_quantity_wad * Wad.from_number(
                    2)
                # conversion_divisor is the divisor that determines how many chunks should Dai be distributed into.
                # It considers the price of the base to convert into base denomination.
                conversion_divisor = average_price * number_of_partitions_for_allocation * buffer_adjustment_factor
                open_position_for_base = self.leverj_api.get_position_in_wad(
                    base)
                total_available_wad = Wad.from_number(
                    Decimal(total_available) /
                    Decimal(Decimal(10)**Decimal(18)))
                base_allocation = total_available_wad / conversion_divisor
                quote_allocation = total_available_wad / number_of_partitions_for_allocation
                self.logger.debug(
                    f'open_position_for_base: {open_position_for_base}')
                # bids are made basis quote_allocation and asks basis base_allocation
                # if open position is net long then quote_allocation is adjusted.
                # if open position is net too long then target_price is adjusted to reduce price of the asks/offers
                if (open_position_for_base.value > 0):
                    open_position_for_base_in_quote = open_position_for_base * average_price
                    net_adjusted_quote_value = quote_allocation.value - abs(
                        open_position_for_base_in_quote.value)
                    self.logger.debug(
                        f'net_adjusted_quote_value: {net_adjusted_quote_value}'
                    )
                    quote_allocation = Wad(
                        net_adjusted_quote_value
                    ) if net_adjusted_quote_value > minimum_required_balance.value else Wad(
                        0)
                    # if open position is within 1 Wad range or more than quote allocations then target price is leaned down by 0.1 percent
                    if Wad(net_adjusted_quote_value) < Wad(1):
                        self.target_price_lean = Wad.from_number(0.999)
                    else:
                        self.target_price_lean = Wad(0)
                elif (open_position_for_base.value < 0):
                    # if open position is net short then base_allocation is adjusted
                    # if open position is net too short then target_price is adjusted to increase price of the bids
                    net_adjusted_base_value = base_allocation.value - abs(
                        open_position_for_base.value)
                    minimum_required_balance_in_base = minimum_required_balance / average_price
                    self.logger.debug(
                        f'net_adjusted_base_value: {net_adjusted_base_value}')
                    base_allocation = Wad(
                        net_adjusted_base_value
                    ) if net_adjusted_base_value > minimum_required_balance_in_base.value else Wad(
                        0)
                    # if open position is within 1 Wad range or more than base allocations then target price is leaned up by 0.1 percent
                    if Wad(net_adjusted_base_value) < Wad(1):
                        self.target_price_lean = Wad.from_number(1.001)
                    else:
                        self.target_price_lean = Wad(0)
        else:
            base_allocation = Wad(0)
            quote_allocation = Wad(0)

        allocation = {base: base_allocation, quote: quote_allocation}
        self.logger.debug(f'allocation: {allocation}')
        return allocation

    def our_sell_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: order.is_sell, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: not order.is_sell, our_orders))

    def adjust_target_price(self, target_price):
        target_price_lean = self.target_price_lean
        if ((target_price is None) or (target_price.buy_price is None)
                or (target_price.sell_price is None)):
            return target_price
        if target_price_lean.value == 0:
            return target_price
        else:
            self.logger.debug(f'target_price_lean: {target_price_lean}')
            adjusted_target_price = target_price
            adjusted_target_price.buy_price = (
                target_price.buy_price) * target_price_lean
            adjusted_target_price.sell_price = (
                target_price.sell_price) * target_price_lean
            return adjusted_target_price

    def synchronize_orders(self):
        bands = Bands.read(self.bands_config, self.spread_feed,
                           self.control_feed, self.history)

        order_book = self.order_book_manager.get_order_book()
        target_price = self.price_feed.get_price()
        target_price = self.adjust_target_price(target_price)
        self.logger.debug(
            f'target_price buy_price: {target_price.buy_price}, target_price sell_price: {target_price.sell_price}'
        )
        # Cancel orders
        cancellable_orders = bands.cancellable_orders(
            our_buy_orders=self.our_buy_orders(order_book.orders),
            our_sell_orders=self.our_sell_orders(order_book.orders),
            target_price=target_price)
        if len(cancellable_orders) > 0:
            self.order_book_manager.cancel_orders(cancellable_orders)
            return

        # Do not place new orders if order book state is not confirmed
        if order_book.orders_being_placed or order_book.orders_being_cancelled:
            self.logger.info(
                "Order book is in progress, not placing new orders")
            return

        # Place new orders
        new_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.allocated_balance(self.token_buy()),
            our_sell_balance=self.allocated_balance(self.token_sell()),
            target_price=target_price)[0]
        self.place_orders(new_orders)

    def place_orders(self, new_orders: List[NewOrder]):
        def place_order_function(new_order_to_be_placed):
            price = round(new_order_to_be_placed.price, self.precision + 2)
            amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount
            self.logger.debug(f'amount: {amount}')
            order_id = str(
                self.leverj_api.place_order(self.pair(), price, 'LMT',
                                            new_order_to_be_placed.is_sell,
                                            price, amount))
            return Order(order_id=order_id,
                         pair=self.pair(),
                         is_sell=new_order_to_be_placed.is_sell,
                         price=price,
                         amount=amount)

        for new_order in new_orders:
            self.order_book_manager.place_order(
                lambda new_order=new_order: place_order_function(new_order))
class OkexMarketTrading:
    """Keeper acting as a market maker on OKEX."""

    logger = logging.getLogger()

    def __init__(self, args: list):
        parser = argparse.ArgumentParser(prog='okex-market-trading')

        parser.add_argument(
            "--okex-api-server",
            type=str,
            default="https://www.okex.com",
            help=
            "Address of the OKEX API server (default: 'https://www.okex.com')")

        parser.add_argument("--okex-api-key",
                            type=str,
                            required=True,
                            help="API key for the OKEX API")

        parser.add_argument("--okex-secret-key",
                            type=str,
                            required=True,
                            help="Secret key for the OKEX API")

        parser.add_argument("--okex-passphrase",
                            type=str,
                            required=True,
                            help="Passphrase for the OKEX API")

        parser.add_argument(
            "--okex-timeout",
            type=float,
            default=9.5,
            help="Timeout for accessing the OKEX API (in seconds, default: 9.5)"
        )

        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed",
                            type=str,
                            help="Source of control feed")

        parser.add_argument(
            "--control-feed-expiry",
            type=int,
            default=86400,
            help="Maximum age of the control feed (in seconds, default: 86400)"
        )

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

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

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)

        # TODO://这两个参数干嘛的
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()
        self.okex_api = OKEXApi(api_server=self.arguments.okex_api_server,
                                api_key=self.arguments.okex_api_key,
                                secret_key=self.arguments.okex_secret_key,
                                passphrase=self.arguments.okex_passphrase,
                                timeout=self.arguments.okex_timeout)

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(
            lambda: self.okex_api.get_orders(self.pair()))
        self.order_book_manager.get_balances_with(
            lambda: self.okex_api.get_balances())
        self.order_book_manager.cancel_orders_with(
            lambda order: self.okex_api.cancel_order(self.pair(), order.
                                                     order_id))
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()

    def main(self):
        balances = self.okex_api.get_balances()
        self.logger.info(f"balances:{balances}")
        balances = self.okex_api.get_balances()
        self.order_book_manager.cancel_all_orders()
        self.logger.info(f"refresh balances:{balances}")
        with Lifecycle() as lifecycle:
            lifecycle.initial_delay(10)
            lifecycle.every(5, self.synchronize_orders)
            lifecycle.on_shutdown(self.shutdown)

    def shutdown(self):
        self.order_book_manager.cancel_all_orders()

    def pair(self):
        return self.arguments.pair.upper()

    def token_sell(self) -> str:
        return self.arguments.pair.split('-')[0].upper()

    def token_buy(self) -> str:
        return self.arguments.pair.split('-')[1].upper()

    def our_available_balance(self, our_balances: list, token: str) -> Wad:
        for item in our_balances:
            if token == item['currency']:
                return Wad.from_number(item['available'])
        return Wad(0)

    def our_sell_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: order.is_sell, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: not order.is_sell, our_orders))

    # 在买一和卖一间隔中下单自动成交
    def synchronize_orders(self):

        # 交易触发规则:随机触发。产生一个随机数,若命中概率则交易
        current_time = time.strftime("%H")
        freq_dict = {
            '00': np.random.randint(50, 200),
            '01': np.random.randint(50, 100),
            '02': np.random.randint(10, 100),
            '03': np.random.randint(10, 50),
            '04': np.random.randint(10, 50),
            '05': np.random.randint(10, 50),
            '06': np.random.randint(50, 100),
            '07': np.random.randint(100, 200),
            '08': np.random.randint(100, 200),
            '09': np.random.randint(100, 300),
            '10': np.random.randint(100, 300),
            '11': np.random.randint(100, 300),
            '12': np.random.randint(100, 300),
            '13': np.random.randint(100, 300),
            '14': np.random.randint(100, 300),
            '15': np.random.randint(100, 300),
            '16': np.random.randint(100, 300),
            '17': np.random.randint(100, 300),
            '18': np.random.randint(100, 300),
            '19': np.random.randint(100, 300),
            '20': np.random.randint(100, 300),
            '21': np.random.randint(100, 200),
            '22': np.random.randint(100, 200),
            '23': np.random.randint(100, 200)
        }
        freq = freq_dict[current_time]
        hit_number = np.random.random()
        hit_range = freq / (12 * 60.0)
        do_trade = True if hit_number < hit_range else False

        if not do_trade:
            total_freq = reduce(lambda x, y: x + y, list(freq_dict.values()))
            logging.debug(
                f"NOT HIT. total freq is {total_freq} per day. hit_number={hit_number}, hit_range={hit_range}"
            )
            return
        logging.info(
            f"[DO TRADING]hit_number={hit_number}, hit_range={hit_range}")

        order_book = self.order_book_manager.get_order_book()
        current_price = self.price_feed.get_price()
        if current_price.buy_price is None or current_price.sell_price is None:
            self.logger.warning(
                "Current_price:buy_price or sell_price is None")
            return
        logging.info(f"Current_price: {current_price}")

        # 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

        # 只会使用到buy_bands的一个配置,同时应用于买和卖,买卖统一数量
        bands = Bands.read(self.bands_config, self.spread_feed,
                           self.control_feed, self.history)
        band = bands.buy_bands[0]
        price_gap = current_price.sell_price - current_price.buy_price

        # 确定交易的数量和价格
        trade_price = current_price.buy_price + Wad.from_number(
            np.random.uniform(0, float(price_gap)))
        trade_amount = Wad.from_number(
            np.random.uniform(float(band.min_amount), float(band.max_amount)))

        # Place new orders
        new_orders = self.create_new_orders(
            trade_amount=trade_amount,
            trade_price=trade_price,
            our_buy_balance=self.our_available_balance(order_book.balances,
                                                       self.token_buy()),
            our_sell_balance=self.our_available_balance(
                order_book.balances, self.token_sell()),
            band=band)
        self.place_orders(new_orders)

    def place_orders(self, new_orders):
        def place_order_function(new_order_to_be_placed):
            amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount
            order_id = self.okex_api.place_order(
                pair=self.pair(),
                is_sell=new_order_to_be_placed.is_sell,
                price=new_order_to_be_placed.price,
                amount=amount)

            return Order(order_id, 0.0, self.pair(),
                         new_order_to_be_placed.is_sell,
                         new_order_to_be_placed.price, amount, Wad(0))

        for new_order in new_orders:
            self.order_book_manager.place_order(
                lambda new_order=new_order: place_order_function(new_order))

    def create_new_orders(self, trade_amount: Wad, trade_price: Wad,
                          our_buy_balance: Wad, our_sell_balance: Wad,
                          band: Band) -> list:
        assert (isinstance(our_buy_balance, Wad))
        assert (isinstance(our_sell_balance, Wad))
        assert (isinstance(trade_price, Wad))

        # 1、构建需要创建的订单
        new_buy_orders = []
        buy_amount = trade_amount
        # pay_amount要付出的token数量, 买单时如usdt
        pay_amount = Wad.min(buy_amount * trade_price, our_buy_balance)
        if (trade_price > Wad(0)) and (pay_amount > Wad(0)) and (buy_amount >
                                                                 Wad(0)):
            new_buy_orders.append(
                NewOrder(is_sell=False,
                         price=trade_price,
                         amount=buy_amount,
                         pay_amount=pay_amount,
                         buy_amount=buy_amount,
                         band=band,
                         confirm_function=lambda: self.buy_limits.use_limit(
                             time.time(), pay_amount)))
            logging.info(
                "Trading new_buy_order, price:%s, buy_amount:%s, pay_amount:%s"
                % (trade_price, buy_amount, pay_amount))

        # 2、构建等量的卖出订单
        new_sell_orders = []
        # pay_amount要付出的token数量,卖单时如tokenx
        pay_amount = Wad.min(trade_amount, our_sell_balance)
        buy_amount = pay_amount * trade_price
        if (trade_price > Wad(0)) and (pay_amount > Wad(0)) and (buy_amount >
                                                                 Wad(0)):
            self.logger.info(
                f"Trading creating new sell order amount {pay_amount} with price {trade_price}"
            )
            new_buy_orders.append(
                NewOrder(is_sell=True,
                         price=trade_price,
                         amount=pay_amount,
                         pay_amount=pay_amount,
                         buy_amount=buy_amount,
                         band=band,
                         confirm_function=lambda: self.buy_limits.use_limit(
                             time.time(), pay_amount)))
            logging.info(
                "Trading new_sell_order, price:%s, buy_amount:%s, pay_amount:%s"
                % (trade_price, buy_amount, pay_amount))

        # 先放卖单,再放买单
        return new_sell_orders + new_buy_orders
Exemple #15
0
class OkexMarketMakerKeeper:
    """Keeper acting as a market maker on OKEX."""

    logger = logging.getLogger()

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

        parser.add_argument(
            "--okex-api-server",
            type=str,
            default="https://www.okex.com",
            help=
            "Address of the OKEX API server (default: 'https://www.okex.com')")

        parser.add_argument("--okex-api-key",
                            type=str,
                            required=True,
                            help="API key for the OKEX API")

        parser.add_argument("--okex-secret-key",
                            type=str,
                            required=True,
                            help="Secret key for the OKEX API")

        parser.add_argument("--okex-password",
                            type=str,
                            required=True,
                            help="Password for the OKEX API key")

        parser.add_argument(
            "--okex-timeout",
            type=float,
            default=9.5,
            help="Timeout for accessing the OKEX API (in seconds, default: 9.5)"
        )

        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed",
                            type=str,
                            help="Source of control feed")

        parser.add_argument(
            "--control-feed-expiry",
            type=int,
            default=86400,
            help="Maximum age of the control feed (in seconds, default: 86400)"
        )

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

        parser.add_argument(
            "--telegram-log-config-file",
            type=str,
            required=False,
            help=
            "config file for send logs to telegram chat (e.g. 'telegram_conf.json')",
            default=None)

        parser.add_argument(
            "--keeper-name",
            type=str,
            required=False,
            help="market maker keeper name (e.g. 'Uniswap_V2_MDTETH')",
            default="okex")

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

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()
        self.okex_api = OKEXApi(api_server=self.arguments.okex_api_server,
                                api_key=self.arguments.okex_api_key,
                                secret_key=self.arguments.okex_secret_key,
                                password=self.arguments.okex_password,
                                timeout=self.arguments.okex_timeout)

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(
            lambda: self.okex_api.get_orders(self.pair()))
        self.order_book_manager.get_balances_with(
            lambda: self.okex_api.get_balances())
        self.order_book_manager.cancel_orders_with(
            lambda order: self.okex_api.cancel_order(self.pair(), order.
                                                     order_id))
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()

    def main(self):
        with Lifecycle() as lifecycle:
            lifecycle.initial_delay(10)
            lifecycle.every(1, self.synchronize_orders)
            lifecycle.on_shutdown(self.shutdown)

    def shutdown(self):
        self.order_book_manager.cancel_all_orders()

    def pair(self):
        return self.arguments.pair.lower()

    def token_sell(self) -> str:
        return self.arguments.pair.split('_')[0].lower()

    def token_buy(self) -> str:
        return self.arguments.pair.split('_')[1].lower()

    def our_available_balance(self, our_balances: dict, token: str) -> Wad:
        return Wad.from_number(our_balances[token.upper()]['available'])

    def our_sell_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: order.is_sell, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: not order.is_sell, our_orders))

    def synchronize_orders(self):
        bands = Bands.read(self.bands_config, self.spread_feed,
                           self.control_feed, self.history)
        order_book = self.order_book_manager.get_order_book()
        target_price = self.price_feed.get_price()

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

        # Do not place new orders if order book state is not confirmed
        if order_book.orders_being_placed or order_book.orders_being_cancelled:
            self.logger.debug(
                "Order book is in progress, not placing new orders")
            return

        # Place new orders
        self.place_orders(
            bands.new_orders(
                our_buy_orders=self.our_buy_orders(order_book.orders),
                our_sell_orders=self.our_sell_orders(order_book.orders),
                our_buy_balance=self.our_available_balance(
                    order_book.balances, self.token_buy()),
                our_sell_balance=self.our_available_balance(
                    order_book.balances, self.token_sell()),
                target_price=target_price)[0])

    def place_orders(self, new_orders):
        def place_order_function(new_order_to_be_placed):
            amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount
            order_id = self.okex_api.place_order(
                pair=self.pair(),
                is_sell=new_order_to_be_placed.is_sell,
                price=new_order_to_be_placed.price,
                amount=amount)

            return Order(str(order_id), 0, self.pair(),
                         new_order_to_be_placed.is_sell,
                         new_order_to_be_placed.price, amount, Wad(0))

        for new_order in new_orders:
            self.order_book_manager.place_order(
                lambda new_order=new_order: place_order_function(new_order))
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
class GOPAXMarketMakerKeeper:

    logger = logging.getLogger()

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

        parser.add_argument("--gopax-api-server", type=str, default="https://api.gopax.co.kr",
                            help="Address of the GOPAX API server (default: 'https://api.gopax.co.kr')")

        parser.add_argument("--gopax-api-key", type=str, required=True,
                            help="API key for the GOPAX API")

        parser.add_argument("--gopax-api-secret", type=str, required=True,
                            help="API secret for the GOPAX API")

        parser.add_argument("--gopax-timeout", type=float, default=9.5,
                            help="Timeout for accessing the GOPAX API (in seconds, default: 9.5)")

        parser.add_argument("--pair", type=str, required=True,
                            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--config", type=str, required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed", type=str, required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed", type=str,
                            help="Source of spread feed")

        parser.add_argument("--spread-feed-expiry", type=int, default=3600,
                            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history", type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument("--order-history-every", type=int, default=30,
                            help="Frequency of reporting active orders (in seconds, default: 30)")

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

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

        self.history = History()
        self.gopax_api = GOPAXApi(api_server=self.arguments.gopax_api_server,
                                  api_key=self.arguments.gopax_api_key,
                                  api_secret=self.arguments.gopax_api_secret,
                                  timeout=self.arguments.gopax_timeout)

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(self.arguments)

        self.order_book_manager = OrderBookManager(refresh_frequency=10)
        self.order_book_manager.get_orders_with(self.get_orders)
        self.order_book_manager.get_balances_with(lambda: self.gopax_api.get_balances())
        self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders)
        self.order_book_manager.start()

    def main(self):
        with Lifecycle() as lifecycle:
            lifecycle.initial_delay(10)
            lifecycle.every(1, self.synchronize_orders)
            lifecycle.on_shutdown(self.shutdown)

    def shutdown(self):
        #TODO I don't think this approach makes sure all orders will always get cancelled!!
        while True:
            try:
                our_orders = self.gopax_api.get_orders(self.pair())
            except:
                continue

            if len(our_orders) == 0:
                break

            self.cancel_orders(our_orders)
            self.order_book_manager.wait_for_order_cancellation()

    def pair(self):
        return self.arguments.pair.upper()

    def token_sell(self) -> str:
        return self.arguments.pair.split('-')[0].upper()

    def token_buy(self) -> str:
        return self.arguments.pair.split('-')[1].upper()

    def get_orders(self) -> list:
        return list(map(lambda order: self.gopax_api.get_order(order.order_id), self.gopax_api.get_orders(self.pair())))

    def our_available_balance(self, our_balances: list, token: str) -> Wad:
        return Wad.from_number(next(filter(lambda coin: coin['asset'] == token, our_balances))['avail'])

    def our_sell_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: order.is_sell, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: not order.is_sell, our_orders))

    def synchronize_orders(self):
        bands = Bands(self.bands_config, self.spread_feed, self.history)
        order_book = self.order_book_manager.get_order_book()
        target_price = self.price_feed.get_price()

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

        # Do not place new orders if order book state is not confirmed
        if order_book.orders_being_placed or order_book.orders_being_cancelled:
            self.logger.debug("Order book is in progress, not placing new orders")
            return

        # Place new orders
        self.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(order_book.orders),
                                           our_sell_orders=self.our_sell_orders(order_book.orders),
                                           our_buy_balance=self.our_available_balance(order_book.balances, self.token_buy()),
                                           our_sell_balance=self.our_available_balance(order_book.balances, self.token_sell()),
                                           target_price=target_price)[0])

    def cancel_orders(self, orders):
        for order in orders:
            self.order_book_manager.cancel_order(order.order_id, lambda order=order: self.gopax_api.cancel_order(order.order_id))

    def place_orders(self, new_orders):
        def place_order_function(new_order_to_be_placed):
            pair = self.pair()
            is_sell = new_order_to_be_placed.is_sell
            price = new_order_to_be_placed.price
            amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount

            new_order_id = self.gopax_api.place_order(pair=pair,
                                                      is_sell=is_sell,
                                                      price=price,
                                                      amount=amount)

            return Order(order_id=new_order_id,
                         pair=pair,
                         is_sell=is_sell,
                         price=price,
                         amount=amount,
                         amount_remaining=amount)

        for new_order in new_orders:
            self.order_book_manager.place_order(lambda new_order=new_order: place_order_function(new_order))
Exemple #18
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='airswap-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("--orderserver-port",
                            type=str,
                            default='5004',
                            help="Port of the order server (default: '5004')")

        parser.add_argument(
            "--orderserver-host",
            type=str,
            default='127.0.0.1',
            help="host of the order server (default: '127.0.0.1')")

        parser.add_argument(
            "--airswap-api-server",
            type=str,
            default='http://localhost:5005',
            help="Address of the Airswap API (default: 'http://localhost:5005')"
        )

        parser.add_argument(
            "--airswap-api-timeout",
            type=float,
            default=9.5,
            help=
            "Timeout for accessing the Airswap API (in seconds, default: 9.5)")

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

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

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

        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

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

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

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

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed",
                            type=str,
                            help="Source of control feed")

        parser.add_argument(
            "--control-feed-expiry",
            type=int,
            default=86400,
            help="Maximum age of the control feed (in seconds, default: 86400)"
        )

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

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

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

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

        self.airswap_api = AirswapApi(self.arguments.airswap_api_server,
                                      self.arguments.airswap_api_timeout)

        if self.arguments.buy_token_address == '0x0000000000000000000000000000000000000000':
            self.token_buy = EthToken(web3=self.web3,
                                      address=Address(
                                          self.arguments.buy_token_address))
        else:
            self.token_buy = ERC20Token(web3=self.web3,
                                        address=Address(
                                            self.arguments.buy_token_address))

        self.eth_token_sell = EthToken(
            web3=self.web3,
            address=Address(self.arguments.eth_sell_token_address))
        self.weth_token_sell = ERC20Token(
            web3=self.web3,
            address=Address(self.arguments.weth_sell_token_address))

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()
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
class GOPAXMarketMakerKeeper:

    logger = logging.getLogger()

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

        parser.add_argument(
            "--gopax-api-server",
            type=str,
            default="https://api.gopax.co.kr",
            help=
            "Address of the GOPAX API server (default: 'https://api.gopax.co.kr')"
        )

        parser.add_argument("--gopax-api-key",
                            type=str,
                            required=True,
                            help="API key for the GOPAX API")

        parser.add_argument("--gopax-api-secret",
                            type=str,
                            required=True,
                            help="API secret for the GOPAX API")

        parser.add_argument(
            "--gopax-timeout",
            type=float,
            default=9.5,
            help=
            "Timeout for accessing the GOPAX API (in seconds, default: 9.5)")

        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

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

        self.history = History()
        self.gopax_api = GOPAXApi(api_server=self.arguments.gopax_api_server,
                                  api_key=self.arguments.gopax_api_key,
                                  api_secret=self.arguments.gopax_api_secret,
                                  timeout=self.arguments.gopax_timeout)

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency, max_workers=1)
        self.order_book_manager.get_orders_with(self.get_orders)
        self.order_book_manager.get_balances_with(
            lambda: self.gopax_api.get_balances())
        self.order_book_manager.cancel_orders_with(
            lambda order: self.gopax_api.cancel_order(order.order_id))
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()

    def main(self):
        with Lifecycle() as lifecycle:
            lifecycle.initial_delay(10)
            lifecycle.every(1, self.synchronize_orders)
            lifecycle.on_shutdown(self.shutdown)

    def shutdown(self):
        self.order_book_manager.cancel_all_orders()

    def pair(self):
        return self.arguments.pair.upper()

    def token_sell(self) -> str:
        return self.arguments.pair.split('-')[0].upper()

    def token_buy(self) -> str:
        return self.arguments.pair.split('-')[1].upper()

    def get_orders(self) -> list:
        return list(
            map(lambda order: self.gopax_api.get_order(order.order_id),
                self.gopax_api.get_orders(self.pair())))

    def our_available_balance(self, our_balances: list, token: str) -> Wad:
        return Wad.from_number(
            next(filter(lambda coin: coin['asset'] == token,
                        our_balances))['avail'])

    def our_sell_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: order.is_sell, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: not order.is_sell, our_orders))

    def synchronize_orders(self):
        bands = Bands.read(self.bands_config, self.spread_feed, self.history)
        order_book = self.order_book_manager.get_order_book()
        target_price = self.price_feed.get_price()

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

        # Do not place new orders if order book state is not confirmed
        if order_book.orders_being_placed or order_book.orders_being_cancelled:
            self.logger.debug(
                "Order book is in progress, not placing new orders")
            return

        # Place new orders
        self.place_orders(
            bands.new_orders(
                our_buy_orders=self.our_buy_orders(order_book.orders),
                our_sell_orders=self.our_sell_orders(order_book.orders),
                our_buy_balance=self.our_available_balance(
                    order_book.balances, self.token_buy()),
                our_sell_balance=self.our_available_balance(
                    order_book.balances, self.token_sell()),
                target_price=target_price)[0])

    def place_orders(self, new_orders):
        def place_order_function(new_order_to_be_placed):
            pair = self.pair()
            is_sell = new_order_to_be_placed.is_sell
            price = new_order_to_be_placed.price
            amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount

            if self.token_buy() == 'KRW':
                price = round(
                    price / Wad.from_number(500)) * Wad.from_number(500)

            if self.token_buy() == 'DAI':
                price = round(price, 2)

            new_order_id = self.gopax_api.place_order(pair=pair,
                                                      is_sell=is_sell,
                                                      price=price,
                                                      amount=amount)

            return Order(order_id=new_order_id,
                         pair=pair,
                         is_sell=is_sell,
                         price=price,
                         amount=amount,
                         amount_remaining=amount)

        for new_order in new_orders:
            self.order_book_manager.place_order(
                lambda new_order=new_order: place_order_function(new_order))
    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()
Exemple #22
0
class BittrexMarketMakerKeeper:
    """Keeper acting as a market maker on bittrex."""

    logger = logging.getLogger()

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

        parser.add_argument(
            "--bittrex-api-server",
            type=str,
            default="https://bittrex.com",
            help=
            "Address of the bittrex API server (default: 'https://bittrex.com')"
        )

        parser.add_argument("--bittrex-api-key",
                            type=str,
                            required=True,
                            help="API key for the bittrex API")

        parser.add_argument("--bittrex-secret-key",
                            type=str,
                            required=True,
                            help="Secret key for the bittrex API")

        parser.add_argument(
            "--bittrex-timeout",
            type=float,
            default=9.5,
            help=
            "Timeout for accessing the bittrex API (in seconds, default: 9.5)")
        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed",
                            type=str,
                            help="Source of control feed")

        parser.add_argument(
            "--control-feed-expiry",
            type=int,
            default=86400,
            help="Maximum age of the control feed (in seconds, default: 86400)"
        )

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

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

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()

        self.bittrex_api = BittrexApi(
            api_server=self.arguments.bittrex_api_server,
            api_key=self.arguments.bittrex_api_key,
            secret_key=self.arguments.bittrex_secret_key,
            timeout=self.arguments.bittrex_timeout)

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(
            lambda: self.bittrex_api.get_orders(self.pair()))
        self.order_book_manager.get_balances_with(
            lambda: self.bittrex_api.get_balances())
        self.order_book_manager.cancel_orders_with(
            lambda order: self.bittrex_api.cancel_order(order.order_id))
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()

    def main(self):
        with Lifecycle() as lifecycle:
            lifecycle.initial_delay(10)
            lifecycle.every(1, self.synchronize_orders)
            lifecycle.on_shutdown(self.shutdown)

    def shutdown(self):
        self.order_book_manager.cancel_all_orders()

    def pair(self):
        # Bittrex is inconsistent here. They call the pair `ETH-DAI`, but in reality all prices are
        # calculated like it was an `DAI-ETH` pair. Same for `ETH-MKR`
        return self.arguments.pair.upper().split(
            '-')[1] + "-" + self.arguments.pair.upper().split('-')[0]

    def token_sell(self) -> str:
        return self.arguments.pair.split('-')[1].upper()

    def token_buy(self) -> str:
        return self.arguments.pair.split('-')[0].upper()

    def our_available_balance(self, our_balances: dict, token: str) -> Wad:
        token_balances = list(
            filter(lambda coin: coin['Currency'].upper() == token,
                   our_balances))
        if token_balances:
            return Wad.from_number(token_balances[0]['Available'])
        else:
            return Wad(0)

    def our_sell_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: order.is_sell, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: not order.is_sell, our_orders))

    def synchronize_orders(self):
        bands = Bands.read(self.bands_config, self.spread_feed,
                           self.control_feed, self.history)

        order_book = self.order_book_manager.get_order_book()
        target_price = self.price_feed.get_price()
        # Cancel orders
        cancellable_orders = bands.cancellable_orders(
            our_buy_orders=self.our_buy_orders(order_book.orders),
            our_sell_orders=self.our_sell_orders(order_book.orders),
            target_price=target_price)
        if len(cancellable_orders) > 0:
            self.order_book_manager.cancel_orders(cancellable_orders)
            return

        # Do not place new orders if order book state is not confirmed
        if order_book.orders_being_placed or order_book.orders_being_cancelled:
            self.logger.debug(
                "Order book is in progress, not placing new orders")
            return

        # Place new orders
        new_orders = bands.new_orders(
            our_buy_orders=self.our_buy_orders(order_book.orders),
            our_sell_orders=self.our_sell_orders(order_book.orders),
            our_buy_balance=self.our_available_balance(order_book.balances,
                                                       self.token_buy()),
            our_sell_balance=self.our_available_balance(
                order_book.balances, self.token_sell()),
            target_price=target_price)[0]

        self.place_orders(new_orders)

    def place_orders(self, new_orders: List[NewOrder]):
        def place_order_function(new_order_to_be_placed):
            price = new_order_to_be_placed.price
            amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount

            order_id = self.bittrex_api.place_order(
                self.pair(), new_order_to_be_placed.is_sell, price, amount)

            return Order(order_id=order_id,
                         pair=self.pair(),
                         is_sell=new_order_to_be_placed.is_sell,
                         price=price,
                         amount=amount,
                         remaining_amount=amount)

        for new_order in new_orders:
            self.order_book_manager.place_order(
                lambda new_order=new_order: place_order_function(new_order))
Exemple #23
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='tethfinex-market-maker-keeper')

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

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

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

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

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

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

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

        parser.add_argument(
            "--tethfinex-api-server",
            type=str,
            default='https://api.ethfinex.com',
            help=
            "Address of the Trustless Ethfinex API server (default: 'https://api.ethfinex.com')"
        )

        parser.add_argument(
            "--tethfinex-timeout",
            type=float,
            default=9.5,
            help="Timeout for accessing the IDEX API (in seconds, default: 9.5)"
        )

        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed",
                            type=str,
                            help="Source of control feed")

        parser.add_argument(
            "--control-feed-expiry",
            type=int,
            default=86400,
            help="Maximum age of the control feed (in seconds, default: 86400)"
        )

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

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

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

        parser.add_argument("--ethgasstation-api-key",
                            type=str,
                            default=None,
                            help="ethgasstation API key")

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

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

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

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

        tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) \
            if self.arguments.tub_address is not None else None
        self.sai = ERC20Token(web3=self.web3, address=tub.sai())
        self.price_feed = PriceFeedFactory().create_price_feed(
            self.arguments, tub)

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()
        self.tethfinex_exchange = ZrxExchange(
            web3=self.web3, address=Address(self.arguments.exchange_address))
        self.tethfinex_api = TEthfinexApi(
            self.tethfinex_exchange,
            self.arguments.tethfinex_api_server,
            timeout=self.arguments.tethfinex_timeout)

        config = self.tethfinex_api.get_config()['0x']
        self.fee_address = Address(config['ethfinexAddress'])

        token_registry = config['tokenRegistry']
        token_sell = self.token_sell()
        token_buy = self.token_buy()
        self.token_sell_wrapper = TEthfinexToken(
            self.web3, Address(token_registry[token_sell]['wrapperAddress']),
            token_sell)
        self.token_buy_wrapper = TEthfinexToken(
            self.web3, Address(token_registry[token_buy]['wrapperAddress']),
            token_buy)

        pair = self.pair()

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency, max_workers=1)
        self.order_book_manager.get_orders_with(
            lambda: self.tethfinex_api.get_orders(pair))
        self.order_book_manager.cancel_orders_with(
            lambda order: self.tethfinex_api.cancel_order(order.order_id))
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()
Exemple #24
0
    def __init__(self, args: list):
        parser = argparse.ArgumentParser(prog='bittrex-market-maker-keeper')

        parser.add_argument(
            "--bittrex-api-server",
            type=str,
            default="https://bittrex.com",
            help=
            "Address of the bittrex API server (default: 'https://bittrex.com')"
        )

        parser.add_argument("--bittrex-api-key",
                            type=str,
                            required=True,
                            help="API key for the bittrex API")

        parser.add_argument("--bittrex-secret-key",
                            type=str,
                            required=True,
                            help="Secret key for the bittrex API")

        parser.add_argument(
            "--bittrex-timeout",
            type=float,
            default=9.5,
            help=
            "Timeout for accessing the bittrex API (in seconds, default: 9.5)")
        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed",
                            type=str,
                            help="Source of control feed")

        parser.add_argument(
            "--control-feed-expiry",
            type=int,
            default=86400,
            help="Maximum age of the control feed (in seconds, default: 86400)"
        )

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

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

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()

        self.bittrex_api = BittrexApi(
            api_server=self.arguments.bittrex_api_server,
            api_key=self.arguments.bittrex_api_key,
            secret_key=self.arguments.bittrex_secret_key,
            timeout=self.arguments.bittrex_timeout)

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(
            lambda: self.bittrex_api.get_orders(self.pair()))
        self.order_book_manager.get_balances_with(
            lambda: self.bittrex_api.get_balances())
        self.order_book_manager.cancel_orders_with(
            lambda order: self.bittrex_api.cancel_order(order.order_id))
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()
Exemple #25
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='0x-market-maker-keeper')

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

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

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

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

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

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

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

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

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

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

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

        parser.add_argument(
            "--order-expiry-threshold",
            type=int,
            default=0,
            help=
            "How long before order expiration it is considered already expired (in seconds)"
        )

        parser.add_argument(
            "--min-eth-balance",
            type=float,
            default=0,
            help="Minimum ETH balance below which keeper will cease operation")

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

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

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

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

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

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

        self.token_buy = ERC20Token(web3=self.web3,
                                    address=Address(
                                        self.arguments.buy_token_address))
        self.token_sell = ERC20Token(web3=self.web3,
                                     address=Address(
                                         self.arguments.sell_token_address))
        self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance)
        self.bands_config = ReloadableConfig(self.arguments.config)
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()
        self.zrx_exchange = ZrxExchange(web3=self.web3,
                                        address=Address(
                                            self.arguments.exchange_address))
        self.zrx_relayer_api = ZrxRelayerApi(
            exchange=self.zrx_exchange,
            api_server=self.arguments.relayer_api_server)

        self.placed_orders = []
        self.placed_orders_lock = Lock()

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(lambda: self.get_orders())
        self.order_book_manager.get_balances_with(lambda: self.get_balances())
        self.order_book_manager.place_orders_with(self.place_order_function)
        self.order_book_manager.cancel_orders_with(self.cancel_order_function)
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()
Exemple #26
0
class ZrxMarketMakerKeeper:
    """Keeper acting as a market maker on any 0x exchange implementing the Standard 0x Relayer API V0."""

    logger = logging.getLogger()

    def add_arguments(self, parser):
        parser.add_argument(
            "--rpc-host",
            type=str,
            default="http://localhost:8545",
            help="JSON-RPC host (default: `http://localhost:8545`)")

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

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

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

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

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

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

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

        parser.add_argument(
            "--eth-token-address",
            type=str,
            required=False,
            default='0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
            help="Ethereum  token address")

        parser.add_argument("--buy-token-decimals",
                            type=int,
                            default=18,
                            help="Number of decimals of the buy token")

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

        parser.add_argument("--sell-token-decimals",
                            type=int,
                            default=18,
                            help="Number of decimals of the sell token")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--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-expiry",
            type=int,
            required=True,
            help="Expiration time of created orders (in seconds)")

        parser.add_argument(
            "--order-expiry-threshold",
            type=int,
            default=0,
            help=
            "How long before order expiration it is considered already expired (in seconds)"
        )

        parser.add_argument(
            "--use-full-balances",
            dest='use_full_balances',
            action='store_true',
            help=
            "Do not subtract the amounts locked by current orders from available balances"
        )

        parser.add_argument(
            "--min-eth-balance",
            type=float,
            default=0,
            help="Minimum ETH balance below which keeper will cease operation")

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

        parser.add_argument(
            "--remember-own-orders",
            dest='remember_own_orders',
            action='store_true',
            help="Whether should the keeper remember his own submitted orders")

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

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

        parser.add_argument("--ethgasstation-api-key",
                            type=str,
                            default=None,
                            help="ethgasstation API key")

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

        parser.add_argument(
            "--telegram-log-config-file",
            type=str,
            required=False,
            help=
            "config file for send logs to telegram chat (e.g. 'telegram_conf.json')",
            default=None)

        parser.add_argument(
            "--keeper-name",
            type=str,
            required=False,
            help="market maker keeper name (e.g. 'Uniswap_V2_MDTETH')",
            default="zrx")

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

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

        provider = HTTPProvider(
            endpoint_uri=self.arguments.rpc_host,
            request_kwargs={'timeout': self.arguments.rpc_timeout})
        self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            provider)

        self.web3.eth.defaultAccount = self.arguments.eth_from
        self.our_address = Address(self.arguments.eth_from)
        register_keys(self.web3, self.arguments.eth_key)

        self.eth_address = Address(self.arguments.eth_token_address)

        self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance)
        self.bands_config = ReloadableConfig(self.arguments.config)
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()

        # Delegate 0x specific init to a function to permit overload for 0xv3
        self.zrx_exchange = None
        self.zrx_relayer_api = None
        self.zrx_api = None
        self.pair = None
        self.init_zrx()

        self.placed_zrx_orders = []
        self.placed_zrx_orders_lock = Lock()

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(lambda: self.get_orders())
        self.order_book_manager.get_balances_with(lambda: self.get_balances())
        self.order_book_manager.place_orders_with(self.place_order_function)
        self.order_book_manager.cancel_orders_with(self.cancel_order_function)
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()

    def init_zrx(self):
        self.zrx_exchange = ZrxExchange(web3=self.web3,
                                        address=Address(
                                            self.arguments.exchange_address))
        self.zrx_relayer_api = ZrxRelayerApi(
            exchange=self.zrx_exchange,
            api_server=self.arguments.relayer_api_server)
        self.zrx_api = ZrxApi(zrx_exchange=self.zrx_exchange)

        self.pair = Pair(
            sell_token_address=Address(self.arguments.sell_token_address),
            sell_token_decimals=self.arguments.sell_token_decimals,
            buy_token_address=Address(self.arguments.buy_token_address),
            buy_token_decimals=self.arguments.buy_token_decimals)

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

    def startup(self):
        self.approve()

    def shutdown(self):
        self.order_book_manager.cancel_all_orders(final_wait_time=60)

    def approve(self):
        token_buy = ERC20Token(web3=self.web3,
                               address=Address(self.pair.buy_token_address))
        token_sell = ERC20Token(web3=self.web3,
                                address=Address(self.pair.sell_token_address))

        self.zrx_exchange.approve([token_sell, token_buy],
                                  directly(gas_price=self.gas_price))

    def remove_expired_orders(self, orders: list) -> list:
        current_timestamp = int(time.time())
        return list(
            filter(
                lambda order: order.zrx_order.expiration > current_timestamp +
                self.arguments.order_expiry_threshold, orders))

    def remove_expired_zrx_orders(self, zrx_orders: list) -> list:
        current_timestamp = int(time.time())
        return list(
            filter(
                lambda order: order.expiration > current_timestamp + self.
                arguments.order_expiry_threshold, zrx_orders))

    def remove_filled_or_cancelled_zrx_orders(self, zrx_orders: list) -> list:
        return list(
            filter(
                lambda order: self.zrx_exchange.get_unavailable_buy_amount(
                    order) < order.buy_amount, zrx_orders))

    def get_orders(self) -> list:
        def remove_old_zrx_orders(zrx_orders: list) -> list:
            return self.remove_filled_or_cancelled_zrx_orders(
                self.remove_expired_zrx_orders(zrx_orders))

        with self.placed_zrx_orders_lock:
            self.placed_zrx_orders = remove_old_zrx_orders(
                self.placed_zrx_orders)

        api_zrx_orders = remove_old_zrx_orders(
            self.zrx_relayer_api.get_orders_by_maker(
                self.our_address, self.arguments.relayer_per_page))

        with self.placed_zrx_orders_lock:
            zrx_orders = list(set(self.placed_zrx_orders + api_zrx_orders))

        return self.zrx_api.get_orders(self.pair, zrx_orders)

    def get_balances(self):
        balances = self.zrx_api.get_balances(self.pair)
        return balances[0], balances[1], eth_balance(self.web3,
                                                     self.our_address)

    def our_total_sell_balance(self, balances) -> Wad:
        return balances[0]

    def our_total_buy_balance(self, balances) -> Wad:
        return balances[1]

    def our_eth_balance(self, balances) -> Wad:
        return balances[2]

    def our_sell_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: order.is_sell, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: not order.is_sell, our_orders))

    def synchronize_orders(self):
        bands = Bands.read(self.bands_config, self.spread_feed,
                           self.control_feed, self.history)
        order_book = self.order_book_manager.get_order_book()
        target_price = self.price_feed.get_price()

        # We filter out expired orders from the order book snapshot. The reason for that is that
        # it allows us to replace expired orders faster. Without it, we would have to wait
        # for the next order book refresh in order to realize an order has expired. Unfortunately,
        # in case of 0x order book refresh can be quite slow as it involves making multiple calls
        # to the Ethereum node.
        #
        # By filtering out expired orders here, we can replace them the next `synchronize_orders`
        # tick after they expire. Which is ~ 1s delay, instead of avg ~ 5s without this trick.
        orders = self.remove_expired_orders(order_book.orders)

        if self.our_eth_balance(order_book.balances) < self.min_eth_balance:
            self.logger.warning(
                "Keeper ETH balance below minimum. Cancelling all orders.")
            self.order_book_manager.cancel_all_orders()
            return

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

        # Do not place new orders if order book state is not confirmed
        if order_book.orders_being_placed or order_book.orders_being_cancelled:
            self.logger.debug(
                "Order book is in progress, not placing new orders")
            return

        # Balances returned by `our_total_***_balance` still contain amounts "locked"
        # by currently open orders, so we need to explicitly subtract these amounts.
        if self.arguments.use_full_balances:
            our_buy_balance = self.our_total_buy_balance(order_book.balances)
            our_sell_balance = self.our_total_sell_balance(order_book.balances)
        else:
            our_buy_balance = self.our_total_buy_balance(
                order_book.balances) - Bands.total_amount(
                    self.our_buy_orders(orders))
            our_sell_balance = self.our_total_sell_balance(
                order_book.balances) - Bands.total_amount(
                    self.our_sell_orders(orders))

        # Place new orders
        self.order_book_manager.place_orders(
            bands.new_orders(our_buy_orders=self.our_buy_orders(orders),
                             our_sell_orders=self.our_sell_orders(orders),
                             our_buy_balance=our_buy_balance,
                             our_sell_balance=our_sell_balance,
                             target_price=target_price)[0])

    def place_order_function(self, new_order: NewOrder):
        assert isinstance(new_order, NewOrder)

        order_expiry = int(
            new_order.band.params.get('orderExpiry',
                                      self.arguments.order_expiry))

        zrx_order = self.zrx_api.place_order(pair=self.pair,
                                             is_sell=new_order.is_sell,
                                             price=new_order.price,
                                             amount=new_order.amount,
                                             expiration=int(time.time()) +
                                             order_expiry)

        zrx_order = self.zrx_relayer_api.calculate_fees(zrx_order)
        zrx_order = self.zrx_exchange.sign_order(zrx_order)

        if self.zrx_relayer_api.submit_order(zrx_order):
            if self.arguments.remember_own_orders:
                with self.placed_zrx_orders_lock:
                    self.placed_zrx_orders.append(zrx_order)

            order = self.zrx_api.get_orders(self.pair, [zrx_order])[0]

            return order

        else:
            return None

    def cancel_order_function(self, order):
        transact = self.zrx_exchange.cancel_order(
            order.zrx_order).transact(gas_price=self.gas_price)
        return transact is not None and transact.successful
class OasisMarketMakerKeeper:
    """Keeper acting as a market maker on OasisDEX."""

    logger = logging.getLogger()

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

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

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

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

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

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

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

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

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

        parser.add_argument("--config", type=str, required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed", type=str, required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed", type=str,
                            help="Source of spread feed")

        parser.add_argument("--spread-feed-expiry", type=int, default=3600,
                            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history", type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument("--order-history-every", type=int, default=30,
                            help="Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument("--round-places", type=int, default=2,
                            help="Number of decimal places to round order prices to (default=2)")

        parser.add_argument("--min-eth-balance", type=float, default=0,
                            help="Minimum ETH balance below which keeper will cease operation")

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

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

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

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

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

        tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) \
            if self.arguments.tub_address is not None else None

        self.token_buy = ERC20Token(web3=self.web3, address=Address(self.arguments.buy_token_address))
        self.token_sell = ERC20Token(web3=self.web3, address=Address(self.arguments.sell_token_address))
        self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance)
        self.bands_config = ReloadableConfig(self.arguments.config)
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments, tub)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(self.arguments)

        self.history = History()
        self.order_book_manager = OrderBookManager(refresh_frequency=3)
        self.order_book_manager.get_orders_with(lambda: self.our_orders())
        self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders)
        self.order_book_manager.start()

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

    def startup(self):
        self.approve()

    @retry(delay=5, logger=logger)
    def shutdown(self):
        self.cancel_all_orders()

    def on_block(self):
        # This method is present only so the lifecycle binds the new block listener, which makes
        # it then terminate the keeper if no new blocks have been arriving for 300 seconds.
        pass

    def approve(self):
        """Approve OasisDEX to access our balances, so we can place orders."""
        self.otc.approve([self.token_sell, self.token_buy], directly(gas_price=self.gas_price))

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

    def our_orders(self):
        return list(filter(lambda order: order.maker == self.our_address,
                           self.otc.get_orders(self.token_sell.address, self.token_buy.address) +
                           self.otc.get_orders(self.token_buy.address, self.token_sell.address)))

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

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

    def synchronize_orders(self):
        # If market is closed, cancel all orders but do not terminate the keeper.
        if self.otc.is_closed():
            self.logger.warning("Market is closed. Cancelling all orders.")
            self.cancel_all_orders()
            return

        # If keeper balance is below `--min-eth-balance`, cancel all orders but do not terminate
        # the keeper, keep processing blocks as the moment the keeper gets a top-up it should
        # resume activity straight away, without the need to restart it.
        if eth_balance(self.web3, self.our_address) < self.min_eth_balance:
            self.logger.warning("Keeper ETH balance below minimum. Cancelling all orders.")
            self.cancel_all_orders()
            return

        bands = Bands(self.bands_config, self.spread_feed, self.history)
        order_book = self.order_book_manager.get_order_book()
        target_price = self.price_feed.get_price()

        # If there are any orders to be cancelled, cancel them. It is deliberate that we wait with topping-up
        # bands until the next block. This way we would create new orders based on the most recent price and
        # order book state. We could theoretically retrieve both (`target_price` and `our_orders`) again here,
        # but it just seems cleaner to do it in one place instead of in two.
        cancellable_orders = bands.cancellable_orders(our_buy_orders=self.our_buy_orders(order_book.orders),
                                                      our_sell_orders=self.our_sell_orders(order_book.orders),
                                                      target_price=target_price)

        if len(cancellable_orders) > 0:
            self.cancel_orders(cancellable_orders)
            return

        # Do not place new orders if order book state is not confirmed
        if order_book.orders_being_placed or order_book.orders_being_cancelled:
            self.logger.debug("Order book is in progress, not placing new orders")
            return

        # Place new orders
        self.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(order_book.orders),
                                           our_sell_orders=self.our_sell_orders(order_book.orders),
                                           our_buy_balance=self.our_available_balance(self.token_buy),
                                           our_sell_balance=self.our_available_balance(self.token_sell),
                                           target_price=target_price)[0])

    def cancel_all_orders(self):
        # Wait for the order book to stabilize
        while True:
            order_book = self.order_book_manager.get_order_book()
            if not order_book.orders_being_cancelled and not order_book.orders_being_placed:
                break

        # Cancel all open orders
        self.cancel_orders(self.order_book_manager.get_order_book().orders)
        self.order_book_manager.wait_for_order_cancellation()

    def cancel_orders(self, orders):
        for order in orders:
            self.order_book_manager.cancel_order(order.order_id, lambda order=order: self.otc.kill(order.order_id).transact(gas_price=self.gas_price).successful)

    def place_orders(self, new_orders):
        def place_order_function(new_order_to_be_placed: NewOrder):
            assert(isinstance(new_order_to_be_placed, NewOrder))

            if new_order_to_be_placed.is_sell:
                pay_token = self.token_sell.address
                buy_token = self.token_buy.address
            else:
                pay_token = self.token_buy.address
                buy_token = self.token_sell.address

            transact = self.otc.make(pay_token=pay_token, pay_amount=new_order_to_be_placed.pay_amount,
                                     buy_token=buy_token, buy_amount=new_order_to_be_placed.buy_amount).transact(gas_price=self.gas_price)

            if transact is not None and transact.successful and transact.result is not None:
                return Order(market=self.otc,
                             order_id=transact.result,
                             maker=self.our_address,
                             pay_token=pay_token,
                             pay_amount=new_order_to_be_placed.pay_amount,
                             buy_token=buy_token,
                             buy_amount=new_order_to_be_placed.buy_amount,
                             timestamp=0)
            else:
                return None

        for new_order in new_orders:
            self.order_book_manager.place_order(lambda new_order=new_order: place_order_function(new_order))
class EToroMarketMakerKeeper:
    """Keeper acting as a market maker on eToro."""

    logger = logging.getLogger()

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

        parser.add_argument("--etoro-api-server",
                            type=str,
                            required=True,
                            help="Address of the eToro API server")

        parser.add_argument("--etoro-account",
                            type=str,
                            default="*****@*****.**",
                            help="Username for eToroX account")

        parser.add_argument("--etoro-api-key",
                            type=str,
                            required=True,
                            help="API key for the eToro API")

        parser.add_argument(
            "--etoro-secret-key",
            type=argparse.FileType('r'),
            required=True,
            help="RSA Private Key for signing requests to the eToroX API")

        parser.add_argument(
            "--etoro-timeout",
            type=float,
            default=9.5,
            help=
            "Timeout for accessing the eToro API (in seconds, default: 9.5)")

        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed",
                            type=str,
                            help="Source of control feed")

        parser.add_argument(
            "--control-feed-expiry",
            type=int,
            default=86400,
            help="Maximum age of the control feed (in seconds, default: 86400)"
        )

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

        parser.add_argument(
            "--telegram-log-config-file",
            type=str,
            required=False,
            help=
            "config file for send logs to telegram chat (e.g. 'telegram_conf.json')",
            default=None)

        parser.add_argument(
            "--keeper-name",
            type=str,
            required=False,
            help="market maker keeper name (e.g. 'Uniswap_V2_MDTETH')",
            default="etoro")

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

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()
        self.etoro_api = EToroApi(api_server=self.arguments.etoro_api_server,
                                  account=self.arguments.etoro_account,
                                  api_key=self.arguments.etoro_api_key,
                                  secret_key=self.arguments.etoro_secret_key,
                                  timeout=self.arguments.etoro_timeout)

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(
            lambda: self.etoro_api.get_orders(self._join_string(self.pair()),
                                              "open"))
        self.order_book_manager.get_balances_with(
            lambda: self.etoro_api.get_balances())
        self.order_book_manager.cancel_orders_with(
            lambda order: self.etoro_api.cancel_order(order.order_id))
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()

    def main(self):
        with Lifecycle() as lifecycle:
            lifecycle.initial_delay(10)
            lifecycle.every(1, self.synchronize_orders)
            lifecycle.on_shutdown(self.shutdown)

    def shutdown(self):
        self.order_book_manager.cancel_all_orders()

    def pair(self):
        return self.arguments.pair.lower()

    def token_sell(self) -> str:
        return self.arguments.pair.split('_')[0].lower()

    def token_buy(self) -> str:
        return self.arguments.pair.split('_')[1].lower()

    def our_available_balance(self, our_balances: list, token: str) -> Wad:
        balance = list(filter(lambda x: x['currency'] == token,
                              our_balances))[0]['balance']
        return Wad.from_number(balance)

    def our_sell_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: order.is_sell, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: not order.is_sell, our_orders))

    def synchronize_orders(self):
        bands = Bands.read(self.bands_config, self.spread_feed,
                           self.control_feed, self.history)
        order_book = self.order_book_manager.get_order_book()
        target_price = self.price_feed.get_price()

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

        # Do not place new orders if order book state is not confirmed
        if order_book.orders_being_placed or order_book.orders_being_cancelled:
            self.logger.debug(
                "Order book is in progress, not placing new orders")
            return

        # Place new orders
        self.place_orders(
            bands.new_orders(
                our_buy_orders=self.our_buy_orders(order_book.orders),
                our_sell_orders=self.our_sell_orders(order_book.orders),
                our_buy_balance=self.our_available_balance(
                    order_book.balances, self.token_buy()),
                our_sell_balance=self.our_available_balance(
                    order_book.balances, self.token_sell()),
                target_price=target_price)[0])

    def place_orders(self, new_orders):
        def place_order_function(new_order_to_be_placed):
            amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount
            side = "sell" if new_order_to_be_placed.is_sell == True else "buy"
            order_id = self.etoro_api.place_order(
                pair=self._join_string(self.pair()),
                side=side,
                price=new_order_to_be_placed.price,
                amount=amount)

            timestamp = datetime.now(tz=timezone.utc).isoformat()

            return Order(str(order_id), timestamp,
                         self._join_string(self.pair()),
                         new_order_to_be_placed.is_sell,
                         new_order_to_be_placed.price, amount)

        for new_order in new_orders:
            self.order_book_manager.place_order(
                lambda new_order=new_order: place_order_function(new_order))

    # Return lower case concatted string. Assumes inputted string is an _ delimited pair
    def _join_string(self, string: str) -> str:
        assert (isinstance(string, str))
        return "".join(string.split('_')).lower()
class ParadexMarketMakerKeeper:
    """Keeper acting as a market maker on Paradex."""

    logger = logging.getLogger()

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

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

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

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

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

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

        parser.add_argument("--paradex-api-server", type=str, default='https://api.paradex.io/consumer',
                            help="Address of the Paradex API (default: 'https://api.paradex.io/consumer')")

        parser.add_argument("--paradex-api-key", type=str, required=True,
                            help="API key for the Paradex API")

        parser.add_argument("--paradex-api-timeout", type=float, default=9.5,
                            help="Timeout for accessing the Paradex API (in seconds, default: 9.5)")

        parser.add_argument("--pair", type=str, required=True,
                            help="Token pair (sell/buy) on which the keeper will operate")

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

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

        parser.add_argument("--config", type=str, required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed", type=str, required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed", type=str,
                            help="Source of spread feed")

        parser.add_argument("--spread-feed-expiry", type=int, default=3600,
                            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history", type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument("--order-history-every", type=int, default=30,
                            help="Frequency of reporting active orders (in seconds, default: 30)")

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

        parser.add_argument("--min-eth-balance", type=float, default=0,
                            help="Minimum ETH balance below which keeper will cease operation")

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

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

        parser.add_argument("--refresh-frequency", type=int, default=3,
                            help="Order book refresh frequency (in seconds, default: 3)")

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

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

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

        self.pair = self.arguments.pair.upper()
        self.token_buy = ERC20Token(web3=self.web3, address=Address(self.arguments.buy_token_address))
        self.token_sell = ERC20Token(web3=self.web3, address=Address(self.arguments.sell_token_address))
        self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance)
        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_max_decimals = None
        self.amount_max_decimals = None
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(self.arguments)

        self.history = History()
        self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address))
        self.paradex_api = ParadexApi(self.zrx_exchange,
                                      self.arguments.paradex_api_server,
                                      self.arguments.paradex_api_key,
                                      self.arguments.paradex_api_timeout)

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

    def startup(self):
        self.approve()

        # Get maximum number of decimals for prices and amounts.
        # Paradex API enforces it.
        markets = self.paradex_api.get_markets()
        market = next(filter(lambda item: item['symbol'] == self.pair, markets))

        self.price_max_decimals = market['priceMaxDecimals']
        self.amount_max_decimals = market['amountMaxDecimals']

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

    def approve(self):
        self.zrx_exchange.approve([self.token_sell, self.token_buy], directly(gas_price=self.gas_price))

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

    def our_orders(self) -> list:
        return self.paradex_api.get_orders(self.pair)

    def our_sell_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: order.is_sell, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: not order.is_sell, our_orders))

    def synchronize_orders(self):
        if eth_balance(self.web3, self.our_address) < self.min_eth_balance:
            self.logger.warning("Keeper ETH balance below minimum. Cancelling all orders.")
            self.cancel_orders(self.our_orders())
            return

        bands = Bands(self.bands_config, self.spread_feed, self.history)
        our_orders = self.our_orders()
        target_price = self.price_feed.get_price()

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

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

        # Place new orders
        self.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(our_orders),
                                           our_sell_orders=self.our_sell_orders(our_orders),
                                           our_buy_balance=our_buy_balance,
                                           our_sell_balance=our_sell_balance,
                                           target_price=target_price)[0])

    def cancel_orders(self, orders):
        for order in orders:
            self.paradex_api.cancel_order(order.order_id)

    def place_orders(self, new_orders):
        for new_order in new_orders:
            amount = new_order.pay_amount if new_order.is_sell else new_order.buy_amount
            self.paradex_api.place_order(pair=self.pair,
                                         is_sell=new_order.is_sell,
                                         price=round(new_order.price, self.price_max_decimals),
                                         amount=round(amount, self.amount_max_decimals),
                                         expiry=self.arguments.order_expiry)
    def __init__(self, args: list):
        parser = argparse.ArgumentParser(prog='eToro-market-maker-keeper')

        parser.add_argument("--etoro-api-server",
                            type=str,
                            required=True,
                            help="Address of the eToro API server")

        parser.add_argument("--etoro-account",
                            type=str,
                            default="*****@*****.**",
                            help="Username for eToroX account")

        parser.add_argument("--etoro-api-key",
                            type=str,
                            required=True,
                            help="API key for the eToro API")

        parser.add_argument(
            "--etoro-secret-key",
            type=argparse.FileType('r'),
            required=True,
            help="RSA Private Key for signing requests to the eToroX API")

        parser.add_argument(
            "--etoro-timeout",
            type=float,
            default=9.5,
            help=
            "Timeout for accessing the eToro API (in seconds, default: 9.5)")

        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed",
                            type=str,
                            help="Source of control feed")

        parser.add_argument(
            "--control-feed-expiry",
            type=int,
            default=86400,
            help="Maximum age of the control feed (in seconds, default: 86400)"
        )

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

        parser.add_argument(
            "--telegram-log-config-file",
            type=str,
            required=False,
            help=
            "config file for send logs to telegram chat (e.g. 'telegram_conf.json')",
            default=None)

        parser.add_argument(
            "--keeper-name",
            type=str,
            required=False,
            help="market maker keeper name (e.g. 'Uniswap_V2_MDTETH')",
            default="etoro")

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

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()
        self.etoro_api = EToroApi(api_server=self.arguments.etoro_api_server,
                                  account=self.arguments.etoro_account,
                                  api_key=self.arguments.etoro_api_key,
                                  secret_key=self.arguments.etoro_secret_key,
                                  timeout=self.arguments.etoro_timeout)

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(
            lambda: self.etoro_api.get_orders(self._join_string(self.pair()),
                                              "open"))
        self.order_book_manager.get_balances_with(
            lambda: self.etoro_api.get_balances())
        self.order_book_manager.cancel_orders_with(
            lambda order: self.etoro_api.cancel_order(order.order_id))
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()
class OkexMarketMakerKeeper:
    """Keeper acting as a market maker on OKEX."""

    logger = logging.getLogger()

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

        parser.add_argument("--okex-api-server", type=str, default="https://www.okex.com",
                            help="Address of the OKEX API server (default: 'https://www.okex.com')")

        parser.add_argument("--okex-api-key", type=str, required=True,
                            help="API key for the OKEX API")

        parser.add_argument("--okex-secret-key", type=str, required=True,
                            help="Secret key for the OKEX API")

        parser.add_argument("--okex-timeout", type=float, default=9.5,
                            help="Timeout for accessing the OKEX API (in seconds, default: 9.5)")

        parser.add_argument("--pair", type=str, required=True,
                            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--config", type=str, required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed", type=str, required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed", type=str,
                            help="Source of spread feed")

        parser.add_argument("--spread-feed-expiry", type=int, default=3600,
                            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history", type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument("--order-history-every", type=int, default=30,
                            help="Frequency of reporting active orders (in seconds, default: 30)")

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

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

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(self.arguments)

        self.history = History()
        self.okex_api = OKEXApi(api_server=self.arguments.okex_api_server,
                                api_key=self.arguments.okex_api_key,
                                secret_key=self.arguments.okex_secret_key,
                                timeout=self.arguments.okex_timeout)

    def main(self):
        with Lifecycle() as lifecycle:
            lifecycle.on_startup(self.startup)
            lifecycle.every(3, self.synchronize_orders)
            lifecycle.on_shutdown(self.shutdown)

    def startup(self):
        self.our_orders()
        self.logger.info(f"OKEX API key seems to be valid")
        self.logger.info(f"Keeper configured to work on the '{self.pair()}' pair")

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

    def pair(self):
        return self.arguments.pair.lower()

    def token_sell(self) -> str:
        return self.arguments.pair.split('_')[0].lower()

    def token_buy(self) -> str:
        return self.arguments.pair.split('_')[1].lower()

    def our_balances(self) -> dict:
        return self.okex_api.get_balances()

    def our_available_balance(self, our_balances: dict, token: str) -> Wad:
        return Wad.from_number(our_balances['free'][token])

    def our_orders(self) -> list:
        return self.okex_api.get_orders(self.pair())

    def our_sell_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: order.is_sell, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: not order.is_sell, our_orders))

    def synchronize_orders(self):
        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

        # Place new orders
        self.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(our_orders),
                                           our_sell_orders=self.our_sell_orders(our_orders),
                                           our_buy_balance=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)[0])

    def cancel_orders(self, orders):
        for order in orders:
            self.okex_api.cancel_order(self.pair(), order.order_id)

    def place_orders(self, new_orders):
        for new_order in new_orders:
            amount = new_order.pay_amount if new_order.is_sell else new_order.buy_amount
            self.okex_api.place_order(pair=self.pair(), is_sell=new_order.is_sell, price=new_order.price, amount=amount)
Exemple #32
0
class CEXKeeperAPI:
    """
    Define a common abstract API for keepers on centralized and hybrid exchanges
    """
    def __init__(self, arguments: Namespace, pyex_api: PyexAPI):

        setup_logging(arguments)

        self.bands_config = ReloadableConfig(arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(arguments)
        self.spread_feed = create_spread_feed(arguments)
        self.control_feed = create_control_feed(arguments)

        self.order_history_reporter = create_order_history_reporter(arguments)
        self.history = History()

        self.init_order_book_manager(arguments, pyex_api)

    def init_order_book_manager(self, arguments: Namespace, pyex_api: PyexAPI):
        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(
            lambda: pyex_api.get_orders(self.pair()))
        self.order_book_manager.get_balances_with(
            lambda: pyex_api.get_balances())
        self.order_book_manager.cancel_orders_with(
            lambda order: pyex_api.cancel_order(order.order_id))
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()

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

    def startup(self):
        pass

    def shutdown(self):
        self.order_book_manager.cancel_all_orders()

    # Each exchange takes pair input as a different format
    def pair(self):
        raise NotImplementedError()

    def token_sell(self) -> str:
        raise NotImplementedError()

    def token_buy(self) -> str:
        raise NotImplementedError()

    # Different keys are used to access balance object for different exchanges
    def our_available_balance(self, our_balances: dict, token: str) -> Wad:
        raise NotImplementedError()

    def our_sell_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: order.is_sell, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: not order.is_sell, our_orders))

    def synchronize_orders(self):
        bands = Bands.read(self.bands_config, self.spread_feed,
                           self.control_feed, self.history)
        order_book = self.order_book_manager.get_order_book()
        target_price = self.price_feed.get_price()

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

        # Do not place new orders if order book state is not confirmed
        if order_book.orders_being_placed or order_book.orders_being_cancelled:
            self.logger.debug(
                "Order book is in progress, not placing new orders")
            return

        # Place new orders
        self.place_orders(
            bands.new_orders(
                our_buy_orders=self.our_buy_orders(order_book.orders),
                our_sell_orders=self.our_sell_orders(order_book.orders),
                our_buy_balance=self.our_available_balance(
                    order_book.balances, self.token_buy()),
                our_sell_balance=self.our_available_balance(
                    order_book.balances, self.token_sell()),
                target_price=target_price)[0])

    def place_orders(self, new_orders: list):
        raise NotImplementedError()
    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()
Exemple #34
0
class OasisMarketMakerKeeper:
    """Keeper acting as a market maker on OasisDEX."""

    logger = logging.getLogger()

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

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

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

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

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

        parser.add_argument("--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=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("--buy-token-name", type=str, required=True,
                            help="Ethereum address of the buy token")

        parser.add_argument("--sell-token-name", type=str, required=True,
                            help="Ethereum address of the sell token")
        
        parser.add_argument("--buy-token-decimals", type=int, required=True,
                            help="Ethereum address of the buy token")

        parser.add_argument("--sell-token-decimals", type=int, required=True,
                            help="Ethereum address of the sell token")
        
        parser.add_argument("--config", type=str, required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed", type=str, required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed", type=str,
                            help="Source of spread feed")

        parser.add_argument("--spread-feed-expiry", type=int, default=3600,
                            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed", type=str,
                            help="Source of control feed")

        parser.add_argument("--control-feed-expiry", type=int, default=86400,
                            help="Maximum age of the control feed (in seconds, default: 86400)")

        parser.add_argument("--order-history", type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument("--order-history-every", type=int, default=30,
                            help="Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument("--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("--ethgasstation-api-key", type=str, default=None, help="ethgasstation API key")

        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
        register_keys(self.web3, self.arguments.eth_key)
        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.buy_token = Token(name=self.arguments.buy_token_name, address=Address(self.arguments.buy_token_address), decimals=self.arguments.buy_token_decimals)
        self.sell_token = Token(name=self.arguments.sell_token_name, address=Address(self.arguments.sell_token_address), decimals=self.arguments.sell_token_decimals)
        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.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(self.arguments)

        self.history = History()
        self.order_book_manager = OrderBookManager(refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(lambda: self.our_orders())
        self.order_book_manager.place_orders_with(self.place_order_function)
        self.order_book_manager.cancel_orders_with(self.cancel_order_function)
        self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders)
        self.order_book_manager.start()

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

    def startup(self):
        self.approve()

    def shutdown(self):
        self.order_book_manager.cancel_all_orders(final_wait_time=60)

    def approve(self):
        """Approve OasisDEX to access our balances, so we can place orders."""
        self.otc.approve([self.token_sell, self.token_buy], directly(gas_price=self.gas_price))

    def our_available_balance(self, token: ERC20Token) -> Wad:
        if token.symbol() == self.buy_token.name:
            return self.buy_token.normalize_amount(token.balance_of(self.our_address))
        else:
            return self.sell_token.normalize_amount(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.sell_token, self.buy_token) +
                           self.otc.get_orders(self.buy_token, self.sell_token)))

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

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

    def synchronize_orders(self):
        # If market is closed, cancel all orders but do not terminate the keeper.
        if self.otc.is_closed():
            self.logger.warning("Market is closed. Cancelling all orders.")
            self.order_book_manager.cancel_all_orders()
            return

        # If keeper balance is below `--min-eth-balance`, cancel all orders but do not terminate
        # the keeper, keep processing blocks as the moment the keeper gets a top-up it should
        # resume activity straight away, without the need to restart it.
        if eth_balance(self.web3, self.our_address) < self.min_eth_balance:
            self.logger.warning("Keeper ETH balance below minimum. Cancelling all orders.")
            self.order_book_manager.cancel_all_orders()
            return

        bands = Bands.read(self.bands_config, self.spread_feed, self.control_feed, self.history)
        order_book = self.order_book_manager.get_order_book()
        target_price = self.price_feed.get_price()
        # Cancel orders
        cancellable_orders = bands.cancellable_orders(our_buy_orders=self.our_buy_orders(order_book.orders),
                                                      our_sell_orders=self.our_sell_orders(order_book.orders),
                                                      target_price=target_price)
        if len(cancellable_orders) > 0:
            self.order_book_manager.cancel_orders(cancellable_orders)
            return

        # Do not place new orders if other new orders are being placed. In contrary to other keepers,
        # we allow placing new orders when other orders are being cancelled. This is because Ethereum
        # transactions are ordered so we are sure that the order placement will not 'overtake'
        # order cancellation.
        if order_book.orders_being_placed:
            self.logger.debug("Other orders are being placed, not placing new orders")
            return

        # Place new orders
        self.order_book_manager.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(order_book.orders),
                                                              our_sell_orders=self.our_sell_orders(order_book.orders),
                                                              our_buy_balance=self.our_available_balance(self.token_buy),
                                                              our_sell_balance=self.our_available_balance(self.token_sell),
                                                              target_price=target_price)[0])

    def place_order_function(self, new_order: NewOrder):
        assert(isinstance(new_order, NewOrder))

        if new_order.is_sell:
            buy_or_sell = "SELL"
            pay_token = self.token_sell.address
            buy_token = self.token_buy.address
            new_order.buy_amount = self.buy_token.unnormalize_amount(new_order.buy_amount)
            b_token = self.buy_token
            p_token = self.sell_token
            new_order.pay_amount = self.sell_token.unnormalize_amount(new_order.pay_amount)
            token_name = self.sell_token.name
            quote_token = self.buy_token.name

        else:
            buy_or_sell = "BUY"
            pay_token = self.token_buy.address
            buy_token = self.token_sell.address
            new_order.pay_amount = self.buy_token.unnormalize_amount(new_order.pay_amount)
            p_token = self.buy_token
            b_token = self.sell_token
            new_order.buy_amount = self.sell_token.unnormalize_amount(new_order.buy_amount)
            token_name = self.sell_token.name
            quote_token = self.buy_token.name


        transact = self.otc.make(p_token=p_token, pay_amount=new_order.pay_amount,
                                 b_token=b_token, buy_amount=new_order.buy_amount).transact(gas_price=self.gas_price)

        if new_order.is_sell:
            new_order.buy_amount = self.buy_token.normalize_amount(new_order.buy_amount)
            new_order.pay_amount = self.sell_token.normalize_amount(new_order.pay_amount)
            buy_or_sell_price = new_order.buy_amount/new_order.pay_amount
            amount = new_order.pay_amount

        else:
            new_order.pay_amount = self.buy_token.normalize_amount(new_order.pay_amount)
            new_order.buy_amount = self.sell_token.normalize_amount(new_order.buy_amount)
            buy_or_sell_price = new_order.pay_amount/new_order.buy_amount
            amount = new_order.buy_amount

        if transact is not None and transact.successful and transact.result is not None:
            self.logger.info(f'Placing {buy_or_sell} order of amount {amount} {token_name} @ price {buy_or_sell_price} {quote_token}') 
            self.logger.info(f'Placing {buy_or_sell} order pay token: {p_token.name} with amount: {new_order.pay_amount}, buy token: {b_token.name} with amount: {new_order.buy_amount}')
            return Order(market=self.otc,
                         order_id=transact.result,
                         maker=self.our_address,
                         pay_token=pay_token,
                         pay_amount=new_order.pay_amount,
                         buy_token=buy_token,
                         buy_amount=new_order.buy_amount,
                         timestamp=0)
        else:
            return None

    def cancel_order_function(self, order):
        transact = self.otc.kill(order.order_id).transact(gas_price=self.gas_price)
        return transact is not None and transact.successful
    def __init__(self, args: list):
        parser = argparse.ArgumentParser(prog='gopax-market-maker-keeper')

        parser.add_argument("--gopax-api-server", type=str, default="https://api.gopax.co.kr",
                            help="Address of the GOPAX API server (default: 'https://api.gopax.co.kr')")

        parser.add_argument("--gopax-api-key", type=str, required=True,
                            help="API key for the GOPAX API")

        parser.add_argument("--gopax-api-secret", type=str, required=True,
                            help="API secret for the GOPAX API")

        parser.add_argument("--gopax-timeout", type=float, default=9.5,
                            help="Timeout for accessing the GOPAX API (in seconds, default: 9.5)")

        parser.add_argument("--pair", type=str, required=True,
                            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--config", type=str, required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed", type=str, required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed", type=str,
                            help="Source of spread feed")

        parser.add_argument("--spread-feed-expiry", type=int, default=3600,
                            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history", type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument("--order-history-every", type=int, default=30,
                            help="Frequency of reporting active orders (in seconds, default: 30)")

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

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

        self.history = History()
        self.gopax_api = GOPAXApi(api_server=self.arguments.gopax_api_server,
                                  api_key=self.arguments.gopax_api_key,
                                  api_secret=self.arguments.gopax_api_secret,
                                  timeout=self.arguments.gopax_timeout)

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(self.arguments)

        self.order_book_manager = OrderBookManager(refresh_frequency=10)
        self.order_book_manager.get_orders_with(self.get_orders)
        self.order_book_manager.get_balances_with(lambda: self.gopax_api.get_balances())
        self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders)
        self.order_book_manager.start()
Exemple #36
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("--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=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("--buy-token-name", type=str, required=True,
                            help="Ethereum address of the buy token")

        parser.add_argument("--sell-token-name", type=str, required=True,
                            help="Ethereum address of the sell token")
        
        parser.add_argument("--buy-token-decimals", type=int, required=True,
                            help="Ethereum address of the buy token")

        parser.add_argument("--sell-token-decimals", type=int, required=True,
                            help="Ethereum address of the sell token")
        
        parser.add_argument("--config", type=str, required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed", type=str, required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed", type=str,
                            help="Source of spread feed")

        parser.add_argument("--spread-feed-expiry", type=int, default=3600,
                            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed", type=str,
                            help="Source of control feed")

        parser.add_argument("--control-feed-expiry", type=int, default=86400,
                            help="Maximum age of the control feed (in seconds, default: 86400)")

        parser.add_argument("--order-history", type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument("--order-history-every", type=int, default=30,
                            help="Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument("--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("--ethgasstation-api-key", type=str, default=None, help="ethgasstation API key")

        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
        register_keys(self.web3, self.arguments.eth_key)
        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.buy_token = Token(name=self.arguments.buy_token_name, address=Address(self.arguments.buy_token_address), decimals=self.arguments.buy_token_decimals)
        self.sell_token = Token(name=self.arguments.sell_token_name, address=Address(self.arguments.sell_token_address), decimals=self.arguments.sell_token_decimals)
        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.control_feed = create_control_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 IdexMarketMakerKeeper:
    """Keeper acting as a market maker on IDEX, on the ETH/SAI pair."""

    logger = logging.getLogger()

    def add_arguments(self, parser):
        parser.add_argument(
            "--rpc-host",
            type=str,
            default="http://localhost:8545",
            help="JSON-RPC host (default: `http://localhost:8545`)")

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

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

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

        parser.add_argument("--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("--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(
            "--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("--ethgasstation-api-key",
                            type=str,
                            default=None,
                            help="ethgasstation API key")

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

        parser.add_argument(
            "--telegram-log-config-file",
            type=str,
            required=False,
            help=
            "config file for send logs to telegram chat (e.g. 'telegram_conf.json')",
            default=None)

        parser.add_argument(
            "--keeper-name",
            type=str,
            required=False,
            help="market maker keeper name (e.g. 'Uniswap_V2_MDTETH')",
            default="idex")

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

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

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

        provider = HTTPProvider(
            endpoint_uri=self.arguments.rpc_host,
            request_kwargs={'timeout': self.arguments.rpc_timeout})
        self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            provider)

        self.web3.eth.defaultAccount = self.arguments.eth_from
        self.our_address = Address(self.arguments.eth_from)
        register_keys(self.web3, self.arguments.eth_key)

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

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.eth_reserve = Wad.from_number(self.arguments.eth_reserve)
        self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance)
        self.min_eth_deposit = Wad.from_number(self.arguments.min_eth_deposit)
        self.min_sai_deposit = Wad.from_number(self.arguments.min_sai_deposit)
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(
            self.arguments, self.tub)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

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

        self.history = History()
        self.idex = IDEX(self.web3, Address(self.arguments.idex_address))
        self.idex_api = IDEXApi(self.idex, self.arguments.idex_api_server,
                                self.arguments.idex_timeout)

    def 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.read(self.bands_config, self.spread_feed,
                           self.control_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
Exemple #38
0
class MpxMarketMakerKeeper:
    """Keeper acting as a market maker on MPExchange."""

    logger = logging.getLogger()

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='mpx-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(
            "--mpx-api-server",
            type=str,
            default='https://api.mpexchange.io',
            help=
            "Address of the MPX API server (default: 'https://api.mpexchange.io')"
        )

        parser.add_argument(
            "--mpx-api-timeout",
            type=float,
            default=9.5,
            help="Timeout for accessing the MPX API (in seconds, default: 9.5)"
        )

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

        parser.add_argument("--fee-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Mpx Fee contract")

        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--sell-token-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Sell Token")

        parser.add_argument("--buy-token-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Buy Token")

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

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

        parser.add_argument("--ethgasstation-api-key",
                            type=str,
                            default=None,
                            help="ethgasstation API key")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed",
                            type=str,
                            help="Source of control feed")

        parser.add_argument(
            "--control-feed-expiry",
            type=int,
            default=86400,
            help="Maximum age of the control feed (in seconds, default: 86400)"
        )

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

        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.token_buy = ERC20Token(web3=self.web3,
                                    address=Address(
                                        self.arguments.buy_token_address))
        self.token_sell = ERC20Token(web3=self.web3,
                                     address=Address(
                                         self.arguments.sell_token_address))

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_max_decimals = None
        self.gas_price = GasPriceFactory().create_gas_price(
            self.web3, self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()

        self.zrx_exchange = ZrxExchangeV2(web3=self.web3,
                                          address=Address(
                                              self.arguments.exchange_address))
        self.mpx_api = MpxApi(api_server=self.arguments.mpx_api_server,
                              zrx_exchange=self.zrx_exchange,
                              fee_recipient=Address(
                                  self.arguments.fee_address),
                              timeout=self.arguments.mpx_api_timeout,
                              our_address=self.arguments.eth_from)

        self.zrx_relayer_api = ZrxRelayerApiV2(
            exchange=self.zrx_exchange,
            api_server=self.arguments.mpx_api_server)
        self.zrx_api = ZrxApiV2(zrx_exchange=self.zrx_exchange,
                                zrx_api=self.zrx_relayer_api)

        markets = self.mpx_api.get_markets()['data']
        market = next(
            filter(
                lambda item: item['attributes']['pair-name'] == self.arguments.
                pair, markets))

        self.pair = MpxPair(self.arguments.pair, self.token_buy.address,
                            int(market['attributes']['base-token-decimals']),
                            self.token_sell.address,
                            int(market['attributes']['quote-token-decimals']))

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(lambda: self.get_orders())
        self.order_book_manager.get_balances_with(lambda: self.get_balances())
        self.order_book_manager.cancel_orders_with(self.cancel_order_function)
        self.order_book_manager.place_orders_with(self.place_order_function)
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()

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

    def startup(self):
        self.approve()

    def approve(self):
        self.zrx_exchange.approve([self.token_sell, self.token_buy],
                                  directly(gas_price=self.gas_price))

    def shutdown(self):
        self.order_book_manager.cancel_all_orders()

    def get_balances(self):
        balances = self.zrx_api.get_balances(self.pair)
        return balances[0], balances[1], eth_balance(self.web3,
                                                     self.our_address)

    def get_orders(self) -> list:
        orders = self.mpx_api.get_orders(self.pair)
        return self.zrx_api.get_orders(self.pair, orders)

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

    def our_sell_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: order.is_sell, our_orders))

    def our_buy_orders(self, our_orders: list) -> list:
        return list(filter(lambda order: not order.is_sell, our_orders))

    def synchronize_orders(self):
        bands = Bands.read(self.bands_config, self.spread_feed,
                           self.control_feed, self.history)
        order_book = self.order_book_manager.get_order_book()
        target_price = self.price_feed.get_price()

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

        # Do not place new orders if order book state is not confirmed
        if order_book.orders_being_placed or order_book.orders_being_cancelled:
            self.logger.debug(
                "Order book is in progress, not placing new orders")
            return

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

        # Place new orders
        self.order_book_manager.place_orders(
            bands.new_orders(
                our_buy_orders=self.our_buy_orders(order_book.orders),
                our_sell_orders=self.our_sell_orders(order_book.orders),
                our_buy_balance=our_buy_balance,
                our_sell_balance=our_sell_balance,
                target_price=target_price)[0])

    def place_order_function(self, new_order: NewOrder):
        assert (isinstance(new_order, NewOrder))

        price = round(new_order.price, self.price_max_decimals)
        amount = new_order.pay_amount if new_order.is_sell else new_order.buy_amount
        zrx_order = self.mpx_api.place_order(pair=self.pair,
                                             is_sell=new_order.is_sell,
                                             price=price,
                                             amount=amount)

        if zrx_order is not None:
            return self.zrx_api.get_orders(self.pair, [zrx_order])[0]
        else:
            return None

    def cancel_order_function(self, order):
        self.logger.info(f"Canceling order {order.zrx_order.order_hash}")
        if self.mpx_api.cancel_order(order.zrx_order.order_hash):
            transact = self.zrx_exchange.cancel_order(
                order.zrx_order).transact(gas_price=self.gas_price)
            return transact is not None and transact.successful

        return False
    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)
Exemple #40
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='mpx-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(
            "--mpx-api-server",
            type=str,
            default='https://api.mpexchange.io',
            help=
            "Address of the MPX API server (default: 'https://api.mpexchange.io')"
        )

        parser.add_argument(
            "--mpx-api-timeout",
            type=float,
            default=9.5,
            help="Timeout for accessing the MPX API (in seconds, default: 9.5)"
        )

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

        parser.add_argument("--fee-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Mpx Fee contract")

        parser.add_argument(
            "--pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--sell-token-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Sell Token")

        parser.add_argument("--buy-token-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Buy Token")

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

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

        parser.add_argument("--ethgasstation-api-key",
                            type=str,
                            default=None,
                            help="ethgasstation API key")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed",
                            type=str,
                            help="Source of control feed")

        parser.add_argument(
            "--control-feed-expiry",
            type=int,
            default=86400,
            help="Maximum age of the control feed (in seconds, default: 86400)"
        )

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=3,
            help="Order book refresh frequency (in seconds, default: 3)")

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

        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.token_buy = ERC20Token(web3=self.web3,
                                    address=Address(
                                        self.arguments.buy_token_address))
        self.token_sell = ERC20Token(web3=self.web3,
                                     address=Address(
                                         self.arguments.sell_token_address))

        self.bands_config = ReloadableConfig(self.arguments.config)
        self.price_max_decimals = None
        self.gas_price = GasPriceFactory().create_gas_price(
            self.web3, self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()

        self.zrx_exchange = ZrxExchangeV2(web3=self.web3,
                                          address=Address(
                                              self.arguments.exchange_address))
        self.mpx_api = MpxApi(api_server=self.arguments.mpx_api_server,
                              zrx_exchange=self.zrx_exchange,
                              fee_recipient=Address(
                                  self.arguments.fee_address),
                              timeout=self.arguments.mpx_api_timeout,
                              our_address=self.arguments.eth_from)

        self.zrx_relayer_api = ZrxRelayerApiV2(
            exchange=self.zrx_exchange,
            api_server=self.arguments.mpx_api_server)
        self.zrx_api = ZrxApiV2(zrx_exchange=self.zrx_exchange,
                                zrx_api=self.zrx_relayer_api)

        markets = self.mpx_api.get_markets()['data']
        market = next(
            filter(
                lambda item: item['attributes']['pair-name'] == self.arguments.
                pair, markets))

        self.pair = MpxPair(self.arguments.pair, self.token_buy.address,
                            int(market['attributes']['base-token-decimals']),
                            self.token_sell.address,
                            int(market['attributes']['quote-token-decimals']))

        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(lambda: self.get_orders())
        self.order_book_manager.get_balances_with(lambda: self.get_balances())
        self.order_book_manager.cancel_orders_with(self.cancel_order_function)
        self.order_book_manager.place_orders_with(self.place_order_function)
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='0x-market-maker-keeper')

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

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

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

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

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

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

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

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

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

        parser.add_argument("--config", type=str, required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed", type=str, required=True,
                            help="Source of price feed")

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

        parser.add_argument("--spread-feed", type=str,
                            help="Source of spread feed")

        parser.add_argument("--spread-feed-expiry", type=int, default=3600,
                            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--order-history", type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument("--order-history-every", type=int, default=30,
                            help="Frequency of reporting active orders (in seconds, default: 30)")

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

        parser.add_argument("--order-expiry-threshold", type=int, default=0,
                            help="How long before order expiration it is considered already expired (in seconds)")

        parser.add_argument("--min-eth-balance", type=float, default=0,
                            help="Minimum ETH balance below which keeper will cease operation")

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

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

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

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

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

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

        self.token_buy = ERC20Token(web3=self.web3, address=Address(self.arguments.buy_token_address))
        self.token_sell = ERC20Token(web3=self.web3, address=Address(self.arguments.sell_token_address))
        self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance)
        self.bands_config = ReloadableConfig(self.arguments.config)
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(self.arguments)

        self.history = History()
        self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address))
        self.zrx_relayer_api = ZrxRelayerApi(exchange=self.zrx_exchange, api_server=self.arguments.relayer_api_server)
        self.placed_orders = []
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='imtoken-pricing-server')

        parser.add_argument("--http-address",
                            type=str,
                            default='',
                            help="Address of the Imtoken Pricing server")

        parser.add_argument("--http-port",
                            type=int,
                            default=8777,
                            help="Port of the Imtoken Pricing server")

        parser.add_argument(
            "--imtoken-api-server",
            type=str,
            default='http://localhost:8157',
            help=
            "Address of the Imtoken API server (default: 'http://localhost:8157')"
        )

        parser.add_argument(
            "--imtoken-api-timeout",
            type=float,
            default=9.5,
            help=
            "Timeout for accessing the Imtoken API (in seconds, default: 9.5)")

        parser.add_argument(
            "--base-pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--base-config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument(
            "--counter-pair",
            type=str,
            required=True,
            help="Token pair (sell/buy) on which the keeper will operate")

        parser.add_argument("--counter-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-cache-maxsize",
                            type=int,
                            default=100000,
                            help="Maximum size of orders cache")

        parser.add_argument("--order-cache-ttl",
                            type=int,
                            default=10,
                            help="Orders time to live")

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

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

        self.cache = TTLCache(maxsize=self.arguments.order_cache_maxsize,
                              ttl=self.arguments.order_cache_ttl)
        self.base_bands_config = ReloadableConfig(self.arguments.base_config)
        self.counter_bands_config = ReloadableConfig(
            self.arguments.counter_config)
        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)

        self.history = History()

        pair = ImtokenPair(self.arguments.base_pair,
                           self.arguments.counter_pair)

        application = tornado.web.Application([
            (r"/pairs", PairsHandler, dict(pair=pair)),
            (r"/indicativePrice", IndicativePriceHandler,
             dict(pair=pair,
                  base_bands_config=self.base_bands_config,
                  counter_bands_config=self.counter_bands_config,
                  price_feed=self.price_feed,
                  spread_feed=self.spread_feed,
                  control_feed=self.control_feed,
                  history=self.history,
                  cache=self.cache)),
            (r"/price", PriceHandler,
             dict(pair=pair,
                  base_bands_config=self.base_bands_config,
                  counter_bands_config=self.counter_bands_config,
                  price_feed=self.price_feed,
                  spread_feed=self.spread_feed,
                  control_feed=self.control_feed,
                  history=self.history,
                  cache=self.cache)),
            (r"/deal", DealHandler, dict(cache=self.cache,
                                         schema=deal_schema())),
        ])
        application.listen(port=self.arguments.http_port,
                           address=self.arguments.http_address)
        tornado.ioloop.IOLoop.current().start()