Exemplo n.º 1
0
def create_risky_cdp(mcd: DssDeployment,
                     c: Collateral,
                     collateral_amount: Wad,
                     gal_address: Address,
                     draw_dai=True) -> Urn:
    assert isinstance(mcd, DssDeployment)
    assert isinstance(c, Collateral)
    assert isinstance(gal_address, Address)

    # Ensure vault isn't already unsafe (if so, this shouldn't be called)
    urn = mcd.vat.urn(c.ilk, gal_address)
    assert is_cdp_safe(mcd.vat.ilk(c.ilk.name), urn)

    # Add collateral to gal vault if necessary
    c.approve(gal_address)
    token = Token(c.ilk.name, c.gem.address, c.adapter.dec())
    print(f"collateral_amount={collateral_amount} ink={urn.ink}")
    dink = collateral_amount - urn.ink
    if dink > Wad(0):
        vat_balance = mcd.vat.gem(c.ilk, gal_address)
        balance = token.normalize_amount(c.gem.balance_of(gal_address))
        print(
            f"before join: dink={dink} vat_balance={vat_balance} balance={balance} vat_gap={dink - vat_balance}"
        )
        if vat_balance < dink:
            # handle dusty balances with non-18-decimal tokens
            vat_gap = dink - vat_balance + token.min_amount
            if balance < vat_gap:
                if c.ilk.name.startswith("ETH"):
                    wrap_eth(mcd, gal_address, vat_gap)
                else:
                    raise RuntimeError("Insufficient collateral balance")
            assert c.adapter.join(gal_address,
                                  token.unnormalize_amount(vat_gap)).transact(
                                      from_address=gal_address)
        vat_balance = mcd.vat.gem(c.ilk, gal_address)
        balance = token.normalize_amount(c.gem.balance_of(gal_address))
        print(
            f"after join: dink={dink} vat_balance={vat_balance} balance={balance} vat_gap={dink - vat_balance}"
        )
        assert vat_balance >= dink
        assert mcd.vat.frob(c.ilk, gal_address, dink,
                            Wad(0)).transact(from_address=gal_address)
        urn = mcd.vat.urn(c.ilk, gal_address)

    # Put gal CDP at max possible debt
    dart = max_dart(mcd, c, gal_address) - Wad(1)
    if dart > Wad(0):
        print(f"Frobbing {c.ilk.name} with ink={urn.ink} and dart={dart}")
        assert mcd.vat.frob(c.ilk, gal_address, Wad(0),
                            dart).transact(from_address=gal_address)

    # Draw our Dai, simulating the usual behavior
    urn = mcd.vat.urn(c.ilk, gal_address)
    if draw_dai and urn.art > Wad(0):
        mcd.approve_dai(gal_address)
        assert mcd.dai_adapter.exit(gal_address,
                                    urn.art).transact(from_address=gal_address)
        print(f"Exited {urn.art} Dai from urn")
    return urn
Exemplo n.º 2
0
    def get_exchange_balance(self, token: Token, pair_address: Address) -> Wad:
        assert (isinstance(token, Token))
        assert (isinstance(pair_address, Address))

        return token.normalize_amount(
            ERC20Token(web3=self.web3,
                       address=token.address).balance_of(pair_address))
Exemplo n.º 3
0
class TestToken:
    def setup_class(self):
        self.token = Token(
            "COW", Address('0xbeef00000000000000000000000000000000BEEF'), 4)

    def test_convert(self):
        # two
        chain_amount = Wad(20000)
        assert self.token.normalize_amount(chain_amount) == Wad.from_number(2)

        # three
        normalized_amount = Wad.from_number(3)
        assert self.token.unnormalize_amount(normalized_amount) == Wad(30000)

    def test_min_amount(self):
        assert self.token.min_amount == Wad.from_number(0.0001)
        assert float(self.token.min_amount) == 0.0001
        assert self.token.unnormalize_amount(self.token.min_amount) == Wad(1)

        assert Wad.from_number(0.0004) > self.token.min_amount
        assert Wad.from_number(0.00005) < self.token.min_amount

        assert self.token.unnormalize_amount(
            Wad.from_number(0.0006)) > self.token.unnormalize_amount(
                self.token.min_amount)
        assert self.token.unnormalize_amount(
            Wad.from_number(0.00007)) < self.token.unnormalize_amount(
                self.token.min_amount)
        assert self.token.unnormalize_amount(
            Wad.from_number(0.00008)) == Wad(0)
Exemplo n.º 4
0
    def get_exchange_balance_at_block(self, token: Token,
                                      pair_address: Address,
                                      block_number: int) -> Wad:
        assert (isinstance(token, Token))
        assert (isinstance(pair_address, Address))
        assert (isinstance(block_number, int))

        return token.normalize_amount(
            ERC20Token(web3=self.web3, address=token.address).balance_at_block(
                pair_address, block_number))
Exemplo n.º 5
0
    def position(self, p_token: Token, pay_amount: Wad, b_token: Token,
                 buy_amount: Wad) -> int:
        """Calculate the position (`pos`) new order should be inserted at to minimize gas costs.

        The `MatchingMarket` contract maintains an internal ordered linked list of orders, which allows the contract
        to do automated matching. Client placing a new order can either let the contract find the correct
        position in the linked list (by passing `0` as the `pos` argument of `make`) or calculate the position
        itself and just pass the right value to the contract (this will happen if you omit the `pos`
        argument of `make`). The latter should always use less gas. If the client decides not to calculate the
        position or it does get it wrong and the number of open orders is high at the same time, the new order
        may not even be placed at all as the attempt to calculate the position by the contract will likely fail
        due to high gas usage.

        This method is responsible for calculating the correct insertion position. It is used internally
        by `make` when `pos` argument is omitted (or is `None`).

        Args:
            p_token: Token object (see `model.py`) of the token you want to put on sale.
            pay_amount: Amount of the `pay_token` token you want to put on sale.
            b_token: Token object (see `model.py`) of the token you want to be paid with.
            buy_amount: Amount of the `buy_token` you want to receive.

        Returns:
            The position (`pos`) new order should be inserted at.
        """
        assert (isinstance(p_token, Token))
        assert (isinstance(pay_amount, Wad))
        assert (isinstance(b_token, Token))
        assert (isinstance(buy_amount, Wad))

        pay_token = p_token.address
        buy_token = b_token.address

        self.logger.debug("Enumerating orders for position calculation...")

        orders = filter(
            lambda order: order.pay_amount / order.buy_amount >= p_token.
            normalize_amount(pay_amount) / b_token.normalize_amount(buy_amount
                                                                    ),
            self.get_orders(p_token, b_token))

        self.logger.debug(
            "Enumerating orders for position calculation finished")

        sorted_orders = sorted(orders,
                               key=lambda o: o.pay_amount / o.buy_amount)
        return sorted_orders[0].order_id if len(sorted_orders) > 0 else 0
class UniswapV2MarketMakerKeeper:
    """Keeper acting as a market maker on Uniswap v2."""

    logger = logging.getLogger()
    send_transaction: bool = False

    def add_arguments(self, parser):
        """Provider info"""
        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')"
        )
        """Exchange info"""
        parser.add_argument(
            "--uniswap-router-address",
            type=str,
            required=True,
            help="Ethereum address of the Uniswap Router v2 contract")
        """Tokens info"""
        parser.add_argument("--first-token-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the first token")

        parser.add_argument("--first-token-name",
                            type=str,
                            required=True,
                            help="name of the first token")

        parser.add_argument("--first-token-decimals",
                            type=int,
                            required=True,
                            help="decimal of the first token")

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

        parser.add_argument("--second-token-name",
                            type=str,
                            required=True,
                            help="name of the second token")

        parser.add_argument("--second-token-decimals",
                            type=int,
                            required=True,
                            help="decimal of the second token")
        """settings"""
        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("--max-delta-on-percent",
                            type=float,
                            default=3,
                            help="Delta permissible margin")

        parser.add_argument(
            "--max-first-token-amount-input",
            type=float,
            default=10000,
            help=
            "The maximum allowed number of first tokens that can be exchanged for installation."
        )

        parser.add_argument(
            "--max-second-token-amount-input",
            type=float,
            default=10000,
            help=
            "The maximum allowed number of second tokens that can be exchanged for installation."
        )

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

        parser.add_argument("--min-first-token-balance",
                            type=float,
                            default=0,
                            help="Minimum first token balance")

        parser.add_argument("--min-second-token-balance",
                            type=float,
                            default=0,
                            help="Minimum second token balance")

        parser.add_argument("--gas-price",
                            type=int,
                            default=50000000000,
                            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")

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

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='uniswap-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
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        self.uniswap_router = UniswapRouter(
            web3=self.web3,
            router=Address(self.arguments.uniswap_router_address))

        self.first_token = ERC20Token(web3=self.web3,
                                      address=Address(
                                          self.arguments.first_token_address))
        self.second_token = ERC20Token(
            web3=self.web3,
            address=Address(self.arguments.second_token_address))

        self.token_first = Token(name=self.arguments.first_token_name,
                                 address=Address(
                                     self.arguments.first_token_address),
                                 decimals=self.arguments.first_token_decimals)

        self.token_second = Token(
            name=self.arguments.second_token_name,
            address=Address(self.arguments.second_token_address),
            decimals=self.arguments.second_token_decimals)

        self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance)

        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.max_delta_on_percent = self.arguments.max_delta_on_percent

        self.price_feed = PriceFeedFactory().create_price_feed(self.arguments)

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

    def startup(self):
        self.approve()

    def approve(self):
        """Approve Uniswap to access our balances, so we can place orders."""
        self.uniswap_router.approve([self.first_token, self.second_token],
                                    directly(gas_price=self.gas_price))

    def our_available_balance(self, token: ERC20Token) -> Wad:
        if token.symbol() == self.token_first.name:
            return self.token_first.normalize_amount(
                token.balance_of(self.our_address))
        else:
            return self.token_second.normalize_amount(
                token.balance_of(self.our_address))

    @staticmethod
    def _get_amounts(market_price: Wad, first_token_liquidity_pool_amount: Wad,
                     second_token_liquidity_pool_amount: Wad):
        liquidity_pool_constant = first_token_liquidity_pool_amount * second_token_liquidity_pool_amount

        new_first_token_liquidity_pool_amount = sqrt(liquidity_pool_constant *
                                                     market_price)
        new_second_token_liquidity_pool_amount = sqrt(liquidity_pool_constant /
                                                      market_price)

        return AttrDict({
            'exact_value':
            first_token_liquidity_pool_amount -
            Wad.from_number(new_first_token_liquidity_pool_amount),
            'limit':
            Wad.from_number(new_second_token_liquidity_pool_amount) -
            second_token_liquidity_pool_amount,
        })

    def set_price(self, market_price: Wad, first_token: Address,
                  second_token: Address,
                  max_delta_on_percent: int) -> Transact:
        pair = self.uniswap_router.get_pair(first_token=first_token,
                                            second_token=second_token)

        reserves = pair.reserves.map()

        uniswap_price = reserves[first_token] / reserves[second_token]
        delta = (market_price.value * 100 / uniswap_price.value) - 100

        self.logger.debug(f"market price = {market_price}")
        self.logger.debug(f"uniswap price = {uniswap_price}")
        self.logger.debug(
            f"the percentage difference between the market price and the uniswap price = {delta}"
        )

        if delta > max_delta_on_percent:
            self.logger.debug(
                "the price for uniswap is higher than the market price")
            input_data = self._get_amounts(
                market_price=market_price,
                first_token_liquidity_pool_amount=reserves[first_token],
                second_token_liquidity_pool_amount=reserves[second_token])

            calculate_amount = self.uniswap_router.get_amounts_out(
                amount_in=abs(input_data.exact_value),
                path=[first_token, second_token])
            calulate_price = abs(input_data.exact_value) / calculate_amount[-1]

            if abs(input_data.exact_value) > Wad.from_number(
                    self.arguments.max_first_token_amount_input):
                self.logger.info(
                    f"Amount to send first_token > maximum allowed ({abs(input_data.exact_value)} > {self.arguments.max_first_token_amount_input})"
                )
            elif abs(input_data.exact_value) > self.first_token.balance_of(
                    self.our_address):
                self.logger.warning(
                    f"There is not enough balance to change the price "
                    f"(required: {abs(input_data.exact_value)}), "
                    f"balance: {self.first_token.balance_of(self.our_address)}, "
                    f"token={self.first_token.address.address}")
            elif calulate_price > market_price:
                self.logger.info(
                    f"new calulate price > market price ({calulate_price} > {market_price}). The price will not be changed"
                )
            else:
                self.logger.info(
                    f"To change the price, you must perform an exchange ({abs(input_data.exact_value)} {first_token.address} -> {calculate_amount[-1]} {second_token.address})"
                )
                return self.uniswap_router.swap_from_exact_amount(
                    amount_in=abs(input_data.exact_value),
                    min_amount_out=calculate_amount[-1],
                    path=[first_token, second_token])

        elif delta < 0 and abs(delta) > max_delta_on_percent:
            self.logger.debug(
                "the market price is higher than the uniswap price")
            input_data = self._get_amounts(
                market_price=market_price,
                first_token_liquidity_pool_amount=reserves[first_token],
                second_token_liquidity_pool_amount=reserves[second_token])

            calculate_amount = self.uniswap_router.get_amounts_in(
                amount_out=abs(input_data.exact_value),
                path=[second_token, first_token])
            calulate_price = abs(input_data.exact_value) / calculate_amount[0]

            if calculate_amount[0] > Wad.from_number(
                    self.arguments.max_second_token_amount_input):
                self.logger.info(
                    f"Amount to send second_token > maximum allowed ({calculate_amount[0]} > {self.arguments.max_second_token_amount_input})"
                )
            elif calculate_amount[0] > self.second_token.balance_of(
                    self.our_address):
                self.logger.warning(
                    f"There is not enough balance to change the price "
                    f"(required: {calculate_amount[0]}), "
                    f"balance: {self.second_token.balance_of(self.our_address)}, "
                    f"token={self.second_token.address.address}")
            elif calulate_price < market_price:
                self.logger.info(
                    f"new calulate price < market price ({calulate_price} < {market_price}). The price will not be changed"
                )

            else:
                self.logger.info(
                    f"To change the price, you must perform an exchange ({calculate_amount[0]} {second_token.address} -> {abs(input_data.exact_value)} {first_token.address})"
                )
                return self.uniswap_router.swap_to_exact_amount(
                    amount_out=abs(input_data.exact_value),
                    max_amount_in=calculate_amount[0],
                    path=[second_token, first_token])
        else:
            self.logger.debug(
                "the price for uniswap satisfies the input accuracy. The price will not be changed"
            )

    def synchronize_price(self):
        # market_maker = MarketMaker(self.uniswap_router)

        # 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.")
            return

        if self.first_token.balance_of(self.our_address) < Wad.from_number(
                self.arguments.min_first_token_balance):
            self.logger.warning(
                f"Keeper {self.token_first.name} balance below minimum.")
            return

        if self.second_token.balance_of(self.our_address) < Wad.from_number(
                self.arguments.min_second_token_balance):
            self.logger.warning(
                f"Keeper {self.token_second.name} balance below minimum.")
            return

        target_price = self.price_feed.get_price()

        transaction = self.set_price(
            market_price=target_price.buy_price,
            first_token=self.first_token.address,
            second_token=self.second_token.address,
            max_delta_on_percent=self.max_delta_on_percent)

        if transaction is not None:
            transact = transaction.transact()
            if transact is not None and transact.successful:
                self.logger.info("The price was set successfully")
Exemplo n.º 7
0
    def get_account_token_balance(self, token: Token) -> Wad:
        assert (isinstance(token, Token))

        return token.normalize_amount(ERC20Token(web3=self.web3, address=token.address).balance_of(self.account_address))
Exemplo n.º 8
0
    def get_orders(self,
                   p_token: Token = None,
                   b_token: Token = None) -> List[Order]:
        """Get all active orders.

        If both `p_token` and `b_token` are specified, orders will be filtered by these.
        In case of the _MatchingMarket_ implementation, order enumeration will be much efficient
        if these two parameters are supplied, as then orders can be fetched using `getBestOffer`
        and a series of `getWorseOffer` calls. This approach will result in much lower number of calls
        comparing to the naive 0..get_last_order_id approach, especially if the number of inactive orders
        is very high.

        Either none or both of these parameters have to be specified.

        Args:
            `p_token`: Token object (see `model.py`) of the `pay_token` to filter the orders by.
            `b_token`: Token object (see `model.py`) of the `buy_token` to filter the orders by.

        Returns:
            A list of `Order` objects representing all active orders on Oasis.
        """

        assert ((isinstance(p_token, Token) and isinstance(b_token, Token))
                or ((p_token is None) and (b_token is None)))

        if (p_token is None) or (b_token is None):
            pay_token = None
            buy_token = None
        else:
            pay_token = p_token.address
            buy_token = b_token.address

        if pay_token is not None and buy_token is not None:
            orders = []

            if self._support_contract:
                result = self._support_contract.functions.getOffers(
                    self.address.address, pay_token.address,
                    buy_token.address).call()

                while True:
                    count = 0
                    for i in range(0, 100):
                        if result[3][
                                i] != '0x0000000000000000000000000000000000000000':
                            count += 1

                            orders.append(
                                Order(market=self,
                                      order_id=result[0][i],
                                      maker=Address(result[3][i]),
                                      pay_token=pay_token,
                                      pay_amount=p_token.normalize_amount(
                                          Wad(result[1][i])),
                                      buy_token=buy_token,
                                      buy_amount=b_token.normalize_amount(
                                          Wad(result[2][i])),
                                      timestamp=result[4][i]))

                    if count == 100:
                        next_order_id = self._contract.functions.getWorseOffer(
                            orders[-1].order_id).call()
                        result = self._support_contract.functions.getOffers(
                            self.address.address, next_order_id).call()

                    else:
                        break

            else:
                order_id = self._contract.functions.getBestOffer(
                    pay_token.address, buy_token.address).call()
                while order_id != 0:
                    order = self.get_order(order_id)
                    if order is not None:
                        orders.append(order)

                    order_id = self._contract.functions.getWorseOffer(
                        order_id).call()

            return sorted(orders, key=lambda order: order.order_id)
        else:
            return super(MatchingMarket, self).get_orders(pay_token, buy_token)
Exemplo n.º 9
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(
            "--endpoint-uri",
            type=str,
            help="JSON-RPC uri (example: `http://localhost:8545`)")

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

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

        parser.add_argument("--rpc-timeout",
                            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)

        if 'web3' in kwargs:
            self.web3 = kwargs['web3']
        elif self.arguments.endpoint_uri:
            self.web3: Web3 = web3_via_http(self.arguments.endpoint_uri,
                                            self.arguments.rpc_timeout)
        else:
            self.logger.warning(
                "Configuring node endpoint by host and port is deprecated; please use --endpoint-uri"
            )
            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)
        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.web3, 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 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
Exemplo n.º 10
0
class TestUniswapV2MarketMakerKeeper:

    Irouter_abi = Contract._load_abi(
        __name__,
        '../lib/pyexchange/pyexchange/abi/IUniswapV2Router02.abi')['abi']
    router_abi = Contract._load_abi(
        __name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Router02.abi')
    router_bin = Contract._load_bin(
        __name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Router02.bin')
    factory_abi = Contract._load_abi(
        __name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Factory.abi')
    factory_bin = Contract._load_bin(
        __name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Factory.bin')
    weth_abi = Contract._load_abi(__name__,
                                  '../lib/pyexchange/pyexchange/abi/WETH.abi')
    weth_bin = Contract._load_bin(__name__,
                                  '../lib/pyexchange/pyexchange/abi/WETH.bin')

    logger = logging.getLogger()

    def setup_method(self):

        # Use Ganache docker container
        self.web3 = Web3(HTTPProvider("http://0.0.0.0:8555"))
        self.web3.eth.defaultAccount = Web3.toChecksumAddress(
            "0x9596C16D7bF9323265C2F2E22f43e6c80eB3d943")
        self.our_address = Address(self.web3.eth.defaultAccount)

        self.private_key = "0x91cf2cc3671a365fcbf38010ff97ee31a5b7e674842663c56769e41600696ead"
        register_private_key(self.web3, self.private_key)

        self.weth_address = Contract._deploy(self.web3, self.weth_abi,
                                             self.weth_bin, [])
        self.factory_address = Contract._deploy(self.web3, self.factory_abi,
                                                self.factory_bin,
                                                [self.our_address.address])
        self.router_address = Contract._deploy(
            self.web3, self.router_abi, self.router_bin,
            [self.factory_address.address, self.weth_address.address])
        self._weth_contract = Contract._get_contract(self.web3, self.weth_abi,
                                                     self.weth_address)

        self.deploy_tokens()

        token_config = {
            "tokens": {
                "DAI": {
                    "tokenAddress": self.ds_dai.address.address
                },
                "KEEP": {
                    "tokenAddress": self.ds_keep.address.address
                },
                "LEV": {
                    "tokenAddress": self.ds_lev.address.address,
                    "tokenDecimals": 9
                },
                "USDC": {
                    "tokenAddress": self.ds_usdc.address.address,
                    "tokenDecimals": 6
                },
                "WBTC": {
                    "tokenAddress": self.ds_wbtc.address.address,
                    "tokenDecimals": 8
                },
                "WETH": {
                    "tokenAddress": self.weth_address.address
                }
            }
        }
        # write token config with locally deployed addresses to file
        with open("test-token-config.json", "w+") as outfile:
            outfile.write(json.dumps(token_config))

    def deploy_tokens(self):
        self.ds_dai = DSToken.deploy(self.web3, 'DAI')
        self.ds_keep = DSToken.deploy(self.web3, 'KEEP')
        self.ds_lev = DSToken.deploy(self.web3, 'LEV')
        self.ds_usdc = DSToken.deploy(self.web3, 'USDC')
        self.ds_wbtc = DSToken.deploy(self.web3, 'WBTC')

        self.token_dai = Token("DAI", self.ds_dai.address, 18)
        self.token_keep = Token("KEEP", self.ds_keep.address, 18)
        self.token_lev = Token("LEV", self.ds_lev.address, 9)
        self.token_usdc = Token("USDC", self.ds_usdc.address, 6)
        self.token_wbtc = Token("WBTC", self.ds_wbtc.address, 8)
        self.token_weth = Token("WETH", self.weth_address, 18)

    def mint_tokens(self):
        self.ds_dai.mint(
            Wad.from_number(500)).transact(from_address=self.our_address)
        self.ds_keep.mint(
            Wad.from_number(5000)).transact(from_address=self.our_address)
        self.ds_usdc.mint(
            self.token_usdc.unnormalize_amount(
                Wad.from_number(505))).transact(from_address=self.our_address)
        self.ds_wbtc.mint(
            self.token_wbtc.unnormalize_amount(
                Wad.from_number(15))).transact(from_address=self.our_address)

    def get_target_balances(self, pair: str) -> dict:
        assert (isinstance(pair, str))

        formatted_pair = "_".join(pair.split("-")).upper()
        token_a = formatted_pair.split("_")[0]
        token_b = formatted_pair.split("_")[1]

        return {
            "min_a": TARGET_AMOUNTS[f"{formatted_pair}_MIN_{token_a}"],
            "max_a": TARGET_AMOUNTS[f"{formatted_pair}_MAX_{token_a}"],
            "min_b": TARGET_AMOUNTS[f"{formatted_pair}_MIN_{token_b}"],
            "max_b": TARGET_AMOUNTS[f"{formatted_pair}_MAX_{token_b}"]
        }

    def instantiate_keeper(self, pair: str) -> UniswapV2MarketMakerKeeper:
        if pair == "DAI-USDC":
            feed_price = "fixed:1.01"
        elif pair == "ETH-DAI":
            feed_price = "fixed:420"
        elif pair == "WBTC-USDC":
            feed_price = "fixed:12000"
        elif pair == "KEEP-ETH":
            feed_price = "fixed:0.00291025"
        elif pair == "LEV-ETH":
            feed_price = "fixed:0.00024496"

        target_balances = self.get_target_balances(pair)

        return UniswapV2MarketMakerKeeper(args=args(
            f"--eth-from {self.our_address} --rpc-host http://localhost"
            f" --rpc-port 8545"
            f" --eth-key {self.private_key}"
            f" --pair {pair}"
            f" --accepted-price-slippage-up 50"
            f" --accepted-price-slippage-down 30"
            f" --target-a-min-balance {target_balances['min_a']}"
            f" --target-a-max-balance {target_balances['max_a']}"
            f" --target-b-min-balance {target_balances['min_b']}"
            f" --target-b-max-balance {target_balances['max_b']}"
            f" --token-config ./test-token-config.json"
            f" --router-address {self.router_address.address}"
            f" --factory-address {self.factory_address.address}"
            f" --initial-delay 3"
            f" --price-feed {feed_price}"),
                                          web3=self.web3)

    def test_calculate_token_liquidity_to_add(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("DAI-USDC")
        keeper.uniswap_current_exchange_price = Wad.from_number(
            PRICES.DAI_USDC_ADD_LIQUIDITY.value)

        # when
        dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai)
        usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)
        liquidity_to_add = keeper.calculate_liquidity_args(
            dai_balance, usdc_balance)

        # then
        assert all(map(lambda x: x > Wad(0), liquidity_to_add.values()))
        assert liquidity_to_add['amount_a_desired'] > liquidity_to_add[
            'amount_a_min']
        assert liquidity_to_add['amount_b_desired'] > liquidity_to_add[
            'amount_b_min']

    def test_calculate_eth_liquidity_to_add(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("ETH-DAI")
        keeper.uniswap_current_exchange_price = Wad.from_number(
            PRICES.ETH_DAI_ADD_LIQUIDITY.value)

        # when
        dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai)
        eth_balance = keeper.uniswap.get_account_eth_balance()

        liquidity_to_add = keeper.calculate_liquidity_args(
            eth_balance, dai_balance)

        # then
        assert all(map(lambda x: x > Wad(0), liquidity_to_add.values()))

        assert liquidity_to_add['amount_b_desired'] > liquidity_to_add[
            'amount_b_min']
        assert liquidity_to_add['amount_a_desired'] > liquidity_to_add[
            'amount_a_min']

    def test_should_ensure_adequate_eth_for_gas(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("ETH-DAI")

        # when
        dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai)
        liquidity_to_add = keeper.calculate_liquidity_args(
            Wad.from_number(0.5), dai_balance)

        # then
        assert liquidity_to_add is None

    def test_should_determine_add_liquidity(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("DAI-USDC")

        # when
        add_liquidity, remove_liquidity = keeper.determine_liquidity_action()

        # then
        assert add_liquidity == True
        assert remove_liquidity == False

    def test_should_add_dai_usdc_liquidity(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("DAI-USDC")

        initial_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        initial_usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)

        # when
        keeper_thread = threading.Thread(target=keeper.main,
                                         daemon=True).start()

        time.sleep(10)

        added_liquidity = keeper.calculate_liquidity_args(
            initial_dai_balance, initial_usdc_balance)

        # then
        exchange_dai_balance = keeper.uniswap.get_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        exchange_usdc_balance = keeper.uniswap.get_exchange_balance(
            self.token_usdc, keeper.uniswap.pair_address)
        final_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        final_usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)

        assert keeper.uniswap.get_our_exchange_balance(
            self.token_usdc, keeper.uniswap.pair_address) > Wad.from_number(0)
        assert keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address) > Wad.from_number(0)
        assert initial_dai_balance > final_dai_balance
        assert initial_usdc_balance > final_usdc_balance
        assert added_liquidity['amount_a_desired'] == exchange_dai_balance
        assert self.token_usdc.normalize_amount(
            added_liquidity['amount_b_desired']) == exchange_usdc_balance

    def test_should_add_wbtc_usdc_liquidity(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("WBTC-USDC")
        initial_wbtc_balance = keeper.uniswap.get_account_token_balance(
            self.token_wbtc)
        initial_usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)

        # when
        keeper_thread = threading.Thread(target=keeper.main,
                                         daemon=True).start()

        time.sleep(10)

        added_liquidity = keeper.calculate_liquidity_args(
            initial_wbtc_balance, initial_usdc_balance)

        # then
        exchange_wbtc_balance = keeper.uniswap.get_exchange_balance(
            self.token_wbtc, keeper.uniswap.pair_address)
        exchange_usdc_balance = keeper.uniswap.get_exchange_balance(
            self.token_usdc, keeper.uniswap.pair_address)
        final_wbtc_balance = keeper.uniswap.get_account_token_balance(
            self.token_wbtc)
        final_usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)

        assert initial_wbtc_balance > final_wbtc_balance
        assert initial_usdc_balance > final_usdc_balance
        assert self.token_wbtc.normalize_amount(
            added_liquidity['amount_a_desired']) == exchange_wbtc_balance
        assert self.token_usdc.normalize_amount(
            added_liquidity['amount_b_desired']) == exchange_usdc_balance

    def test_should_add_dai_eth_liquidity(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("ETH-DAI")
        dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai)
        eth_balance = keeper.uniswap.get_account_eth_balance()

        # when
        keeper_thread = threading.Thread(target=keeper.main,
                                         daemon=True).start()
        time.sleep(10)

        # then
        final_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        final_eth_balance = keeper.uniswap.get_account_eth_balance()

        assert dai_balance > final_dai_balance
        assert eth_balance > final_eth_balance
        assert keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address) > Wad.from_number(0)
        assert keeper.uniswap.get_our_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address) > Wad.from_number(0)

    def test_should_remove_dai_usdc_liquidity(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("DAI-USDC")
        initial_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        initial_usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)

        # when
        keeper_thread = threading.Thread(target=keeper.main,
                                         daemon=True).start()

        time.sleep(10)

        added_liquidity = keeper.calculate_liquidity_args(
            initial_dai_balance, initial_usdc_balance)

        post_add_exchange_dai_balance = keeper.uniswap.get_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_add_exchange_usdc_balance = keeper.uniswap.get_exchange_balance(
            self.token_usdc, keeper.uniswap.pair_address)
        post_add_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_add_usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)

        assert initial_dai_balance > post_add_dai_balance
        assert initial_usdc_balance > post_add_usdc_balance
        assert added_liquidity[
            'amount_a_desired'] == post_add_exchange_dai_balance
        assert self.token_usdc.normalize_amount(
            added_liquidity['amount_b_desired']
        ) == post_add_exchange_usdc_balance

        keeper.testing_feed_price = True
        keeper.test_price = Wad.from_number(
            PRICES.DAI_USDC_REMOVE_LIQUIDITY.value)

        time.sleep(10)

        post_remove_exchange_dai_balance = keeper.uniswap.get_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_remove_exchange_usdc_balance = keeper.uniswap.get_exchange_balance(
            self.token_usdc, keeper.uniswap.pair_address)
        post_remove_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_remove_usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)

        assert post_add_exchange_dai_balance > post_remove_exchange_dai_balance
        assert post_add_exchange_usdc_balance > post_remove_exchange_usdc_balance
        assert post_remove_dai_balance > post_add_dai_balance
        assert post_remove_usdc_balance > post_add_usdc_balance

    def test_should_remove_dai_eth_liquidity(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("ETH-DAI")
        initial_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        initial_eth_balance = keeper.uniswap.get_account_eth_balance()

        # when
        keeper_thread = threading.Thread(target=keeper.main,
                                         daemon=True).start()

        time.sleep(10)

        # then
        post_add_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_add_eth_balance = keeper.uniswap.get_account_eth_balance()
        post_add_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_add_exchange_weth_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address)

        assert initial_dai_balance > post_add_dai_balance
        assert initial_eth_balance > post_add_eth_balance

        keeper.testing_feed_price = True
        keeper.test_price = Wad.from_number(
            PRICES.ETH_DAI_REMOVE_LIQUIDITY.value)

        time.sleep(10)

        post_remove_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_remove_eth_balance = keeper.uniswap.get_account_eth_balance()
        post_remove_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_remove_exchange_weth_balance = keeper.uniswap.get_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address)

        assert post_remove_exchange_dai_balance < post_add_exchange_dai_balance
        assert post_remove_exchange_weth_balance < post_add_exchange_weth_balance
        assert post_remove_dai_balance > post_add_dai_balance
        assert post_remove_eth_balance > post_add_eth_balance

    def test_should_remove_liquidity_if_price_feed_is_null(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("ETH-DAI")
        initial_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        initial_eth_balance = keeper.uniswap.get_account_eth_balance()

        # when
        keeper_thread = threading.Thread(target=keeper.main,
                                         daemon=True).start()

        time.sleep(10)

        # then
        post_add_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_add_eth_balance = keeper.uniswap.get_account_eth_balance()
        post_add_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_add_exchange_weth_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address)

        assert post_add_exchange_dai_balance > Wad.from_number(0)
        assert post_add_exchange_weth_balance > Wad.from_number(0)
        assert initial_dai_balance > post_add_dai_balance
        assert initial_eth_balance > post_add_eth_balance

        # when
        keeper.testing_feed_price = True
        keeper.test_price = None
        keeper.price_feed_accepted_delay = 2

        time.sleep(25)

        # then
        post_remove_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_remove_eth_balance = keeper.uniswap.get_account_eth_balance()
        post_remove_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_remove_exchange_weth_balance = keeper.uniswap.get_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address)

        assert post_remove_exchange_dai_balance < post_add_exchange_dai_balance
        assert post_remove_exchange_weth_balance < post_add_exchange_weth_balance
        assert post_remove_dai_balance > post_add_dai_balance
        assert post_remove_eth_balance > post_add_eth_balance

    @unittest.skip
    def test_should_remove_liquidity_if_shutdown_signal_received(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("ETH-DAI")
        initial_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        initial_eth_balance = keeper.uniswap.get_account_eth_balance()

        # when
        # keeper_thread = threading.Thread(target=keeper.main, daemon=True).start()
        keeper_process = Process(target=keeper.main, daemon=True).start()
        time.sleep(10)

        # then
        post_add_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_add_eth_balance = keeper.uniswap.get_account_eth_balance()
        post_add_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_add_exchange_weth_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address)

        assert post_add_exchange_dai_balance > Wad.from_number(0)
        assert post_add_exchange_weth_balance > Wad.from_number(0)
        assert initial_dai_balance > post_add_dai_balance
        assert initial_eth_balance > post_add_eth_balance

        # when
        # send system interrupt signal to the process and wait for shutdown
        # pid = os.getpid()
        pid = keeper_process.current_process().pid
        os.kill(pid, signal.SIGINT)
        time.sleep(10)

        # then
        post_remove_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_remove_eth_balance = keeper.uniswap.get_account_eth_balance()
        post_remove_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_remove_exchange_weth_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address)

        assert post_add_exchange_weth_balance > post_remove_exchange_dai_balance
        assert post_add_exchange_weth_balance > post_remove_exchange_weth_balance

    def test_should_remove_liquidity_if_target_amounts_are_breached(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("KEEP-ETH")
        initial_keep_balance = keeper.uniswap.get_account_token_balance(
            self.token_keep)
        initial_eth_balance = keeper.uniswap.get_account_eth_balance()

        # when
        keeper_thread = threading.Thread(target=keeper.main,
                                         daemon=True).start()
        time.sleep(10)

        # then
        post_add_keep_balance = keeper.uniswap.get_account_token_balance(
            self.token_keep)
        post_add_eth_balance = keeper.uniswap.get_account_eth_balance()
        post_add_exchange_keep_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_keep, keeper.uniswap.pair_address)
        post_add_exchange_weth_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address)

        assert initial_keep_balance > post_add_keep_balance
        assert initial_eth_balance > post_add_eth_balance

        # when
        # execute a swap that will break the balances target amount and wait for removal
        eth_to_swap = Wad.from_number(15)
        min_amount_out = keeper.uniswap.get_amounts_out(
            eth_to_swap, [self.token_weth, self.token_keep])

        keeper.uniswap.swap_exact_eth_for_tokens(
            eth_to_swap, min_amount_out[1],
            [self.token_weth.address.address, self.token_keep.address.address
             ]).transact()
        time.sleep(25)

        # then
        post_remove_keep_balance = keeper.uniswap.get_account_token_balance(
            self.token_keep)
        post_remove_eth_balance = keeper.uniswap.get_account_eth_balance()

        assert post_remove_keep_balance > post_add_keep_balance
        assert post_remove_eth_balance > post_add_eth_balance
        assert initial_keep_balance > post_remove_keep_balance
        assert initial_eth_balance > post_remove_eth_balance