def __init__(self, args: list): parser = argparse.ArgumentParser(prog='bitzlato-market-maker-keeper') self.add_arguments(parser) self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.bittrex_api = BittrexApi( api_server=self.arguments.bittrex_api_server, api_key=self.arguments.bittrex_api_key, secret_key=self.arguments.bittrex_secret_key, timeout=self.arguments.bittrex_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.bittrex_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.bittrex_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.bittrex_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
def init_order_book_manager(self, arguments: Namespace, pyex_api: PyexAPI): self.order_book_manager = OrderBookManager(refresh_frequency=arguments.refresh_frequency) self.order_book_manager.get_orders_with(lambda: pyex_api.get_orders(self.pair())) self.order_book_manager.get_balances_with(lambda: pyex_api.get_balances()) self.order_book_manager.cancel_orders_with(lambda order: pyex_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
def init_order_book_manager(self, arguments, coinone_api): self.order_book_manager = OrderBookManager(refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with(lambda: self.coinone_api.get_orders(self.pair())) self.order_book_manager.get_balances_with(lambda: self.coinone_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.coinone_api.cancel_order(order.order_id, self.pair(), order.price, order.amount, order.is_sell)) self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.pair = self.pair() self.order_book_manager.start()
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='paradex-market-maker-keeper') self.add_arguments(parser=parser) self.arguments = parser.parse_args(args) setup_logging(self.arguments) provider = HTTPProvider( endpoint_uri=self.arguments.rpc_host, request_kwargs={'timeout': self.arguments.rpc_timeout}) self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3( provider) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) register_keys(self.web3, self.arguments.eth_key) self.pair = self.arguments.pair.upper() self.token_buy = ERC20Token(web3=self.web3, address=Address( self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address( self.arguments.sell_token_address)) self.bands_config = ReloadableConfig(self.arguments.config) self.price_max_decimals = None self.amount_max_decimals = None self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.zrx_exchange = ZrxExchangeV2(web3=self.web3, address=Address( self.arguments.exchange_address)) self.paradex_api = ParadexApi(self.zrx_exchange, self.arguments.paradex_api_server, self.arguments.paradex_api_key, self.arguments.paradex_api_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency, max_workers=1) self.order_book_manager.get_orders_with( lambda: self.paradex_api.get_orders(self.pair)) self.order_book_manager.get_balances_with(lambda: self.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.paradex_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
def __init__(self, args: list): parser = argparse.ArgumentParser(prog='bibox-market-maker-keeper') parser.add_argument("--bibox-api-server", type=str, default="https://api.bibox.com", help="Address of the Bibox API server (default: 'https://api.bibox.com')") parser.add_argument("--bibox-api-key", type=str, required=True, help="API key for the Bibox API") parser.add_argument("--bibox-secret", type=str, required=True, help="Secret for the Bibox API") parser.add_argument("--bibox-timeout", type=float, default=9.5, help="Timeout for accessing the Bibox API (in seconds, default: 9.5)") parser.add_argument("--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument("--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument("--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.history = History() self.bibox_api = BiboxApi(api_server=self.arguments.bibox_api_server, api_key=self.arguments.bibox_api_key, secret=self.arguments.bibox_secret, timeout=self.arguments.bibox_timeout) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_book_manager = OrderBookManager(refresh_frequency=3) self.order_book_manager.get_orders_with(lambda: self.bibox_api.get_orders(pair=self.pair(), retry=True)) self.order_book_manager.get_balances_with(lambda: self.bibox_api.coin_list(retry=True)) self.order_book_manager.start()
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='0x-market-maker-keeper') self.add_arguments(parser=parser) self.arguments = parser.parse_args(args) setup_logging(self.arguments) provider = HTTPProvider( endpoint_uri=self.arguments.rpc_host, request_kwargs={'timeout': self.arguments.rpc_timeout}) self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3( provider) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) register_keys(self.web3, self.arguments.eth_key) self.eth_address = Address(self.arguments.eth_token_address) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() # Delegate 0x specific init to a function to permit overload for 0xv3 self.zrx_exchange = None self.zrx_relayer_api = None self.zrx_api = None self.pair = None self.init_zrx() self.placed_zrx_orders = [] self.placed_zrx_orders_lock = Lock() self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with(lambda: self.get_orders()) self.order_book_manager.get_balances_with(lambda: self.get_balances()) self.order_book_manager.place_orders_with(self.place_order_function) self.order_book_manager.cancel_orders_with(self.cancel_order_function) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
def __init__(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): parser = argparse.ArgumentParser(prog='leverj-market-maker-keeper') parser.add_argument( "--leverj-api-server", type=str, default="https://test.leverj.io", help= "Address of the leverj API server (default: 'https://test.leverj.io')" ) parser.add_argument("--account-id", type=str, default="", help="Address of leverj api account id") parser.add_argument("--api-key", type=str, default="", help="Address of leverj api key") parser.add_argument("--api-secret", type=str, default="", help="Address of leverj api secret") parser.add_argument( "--leverj-timeout", type=float, default=9.5, help= "Timeout for accessing the Leverj API (in seconds, default: 9.5)") parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to watch our trades") parser.add_argument( "--eth-key", type=str, nargs='*', help= "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')" ) parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--leverage", type=float, default=1.0, help="Leverage chosen for futures orders") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) self.web3 = Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from register_keys(self.web3, self.arguments.eth_key) setup_logging(self.arguments) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.target_price_lean = Wad(0) self.leverage = self.arguments.leverage self.history = History() self.leverj_api = LeverjFuturesAPI( web3=self.web3, api_server=self.arguments.leverj_api_server, account_id=self.arguments.account_id, api_key=self.arguments.api_key, api_secret=self.arguments.api_secret, timeout=self.arguments.leverj_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.leverj_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.leverj_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.leverj_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='theocean-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument( "--theocean-api-server", type=str, default='https://api.theocean.trade/api', help= "Address of the TheOcean API (default: 'https://api.theocean.trade/api')" ) parser.add_argument("--theocean-api-key", type=str, required=True, help="API key for the TheOcean API") parser.add_argument("--theocean-api-secret", type=str, required=True, help="API secret for the TheOcean API") parser.add_argument( "--theocean-api-timeout", type=float, default=9.5, help= "Timeout for accessing the TheOcean API (in seconds, default: 9.5)" ) parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.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.pair = Pair(self.token_sell.address, self.token_buy.address) self.bands_config = ReloadableConfig(self.arguments.config) self.price_max_decimals = None self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) self.theocean_api = TheOceanApi(self.zrx_exchange, self.arguments.theocean_api_server, self.arguments.theocean_api_key, self.arguments.theocean_api_secret, self.arguments.theocean_api_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.theocean_api.get_orders(self.pair)) self.order_book_manager.get_balances_with(lambda: self.get_balances()) self.order_book_manager.place_orders_with(self.place_order_function) self.order_book_manager.cancel_orders_with( lambda order: self.theocean_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
class BitsoMarketMakerKeeper: """Keeper acting as a market maker on Bitso.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='bitso-market-maker-keeper') parser.add_argument( "--bitso-api-server", type=str, default="https://api.bitso.com", help= "Address of the bitso API server (default: 'https://api.bitso.com')" ) parser.add_argument("--bitso-api-key", type=str, required=True, help="API key for the Bitso API") parser.add_argument( "--bitso-secret-key", type=str, required=True, help="RSA Private Key for signing requests to the BitsoX API") parser.add_argument( "--bitso-timeout", type=float, default=9.5, help= "Timeout for accessing the Bitso API (in seconds, default: 9.5)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.bitso_api = BitsoApi(api_server=self.arguments.bitso_api_server, api_key=self.arguments.bitso_api_key, secret_key=self.arguments.bitso_secret_key, timeout=self.arguments.bitso_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.bitso_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.bitso_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.bitso_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def shutdown(self): self.order_book_manager.cancel_all_orders() def pair(self): return self.arguments.pair.lower() def token_sell(self) -> str: return self.arguments.pair.split('_')[0].lower() def token_buy(self) -> str: return self.arguments.pair.split('_')[1].lower() def our_available_balance(self, our_balances: list, token: str) -> Wad: balance = list(filter(lambda x: x['currency'] == token, our_balances))[0]['total'] return Wad.from_number(balance) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.control_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # Place new orders self.place_orders( bands.new_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.our_available_balance( order_book.balances, self.token_buy()), our_sell_balance=self.our_available_balance( order_book.balances, self.token_sell()), target_price=target_price)[0]) def place_orders(self, new_orders): def place_order_function(new_order_to_be_placed): amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount # Convert wad to float as Bitso limits amount decimal places to 8, and price to 2 float_price = round(Wad.__float__(new_order_to_be_placed.price), 2) float_amount = round(Wad.__float__(amount), 8) side = "sell" if new_order_to_be_placed.is_sell == True else "buy" order_id = self.bitso_api.place_order(book=self.pair(), side=side, price=float_price, amount=float_amount) timestamp = datetime.now(tz=timezone.utc).isoformat() return Order(str(order_id), timestamp, self.pair(), new_order_to_be_placed.is_sell, new_order_to_be_placed.price, amount) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order))
class ParadexMarketMakerKeeper: """Keeper acting as a market maker on Paradex.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='paradex-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument( "--paradex-api-server", type=str, default='https://api.paradex.io/consumer', help= "Address of the Paradex API (default: 'https://api.paradex.io/consumer')" ) parser.add_argument("--paradex-api-key", type=str, required=True, help="API key for the Paradex API") parser.add_argument( "--paradex-api-timeout", type=float, default=9.5, help= "Timeout for accessing the Paradex API (in seconds, default: 9.5)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--order-expiry", type=int, required=True, help="Expiration time of created orders (in seconds)") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.pair = self.arguments.pair.upper() self.token_buy = ERC20Token(web3=self.web3, address=Address( self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address( self.arguments.sell_token_address)) self.bands_config = ReloadableConfig(self.arguments.config) self.price_max_decimals = None self.amount_max_decimals = None self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.zrx_exchange = ZrxExchangeV2(web3=self.web3, address=Address( self.arguments.exchange_address)) self.paradex_api = ParadexApi(self.zrx_exchange, self.arguments.paradex_api_server, self.arguments.paradex_api_key, self.arguments.paradex_api_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency, max_workers=1) self.order_book_manager.get_orders_with( lambda: self.paradex_api.get_orders(self.pair)) self.order_book_manager.get_balances_with(lambda: self.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.paradex_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): self.approve() # Get maximum number of decimals for prices and amounts. # Paradex API enforces it. markets = self.paradex_api.get_markets() market = next(filter(lambda item: item['symbol'] == self.pair, markets)) self.price_max_decimals = market['priceMaxDecimals'] self.amount_max_decimals = market['amountMaxDecimals'] def shutdown(self): self.order_book_manager.cancel_all_orders() def approve(self): self.zrx_exchange.approve([self.token_sell, self.token_buy], directly(gas_price=self.gas_price)) def get_balances(self): return self.token_sell.balance_of( self.our_address), self.token_buy.balance_of(self.our_address) def our_total_sell_balance(self, balances) -> Wad: return balances[0] def our_total_buy_balance(self, balances) -> Wad: return balances[1] def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # In case of Paradex, balances returned by `our_total_balance` still contain amounts "locked" # by currently open orders, so we need to explicitly subtract these amounts. our_buy_balance = self.our_total_buy_balance( order_book.balances) - Bands.total_amount( self.our_buy_orders(order_book.orders)) our_sell_balance = self.our_total_sell_balance( order_book.balances) - Bands.total_amount( self.our_sell_orders(order_book.orders)) # Place new orders self.place_orders( bands.new_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=our_buy_balance, our_sell_balance=our_sell_balance, target_price=target_price)[0]) def place_orders(self, new_orders): def place_order_function(new_order_to_be_placed): price = round(new_order_to_be_placed.price, self.price_max_decimals) amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount amount = round(amount, self.amount_max_decimals) order_id = self.paradex_api.place_order( pair=self.pair, is_sell=new_order_to_be_placed.is_sell, price=price, amount=amount, expiry=self.arguments.order_expiry) return Order(order_id, self.pair, new_order_to_be_placed.is_sell, price, amount, amount) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order))
def __init__(self, args: list): parser = argparse.ArgumentParser(prog='Korbit-market-maker-keeper') parser.add_argument( "--korbit-api-server", type=str, default="https://api.korbit.co.kr", help= "Address of the korbit API server (default: 'https://api.korbit.co.kr')" ) parser.add_argument("--korbit-api-key", type=str, required=True, help="API key for the Korbit API") parser.add_argument("--korbit-secret-key", type=str, required=True, help="Secret key for the Korbit API") parser.add_argument( "--korbit-timeout", type=float, default=9.5, help= "Timeout for accessing the Korbit API (in seconds, default: 9.5)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") parser.add_argument( "--telegram-log-config-file", type=str, required=False, help= "config file for send logs to telegram chat (e.g. 'telegram_conf.json')", default=None) parser.add_argument( "--keeper-name", type=str, required=False, help="market maker keeper name (e.g. 'Uniswap_V2_MDTETH')", default="korbit") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.korbit_api = KorbitApi( api_server=self.arguments.korbit_api_server, api_key=self.arguments.korbit_api_key, secret_key=self.arguments.korbit_secret_key, timeout=self.arguments.korbit_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.korbit_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.korbit_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.korbit_api.cancel_order(int(order.order_id), self.pair())) 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): parser = argparse.ArgumentParser(prog='gopax-market-maker-keeper') parser.add_argument("--gopax-api-server", type=str, default="https://api.gopax.co.kr", help="Address of the GOPAX API server (default: 'https://api.gopax.co.kr')") parser.add_argument("--gopax-api-key", type=str, required=True, help="API key for the GOPAX API") parser.add_argument("--gopax-api-secret", type=str, required=True, help="API secret for the GOPAX API") parser.add_argument("--gopax-timeout", type=float, default=9.5, help="Timeout for accessing the GOPAX API (in seconds, default: 9.5)") parser.add_argument("--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument("--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument("--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument("--order-history-every", type=int, default=30, help="Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.history = History() self.gopax_api = GOPAXApi(api_server=self.arguments.gopax_api_server, api_key=self.arguments.gopax_api_key, api_secret=self.arguments.gopax_api_secret, timeout=self.arguments.gopax_timeout) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter(self.arguments) self.order_book_manager = OrderBookManager(refresh_frequency=10) self.order_book_manager.get_orders_with(self.get_orders) self.order_book_manager.get_balances_with(lambda: self.gopax_api.get_balances()) self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
class GOPAXMarketMakerKeeper: logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='gopax-market-maker-keeper') parser.add_argument("--gopax-api-server", type=str, default="https://api.gopax.co.kr", help="Address of the GOPAX API server (default: 'https://api.gopax.co.kr')") parser.add_argument("--gopax-api-key", type=str, required=True, help="API key for the GOPAX API") parser.add_argument("--gopax-api-secret", type=str, required=True, help="API secret for the GOPAX API") parser.add_argument("--gopax-timeout", type=float, default=9.5, help="Timeout for accessing the GOPAX API (in seconds, default: 9.5)") parser.add_argument("--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument("--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument("--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument("--order-history-every", type=int, default=30, help="Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.history = History() self.gopax_api = GOPAXApi(api_server=self.arguments.gopax_api_server, api_key=self.arguments.gopax_api_key, api_secret=self.arguments.gopax_api_secret, timeout=self.arguments.gopax_timeout) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter(self.arguments) self.order_book_manager = OrderBookManager(refresh_frequency=10) self.order_book_manager.get_orders_with(self.get_orders) self.order_book_manager.get_balances_with(lambda: self.gopax_api.get_balances()) self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def shutdown(self): #TODO I don't think this approach makes sure all orders will always get cancelled!! while True: try: our_orders = self.gopax_api.get_orders(self.pair()) except: continue if len(our_orders) == 0: break self.cancel_orders(our_orders) self.order_book_manager.wait_for_order_cancellation() def pair(self): return self.arguments.pair.upper() def token_sell(self) -> str: return self.arguments.pair.split('-')[0].upper() def token_buy(self) -> str: return self.arguments.pair.split('-')[1].upper() def get_orders(self) -> list: return list(map(lambda order: self.gopax_api.get_order(order.order_id), self.gopax_api.get_orders(self.pair()))) def our_available_balance(self, our_balances: list, token: str) -> Wad: return Wad.from_number(next(filter(lambda coin: coin['asset'] == token, our_balances))['avail']) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands(self.bands_config, self.spread_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders(our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug("Order book is in progress, not placing new orders") return # Place new orders self.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.our_available_balance(order_book.balances, self.token_buy()), our_sell_balance=self.our_available_balance(order_book.balances, self.token_sell()), target_price=target_price)[0]) def cancel_orders(self, orders): for order in orders: self.order_book_manager.cancel_order(order.order_id, lambda order=order: self.gopax_api.cancel_order(order.order_id)) def place_orders(self, new_orders): def place_order_function(new_order_to_be_placed): pair = self.pair() is_sell = new_order_to_be_placed.is_sell price = new_order_to_be_placed.price amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount new_order_id = self.gopax_api.place_order(pair=pair, is_sell=is_sell, price=price, amount=amount) return Order(order_id=new_order_id, pair=pair, is_sell=is_sell, price=price, amount=amount, amount_remaining=amount) for new_order in new_orders: self.order_book_manager.place_order(lambda new_order=new_order: place_order_function(new_order))
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='oasis-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument("--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument("--tub-address", type=str, required=False, help="Ethereum address of the Tub contract") parser.add_argument("--oasis-address", type=str, required=True, help="Ethereum address of the OasisDEX contract") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument("--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument("--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument("--order-history-every", type=int, default=30, help="Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--round-places", type=int, default=2, help="Number of decimal places to round order prices to (default=2)") parser.add_argument("--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument("--smart-gas-price", dest='smart_gas_price', action='store_true', help="Use smart gas pricing strategy, based on the ethgasstation.info feed") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.otc = MatchingMarket(web3=self.web3, address=Address(self.arguments.oasis_address)) tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) \ if self.arguments.tub_address is not None else None self.token_buy = ERC20Token(web3=self.web3, address=Address(self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address(self.arguments.sell_token_address)) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments, tub) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter(self.arguments) self.history = History() self.order_book_manager = OrderBookManager(refresh_frequency=3) self.order_book_manager.get_orders_with(lambda: self.our_orders()) self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
class EthfinexMarketMakerKeeper: logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='ethfinex-market-maker-keeper') parser.add_argument( "--ethfinex-api-server", type=str, default="https://api.ethfinex.com", help= "Address of the Ethfinex API server (default: 'https://api.ethfinex.com')" ) parser.add_argument("--ethfinex-api-key", type=str, required=True, help="API key for the Ethfinex API") parser.add_argument("--ethfinex-api-secret", type=str, required=True, help="API secret for the Ethfinex API") parser.add_argument( "--ethfinex-timeout", type=float, default=9.5, help= "Timeout for accessing the Ethfinex API (in seconds, default: 9.5)" ) parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.history = History() self.ethfinex_api = EthfinexApi( api_server=self.arguments.ethfinex_api_server, api_key=self.arguments.ethfinex_api_key, api_secret=self.arguments.ethfinex_api_secret, timeout=self.arguments.ethfinex_timeout) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency, max_workers=1) self.order_book_manager.get_orders_with( lambda: self.ethfinex_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.ethfinex_api.get_balances()) self.order_book_manager.place_orders_with(self.place_order_function) self.order_book_manager.cancel_orders_with( lambda order: self.ethfinex_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def shutdown(self): self.order_book_manager.cancel_all_orders() def pair(self): return self.arguments.pair def token_sell(self) -> str: return self.arguments.pair[:3] def token_buy(self) -> str: return self.arguments.pair[3:] def our_available_balance(self, our_balances: list, token: str) -> Wad: try: return Wad.from_number( next( filter(lambda coin: coin['currency'].upper() == token, our_balances))['available']) except: return Wad(0) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands(self.bands_config, self.spread_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # Place new orders self.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( order_book.balances, self.token_buy()), our_sell_balance=self.our_available_balance( order_book.balances, self.token_sell()), target_price=target_price)[0]) def place_order_function(self, new_order): pair = self.pair() is_sell = new_order.is_sell price = new_order.price amount = new_order.pay_amount if new_order.is_sell else new_order.buy_amount new_order_id = self.ethfinex_api.place_order(pair=pair, is_sell=is_sell, price=price, amount=amount) return Order(order_id=new_order_id, pair=pair, is_sell=is_sell, price=price, amount=amount)
class OasisMarketMakerKeeper: """Keeper acting as a market maker on OasisDEX.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='oasis-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument("--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument("--tub-address", type=str, required=False, help="Ethereum address of the Tub contract") parser.add_argument("--oasis-address", type=str, required=True, help="Ethereum address of the OasisDEX contract") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument("--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument("--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument("--order-history-every", type=int, default=30, help="Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--round-places", type=int, default=2, help="Number of decimal places to round order prices to (default=2)") parser.add_argument("--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument("--smart-gas-price", dest='smart_gas_price', action='store_true', help="Use smart gas pricing strategy, based on the ethgasstation.info feed") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.otc = MatchingMarket(web3=self.web3, address=Address(self.arguments.oasis_address)) tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) \ if self.arguments.tub_address is not None else None self.token_buy = ERC20Token(web3=self.web3, address=Address(self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address(self.arguments.sell_token_address)) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments, tub) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter(self.arguments) self.history = History() self.order_book_manager = OrderBookManager(refresh_frequency=3) self.order_book_manager.get_orders_with(lambda: self.our_orders()) self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.on_block(self.on_block) lifecycle.every(3, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): self.approve() @retry(delay=5, logger=logger) def shutdown(self): self.cancel_all_orders() def on_block(self): # This method is present only so the lifecycle binds the new block listener, which makes # it then terminate the keeper if no new blocks have been arriving for 300 seconds. pass def approve(self): """Approve OasisDEX to access our balances, so we can place orders.""" self.otc.approve([self.token_sell, self.token_buy], directly(gas_price=self.gas_price)) def our_available_balance(self, token: ERC20Token) -> Wad: return token.balance_of(self.our_address) def our_orders(self): return list(filter(lambda order: order.maker == self.our_address, self.otc.get_orders(self.token_sell.address, self.token_buy.address) + self.otc.get_orders(self.token_buy.address, self.token_sell.address))) def our_sell_orders(self, our_orders: list): return list(filter(lambda order: order.buy_token == self.token_buy.address and order.pay_token == self.token_sell.address, our_orders)) def our_buy_orders(self, our_orders: list): return list(filter(lambda order: order.buy_token == self.token_sell.address and order.pay_token == self.token_buy.address, our_orders)) def synchronize_orders(self): # If market is closed, cancel all orders but do not terminate the keeper. if self.otc.is_closed(): self.logger.warning("Market is closed. Cancelling all orders.") self.cancel_all_orders() return # If keeper balance is below `--min-eth-balance`, cancel all orders but do not terminate # the keeper, keep processing blocks as the moment the keeper gets a top-up it should # resume activity straight away, without the need to restart it. if eth_balance(self.web3, self.our_address) < self.min_eth_balance: self.logger.warning("Keeper ETH balance below minimum. Cancelling all orders.") self.cancel_all_orders() return bands = Bands(self.bands_config, self.spread_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # If there are any orders to be cancelled, cancel them. It is deliberate that we wait with topping-up # bands until the next block. This way we would create new orders based on the most recent price and # order book state. We could theoretically retrieve both (`target_price` and `our_orders`) again here, # but it just seems cleaner to do it in one place instead of in two. cancellable_orders = bands.cancellable_orders(our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug("Order book is in progress, not placing new orders") return # Place new orders self.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.our_available_balance(self.token_buy), our_sell_balance=self.our_available_balance(self.token_sell), target_price=target_price)[0]) def cancel_all_orders(self): # Wait for the order book to stabilize while True: order_book = self.order_book_manager.get_order_book() if not order_book.orders_being_cancelled and not order_book.orders_being_placed: break # Cancel all open orders self.cancel_orders(self.order_book_manager.get_order_book().orders) self.order_book_manager.wait_for_order_cancellation() def cancel_orders(self, orders): for order in orders: self.order_book_manager.cancel_order(order.order_id, lambda order=order: self.otc.kill(order.order_id).transact(gas_price=self.gas_price).successful) def place_orders(self, new_orders): def place_order_function(new_order_to_be_placed: NewOrder): assert(isinstance(new_order_to_be_placed, NewOrder)) if new_order_to_be_placed.is_sell: pay_token = self.token_sell.address buy_token = self.token_buy.address else: pay_token = self.token_buy.address buy_token = self.token_sell.address transact = self.otc.make(pay_token=pay_token, pay_amount=new_order_to_be_placed.pay_amount, buy_token=buy_token, buy_amount=new_order_to_be_placed.buy_amount).transact(gas_price=self.gas_price) if transact is not None and transact.successful and transact.result is not None: return Order(market=self.otc, order_id=transact.result, maker=self.our_address, pay_token=pay_token, pay_amount=new_order_to_be_placed.pay_amount, buy_token=buy_token, buy_amount=new_order_to_be_placed.buy_amount, timestamp=0) else: return None for new_order in new_orders: self.order_book_manager.place_order(lambda new_order=new_order: place_order_function(new_order))
class GateIOMarketMakerKeeper: """Keeper acting as a market maker on Gate.io.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='gateio-market-maker-keeper') parser.add_argument( "--gateio-api-server", type=str, default="https://data.gate.io", help= "Address of the Gate.io API server (default: 'https://data.gate.io')" ) parser.add_argument("--gateio-api-key", type=str, required=True, help="API key for the Gate.io API") parser.add_argument("--gateio-secret-key", type=str, required=True, help="Secret key for the Gate.io API") parser.add_argument( "--gateio-timeout", type=float, default=9.5, help= "Timeout for accessing the Gate.io API (in seconds, default: 9.5)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.gateio_api = GateIOApi( api_server=self.arguments.gateio_api_server, api_key=self.arguments.gateio_api_key, secret_key=self.arguments.gateio_secret_key, timeout=self.arguments.gateio_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.gateio_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.gateio_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.gateio_api.cancel_order(self.pair(), order. order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() self._last_order_creation = 0 def main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def shutdown(self): self.order_book_manager.cancel_all_orders() def pair(self): return self.arguments.pair.lower() def token_sell(self) -> str: return self.arguments.pair.split('_')[0].upper() def token_buy(self) -> str: return self.arguments.pair.split('_')[1].upper() def our_available_balance(self, our_balances: dict, token: str) -> Wad: try: return Wad.from_number(our_balances['available'][token]) except KeyError: return Wad(0) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # Place new orders new_orders = bands.new_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.our_available_balance(order_book.balances, self.token_buy()), our_sell_balance=self.our_available_balance( order_book.balances, self.token_sell()), target_price=target_price)[0] if len(new_orders) > 0: if self.can_create_orders(): self.place_orders(new_orders) self.register_order_creation() else: self.logger.info( "Too little time elapsed from last order creation, waiting..." ) # Unfortunately the gate.io API does not immediately reflect the fact that our orders have # been placed. In order to avoid placing orders twice we explicitly wait some time here. def can_create_orders(self) -> bool: return time.time() - self._last_order_creation > 15 def register_order_creation(self): self._last_order_creation = time.time() def place_orders(self, new_orders: List[NewOrder]): def place_order_function(new_order_to_be_placed): amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount order_id = self.gateio_api.place_order( self.pair(), new_order_to_be_placed.is_sell, new_order_to_be_placed.price, amount) return Order(order_id=order_id, timestamp=0, pair=self.pair(), is_sell=new_order_to_be_placed.is_sell, price=new_order_to_be_placed.price, amount=amount, amount_symbol=self.token_sell(), money=amount * new_order_to_be_placed.price, money_symbol=self.token_buy(), initial_amount=amount, filled_amount=Wad(0)) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order))
class LiquidMarketMakerKeeper: """Keeper acting as a market maker on Liquid.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='liquid-market-maker-keeper') parser.add_argument( "--liquid-api-server", type=str, default="https://api.liquid.com", help= "Address of the liquid API server (default: 'https://api.liquid.com')" ) parser.add_argument("--liquid-api-key", type=str, required=True, help="API key for the liquid API") parser.add_argument("--liquid-secret-key", type=str, required=True, help="Secret key for the liquid API") parser.add_argument( "--liquid-timeout", type=float, default=9.5, help= "Timeout for accessing the liquid API (in seconds, default: 9.5)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.liquid_api = LiquidApi( api_server=self.arguments.liquid_api_server, api_key=self.arguments.liquid_api_key, secret_key=self.arguments.liquid_secret_key, timeout=self.arguments.liquid_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.liquid_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.liquid_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.liquid_api.cancel_order(str(order.order_id))) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def shutdown(self): self.order_book_manager.cancel_all_orders() def pair(self): return self.arguments.pair.upper() def token_sell(self) -> str: return self.arguments.pair[:3] def token_buy(self) -> str: return self.arguments.pair[3:] def our_available_balance(self, our_balances: dict, token: str) -> Wad: token_balances = list( filter(lambda coin: coin['currency'].upper() == token, our_balances)) if token_balances: return Wad.from_number(token_balances[0]['balance']) else: return Wad(0) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.control_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # In case of Liquid, balances returned by `our_total_balance` still contain amounts "locked" # by currently open orders, so we need to explicitly subtract these amounts. our_buy_balance = self.our_available_balance( order_book.balances, self.token_buy()) - Bands.total_amount( self.our_buy_orders(order_book.orders)) our_sell_balance = self.our_available_balance( order_book.balances, self.token_sell()) - Bands.total_amount( self.our_sell_orders(order_book.orders)) # Place new orders new_orders = bands.new_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=our_buy_balance, our_sell_balance=our_sell_balance, target_price=target_price)[0] self.place_orders(new_orders) def place_orders(self, new_orders: List[NewOrder]): def place_order_function(new_order_to_be_placed): amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount order_id = self.liquid_api.place_order( self.pair(), new_order_to_be_placed.is_sell, new_order_to_be_placed.price, amount) return Order(order_id=order_id, pair=self.pair(), is_sell=new_order_to_be_placed.is_sell, price=new_order_to_be_placed.price, amount=amount, filled_amount=Wad(0)) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order))
class ZrxMarketMakerKeeper: """Keeper acting as a market maker on any 0x exchange implementing the Standard 0x Relayer API V0.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='0x-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument("--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument("--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--relayer-api-server", type=str, required=True, help="Address of the 0x Relayer API") parser.add_argument("--relayer-per-page", type=int, default=100, help="Number of orders to fetch per one page from the 0x Relayer API (default: 100)") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--buy-token-decimals", type=int, default=18, help="Number of decimals of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--sell-token-decimals", type=int, default=18, help="Number of decimals of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument("--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument("--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument("--order-history-every", type=int, default=30, help="Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--order-expiry", type=int, required=True, help="Expiration time of created orders (in seconds)") parser.add_argument("--order-expiry-threshold", type=int, default=0, help="How long before order expiration it is considered already expired (in seconds)") parser.add_argument("--use-full-balances", dest='use_full_balances', action='store_true', help="Do not subtract the amounts locked by current orders from available balances") parser.add_argument("--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument('--cancel-on-shutdown', dest='cancel_on_shutdown', action='store_true', help="Whether should cancel all open orders on keeper shutdown") parser.add_argument("--remember-own-orders", dest='remember_own_orders', action='store_true', help="Whether should the keeper remember his own submitted orders") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument("--smart-gas-price", dest='smart_gas_price', action='store_true', help="Use smart gas pricing strategy, based on the ethgasstation.info feed") parser.add_argument("--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter(self.arguments) self.history = History() # Delegate 0x specific init to a function to permit overload for 0xv2 self.zrx_exchange = None self.zrx_relayer_api = None self.zrx_api = None self.pair = None self.init_zrx() self.placed_zrx_orders = [] self.placed_zrx_orders_lock = Lock() self.order_book_manager = OrderBookManager(refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with(lambda: self.get_orders()) self.order_book_manager.get_balances_with(lambda: self.get_balances()) self.order_book_manager.place_orders_with(self.place_order_function) self.order_book_manager.cancel_orders_with(self.cancel_order_function) self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def init_zrx(self): self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) self.zrx_relayer_api = ZrxRelayerApi(exchange=self.zrx_exchange, api_server=self.arguments.relayer_api_server) self.zrx_api = ZrxApi(zrx_exchange=self.zrx_exchange) self.pair = Pair(sell_token_address=Address(self.arguments.sell_token_address), sell_token_decimals=self.arguments.sell_token_decimals, buy_token_address=Address(self.arguments.buy_token_address), buy_token_decimals=self.arguments.buy_token_decimals) def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): self.approve() def shutdown(self): self.order_book_manager.cancel_all_orders(final_wait_time=60) def approve(self): token_buy = ERC20Token(web3=self.web3, address=Address(self.pair.buy_token_address)) token_sell = ERC20Token(web3=self.web3, address=Address(self.pair.sell_token_address)) self.zrx_exchange.approve([token_sell, token_buy], directly(gas_price=self.gas_price)) def remove_expired_orders(self, orders: list) -> list: current_timestamp = int(time.time()) return list(filter(lambda order: order.zrx_order.expiration > current_timestamp - self.arguments.order_expiry_threshold, orders)) def remove_expired_zrx_orders(self, zrx_orders: list) -> list: current_timestamp = int(time.time()) return list(filter(lambda order: order.expiration > current_timestamp - self.arguments.order_expiry_threshold, zrx_orders)) def remove_filled_or_cancelled_zrx_orders(self, zrx_orders: list) -> list: return list(filter(lambda order: self.zrx_exchange.get_unavailable_buy_amount(order) < order.buy_amount, zrx_orders)) def get_orders(self) -> list: def remove_old_zrx_orders(zrx_orders: list) -> list: return self.remove_filled_or_cancelled_zrx_orders(self.remove_expired_zrx_orders(zrx_orders)) with self.placed_zrx_orders_lock: self.placed_zrx_orders = remove_old_zrx_orders(self.placed_zrx_orders) api_zrx_orders = remove_old_zrx_orders(self.zrx_relayer_api.get_orders_by_maker(self.our_address, self.arguments.relayer_per_page)) with self.placed_zrx_orders_lock: zrx_orders = list(set(self.placed_zrx_orders + api_zrx_orders)) return self.zrx_api.get_orders(self.pair, zrx_orders) def get_balances(self): balances = self.zrx_api.get_balances(self.pair) return balances[0], balances[1], eth_balance(self.web3, self.our_address) def our_total_sell_balance(self, balances) -> Wad: return balances[0] def our_total_buy_balance(self, balances) -> Wad: return balances[1] def our_eth_balance(self, balances) -> Wad: return balances[2] def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # We filter out expired orders from the order book snapshot. The reason for that is that # it allows us to replace expired orders faster. Without it, we would have to wait # for the next order book refresh in order to realize an order has expired. Unfortunately, # in case of 0x order book refresh can be quite slow as it involves making multiple calls # to the Ethereum node. # # By filtering out expired orders here, we can replace them the next `synchronize_orders` # tick after they expire. Which is ~ 1s delay, instead of avg ~ 5s without this trick. orders = self.remove_expired_orders(order_book.orders) if self.our_eth_balance(order_book.balances) < self.min_eth_balance: self.logger.warning("Keeper ETH balance below minimum. Cancelling all orders.") self.order_book_manager.cancel_all_orders() return # Cancel orders cancellable_orders = bands.cancellable_orders(our_buy_orders=self.our_buy_orders(orders), our_sell_orders=self.our_sell_orders(orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug("Order book is in progress, not placing new orders") return # Balances returned by `our_total_***_balance` still contain amounts "locked" # by currently open orders, so we need to explicitly subtract these amounts. if self.arguments.use_full_balances: our_buy_balance = self.our_total_buy_balance(order_book.balances) our_sell_balance = self.our_total_sell_balance(order_book.balances) else: our_buy_balance = self.our_total_buy_balance(order_book.balances) - Bands.total_amount(self.our_buy_orders(orders)) our_sell_balance = self.our_total_sell_balance(order_book.balances) - Bands.total_amount(self.our_sell_orders(orders)) # Place new orders self.order_book_manager.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(orders), our_sell_orders=self.our_sell_orders(orders), our_buy_balance=our_buy_balance, our_sell_balance=our_sell_balance, target_price=target_price)[0]) def place_order_function(self, new_order: NewOrder): assert(isinstance(new_order, NewOrder)) zrx_order = self.zrx_api.place_order(pair=self.pair, is_sell=new_order.is_sell, price=new_order.price, amount=new_order.amount, expiration=int(time.time()) + self.arguments.order_expiry) zrx_order = self.zrx_relayer_api.calculate_fees(zrx_order) zrx_order = self.zrx_exchange.sign_order(zrx_order) if self.zrx_relayer_api.submit_order(zrx_order): if self.arguments.remember_own_orders: with self.placed_zrx_orders_lock: self.placed_zrx_orders.append(zrx_order) order = self.zrx_api.get_orders(self.pair, [zrx_order])[0] return order else: return None def cancel_order_function(self, order): transact = self.zrx_exchange.cancel_order(order.zrx_order).transact(gas_price=self.gas_price) return transact is not None and transact.successful
class DEXKeeperAPI: """ Define a common abstract API for keepers on decentralized exchanges """ def __init__(self, arguments: Namespace, pyex_api: PyexAPI): setup_logging(arguments) if arguments.__contains__('web3'): self.web3 = arguments.web3 else: web3_endpoint = f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}" web3_options = {"timeout": self.arguments.rpc_timeout} self.web3 = Web3(HTTPProvider(endpoint_uri=web3_endpoint, request_kwargs=web3_options)) 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.bands_config = ReloadableConfig(arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(arguments) self.spread_feed = create_spread_feed(arguments) self.control_feed = create_control_feed(arguments) self.order_history_reporter = create_order_history_reporter(arguments) self.history = History() self.init_order_book_manager(arguments, pyex_api) def init_order_book_manager(self, arguments: Namespace, pyex_api: PyexAPI): self.order_book_manager = OrderBookManager(refresh_frequency=arguments.refresh_frequency) self.order_book_manager.get_orders_with(lambda: pyex_api.get_orders(self.pair())) self.order_book_manager.get_balances_with(lambda: pyex_api.get_balances()) self.order_book_manager.cancel_orders_with(lambda order: pyex_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(10) if self.is_zrx: 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() def approve(self): raise NotImplementedError() # Each exchange takes pair input as a different format def pair(self): raise NotImplementedError() def token_sell(self) -> str: raise NotImplementedError() def token_buy(self) -> str: raise NotImplementedError() # Different keys are used to access balance object for different exchanges def our_available_balance(self, our_balances: dict, token: str) -> Wad: raise NotImplementedError() def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): raise NotImplementedError() def place_orders(self, new_orders: list): raise NotImplementedError()
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='0x-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument("--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument("--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--relayer-api-server", type=str, required=True, help="Address of the 0x Relayer API") parser.add_argument("--relayer-per-page", type=int, default=100, help="Number of orders to fetch per one page from the 0x Relayer API (default: 100)") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--buy-token-decimals", type=int, default=18, help="Number of decimals of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--sell-token-decimals", type=int, default=18, help="Number of decimals of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument("--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument("--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument("--order-history-every", type=int, default=30, help="Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--order-expiry", type=int, required=True, help="Expiration time of created orders (in seconds)") parser.add_argument("--order-expiry-threshold", type=int, default=0, help="How long before order expiration it is considered already expired (in seconds)") parser.add_argument("--use-full-balances", dest='use_full_balances', action='store_true', help="Do not subtract the amounts locked by current orders from available balances") parser.add_argument("--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument('--cancel-on-shutdown', dest='cancel_on_shutdown', action='store_true', help="Whether should cancel all open orders on keeper shutdown") parser.add_argument("--remember-own-orders", dest='remember_own_orders', action='store_true', help="Whether should the keeper remember his own submitted orders") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument("--smart-gas-price", dest='smart_gas_price', action='store_true', help="Use smart gas pricing strategy, based on the ethgasstation.info feed") parser.add_argument("--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter(self.arguments) self.history = History() # Delegate 0x specific init to a function to permit overload for 0xv2 self.zrx_exchange = None self.zrx_relayer_api = None self.zrx_api = None self.pair = None self.init_zrx() self.placed_zrx_orders = [] self.placed_zrx_orders_lock = Lock() self.order_book_manager = OrderBookManager(refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with(lambda: self.get_orders()) self.order_book_manager.get_balances_with(lambda: self.get_balances()) self.order_book_manager.place_orders_with(self.place_order_function) self.order_book_manager.cancel_orders_with(self.cancel_order_function) self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
class TheOceanMarketMakerKeeper: """Keeper acting as a market maker on TheOcean.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='theocean-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument( "--theocean-api-server", type=str, default='https://api.theocean.trade/api', help= "Address of the TheOcean API (default: 'https://api.theocean.trade/api')" ) parser.add_argument("--theocean-api-key", type=str, required=True, help="API key for the TheOcean API") parser.add_argument("--theocean-api-secret", type=str, required=True, help="API secret for the TheOcean API") parser.add_argument( "--theocean-api-timeout", type=float, default=9.5, help= "Timeout for accessing the TheOcean API (in seconds, default: 9.5)" ) parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.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.pair = Pair(self.token_sell.address, self.token_buy.address) self.bands_config = ReloadableConfig(self.arguments.config) self.price_max_decimals = None self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) self.theocean_api = TheOceanApi(self.zrx_exchange, self.arguments.theocean_api_server, self.arguments.theocean_api_key, self.arguments.theocean_api_secret, self.arguments.theocean_api_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.theocean_api.get_orders(self.pair)) self.order_book_manager.get_balances_with(lambda: self.get_balances()) self.order_book_manager.place_orders_with(self.place_order_function) self.order_book_manager.cancel_orders_with( lambda order: self.theocean_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): self.approve() # Get maximum number of decimals for prices. market = self.theocean_api.get_market(self.pair) assert (int(market['baseToken']['decimals']) == 18) assert (int(market['quoteToken']['decimals']) == 18) assert (int(market['baseToken']['precision']) == int( market['quoteToken']['precision'])) self.price_max_decimals = int(market['baseToken']['precision']) def shutdown(self): self.order_book_manager.cancel_all_orders() def approve(self): self.zrx_exchange.approve([self.token_sell, self.token_buy], directly(gas_price=self.gas_price)) def get_balances(self): return self.theocean_api.get_balance( self.pair.sell_token), self.theocean_api.get_balance( self.pair.buy_token) def our_sell_balance(self, balances) -> Wad: return balances[0] def our_buy_balance(self, balances) -> Wad: return balances[1] def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # Place new orders self.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_buy_balance(order_book.balances), our_sell_balance=self.our_sell_balance(order_book.balances), target_price=target_price)[0]) def place_order_function(self, new_order: NewOrder): assert (isinstance(new_order, NewOrder)) pair = self.pair is_sell = new_order.is_sell price = round(new_order.price, self.price_max_decimals) amount = new_order.pay_amount if new_order.is_sell else new_order.buy_amount new_order_id = self.theocean_api.place_order(pair=pair, is_sell=is_sell, price=price, amount=amount) if new_order_id is not None: return Order(order_id=new_order_id, pair=pair, is_sell=is_sell, price=price, amount=amount) else: return None
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
class DEXKeeperAPI: """ Define a common abstract API for keepers on decentralized exchanges """ def __init__(self, arguments: Namespace, pyex_api: PyexAPI): setup_logging(arguments) if arguments.__contains__('web3'): self.web3 = arguments.web3 else: web3_endpoint = f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}" web3_options = {"timeout": self.arguments.rpc_timeout} self.web3 = Web3(HTTPProvider(endpoint_uri=web3_endpoint, request_kwargs=web3_options)) 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.bands_config = ReloadableConfig(arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(arguments) self.spread_feed = create_spread_feed(arguments) self.control_feed = create_control_feed(arguments) self.order_history_reporter = create_order_history_reporter(arguments) self.history = History() self.init_order_book_manager(arguments, pyex_api) def init_order_book_manager(self, arguments: Namespace, pyex_api: PyexAPI): self.order_book_manager = OrderBookManager(refresh_frequency=arguments.refresh_frequency) self.order_book_manager.get_orders_with(lambda: pyex_api.get_orders(self.pair())) self.order_book_manager.get_balances_with(lambda: pyex_api.get_balances()) self.order_book_manager.cancel_orders_with(lambda order: pyex_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(10) if self.is_zrx: lifecycle.on_startup(self.startup) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def plunge(self): """ Method to automatically plunge any pending transactions on keeper startup """ pending_txes = get_pending_transactions(self.web3, self.our_address) logging.info(f"There are {len(pending_txes)} pending transactions in the queue") if len(pending_txes) > 0: for index, tx in enumerate(pending_txes): logging.warning(f"Cancelling {index+1} of {len(pending_txes)} pending transactions") # Note this can raise a "Transaction nonce is too low" error, stopping the service. # This means one of the pending TXes was mined, and the service can be restarted to either resume # plunging or normal operation. tx.cancel(gas_price=self.gas_price) def startup(self): self.approve() def shutdown(self): self.order_book_manager.cancel_all_orders() def approve(self): raise NotImplementedError() # Each exchange takes pair input as a different format def pair(self): raise NotImplementedError() def token_sell(self) -> str: raise NotImplementedError() def token_buy(self) -> str: raise NotImplementedError() # Different keys are used to access balance object for different exchanges def our_available_balance(self, our_balances: dict, token: str) -> Wad: raise NotImplementedError() def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): raise NotImplementedError() def place_orders(self, new_orders: list): raise NotImplementedError()
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='oasis-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument("--tub-address", type=str, required=False, help="Ethereum address of the Tub contract") parser.add_argument("--oasis-address", type=str, required=True, help="Ethereum address of the OasisDEX contract") parser.add_argument( "--oasis-support-address", type=str, required=False, help="Ethereum address of the OasisDEX support contract") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--round-places", type=int, default=2, help="Number of decimal places to round order prices to (default=2)" ) parser.add_argument( "--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument( "--refresh-frequency", type=int, default=10, help="Order book refresh frequency (in seconds, default: 10)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) self.otc = MatchingMarket( web3=self.web3, address=Address(self.arguments.oasis_address), support_address=Address(self.arguments.oasis_support_address) if self.arguments.oasis_support_address else None) tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) \ if self.arguments.tub_address is not None else None self.token_buy = ERC20Token(web3=self.web3, address=Address( self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address( self.arguments.sell_token_address)) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed( self.arguments, tub) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with(lambda: self.our_orders()) self.order_book_manager.place_orders_with(self.place_order_function) self.order_book_manager.cancel_orders_with(self.cancel_order_function) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
class LeverjMarketMakerKeeper: """Keeper acting as a market maker on leverj.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='leverj-market-maker-keeper') parser.add_argument( "--leverj-api-server", type=str, default="https://test.leverj.io", help= "Address of the leverj API server (default: 'https://test.leverj.io')" ) parser.add_argument("--account-id", type=str, default="", help="Address of leverj api account id") parser.add_argument("--api-key", type=str, default="", help="Address of leverj api key") parser.add_argument("--api-secret", type=str, default="", help="Address of leverj api secret") parser.add_argument( "--leverj-timeout", type=float, default=9.5, help= "Timeout for accessing the Leverj API (in seconds, default: 9.5)") parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to watch our trades") parser.add_argument( "--eth-key", type=str, nargs='*', help= "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')" ) parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--leverage", type=float, default=1.0, help="Leverage chosen for futures orders") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) self.web3 = Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from register_keys(self.web3, self.arguments.eth_key) setup_logging(self.arguments) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.target_price_lean = Wad(0) self.leverage = self.arguments.leverage self.history = History() self.leverj_api = LeverjFuturesAPI( web3=self.web3, api_server=self.arguments.leverj_api_server, account_id=self.arguments.account_id, api_key=self.arguments.api_key, api_secret=self.arguments.api_secret, timeout=self.arguments.leverj_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.leverj_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.leverj_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.leverj_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(1) lifecycle.on_startup(self.startup) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): quote_increment = self.leverj_api.get_tickSize(self.pair()) self.precision = -(int(log10(float(quote_increment))) + 1) def shutdown(self): self.order_book_manager.cancel_all_orders() def pair(self): name_to_id_map = {'BTCDAI': '1', 'ETHDAI': '2'} return name_to_id_map[self.arguments.pair.upper()] def token_sell(self) -> str: return self.arguments.pair.upper()[:3] def token_buy(self) -> str: return self.arguments.pair.upper()[3:] def allocated_balance(self, token: str) -> Wad: quote_asset_address = self.leverj_api.get_product( self.pair())["quote"]["address"] # for perpetual contracts, the quote balance is allocated across instruments and sides to enter into trades total_available = self.leverj_api.get_plasma_balance( quote_asset_address) # adjust for leverage total_available = Decimal(total_available) * Decimal(self.leverage) self.logger.debug(f'total_available: {total_available}') return self._allocate_to_pair(total_available).get(token) def _allocate_to_pair(self, total_available): # total number of instruments across which the total_available balance is distributed # total_available is denominated in quote units total_number_of_instruments = 1 # there are 2 partitions for allocation per instrument # the dai amount is divided in 2, one for the buy side and another for the sell side number_of_partitions_for_allocation = Wad.from_number( total_number_of_instruments * 2) # buffer_adjustment_factor is a small intentional buffer to avoid allocating the maximum possible. # the allocated amount is a little smaller than the maximum possible allocation # and that is determined by the buffer_adjustment_factor buffer_adjustment_factor = Wad.from_number(1.05) base = self.arguments.pair.upper()[:3] quote = self.arguments.pair.upper()[3:] target_price = self.price_feed.get_price() product = self.leverj_api.get_product(self.pair()) minimum_order_quantity = self.leverj_api.get_minimum_order_quantity( self.pair()) minimum_quantity_wad = Wad.from_number(minimum_order_quantity) if ((base == product['baseSymbol']) and (quote == product['quoteSymbol'])): if ((target_price is None) or (target_price.buy_price is None) or (target_price.sell_price is None)): base_allocation = Wad(0) quote_allocation = Wad(0) self.logger.debug( f'target_price not available to calculate allocations') else: average_price = (target_price.buy_price + target_price.sell_price) / Wad.from_number(2) # at 1x average_price * minimum_quantity_wad is the minimum_required_balance # multiplying this minimum_required_balance by 2 to avoid sending very small orders to the exchange minimum_required_balance = average_price * minimum_quantity_wad * Wad.from_number( 2) # conversion_divisor is the divisor that determines how many chunks should Dai be distributed into. # It considers the price of the base to convert into base denomination. conversion_divisor = average_price * number_of_partitions_for_allocation * buffer_adjustment_factor open_position_for_base = self.leverj_api.get_position_in_wad( base) total_available_wad = Wad.from_number( Decimal(total_available) / Decimal(Decimal(10)**Decimal(18))) base_allocation = total_available_wad / conversion_divisor quote_allocation = total_available_wad / number_of_partitions_for_allocation self.logger.debug( f'open_position_for_base: {open_position_for_base}') # bids are made basis quote_allocation and asks basis base_allocation # if open position is net long then quote_allocation is adjusted. # if open position is net too long then target_price is adjusted to reduce price of the asks/offers if (open_position_for_base.value > 0): open_position_for_base_in_quote = open_position_for_base * average_price net_adjusted_quote_value = quote_allocation.value - abs( open_position_for_base_in_quote.value) self.logger.debug( f'net_adjusted_quote_value: {net_adjusted_quote_value}' ) quote_allocation = Wad( net_adjusted_quote_value ) if net_adjusted_quote_value > minimum_required_balance.value else Wad( 0) # if open position is within 1 Wad range or more than quote allocations then target price is leaned down by 0.1 percent if Wad(net_adjusted_quote_value) < Wad(1): self.target_price_lean = Wad.from_number(0.999) else: self.target_price_lean = Wad(0) elif (open_position_for_base.value < 0): # if open position is net short then base_allocation is adjusted # if open position is net too short then target_price is adjusted to increase price of the bids net_adjusted_base_value = base_allocation.value - abs( open_position_for_base.value) minimum_required_balance_in_base = minimum_required_balance / average_price self.logger.debug( f'net_adjusted_base_value: {net_adjusted_base_value}') base_allocation = Wad( net_adjusted_base_value ) if net_adjusted_base_value > minimum_required_balance_in_base.value else Wad( 0) # if open position is within 1 Wad range or more than base allocations then target price is leaned up by 0.1 percent if Wad(net_adjusted_base_value) < Wad(1): self.target_price_lean = Wad.from_number(1.001) else: self.target_price_lean = Wad(0) else: base_allocation = Wad(0) quote_allocation = Wad(0) allocation = {base: base_allocation, quote: quote_allocation} self.logger.debug(f'allocation: {allocation}') return allocation def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def adjust_target_price(self, target_price: Price) -> Price: assert (isinstance(target_price, Price)) target_price_lean = self.target_price_lean if ((target_price is None) or (target_price.buy_price is None) or (target_price.sell_price is None)): return target_price if target_price_lean.value == 0: return target_price else: self.logger.debug(f'target_price_lean: {target_price_lean}') adjusted_target_price = target_price adjusted_target_price.buy_price = ( target_price.buy_price) * target_price_lean adjusted_target_price.sell_price = ( target_price.sell_price) * target_price_lean return adjusted_target_price def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.control_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() target_price = self.adjust_target_price(target_price) self.logger.debug( f'target_price buy_price: {target_price.buy_price}, target_price sell_price: {target_price.sell_price}' ) # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.info( "Order book is in progress, not placing new orders") return # Place new orders new_orders = bands.new_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.allocated_balance(self.token_buy()), our_sell_balance=self.allocated_balance(self.token_sell()), target_price=target_price)[0] self.place_orders(new_orders) def place_orders(self, new_orders: List[NewOrder]): def place_order_function(new_order_to_be_placed): price = round(new_order_to_be_placed.price, self.precision + 2) amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount self.logger.debug(f'amount: {amount}') leverage_in_wad = Wad.from_number(self.leverage) order_id = str( self.leverj_api.place_order(self.pair(), price, 'LMT', new_order_to_be_placed.is_sell, price, amount, leverage_in_wad, False)) return Order(order_id=order_id, pair=self.pair(), is_sell=new_order_to_be_placed.is_sell, price=price, amount=amount) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order))
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='mpx-market-maker-keeper') self.add_arguments(parser=parser) self.arguments = parser.parse_args(args) setup_logging(self.arguments) provider = HTTPProvider( endpoint_uri=self.arguments.rpc_host, request_kwargs={'timeout': self.arguments.rpc_timeout}) self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3( provider) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) register_keys(self.web3, self.arguments.eth_key) self.token_buy = ERC20Token(web3=self.web3, address=Address( self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address( self.arguments.sell_token_address)) self.bands_config = ReloadableConfig(self.arguments.config) self.price_max_decimals = None self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.zrx_exchange = ZrxExchangeV2(web3=self.web3, address=Address( self.arguments.exchange_address)) self.mpx_api = MpxApi(api_server=self.arguments.mpx_api_server, zrx_exchange=self.zrx_exchange, fee_recipient=Address( self.arguments.fee_address), timeout=self.arguments.mpx_api_timeout, our_address=self.arguments.eth_from) self.zrx_relayer_api = ZrxRelayerApiV2( exchange=self.zrx_exchange, api_server=self.arguments.mpx_api_server) self.zrx_api = ZrxApiV2(zrx_exchange=self.zrx_exchange, zrx_api=self.zrx_relayer_api) markets = self.mpx_api.get_markets()['data'] market = next( filter( lambda item: item['attributes']['pair-name'] == self.arguments. pair, markets)) self.pair = MpxPair(self.arguments.pair, self.token_buy.address, int(market['attributes']['base-token-decimals']), self.token_sell.address, int(market['attributes']['quote-token-decimals'])) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with(lambda: self.get_orders()) self.order_book_manager.get_balances_with(lambda: self.get_balances()) self.order_book_manager.cancel_orders_with(self.cancel_order_function) self.order_book_manager.place_orders_with(self.place_order_function) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
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))
class MpxMarketMakerKeeper: """Keeper acting as a market maker on MPExchange.""" logger = logging.getLogger() def add_arguments(self, parser): parser.add_argument( "--rpc-host", type=str, default="http://localhost:8545", help="JSON-RPC host (default: `http://localhost:8545`)") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--eth-key", type=str, nargs='*', help= "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')" ) parser.add_argument( "--mpx-api-server", type=str, default='https://api.mpexchange.io', help= "Address of the MPX API server (default: 'https://api.mpexchange.io')" ) parser.add_argument( "--mpx-api-timeout", type=float, default=9.5, help="Timeout for accessing the MPX API (in seconds, default: 9.5)" ) parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the Mpx Exchange contract") parser.add_argument("--fee-address", type=str, required=True, help="Ethereum address of the Mpx Fee contract") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the Sell Token") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the Buy Token") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument("--ethgasstation-api-key", type=str, default=None, help="ethgasstation API key") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") pparser.add_argument( "--telegram-log-config-file", type=str, required=False, help= "config file for send logs to telegram chat (e.g. 'telegram_conf.json')", default=None) parser.add_argument( "--keeper-name", type=str, required=False, help="market maker keeper name (e.g. 'Uniswap_V2_MDTETH')", default="mpx") def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='mpx-market-maker-keeper') self.add_arguments(parser=parser) self.arguments = parser.parse_args(args) setup_logging(self.arguments) provider = HTTPProvider( endpoint_uri=self.arguments.rpc_host, request_kwargs={'timeout': self.arguments.rpc_timeout}) self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3( provider) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) register_keys(self.web3, self.arguments.eth_key) self.token_buy = ERC20Token(web3=self.web3, address=Address( self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address( self.arguments.sell_token_address)) self.bands_config = ReloadableConfig(self.arguments.config) self.price_max_decimals = None self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.zrx_exchange = ZrxExchangeV2(web3=self.web3, address=Address( self.arguments.exchange_address)) self.mpx_api = MpxApi(api_server=self.arguments.mpx_api_server, zrx_exchange=self.zrx_exchange, fee_recipient=Address( self.arguments.fee_address), timeout=self.arguments.mpx_api_timeout, our_address=self.arguments.eth_from) self.zrx_relayer_api = ZrxRelayerApiV2( exchange=self.zrx_exchange, api_server=self.arguments.mpx_api_server) self.zrx_api = ZrxApiV2(zrx_exchange=self.zrx_exchange, zrx_api=self.zrx_relayer_api) markets = self.mpx_api.get_markets()['data'] market = next( filter( lambda item: item['attributes']['pair-name'] == self.arguments. pair, markets)) self.pair = MpxPair(self.arguments.pair, self.token_buy.address, int(market['attributes']['base-token-decimals']), self.token_sell.address, int(market['attributes']['quote-token-decimals'])) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with(lambda: self.get_orders()) self.order_book_manager.get_balances_with(lambda: self.get_balances()) self.order_book_manager.cancel_orders_with(self.cancel_order_function) self.order_book_manager.place_orders_with(self.place_order_function) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): self.approve() def approve(self): self.zrx_exchange.approve([self.token_sell, self.token_buy], directly(gas_price=self.gas_price)) def shutdown(self): self.order_book_manager.cancel_all_orders() def get_balances(self): balances = self.zrx_api.get_balances(self.pair) return balances[0], balances[1], eth_balance(self.web3, self.our_address) def get_orders(self) -> list: orders = self.mpx_api.get_orders(self.pair) return self.zrx_api.get_orders(self.pair, orders) def our_total_balance(self, token: ERC20Token) -> Wad: return token.balance_of(self.our_address) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.control_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # In case of MPX, balances returned by `our_total_balance` still contain amounts "locked" # by currently open orders, so we need to explicitly subtract these amounts. our_buy_balance = self.our_total_balance( self.token_buy) - Bands.total_amount( self.our_buy_orders(order_book.orders)) our_sell_balance = self.our_total_balance( self.token_sell) - Bands.total_amount( self.our_sell_orders(order_book.orders)) # Place new orders self.order_book_manager.place_orders( bands.new_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=our_buy_balance, our_sell_balance=our_sell_balance, target_price=target_price)[0]) def place_order_function(self, new_order: NewOrder): assert (isinstance(new_order, NewOrder)) price = round(new_order.price, self.price_max_decimals) amount = new_order.pay_amount if new_order.is_sell else new_order.buy_amount zrx_order = self.mpx_api.place_order(pair=self.pair, is_sell=new_order.is_sell, price=price, amount=amount) if zrx_order is not None: return self.zrx_api.get_orders(self.pair, [zrx_order])[0] else: return None def cancel_order_function(self, order): self.logger.info(f"Canceling order {order.zrx_order.order_hash}") if self.mpx_api.cancel_order(order.zrx_order.order_hash): transact = self.zrx_exchange.cancel_order( order.zrx_order).transact(gas_price=self.gas_price) return transact is not None and transact.successful return False
class KucoinMarketMakerKeeper: """Keeper acting as a market maker on kucoin.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='kucoin-market-maker-keeper') parser.add_argument("--kucoin-api-server", type=str, default="https://api.kucoin.com", help="Address of the kucoin API server (default: 'https://api.kucoin.com')") parser.add_argument("--kucoin-api-key", type=str, required=True, help="API key for the kucoin API") parser.add_argument("--kucoin-secret-key", type=str, required=True, help="Secret key for the kucoin API") parser.add_argument("--kucoin-timeout", type=float, default=9.5, help="Timeout for accessing the kucoin API (in seconds, default: 9.5)") parser.add_argument("--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument("--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument("--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument("--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument("--order-history-every", type=int, default=30, help="Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter(self.arguments) self.history = History() self.kucoin_api = KucoinApi(api_server=self.arguments.kucoin_api_server, api_key=self.arguments.kucoin_api_key, secret_key=self.arguments.kucoin_secret_key, timeout=self.arguments.kucoin_timeout) self.order_book_manager = OrderBookManager(refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with(lambda: self.kucoin_api.get_orders(self.pair())) self.order_book_manager.get_balances_with(lambda: self.kucoin_api.get_balances()) self.order_book_manager.cancel_orders_with(lambda order: self.kucoin_api.cancel_order(order.order_id, order.is_sell, self.pair())) self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): # Get maximum number of decimals for prices and amounts. self.price_precision = self.kucoin_api.get_coin_info(self.token_buy())['tradePrecision'] self.amount_precision = self.kucoin_api.get_coin_info(self.token_sell())['tradePrecision'] def shutdown(self): self.order_book_manager.cancel_all_orders() def pair(self): return self.arguments.pair.upper() def token_sell(self) -> str: return self.arguments.pair.split('-')[0].upper() def token_buy(self) -> str: return self.arguments.pair.split('-')[1].upper() def our_available_balance(self, our_balances: dict, token: str) -> Wad: token_balances = list(filter(lambda coin: coin['coinType'].upper() == token, our_balances)) if token_balances: return Wad.from_number(self.round_down(token_balances[0]['balance'], self.amount_precision)) else: return Wad(0) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.control_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders(our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug("Order book is in progress, not placing new orders") return # Place new orders new_orders = bands.new_orders(our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.our_available_balance(order_book.balances, self.token_buy()), our_sell_balance=self.our_available_balance(order_book.balances, self.token_sell()), target_price=target_price)[0] self.place_orders(new_orders) def place_orders(self, new_orders: List[NewOrder]): def place_order_function(new_order_to_be_placed): price = round(new_order_to_be_placed.price, self.price_precision) amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount amount = round(amount, self.amount_precision) order_id = self.kucoin_api.place_order(self.pair(), new_order_to_be_placed.is_sell, price, amount) return Order(order_id=order_id, pair=self.pair(), is_sell=new_order_to_be_placed.is_sell, price=price, amount=amount) for new_order in new_orders: self.order_book_manager.place_order(lambda new_order=new_order: place_order_function(new_order)) @staticmethod def round_down(num, precision): multiplier = pow(10, precision) return floor(num * multiplier) / multiplier
def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='ddex-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--eth-key", type=str, nargs='*', help= "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')" ) parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument( "--ddex-api-server", type=str, default='https://api.ddex.io', help="Address of the Ddex API (default: 'https://api.ddex.io')") parser.add_argument( "--ddex-api-timeout", type=float, default=9.5, help="Timeout for accessing the Ddex API (in seconds, default: 9.5)" ) parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--buy-token-address", type=str, required=True, help="Ethereum address of the buy token") parser.add_argument("--sell-token-address", type=str, required=True, help="Ethereum address of the sell token") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) register_keys(self.web3, self.arguments.eth_key) if self.arguments.pair != 'USStocks-DAI': self.pair = self.arguments.pair.upper() else: self.pair = self.arguments.pair self.token_buy = ERC20Token(web3=self.web3, address=Address( self.arguments.buy_token_address)) self.token_sell = ERC20Token(web3=self.web3, address=Address( self.arguments.sell_token_address)) self.bands_config = ReloadableConfig(self.arguments.config) self.price_max_decimals = None self.amount_max_decimals = None self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address( self.arguments.exchange_address)) self.ddex_api = DdexApi(self.web3, self.arguments.ddex_api_server, self.arguments.ddex_api_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency, max_workers=1) self.order_book_manager.get_orders_with( lambda: self.ddex_api.get_orders(self.pair)) self.order_book_manager.cancel_orders_with( lambda order: self.ddex_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
def __init__(self, args: list): parser = argparse.ArgumentParser(prog='ethfinex-market-maker-keeper') parser.add_argument( "--ethfinex-api-server", type=str, default="https://api.ethfinex.com", help= "Address of the Ethfinex API server (default: 'https://api.ethfinex.com')" ) parser.add_argument("--ethfinex-api-key", type=str, required=True, help="API key for the Ethfinex API") parser.add_argument("--ethfinex-api-secret", type=str, required=True, help="API secret for the Ethfinex API") parser.add_argument( "--ethfinex-timeout", type=float, default=9.5, help= "Timeout for accessing the Ethfinex API (in seconds, default: 9.5)" ) parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.history = History() self.ethfinex_api = EthfinexApi( api_server=self.arguments.ethfinex_api_server, api_key=self.arguments.ethfinex_api_key, api_secret=self.arguments.ethfinex_api_secret, timeout=self.arguments.ethfinex_timeout) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency, max_workers=1) self.order_book_manager.get_orders_with( lambda: self.ethfinex_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.ethfinex_api.get_balances()) self.order_book_manager.place_orders_with(self.place_order_function) self.order_book_manager.cancel_orders_with( lambda order: self.ethfinex_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
class KorbitMarketMakerKeeper: """Keeper acting as a market maker on Korbit.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='Korbit-market-maker-keeper') parser.add_argument( "--korbit-api-server", type=str, default="https://api.korbit.co.kr", help= "Address of the korbit API server (default: 'https://api.korbit.co.kr')" ) parser.add_argument("--korbit-api-key", type=str, required=True, help="API key for the Korbit API") parser.add_argument("--korbit-secret-key", type=str, required=True, help="Secret key for the Korbit API") parser.add_argument( "--korbit-timeout", type=float, default=9.5, help= "Timeout for accessing the Korbit API (in seconds, default: 9.5)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") parser.add_argument( "--telegram-log-config-file", type=str, required=False, help= "config file for send logs to telegram chat (e.g. 'telegram_conf.json')", default=None) parser.add_argument( "--keeper-name", type=str, required=False, help="market maker keeper name (e.g. 'Uniswap_V2_MDTETH')", default="korbit") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.korbit_api = KorbitApi( api_server=self.arguments.korbit_api_server, api_key=self.arguments.korbit_api_key, secret_key=self.arguments.korbit_secret_key, timeout=self.arguments.korbit_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.korbit_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.korbit_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.korbit_api.cancel_order(int(order.order_id), self.pair())) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def shutdown(self): self.order_book_manager.cancel_all_orders() def pair(self): return self.arguments.pair.lower() def token_sell(self) -> str: return self.arguments.pair.split('_')[0].lower() def token_buy(self) -> str: return self.arguments.pair.split('_')[1].lower() def our_available_balance(self, our_balances: dict, token: str) -> Wad: balance = our_balances[f"{token}"]["available"] return Wad.from_number(balance) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.control_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # Place new orders self.place_orders( bands.new_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.our_available_balance( order_book.balances, self.token_buy()), our_sell_balance=self.our_available_balance( order_book.balances, self.token_sell()), target_price=target_price)[0]) def place_orders(self, new_orders): def place_order_function(new_order_to_be_placed): amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount order_id = self.korbit_api.place_order( pair=self.pair(), is_sell=new_order_to_be_placed.is_sell, price=new_order_to_be_placed.price, amount=amount) timestamp = int(round(time.time())) return Order(str(order_id), timestamp, self.pair(), new_order_to_be_placed.is_sell, new_order_to_be_placed.price, amount) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order))