class OasisMarketMakerCancel:
    """Tool to cancel all our open orders on OasisDEX."""

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='oasis-market-maker-cancel')
        parser.add_argument("--rpc-host", help="JSON-RPC host (default: `localhost')", default="localhost", type=str)
        parser.add_argument("--rpc-port", help="JSON-RPC port (default: `8545')", default=8545, type=int)
        parser.add_argument("--rpc-timeout", help="JSON-RPC timeout (in seconds, default: 10)", default=10, type=int)
        parser.add_argument("--eth-from", help="Ethereum account from which to send transactions", required=True, type=str)
        parser.add_argument("--oasis-address", help="Ethereum address of the OasisDEX contract", required=True, type=str)
        parser.add_argument("--gas-price", help="Gas price in Wei (default: node default)", default=0, type=int)
        self.arguments = parser.parse_args(args)

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

        logging.basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s', level=logging.INFO)

    def main(self):
        self.cancel_orders(self.our_orders(self.otc.get_orders()))

    def our_orders(self, orders: list):
        return list(filter(lambda order: order.maker == self.our_address, orders))

    def cancel_orders(self, orders: list):
        synchronize([self.otc.kill(order.order_id).transact_async(gas_price=self.gas_price()) for order in orders])

    def gas_price(self):
        if self.arguments.gas_price > 0:
            return FixedGasPrice(self.arguments.gas_price)
        else:
            return DefaultGasPrice()
Example #2
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='oasis-market-maker-cancel')
        parser.add_argument("--endpoint-uri", type=str,
                            help="JSON-RPC uri (example: `http://localhost:8545`)")
        parser.add_argument("--rpc-host", default="localhost", type=str, help="[DEPRECATED] JSON-RPC host (default: `localhost')")
        parser.add_argument("--rpc-port", default=8545, type=int, help="[DEPRECATED] JSON-RPC port (default: `8545')")
        parser.add_argument("--rpc-timeout", help="JSON-RPC timeout (in seconds, default: 10)", default=10, type=int)
        parser.add_argument("--eth-from", help="Ethereum account from which to send transactions", required=True, type=str)
        parser.add_argument("--eth-key", type=str, nargs='*', help="Ethereum private key(s) to use")
        parser.add_argument("--oasis-address", help="Ethereum address of the OasisDEX contract", required=True, type=str)
        parser.add_argument("--gas-price", help="Gas price in Wei (default: node default)", default=0, type=int)
        self.arguments = parser.parse_args(args)

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

        self.web3.eth.defaultAccount = self.arguments.eth_from
        self.our_address = Address(self.arguments.eth_from)
        register_keys(self.web3, self.arguments.eth_key)
        self.otc = MatchingMarket(web3=self.web3, address=Address(self.arguments.oasis_address))

        logging.basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s', level=logging.INFO)
Example #3
0
 def __init__(self, web3):
     self.web3 = web3
     self.oasis = MatchingMarket(web3=web3, address=Address(oasis_addr))
     self.tokens = {}
     for token in tokens:
         self.tokens[token] = ERC20Token(web3, Address(tokens[token]))
     # Pop GNT because its not an ERC20 token
     self.tokens.pop('GNT')
Example #4
0
 def test_fail_when_no_support_contract_under_that_address(self):
     # expect
     with pytest.raises(Exception):
         MatchingMarket(web3=self.web3,
                        address=self.otc.address,
                        support_address=Address(
                            '0xdeadadd1e5500000000000000000000000000000'))
Example #5
0
class OasisMarketMakerCancel:
    """Tool to cancel all our open orders on OasisDEX."""

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='oasis-market-maker-cancel')
        parser.add_argument("--endpoint-uri", type=str,
                            help="JSON-RPC uri (example: `http://localhost:8545`)")
        parser.add_argument("--rpc-host", default="localhost", type=str, help="[DEPRECATED] JSON-RPC host (default: `localhost')")
        parser.add_argument("--rpc-port", default=8545, type=int, help="[DEPRECATED] JSON-RPC port (default: `8545')")
        parser.add_argument("--rpc-timeout", help="JSON-RPC timeout (in seconds, default: 10)", default=10, type=int)
        parser.add_argument("--eth-from", help="Ethereum account from which to send transactions", required=True, type=str)
        parser.add_argument("--eth-key", type=str, nargs='*', help="Ethereum private key(s) to use")
        parser.add_argument("--oasis-address", help="Ethereum address of the OasisDEX contract", required=True, type=str)
        parser.add_argument("--gas-price", help="Gas price in Wei (default: node default)", default=0, type=int)
        self.arguments = parser.parse_args(args)

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

        self.web3.eth.defaultAccount = self.arguments.eth_from
        self.our_address = Address(self.arguments.eth_from)
        register_keys(self.web3, self.arguments.eth_key)
        self.otc = MatchingMarket(web3=self.web3, address=Address(self.arguments.oasis_address))

        logging.basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s', level=logging.INFO)

    def main(self):
        self.cancel_orders(self.our_orders(self.otc.get_orders()))

    def our_orders(self, orders: list):
        return list(filter(lambda order: order.maker == self.our_address, orders))

    def cancel_orders(self, orders: list):
        synchronize([self.otc.kill(order.order_id).transact_async(gas_price=self.gas_price()) for order in orders])

    def gas_price(self):
        if self.arguments.gas_price > 0:
            return FixedGasPrice(self.arguments.gas_price)
        else:
            return DefaultGasPrice()
Example #6
0
 def setup_method(self):
     GeneralMarketTest.setup_method(self)
     self.otc = MatchingMarket.deploy(self.web3, 2500000000)
     self.otc.add_token_pair_whitelist(self.token1.address,
                                       self.token2.address).transact()
     self.otc.add_token_pair_whitelist(self.token1.address,
                                       self.token3.address).transact()
     self.otc.add_token_pair_whitelist(self.token2.address,
                                       self.token3.address).transact()
Example #7
0
    def setup_method(self):
        GeneralMarketTest.setup_method(self)

        support_abi = Contract._load_abi(__name__, '../pymaker/abi/MakerOtcSupportMethods.abi')
        support_bin = Contract._load_bin(__name__, '../pymaker/abi/MakerOtcSupportMethods.bin')
        support_address = Contract._deploy(self.web3, support_abi, support_bin, [])

        self.price_oracle = OasisMockPriceOracle.deploy(self.web3)
        self.otc = MatchingMarket.deploy(self.web3, self.token1.address, Wad(0), self.price_oracle.address, support_address)
        self.otc.add_token_pair_whitelist(self.token1.address, self.token2.address).transact()
        self.otc.add_token_pair_whitelist(self.token1.address, self.token3.address).transact()
        self.otc.add_token_pair_whitelist(self.token2.address, self.token3.address).transact()
Example #8
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='oasis-market-maker-cancel')
        parser.add_argument("--rpc-host",
                            help="JSON-RPC host (default: `localhost')",
                            default="localhost",
                            type=str)
        parser.add_argument("--rpc-port",
                            help="JSON-RPC port (default: `8545')",
                            default=8545,
                            type=int)
        parser.add_argument(
            "--eth-from",
            help="Ethereum account from which to send transactions",
            required=True,
            type=str)
        parser.add_argument("--oasis-address",
                            help="Ethereum address of the OasisDEX contract",
                            required=True,
                            type=str)
        parser.add_argument("--gas-price",
                            help="Gas price in Wei (default: node default)",
                            default=0,
                            type=int)
        self.arguments = parser.parse_args(args)

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

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=logging.INFO)
Example #9
0
 def setup_method(self):
     self.web3 = Web3(EthereumTesterProvider())
     self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
     self.our_address = Address(self.web3.eth.defaultAccount)
     self.token1 = DSToken.deploy(self.web3, 'AAA')
     self.token1.mint(Wad.from_number(10000)).transact()
     self.token2 = DSToken.deploy(self.web3, 'BBB')
     self.token2.mint(Wad.from_number(10000)).transact()
     self.otc = MatchingMarket.deploy(self.web3, 2500000000)
     self.otc.add_token_pair_whitelist(self.token1.address, self.token2.address).transact()
     self.otc.approve([self.token1, self.token2], directly())
     for amount in [11,55,44,34,36,21,45,51,15]:
         self.otc.make(pay_token=self.token1.address, pay_amount=Wad.from_number(1),
                       buy_token=self.token2.address, buy_amount=Wad.from_number(amount)).transact()
Example #10
0
 def setup_method(self):
     self.web3 = Web3(HTTPProvider("http://localhost:8555"))
     self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
     self.our_address = Address(self.web3.eth.defaultAccount)
     self.token1 = DSToken.deploy(self.web3, 'AAA')
     self.token1.mint(Wad.from_number(10000)).transact()
     self.token1_tokenclass = Token('AAA', self.token1.address, 18)
     self.token2 = DSToken.deploy(self.web3, 'BBB')
     self.token2.mint(Wad.from_number(10000)).transact()
     self.token2_tokenclass = Token('BBB', self.token2.address, 18)
     price_oracle = OasisMockPriceOracle.deploy(self.web3)
     self.otc = MatchingMarket.deploy(self.web3, self.token1.address, Wad(0), price_oracle.address)
     self.otc.add_token_pair_whitelist(self.token1.address, self.token2.address).transact()
     self.otc.approve([self.token1, self.token2], directly())
     for amount in [11,55,44,34,36,21,45,51,15]:
         self.otc.make(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
                       b_token=self.token2_tokenclass, buy_amount=Wad.from_number(amount)).transact()
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='oasis-market-maker-cancel')
        parser.add_argument("--rpc-host", help="JSON-RPC host (default: `localhost')", default="localhost", type=str)
        parser.add_argument("--rpc-port", help="JSON-RPC port (default: `8545')", default=8545, type=int)
        parser.add_argument("--rpc-timeout", help="JSON-RPC timeout (in seconds, default: 10)", default=10, type=int)
        parser.add_argument("--eth-from", help="Ethereum account from which to send transactions", required=True, type=str)
        parser.add_argument("--oasis-address", help="Ethereum address of the OasisDEX contract", required=True, type=str)
        parser.add_argument("--gas-price", help="Gas price in Wei (default: node default)", default=0, type=int)
        self.arguments = parser.parse_args(args)

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

        logging.basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s', level=logging.INFO)
Example #12
0
    def setup_method(self):
        self.web3 = Web3(HTTPProvider("http://localhost:8555"))
        self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
        self.our_address = Address(self.web3.eth.defaultAccount)
        self.token1 = DSToken.deploy(self.web3, 'AAA')
        self.token1_tokenclass = Token('AAA', self.token1.address, 18)
        self.token1.mint(Wad.from_number(10000)).transact()
        self.token2 = DSToken.deploy(self.web3, 'BBB')
        self.token2_tokenclass = Token('BBB', self.token2.address, 6)
        self.token2.mint(Wad.from_number(10000)).transact()

        support_abi = Contract._load_abi(__name__, '../pymaker/abi/MakerOtcSupportMethods.abi')
        support_bin = Contract._load_bin(__name__, '../pymaker/abi/MakerOtcSupportMethods.bin')
        support_address = Contract._deploy(self.web3, support_abi, support_bin, [])

        price_oracle = OasisMockPriceOracle.deploy(self.web3)
        self.otc = MatchingMarket.deploy(self.web3, self.token1.address, Wad(0), price_oracle.address, support_address)
        self.otc.add_token_pair_whitelist(self.token1.address, self.token2.address).transact()
        self.otc.approve([self.token1, self.token2], directly())
Example #13
0
    def main(self):
        total = {}
        member_balances = {}

        for member in self.members:
            if member['type'] == 'oasis-market-maker-keeper':
                member_balances[member['id']] = {'address': '', 'balances': {}}
                member_balances[member['id']]['address'] = member['config'][
                    'marketMakerAddress']
                oasis = MatchingMarket(
                    self.web3, Address(member['config']['oasisAddress']))
                member_balances[member['id']]['balances'] = self.get_balance(
                    oasis, member)

                total = self.add_member_to_total(total,
                                                 member_balances[member['id']])

        print(f"--- MEMBERS ---: ")
        pprint(member_balances)
        print(f"--- TOTALS ---: ")
        pprint(total)
    def __init__(self, args, **kwargs):
        """Pass in arguements assign necessary variables/objects and instantiate other Classes"""

        parser = argparse.ArgumentParser("simple-arbitrage-keeper")

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

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

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

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help=
            "Ethereum address from which to send transactions; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            required=True,
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=/path/to/keystore.json,pass_file=/path/to/passphrase.txt')"
        )

        parser.add_argument(
            "--uniswap-entry-exchange",
            type=str,
            required=True,
            help=
            "Ethereum address of the Uniswap Exchange contract for the entry token market; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--uniswap-arb-exchange",
            type=str,
            required=True,
            help=
            "Ethereum address of the Uniswap Exchange contract for the arb token market; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--oasis-address",
            type=str,
            required=True,
            help=
            "Ethereum address of the OasisDEX contract; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--oasis-api-endpoint",
            type=str,
            required=True,
            help=
            "Endpoint of of the Oasis V2 REST API (e.g. 'https://kovan-api.oasisdex.com' )"
        )

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

        parser.add_argument(
            "--tx-manager",
            type=str,
            required=True,
            help=
            "Ethereum address of the TxManager contract to use for multi-step arbitrage; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--gas-price",
            type=int,
            default=0,
            help=
            "Gas price in Wei (default: node default), (e.g. 1000000000 for 1 GWei)"
        )

        parser.add_argument(
            "--entry-token",
            type=str,
            required=True,
            help=
            "The token address that the bot starts and ends with in every transaction; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--arb-token",
            type=str,
            required=True,
            help=
            "The token address that arbitraged between both exchanges; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--arb-token-name",
            type=str,
            required=True,
            help=
            "The token name that arbitraged between both exchanges (e.g. 'SAI', 'WETH', 'REP')"
        )

        parser.add_argument(
            "--min-profit",
            type=int,
            required=True,
            help=
            "Ether amount of minimum profit (in base token) from one arbitrage operation (e.g. 1 for 1 Sai min profit)"
        )

        parser.add_argument(
            "--max-engagement",
            type=int,
            required=True,
            help=
            "Ether amount of maximum engagement (in base token) in one arbitrage operation (e.g. 100 for 100 Sai max engagement)"
        )

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

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

        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"https://{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.sai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359'))  # Mainnet Sai
        self.dai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0x6b175474e89094c44da98b954eedeac495271d0f'))  # Mainnet Dai

        self.ksai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0xC4375B7De8af5a38a93548eb8453a498222C4fF2'))  #Kovan Sai
        self.kdai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa'))  #Kovan Dai

        self.entry_token = ERC20Token(web3=self.web3,
                                      address=Address(
                                          self.arguments.entry_token))
        self.arb_token = ERC20Token(web3=self.web3,
                                    address=Address(self.arguments.arb_token))
        self.arb_token.name = self.arguments.arb_token_name \
            if self.arguments.arb_token_name != 'WETH' else 'ETH'


        self.uniswap_entry_exchange = UniswapWrapper(self.web3, self.entry_token.address, Address(self.arguments.uniswap_entry_exchange)) \
            if self.arguments.uniswap_entry_exchange is not None else None

        self.uniswap_arb_exchange = UniswapWrapper(self.web3, self.arb_token.address, Address(self.arguments.uniswap_arb_exchange)) \
            if self.arguments.uniswap_arb_exchange is not None else None

        self.oasis_api_endpoint = OasisAPI(api_server=self.arguments.oasis_api_endpoint,
                                           entry_token_name=self.token_name(self.entry_token.address),
                                           arb_token_name=self.arb_token.name) \
            if self.arguments.oasis_api_endpoint is not None else None

        self.oasis = MatchingMarket(web3=self.web3,
                                    address=Address(
                                        self.arguments.oasis_address))

        self.min_profit = Wad(int(self.arguments.min_profit * 10**18))
        self.max_engagement = Wad(int(self.arguments.max_engagement * 10**18))
        self.max_errors = self.arguments.max_errors
        self.errors = 0

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

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))
class SimpleArbitrageKeeper:
    """Keeper to arbitrage on OasisDEX and Uniswap"""

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

    def __init__(self, args, **kwargs):
        """Pass in arguements assign necessary variables/objects and instantiate other Classes"""

        parser = argparse.ArgumentParser("simple-arbitrage-keeper")

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

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

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

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help=
            "Ethereum address from which to send transactions; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            required=True,
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=/path/to/keystore.json,pass_file=/path/to/passphrase.txt')"
        )

        parser.add_argument(
            "--uniswap-entry-exchange",
            type=str,
            required=True,
            help=
            "Ethereum address of the Uniswap Exchange contract for the entry token market; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--uniswap-arb-exchange",
            type=str,
            required=True,
            help=
            "Ethereum address of the Uniswap Exchange contract for the arb token market; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--oasis-address",
            type=str,
            required=True,
            help=
            "Ethereum address of the OasisDEX contract; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--oasis-api-endpoint",
            type=str,
            required=True,
            help=
            "Endpoint of of the Oasis V2 REST API (e.g. 'https://kovan-api.oasisdex.com' )"
        )

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

        parser.add_argument(
            "--tx-manager",
            type=str,
            required=True,
            help=
            "Ethereum address of the TxManager contract to use for multi-step arbitrage; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--gas-price",
            type=int,
            default=0,
            help=
            "Gas price in Wei (default: node default), (e.g. 1000000000 for 1 GWei)"
        )

        parser.add_argument(
            "--entry-token",
            type=str,
            required=True,
            help=
            "The token address that the bot starts and ends with in every transaction; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--arb-token",
            type=str,
            required=True,
            help=
            "The token address that arbitraged between both exchanges; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--arb-token-name",
            type=str,
            required=True,
            help=
            "The token name that arbitraged between both exchanges (e.g. 'SAI', 'WETH', 'REP')"
        )

        parser.add_argument(
            "--min-profit",
            type=int,
            required=True,
            help=
            "Ether amount of minimum profit (in base token) from one arbitrage operation (e.g. 1 for 1 Sai min profit)"
        )

        parser.add_argument(
            "--max-engagement",
            type=int,
            required=True,
            help=
            "Ether amount of maximum engagement (in base token) in one arbitrage operation (e.g. 100 for 100 Sai max engagement)"
        )

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

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

        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"https://{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.sai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359'))  # Mainnet Sai
        self.dai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0x6b175474e89094c44da98b954eedeac495271d0f'))  # Mainnet Dai

        self.ksai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0xC4375B7De8af5a38a93548eb8453a498222C4fF2'))  #Kovan Sai
        self.kdai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa'))  #Kovan Dai

        self.entry_token = ERC20Token(web3=self.web3,
                                      address=Address(
                                          self.arguments.entry_token))
        self.arb_token = ERC20Token(web3=self.web3,
                                    address=Address(self.arguments.arb_token))
        self.arb_token.name = self.arguments.arb_token_name \
            if self.arguments.arb_token_name != 'WETH' else 'ETH'


        self.uniswap_entry_exchange = UniswapWrapper(self.web3, self.entry_token.address, Address(self.arguments.uniswap_entry_exchange)) \
            if self.arguments.uniswap_entry_exchange is not None else None

        self.uniswap_arb_exchange = UniswapWrapper(self.web3, self.arb_token.address, Address(self.arguments.uniswap_arb_exchange)) \
            if self.arguments.uniswap_arb_exchange is not None else None

        self.oasis_api_endpoint = OasisAPI(api_server=self.arguments.oasis_api_endpoint,
                                           entry_token_name=self.token_name(self.entry_token.address),
                                           arb_token_name=self.arb_token.name) \
            if self.arguments.oasis_api_endpoint is not None else None

        self.oasis = MatchingMarket(web3=self.web3,
                                    address=Address(
                                        self.arguments.oasis_address))

        self.min_profit = Wad(int(self.arguments.min_profit * 10**18))
        self.max_engagement = Wad(int(self.arguments.max_engagement * 10**18))
        self.max_errors = self.arguments.max_errors
        self.errors = 0

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

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

    def main(self):
        """ Initialize the lifecycle and enter into the Keeper Lifecycle controller

        Each function supplied by the lifecycle will accept a callback function that will be executed.
        The lifecycle.on_block() function will enter into an infinite loop, but will gracefully shutdown
        if it recieves a SIGINT/SIGTERM signal.

        """

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

    def startup(self):
        self.approve()

    def approve(self):
        """ Approve all components that need to access our balances

        Approve Oasis to access our tokens from our TxManager
        Approve Uniswap exchanges to access our tokens that they swap from our TxManager
        Approve TxManager to access our tokens from our ETH_FROM address

        """
        approval_method = via_tx_manager(self.tx_manager, gas_price=self.gas_price()) if self.tx_manager \
            else directly(gas_price=self.gas_price())

        self.oasis.approve([self.entry_token, self.arb_token], approval_method)

        if self.uniswap_entry_exchange:
            self.uniswap_entry_exchange.approve([self.entry_token],
                                                approval_method)

        if self.uniswap_arb_exchange:
            self.uniswap_arb_exchange.approve([self.arb_token],
                                              approval_method)

        if self.tx_manager:
            self.tx_manager.approve([self.entry_token, self.arb_token],
                                    directly(gas_price=self.gas_price()))

    def token_name(self, address: Address) -> str:
        if address == self.ksai.address or address == self.sai.address:
            return "SAI"
        elif address == self.kdai.address or address == self.dai.address:
            return "DAI"
        else:
            return str(address)

    def oasis_order_size(self, size: Wad = None):
        """ Calculate the an oasis order buy size when buying/selling the arb_token

        Query's the Oasis REST API and, if a `size` is not supplied, calculates a total amount of arb_token to be purchased.
        However, if size is supplied, it will calculate the total amount of entry_token that is purchased

        Args:
            size: The size of the arb_token that will be sold

        Returns:
            A :py:class:`pymaker.numeric.Wad` instance of either a final entry_token amount to be bought
            or a final arb_token amount to be bought
        """

        if self.oasis_api_endpoint is None:
            return None

        (bids, asks) = self.oasis_api_endpoint.get_orders()

        entry_token_amount = 0
        arb_token_amount = 0

        if size is None:
            for order in asks:
                entry_token_amount = entry_token_amount + order[0] * order[1]
                arb_token_amount = arb_token_amount + order[1]

                if entry_token_amount >= float(self.entry_amount):
                    #some linear interpolation
                    final_arb_token_amount = arb_token_amount - order[1] + \
                        (float(self.entry_amount) - (entry_token_amount - order[0] * order[1])) * (1/order[0])
                    return Wad(int(final_arb_token_amount * 10**18))

        else:
            for order in bids:
                entry_token_amount = entry_token_amount + order[0] * order[1]
                arb_token_amount = arb_token_amount + order[1]

                if arb_token_amount >= float(size):
                    #some linear interpolation
                    final_entry_token_amount = entry_token_amount - (order[0] * order[1]) + \
                        (float(size) - (arb_token_amount - order[1])) * (order[0])
                    return Wad(int(final_entry_token_amount * 10**18))

    def uniswap_order_size(self, size: Wad = None):
        """ Calculate the an Uniswap buy size when buying/selling the arb_token

        Reads the Uniswap entry, arb and eth exchange contracts, and if a `size` is not supplied,
        calculates a total amount of arb_token to be purchased. However, if size is supplied,
        it will calculate the total amount of entry_token that is purchased

        Args:
            size: The size of the arb_token that will be sold

        Returns:
            A :py:class:`pymaker.numeric.Wad` instance of either a final entry_token amount to be bought
            or a final arb_token amount to be bought
        """
        if size is None:
            ethAmt = self.uniswap_entry_exchange.uniswap_base.get_token_eth_input_price(
                self.entry_amount)
            arbAmt = self.uniswap_arb_exchange.uniswap_base.get_eth_token_input_price(
                ethAmt)
            return Wad(arbAmt)
        else:
            ethAmt = self.uniswap_arb_exchange.uniswap_base.get_token_eth_input_price(
                size)
            entryAmt = self.uniswap_entry_exchange.uniswap_base.get_eth_token_input_price(
                ethAmt)
            return Wad(entryAmt)

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

    def find_best_opportunity_available(self):
        """Find the best arbitrage opportunity present and execute it.

        With an entry_token of entry_amount, calculate the profitability of buying an arb_token
        on Oasis and selling it on Uniswap. Calculate the profitability of the same operation
        but starting on Uniswap. Depending on the comparison between these profitabilities,
        assign the variables pertaining to start_exchange and end_exchange.

        If the highestProfit is beyond the minimum profit as set by the user, print the opportunity
        and attempt to execute it in one transaction
        """

        self.entry_amount = Wad.min(
            self.entry_token.balance_of(self.our_address), self.max_engagement)

        oasis_arb_amount = self.oasis_order_size()
        profit_oasis_to_uniswap = self.uniswap_order_size(
            oasis_arb_amount) - self.entry_amount

        uniswap_arb_amount = self.uniswap_order_size()
        profit_uniswap_to_oasis = self.oasis_order_size(
            uniswap_arb_amount) - self.entry_amount

        if profit_oasis_to_uniswap > profit_uniswap_to_oasis:
            self.start_exchange, self.start_exchange.name = self.oasis, 'Oasis'
            self.arb_amount = oasis_arb_amount * Wad.from_number(0.999999)
            self.end_exchange, self.end_exchange.name = self.uniswap_arb_exchange, 'Uniswap'

        else:
            self.start_exchange, self.start_exchange.name = self.uniswap_entry_exchange, 'Uniswap'
            self.arb_amount = uniswap_arb_amount * Wad.from_number(0.999999)
            self.end_exchange, self.end_exchange.name = self.oasis, 'Oasis'

        highestProfit = max(profit_oasis_to_uniswap, profit_uniswap_to_oasis)
        self.exit_amount = (highestProfit +
                            self.entry_amount) * Wad.from_number(0.999999)

        #Print the highest profit/(loss) to see how close we come to breaking even
        self.logger.info(
            f"Best trade regardless of profit/min-profit: {highestProfit} {self.token_name(self.entry_token.address)} "
            f"from {self.start_exchange.name} to {self.end_exchange.name}")

        opportunity = highestProfit if highestProfit > self.min_profit else None

        if opportunity:
            self.print_opportunity(opportunity)
            self.execute_opportunity_in_one_transaction()

    def print_opportunity(self, opportunity: Wad):
        """Print the details of the opportunity."""
        self.logger.info(
            f"Profit opportunity of {opportunity} {self.token_name(self.entry_token.address)} "
            f"from {self.start_exchange.name} to {self.end_exchange.name}")

    def execute_opportunity_in_one_transaction(self):
        """Execute the opportunity in one transaction, using the `tx_manager`.

        Sell entry_token and buy arb_token on start_exchange
        Sell arb_token and buy entry_token on end_exchange

        """

        tokens = [self.entry_token.address, self.arb_token.address]

        invocations = [
            self.start_exchange.make(pay_token=self.entry_token.address,
                                     pay_amount=self.entry_amount,
                                     buy_token=self.arb_token.address,
                                     buy_amount=self.arb_amount).invocation(),
            self.end_exchange.make(pay_token=self.arb_token.address,
                                   pay_amount=self.arb_amount,
                                   buy_token=self.entry_token.address,
                                   buy_amount=self.exit_amount).invocation()
        ]

        receipt = self.tx_manager.execute(tokens, invocations).transact(
            gas_price=self.gas_price(), gas_buffer=300000)

        if receipt:
            self.logger.info(
                f"The profit we made is {TransferFormatter().format_net(receipt.transfers, self.our_address, self.token_name)}"
            )
        else:
            self.errors += 1

    def gas_price(self):
        """ FixedGasPrice if gas_price argument present, otherwise node DefaultGasPrice """
        if self.arguments.gas_price > 0:
            return FixedGasPrice(self.arguments.gas_price)
        else:
            return DefaultGasPrice()
class OasisMarketMakerKeeper:
    """Keeper acting as a market maker on OasisDEX."""

    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))
Example #17
0
class OasisMarket(Market):
    def __init__(self, web3):
        self.web3 = web3
        self.oasis = MatchingMarket(web3=web3, address=Address(oasis_addr))
        self.tokens = {}
        for token in tokens:
            self.tokens[token] = ERC20Token(web3, Address(tokens[token]))
        # Pop GNT because its not an ERC20 token
        self.tokens.pop('GNT')

    def cast_order(self, order, base_addr, quote_addr):
        base = Address(base_addr)
        quote = Address(quote_addr)

        sell_amount = order.pay_amount
        sell_token = order.pay_token
        buy_amount = order.buy_amount
        buy_token = order.buy_token
        oid = order.order_id

        side = None
        casted_order = {}

        # price = quote/base
        if sell_token == base and buy_token == quote:
            side = 'asks'
            casted_order = {
                'price': float(order.buy_to_sell_price),
                'amount': float(sell_amount),
                'id': oid,
            }
        if sell_token == quote and buy_token == base:
            side = 'bids'
            casted_order = {
                'price': float(order.sell_to_buy_price),
                'amount': float(sell_amount),
                'id': oid,
            }

        return (side, casted_order)

    def get_orders(self, base_addr, quote_addr):
        #TODO use filter to update book
        depth = {}
        depth['bids'] = []
        depth['asks'] = []
        for order in self.oasis.get_orders():
            (side, order) = self.cast_order(order, base_addr, quote_addr)
            if side:
                depth[side].append(order)

        depth['asks'].sort(key=lambda order: order['price'], reverse=True)
        depth['bids'].sort(key=lambda order: order['price'], reverse=True)
        return depth

    def get_pairs(self):
        #TODO use filter to update book
        pairs = []
        addr2token = {tokens[t]: t for t in tokens}
        for order in self.oasis.get_orders():
            sell_token = addr2token[str(order.pay_token)]
            buy_token = addr2token[str(order.buy_token)]
            pairs.append(f'{sell_token}/{buy_token}')

        # Get uniq paris
        pairs = list(set(pairs))
        return pairs

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

        return accounts
Example #18
0
    def __init__(self):
        web3 = Web3(HTTPProvider("http://localhost:8555"))
        web3.eth.defaultAccount = web3.eth.accounts[0]
        our_address = Address(web3.eth.defaultAccount)
        sai = DSToken.deploy(web3, 'DAI')
        sin = DSToken.deploy(web3, 'SIN')
        skr = DSToken.deploy(web3, 'PETH')
        gem = DSToken.deploy(web3, 'WETH')
        gov = DSToken.deploy(web3, 'MKR')
        pip = DSValue.deploy(web3)
        pep = DSValue.deploy(web3)
        pit = DSVault.deploy(web3)

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

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

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

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

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

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

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

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

        self.web3 = web3
        self.our_address = our_address
        self.sai = sai
        self.sin = sin
        self.skr = skr
        self.gem = gem
        self.gov = gov
        self.vox = vox
        self.tub = tub
        self.tap = tap
        self.top = top
        self.otc = otc
        self.etherdelta = etherdelta
Example #19
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='oasis-market-maker-keeper')

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.history = History()
        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(lambda: self.our_orders())
        self.order_book_manager.place_orders_with(self.place_order_function)
        self.order_book_manager.cancel_orders_with(self.cancel_order_function)
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()
    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 __init__(self, args, **kwargs):
        parser = argparse.ArgumentParser("arbitrage-keeper")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.arguments = parser.parse_args(args)

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

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

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

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

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

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

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))
Example #22
0
class OasisMarketMakerKeeper:
    """Keeper acting as a market maker on OasisDEX, on the W-ETH/SAI pair."""

    logger = logging.getLogger('oasis-market-maker-keeper')

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.arguments = parser.parse_args(args)

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

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

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

    def main(self):
        with Web3Lifecycle(self.web3) as lifecycle:
            self.lifecycle = lifecycle
            lifecycle.initial_delay(10)
            lifecycle.on_startup(self.startup)
            lifecycle.on_block(self.synchronize_orders)
            lifecycle.on_shutdown(self.shutdown)

    def startup(self):
        self.approve()

    def shutdown(self):
        self.cancel_all_orders()

    def approve(self):
        """Approve OasisDEX to access our balances, so we can place orders."""
        self.otc.approve([self.gem, self.sai],
                         directly(gas_price=self.gas_price))

    def our_orders(self):
        return list(
            filter(lambda order: order.maker == self.our_address,
                   self.otc.get_orders()))

    def our_sell_orders(self, our_orders: list):
        return list(
            filter(
                lambda order: order.buy_token == self.sai.address and order.
                pay_token == self.gem.address, our_orders))

    def our_buy_orders(self, our_orders: list):
        return list(
            filter(
                lambda order: order.buy_token == self.gem.address and order.
                pay_token == self.sai.address, our_orders))

    def synchronize_orders(self):
        # If market is closed, cancel all orders but do not terminate the keeper.
        if self.otc.is_closed():
            self.logger.warning("Marked is closed. Cancelling all orders.")
            self.cancel_all_orders()
            return

        # If keeper balance is below `--min-eth-balance`, cancel all orders but do not terminate
        # the keeper, keep processing blocks as the moment the keeper gets a top-up it should
        # resume activity straight away, without the need to restart it.
        if eth_balance(self.web3, self.our_address) < self.min_eth_balance:
            self.logger.warning(
                "Keeper ETH balance below minimum. Cancelling all orders.")
            self.cancel_all_orders()
            return

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

        # If the is no target price feed, cancel all orders but do not terminate the keeper.
        # The moment the price feed comes back, the keeper will resume placing orders.
        if target_price is None:
            self.logger.warning(
                "No price feed available. Cancelling all orders.")
            self.cancel_all_orders()
            return

        # If there are any orders to be cancelled, cancel them. It is deliberate that we wait with topping-up
        # bands until the next block. This way we would create new orders based on the most recent price and
        # order book state. We could theoretically retrieve both (`target_price` and `our_orders`) again here,
        # but it just seems cleaner to do it in one place instead of in two.
        orders_to_cancel = list(
            itertools.chain(
                bands.excessive_buy_orders(self.our_buy_orders(our_orders),
                                           target_price),
                bands.excessive_sell_orders(self.our_sell_orders(our_orders),
                                            target_price),
                bands.outside_orders(self.our_buy_orders(our_orders),
                                     self.our_sell_orders(our_orders),
                                     target_price)))
        if len(orders_to_cancel) > 0:
            self.cancel_orders(orders_to_cancel)
        else:
            self.top_up_bands(our_orders, bands.buy_bands, bands.sell_bands,
                              target_price)

            # We do wait some time after the orders have been created. The reason for that is sometimes
            # orders that have been just placed were not picked up by the next `our_orders()` call
            # (one can presume the block hasn't been fully imported into the node yet), which made
            # the keeper try to place same order(s) again. Of course the second transaction did fail, but it
            # resulted in wasted gas and significant delay in keeper operation.
            #
            # There is no specific reason behind choosing to wait exactly 7s.
            time.sleep(7)

    def cancel_all_orders(self):
        """Cancel all orders owned by the keeper."""
        self.cancel_orders(self.our_orders())

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

    def top_up_bands(self, our_orders: list, buy_bands: list, sell_bands: list,
                     target_price: Wad):
        """Asynchronously create new buy and sell orders in all send and buy bands if necessary."""
        synchronize([
            transact.transact_async(gas_price=self.gas_price)
            for transact in itertools.chain(
                self.top_up_buy_bands(our_orders, buy_bands, target_price),
                self.top_up_sell_bands(our_orders, sell_bands, target_price))
        ])

    def top_up_sell_bands(self, our_orders: list, sell_bands: list,
                          target_price: Wad):
        """Ensure our WETH engagement is not below minimum in all sell bands. Yield new orders if necessary."""
        our_balance = self.gem.balance_of(self.our_address)
        for band in sell_bands:
            orders = [
                order for order in self.our_sell_orders(our_orders)
                if band.includes(order, target_price)
            ]
            total_amount = self.total_amount(orders)
            if total_amount < band.min_amount:
                have_amount = Wad.min(band.avg_amount - total_amount,
                                      our_balance)
                want_amount = have_amount * round(band.avg_price(target_price),
                                                  self.arguments.round_places)
                if (have_amount >= band.dust_cutoff) and (
                        have_amount > Wad(0)) and (want_amount > Wad(0)):
                    our_balance = our_balance - have_amount
                    yield self.otc.make(pay_token=self.gem.address,
                                        pay_amount=have_amount,
                                        buy_token=self.sai.address,
                                        buy_amount=want_amount)

    def top_up_buy_bands(self, our_orders: list, buy_bands: list,
                         target_price: Wad):
        """Ensure our SAI engagement is not below minimum in all buy bands. Yield new orders if necessary."""
        our_balance = self.sai.balance_of(self.our_address)
        for band in buy_bands:
            orders = [
                order for order in self.our_buy_orders(our_orders)
                if band.includes(order, target_price)
            ]
            total_amount = self.total_amount(orders)
            if total_amount < band.min_amount:
                have_amount = Wad.min(band.avg_amount - total_amount,
                                      our_balance)
                want_amount = have_amount / round(band.avg_price(target_price),
                                                  self.arguments.round_places)
                if (have_amount >= band.dust_cutoff) and (
                        have_amount > Wad(0)) and (want_amount > Wad(0)):
                    our_balance = our_balance - have_amount
                    yield self.otc.make(pay_token=self.sai.address,
                                        pay_amount=have_amount,
                                        buy_token=self.gem.address,
                                        buy_amount=want_amount)

    @staticmethod
    def total_amount(orders: List[Order]):
        return reduce(operator.add, map(lambda order: order.pay_amount,
                                        orders), Wad(0))
    def __init__(self, args, **kwargs):
        parser = argparse.ArgumentParser("arbitrage-keeper")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        parser.add_argument("--ipcpath", type=str, required=True,
                            help="Local IPC Path")

        parser.add_argument("--eth-from-password", type=str, required=True,
                            help="Eth account password")

        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(IPCProvider(ipc_path=self.arguments.ipcpath))

        self.web3.eth.defaultAccount = self.arguments.eth_from

        if !self.web3.personal.unlockAccount(self.web3.eth.defaultAccount, self.arguments.eth_from_password):
            raise Exception(f"Incorrect account password")

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

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

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

        logging.basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s',
                            level=(logging.DEBUG if self.arguments.debug else logging.INFO))
class OasisMarketMakerKeeper:
    """Keeper acting as a market maker on OasisDEX, on the W-ETH/SAI pair."""

    logger = logging.getLogger()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.arguments = parser.parse_args(args)

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

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

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

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

    def startup(self):
        self.approve()

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

    def on_block(self):
        # This method is present only so the lifecycle binds the new block listener, which makes
        # it then terminate the keeper if no new blocks have been arriving for 300 seconds.
        pass

    def approve(self):
        """Approve OasisDEX to access our balances, so we can place orders."""
        self.otc.approve(
            [self.token_sell(), self.token_buy()],
            directly(gas_price=self.gas_price))

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

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

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

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

    def our_orders(self):
        return self.otc.get_orders_by_maker(self.our_address)

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

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

    def synchronize_orders(self):
        # If market is closed, cancel all orders but do not terminate the keeper.
        if self.otc.is_closed():
            self.logger.warning("Market is closed. Cancelling all orders.")
            self.cancel_all_orders()
            return

        # If keeper balance is below `--min-eth-balance`, cancel all orders but do not terminate
        # the keeper, keep processing blocks as the moment the keeper gets a top-up it should
        # resume activity straight away, without the need to restart it.
        if eth_balance(self.web3, self.our_address) < self.min_eth_balance:
            self.logger.warning(
                "Keeper ETH balance below minimum. Cancelling all orders.")
            self.cancel_all_orders()
            return

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

        # If the is no target price feed, cancel all orders but do not terminate the keeper.
        # The moment the price feed comes back, the keeper will resume placing orders.
        if target_price is None:
            self.logger.warning(
                "No price feed available. Cancelling all orders.")
            self.cancel_all_orders()
            return

        # If there are any orders to be cancelled, cancel them. It is deliberate that we wait with topping-up
        # bands until the next block. This way we would create new orders based on the most recent price and
        # order book state. We could theoretically retrieve both (`target_price` and `our_orders`) again here,
        # but it just seems cleaner to do it in one place instead of in two.
        cancellable_orders = bands.cancellable_orders(
            our_buy_orders=self.our_buy_orders(our_orders),
            our_sell_orders=self.our_sell_orders(our_orders),
            target_price=target_price)

        if len(cancellable_orders) > 0:
            self.cancel_orders(cancellable_orders)
            return

        # If there are any new orders to be created, create them.
        new_orders = bands.new_orders(
            our_buy_orders=self.our_buy_orders(our_orders),
            our_sell_orders=self.our_sell_orders(our_orders),
            our_buy_balance=self.our_balance(self.token_buy()),
            our_sell_balance=self.our_balance(self.token_sell()),
            target_price=target_price)

        if len(new_orders) > 0:
            self.create_orders(new_orders)

            # We do wait some time after the orders have been created. The reason for that is sometimes
            # orders that have been just placed were not picked up by the next `our_orders()` call
            # (one can presume the block hasn't been fully imported into the node yet), which made
            # the keeper try to place same order(s) again. Of course the second transaction did fail, but it
            # resulted in wasted gas and significant delay in keeper operation.
            #
            # There is no specific reason behind choosing to wait exactly 3s.
            time.sleep(3)

    def cancel_all_orders(self):
        """Cancel all orders owned by the keeper."""
        self.cancel_orders(self.our_orders())

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

    def create_orders(self, new_orders):
        """Create orders asynchronously."""
        def to_transaction(new_order: NewOrder):
            assert (isinstance(new_order, NewOrder))

            if new_order.is_sell:
                return self.otc.make(pay_token=self.token_sell().address,
                                     pay_amount=new_order.pay_amount,
                                     buy_token=self.token_buy().address,
                                     buy_amount=new_order.buy_amount)
            else:
                return self.otc.make(pay_token=self.token_buy().address,
                                     pay_amount=new_order.pay_amount,
                                     buy_token=self.token_sell().address,
                                     buy_amount=new_order.buy_amount)

        synchronize([
            transaction.transact_async(gas_price=self.gas_price)
            for transaction in map(to_transaction, new_orders)
        ])
Example #25
0
class OasisMarketMakerKeeper:
    """Keeper acting as a market maker on OasisDEX, on the W-ETH/SAI pair."""

    logger = logging.getLogger()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.order_book_manager = OrderBookManager(refresh_frequency=3)
        self.order_book_manager.get_orders_with(lambda: self.our_orders())
        self.order_book_manager.start()

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

    def startup(self):
        self.approve()

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

    def on_block(self):
        # This method is present only so the lifecycle binds the new block listener, which makes
        # it then terminate the keeper if no new blocks have been arriving for 300 seconds.
        pass

    def approve(self):
        """Approve OasisDEX to access our balances, so we can place orders."""
        self.otc.approve(
            [self.token_sell(), self.token_buy()],
            directly(gas_price=self.gas_price))

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

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

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

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

    def our_orders(self):
        return list(
            filter(
                lambda order: order.maker == self.our_address,
                self.otc.get_orders(self.token_sell().address,
                                    self.token_buy().address) +
                self.otc.get_orders(self.token_buy().address,
                                    self.token_sell().address)))

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

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

    def synchronize_orders(self):
        # If market is closed, cancel all orders but do not terminate the keeper.
        if self.otc.is_closed():
            self.logger.warning("Market is closed. Cancelling all orders.")
            self.cancel_all_orders()
            return

        # If keeper balance is below `--min-eth-balance`, cancel all orders but do not terminate
        # the keeper, keep processing blocks as the moment the keeper gets a top-up it should
        # resume activity straight away, without the need to restart it.
        if eth_balance(self.web3, self.our_address) < self.min_eth_balance:
            self.logger.warning(
                "Keeper ETH balance below minimum. Cancelling all orders.")
            self.cancel_all_orders()
            return

        bands = Bands(self.bands_config)
        order_book = self.order_book_manager.get_order_book()
        target_price = self.price()

        # If the is no target price feed, cancel all orders but do not terminate the keeper.
        # The moment the price feed comes back, the keeper will resume placing orders.
        if target_price is None:
            self.logger.warning(
                "No price feed available. Cancelling all orders.")
            self.cancel_all_orders()
            return

        # If there are any orders to be cancelled, cancel them. It is deliberate that we wait with topping-up
        # bands until the next block. This way we would create new orders based on the most recent price and
        # order book state. We could theoretically retrieve both (`target_price` and `our_orders`) again here,
        # but it just seems cleaner to do it in one place instead of in two.
        cancellable_orders = bands.cancellable_orders(
            our_buy_orders=self.our_buy_orders(order_book.orders),
            our_sell_orders=self.our_sell_orders(order_book.orders),
            target_price=target_price)

        if len(cancellable_orders) > 0:
            self.cancel_orders(cancellable_orders)
            return

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

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

    def cancel_all_orders(self):
        # Wait for the order book to stabilize
        while True:
            order_book = self.order_book_manager.get_order_book()
            if not order_book.orders_being_cancelled and not order_book.orders_being_placed:
                break

        # Cancel all open orders
        self.cancel_orders(self.order_book_manager.get_order_book().orders)
        self.order_book_manager.wait_for_order_cancellation()

    def cancel_orders(self, orders):
        for order in orders:
            self.order_book_manager.cancel_order(
                order.order_id, lambda: self.otc.kill(order.order_id).transact(
                    gas_price=self.gas_price).successful)

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

            if new_order.is_sell:
                pay_token = self.token_sell().address
                buy_token = self.token_buy().address
            else:
                pay_token = self.token_buy().address
                buy_token = self.token_sell().address

            transact = self.otc.make(pay_token=pay_token,
                                     pay_amount=new_order.pay_amount,
                                     buy_token=buy_token,
                                     buy_amount=new_order.buy_amount).transact(
                                         gas_price=self.gas_price)

            if transact is not None and transact.successful and transact.result is not None:
                return Order(market=self.otc,
                             order_id=transact.result,
                             maker=self.our_address,
                             pay_token=pay_token,
                             pay_amount=new_order.pay_amount,
                             buy_token=buy_token,
                             buy_amount=new_order.buy_amount,
                             timestamp=0)
            else:
                return None

        for new_order in new_orders:
            self.order_book_manager.place_order(
                lambda: place_order_function(new_order))
Example #26
0
class OasisMarketMakerKeeper:
    """Keeper acting as a market maker on OasisDEX."""

    logger = logging.getLogger()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.history = History()
        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(lambda: self.our_orders())
        self.order_book_manager.place_orders_with(self.place_order_function)
        self.order_book_manager.cancel_orders_with(self.cancel_order_function)
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()

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

    def startup(self):
        self.approve()

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

    def approve(self):
        """Approve OasisDEX to access our balances, so we can place orders."""
        self.otc.approve([self.token_sell, self.token_buy],
                         directly(gas_price=self.gas_price))

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

    def our_orders(self):
        return list(
            filter(
                lambda order: order.maker == self.our_address,
                self.otc.get_orders(self.token_sell.address,
                                    self.token_buy.address) +
                self.otc.get_orders(self.token_buy.address,
                                    self.token_sell.address)))

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

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

    def synchronize_orders(self):
        # If market is closed, cancel all orders but do not terminate the keeper.
        if self.otc.is_closed():
            self.logger.warning("Market is closed. Cancelling all orders.")
            self.order_book_manager.cancel_all_orders()
            return

        # If keeper balance is below `--min-eth-balance`, cancel all orders but do not terminate
        # the keeper, keep processing blocks as the moment the keeper gets a top-up it should
        # resume activity straight away, without the need to restart it.
        if eth_balance(self.web3, self.our_address) < self.min_eth_balance:
            self.logger.warning(
                "Keeper ETH balance below minimum. Cancelling all orders.")
            self.order_book_manager.cancel_all_orders()
            return

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

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

        # Do not place new orders if other new orders are being placed. In contrary to other keepers,
        # we allow placing new orders when other orders are being cancelled. This is because Ethereum
        # transactions are ordered so we are sure that the order placement will not 'overtake'
        # order cancellation.
        if order_book.orders_being_placed:
            self.logger.debug(
                "Other orders are being placed, not placing new orders")
            return

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

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

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

        transact = self.otc.make(
            pay_token=pay_token,
            pay_amount=new_order.pay_amount,
            buy_token=buy_token,
            buy_amount=new_order.buy_amount).transact(gas_price=self.gas_price)

        if transact is not None and transact.successful and transact.result is not None:
            return Order(market=self.otc,
                         order_id=transact.result,
                         maker=self.our_address,
                         pay_token=pay_token,
                         pay_amount=new_order.pay_amount,
                         buy_token=buy_token,
                         buy_amount=new_order.buy_amount,
                         timestamp=0)
        else:
            return None

    def cancel_order_function(self, order):
        transact = self.otc.kill(
            order.order_id).transact(gas_price=self.gas_price)
        return transact is not None and transact.successful
Example #27
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='oasis-market-maker-keeper')

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.order_book_manager = OrderBookManager(refresh_frequency=3)
        self.order_book_manager.get_orders_with(lambda: self.our_orders())
        self.order_book_manager.start()
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='oasis-market-maker-keeper')

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.arguments = parser.parse_args(args)

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

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

        self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance)
        self.bands_config = ReloadableConfig(self.arguments.config)
        self.gas_price = GasPriceFactory().create_gas_price(self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(
            self.arguments.price_feed, self.arguments.price_feed_expiry,
            self.tub, self.vox)
class ArbitrageKeeper:
    """Keeper to arbitrage on OasisDEX, `join`, `exit`, `boom` and `bust`."""

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.arguments = parser.parse_args(args)

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

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

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

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

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

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

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

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

    def startup(self):
        self.approve()

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

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

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

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

        else:
            return str(address)

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

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

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

        return orders

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

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

        orders = []

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def gas_price(self):
        if self.arguments.gas_price > 0:
            return FixedGasPrice(self.arguments.gas_price)
        else:
            return DefaultGasPrice()
Example #30
0
class OasisMarketMakerCancel:
    """Tool to cancel all our open orders on OasisDEX."""
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='oasis-market-maker-cancel')
        parser.add_argument("--rpc-host",
                            help="JSON-RPC host (default: `localhost')",
                            default="localhost",
                            type=str)
        parser.add_argument("--rpc-port",
                            help="JSON-RPC port (default: `8545')",
                            default=8545,
                            type=int)
        parser.add_argument(
            "--eth-from",
            help="Ethereum account from which to send transactions",
            required=True,
            type=str)
        parser.add_argument("--oasis-address",
                            help="Ethereum address of the OasisDEX contract",
                            required=True,
                            type=str)
        parser.add_argument("--gas-price",
                            help="Gas price in Wei (default: node default)",
                            default=0,
                            type=int)
        self.arguments = parser.parse_args(args)

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

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=logging.INFO)

    def main(self):
        self.cancel_orders(self.our_orders(self.otc.get_orders()))

    def our_orders(self, orders: list):
        """Return list of orders owned by us."""
        return list(
            filter(lambda order: order.maker == self.our_address, orders))

    def cancel_orders(self, orders: list):
        """Cancel orders asynchronously."""
        synchronize([
            self.otc.kill(
                order.order_id).transact_async(gas_price=self.gas_price())
            for order in orders
        ])

    def gas_price(self):
        if self.arguments.gas_price > 0:
            return FixedGasPrice(self.arguments.gas_price)
        else:
            return DefaultGasPrice()