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 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 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))
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
class EToroMarketMakerKeeper: """Keeper acting as a market maker on eToro.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='eToro-market-maker-keeper') parser.add_argument("--etoro-api-server", type=str, required=True, help="Address of the eToro API server") parser.add_argument("--etoro-account", type=str, default="*****@*****.**", help="Username for eToroX account") parser.add_argument("--etoro-api-key", type=str, required=True, help="API key for the eToro API") parser.add_argument( "--etoro-secret-key", type=argparse.FileType('r'), required=True, help="RSA Private Key for signing requests to the eToroX API") parser.add_argument( "--etoro-timeout", type=float, default=9.5, help= "Timeout for accessing the eToro API (in seconds, default: 9.5)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") parser.add_argument( "--telegram-log-config-file", type=str, required=False, help= "config file for send logs to telegram chat (e.g. 'telegram_conf.json')", default=None) parser.add_argument( "--keeper-name", type=str, required=False, help="market maker keeper name (e.g. 'Uniswap_V2_MDTETH')", default="etoro") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.etoro_api = EToroApi(api_server=self.arguments.etoro_api_server, account=self.arguments.etoro_account, api_key=self.arguments.etoro_api_key, secret_key=self.arguments.etoro_secret_key, timeout=self.arguments.etoro_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.etoro_api.get_orders(self._join_string(self.pair()), "open")) self.order_book_manager.get_balances_with( lambda: self.etoro_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.etoro_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def shutdown(self): self.order_book_manager.cancel_all_orders() def pair(self): return self.arguments.pair.lower() def token_sell(self) -> str: return self.arguments.pair.split('_')[0].lower() def token_buy(self) -> str: return self.arguments.pair.split('_')[1].lower() def our_available_balance(self, our_balances: list, token: str) -> Wad: balance = list(filter(lambda x: x['currency'] == token, our_balances))[0]['balance'] return Wad.from_number(balance) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.control_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # Place new orders self.place_orders( bands.new_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.our_available_balance( order_book.balances, self.token_buy()), our_sell_balance=self.our_available_balance( order_book.balances, self.token_sell()), target_price=target_price)[0]) def place_orders(self, new_orders): def place_order_function(new_order_to_be_placed): amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount side = "sell" if new_order_to_be_placed.is_sell == True else "buy" order_id = self.etoro_api.place_order( pair=self._join_string(self.pair()), side=side, price=new_order_to_be_placed.price, amount=amount) timestamp = datetime.now(tz=timezone.utc).isoformat() return Order(str(order_id), timestamp, self._join_string(self.pair()), new_order_to_be_placed.is_sell, new_order_to_be_placed.price, amount) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order)) # Return lower case concatted string. Assumes inputted string is an _ delimited pair def _join_string(self, string: str) -> str: assert (isinstance(string, str)) return "".join(string.split('_')).lower()
class 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 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()
class BiboxMarketSurfer: """Keeper acting as a market maker on Bibox.""" logger = logging.getLogger() 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("--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("--base_price", type=str, default=0.00205375, help="base price while initial base price ") parser.add_argument("--total_amount", type=str, default=5, help="the total assets for investing ") parser.add_argument( "--transaction_percent", type=str, default=0.02, help= "percent of total amount of each transaction or each order, fix percent 2%" ) parser.add_argument( "--arbitrage_percent", type=str, default=0.005, help= "the percent of current pirce as margin between two adjacent price orders, 0.5%" ) parser.add_argument( "--order_num", type=str, default=3, help="the number of orders in each sell and buy bands ") 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_history_reporter = create_order_history_reporter( self.arguments) self.local_orders = [] self.base_price = 0.00243602 self.total_amount = 2000 self.each_order_percent = 0.1 # percent of total amount of each transaction or each order self.arbitrage_percent = 0.01 self.band_order_limit = 3 # the order count of sell or buy bands must less than limit self.each_order_amount = self.total_amount * self.each_order_percent # To implement abstract function with different exchanges API self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) 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.cancel_orders_with( lambda order: self.bibox_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): # Place new orders while initialize the whole surfer system self.initialize_orders(self.base_price, self.each_order_amount, self.arbitrage_percent, self.band_order_limit) time.sleep( 5) # wait for order book manager to get placed orders 足够时间保证系统稳定返回 self.local_orders = self.order_book_manager.get_order_book().orders with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(10, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def initialize_orders(self, base_price, each_order_amount, arbitrage_percent, band_order_limit): orders = [] i = 1 while band_order_limit + 1 > i: # place sell order price = base_price * (1 + arbitrage_percent * i) # pay_amount = Wad.min(band.avg_amount - total_amount, our_sell_balance, limit_amount) pay_amount = each_order_amount * self.amount_disguise( ) #bix amount # add unique amount number to identify different order for result performance statics pay_amount = pay_amount + self.amount_identify() buy_amount = pay_amount * price #eth money orders.append( NewOrder(is_sell=True, price=Wad.from_number(price), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits.use_limit( time.time(), pay_amount))) # place buy order, pay attention to rotate bix - eth price = base_price * (1 - arbitrage_percent * i) # pay_amount = Wad.min(band.avg_amount - total_amount, our_sell_balance, limit_amount) tmp = each_order_amount * self.amount_disguise( ) + self.amount_identify() pay_amount = tmp * price #eth money 25 buy_amount = tmp #bix amount 0.05 orders.append( NewOrder(is_sell=False, price=Wad.from_number(price), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits.use_limit( time.time(), pay_amount))) i = i + 1 self.place_orders(orders) # 偶尔有 bug,提交的完成慢,导致 local orders 比 下一次 获取回来少, initial_delay 加长时间到15秒,时间太长也麻烦, # 会导致一开始提交就成交的那部分订单不会存到 local orders # 需要换地方,order_book_manager更新不及时的情况下,会导致返回的订单数据不全,或者订单里的参数默认为0的情况 # self.local_orders = self.order_book_manager.get_order_book().orders @staticmethod def amount_disguise(): rand = [ 0.8, 0.84, 0.88, 0.92, 0.95, 0.99, 1.03, 1.06, 1.09, 1.12, 1.16, 1.2 ] return rand[random.randint(0, 11)] @staticmethod def amount_identify(): return round(random.random() / 10000.0, 10) def shutdown(self): self.order_book_manager.cancel_all_orders(final_wait_time=30) def pair(self): return self.arguments.pair.upper() def token_sell(self) -> str: return self.arguments.pair.split('_')[0].upper() def token_buy(self) -> str: return self.arguments.pair.split('_')[1].upper() def our_available_balance(self, our_balances: list, token: str) -> Wad: return Wad.from_number( next(filter(lambda coin: coin['symbol'] == token, our_balances))['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 count_sell_orders(self, our_orders: list) -> int: return len(list(filter(lambda order: order.is_sell, our_orders))) def count_buy_orders(self, our_orders: list) -> int: return len(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() # print(type(self.local_orders)) # print(self.local_orders) # print(type(order_book.orders)) # print(order_book.orders) print("---**---The lenght of local_orders " + str(self.local_orders.__len__())) print("---**---The lenght of order_book.orders " + str(len(order_book.orders))) local_order_ids = set(order.order_id for order in self.local_orders) order_book_ids = set(order.order_id for order in order_book.orders) completed_order_ids = list(local_order_ids - order_book_ids) # 如果没有后续的更新 local orders,只有这里的更新模块,肯定有问题的,因为一旦有成交, # completed_order_ids 不为0,则永远更新不了local orders 了 # return if there none order be completed # 下面这种情况,只有在order_book订单完全"包含"local_order订单时,但是两者并不相等时,才会让本地订单等于远程订单; # 这种一般是远程订单比本地订单多,往往比如人工在系统提交了新的订单 if completed_order_ids.__len__() == 0: if local_order_ids.__len__() != order_book_ids.__len__(): print("update local order") self.local_orders = order_book.orders return # completed_orders = list(filter(lambda order: order.order_id in completed_order_ids, self.local_orders)) completed_orders = [ order for order in self.local_orders if order.order_id in completed_order_ids ] # completed_orders = list(filter(lambda order: order.order_id in local_order_ids, order_book.orders)) print("---**---The lenght of completed orders " + str(len(completed_orders))) print(completed_orders) # completed_orders_new = list(set(self.local_orders) - set(order_book.orders)) # print("---**---The lenght of completed new orders " + str(len(completed_orders_new))) # print(completed_orders_new) # completed_orders = [{'amount': Wad(2220000000000000000), # 'amount_symbol': 'BIX', # 'created_at': 1528203670000, # 'is_sell': True, # 'money': Wad(52779250000000000), # 'money_symbol': 'ETH', # 'order_id': 606026215, # 'price': Wad(2294750000000000)}, {'amount': Wad(2990000000000000000), # 'amount_symbol': 'BIX', # 'created_at': 1528203670000, # 'is_sell': False, # 'money': Wad(55779250000000000), # 'money_symbol': 'ETH', # 'order_id': 606026215, # 'price': Wad(2394750000000000)}] # our_buy_orders = self.our_buy_orders(order_book.orders) # our_sell_orders = self.our_sell_orders(order_book.orders) # print(our_buy_orders) # print(our_sell_orders) # 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 # if (self.local_orders.__len__() - len(order_book.orders) > 0): if len(completed_orders) > 0: print("--------- some orders have been done --------") new_orders = [] step = 1 count_sell_order = self.count_sell_orders(order_book.orders) count_buy_order = self.count_buy_orders(order_book.orders) for cod in completed_orders: # print(type(cod)) # print(cod.is_sell) # the completed order is sell order, buy order should be placed if cod.is_sell: # place buy order, pay attention to rotate bix - eth price = float(cod.price) * (1 - self.arbitrage_percent) print("----to submit a new buy order with price " + str(price)) pay_amount = float(cod.amount) * price # eth money 25 buy_amount = float(cod.amount) # bix amount 0.05 new_orders.append( NewOrder(is_sell=False, price=Wad.from_number(price), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) # 以当前价格为基数,重新submit一个高价格的 sell 订单,补充 sell list # place sell a new order with higher price # 需要判断订单的数量是否小于band order limits,并且按照差异补充订单 # count_sell_order = self.count_sell_orders(order_book.orders) print(count_sell_order) band_sell_order_gap = self.band_order_limit - count_sell_order print("---band gap---- " + str(band_sell_order_gap)) # while band_sell_order_gap > 0: # 外部已经有循环了,不需要这个循环了,否则在多订单被吃时,会加倍补充 # 这里只需要判断,控制数量就够了 if band_sell_order_gap > 0: current_price = self.bibox_api.get_last_price( self.pair()) print("------current price---- " + str(current_price)) price = float(current_price) * ( 1 + self.arbitrage_percent * (step + count_sell_order)) print("----higher price to sell--- " + str(price)) pay_amount = self.each_order_amount * self.amount_disguise( ) # bix amount buy_amount = pay_amount * price # eth money new_orders.append( NewOrder(is_sell=True, price=Wad.from_number(price), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) # step = step + 1 # band_sell_order_gap = band_sell_order_gap - 1 count_sell_order = count_sell_order + 1 else: # buy order had been completed # to place a sell order price = float(cod.price) * (1 + self.arbitrage_percent) print("----price--- sell--- ") print(price) pay_amount = float(cod.amount) # bix amount buy_amount = pay_amount * price # eth money new_orders.append( NewOrder(is_sell=True, price=Wad.from_number(price), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) # 以当前价格为基数,重新submit一个 buy 订单,补充 buy list # 需要判断订单的数量是否小于band order limits,并且按照差异补充订单 # count_buy_order = self.count_buy_orders(order_book.orders) band_buy_order_gap = self.band_order_limit - count_buy_order print("---band gap----" + str(band_buy_order_gap)) # while band_buy_order_gap > 0: if band_buy_order_gap > 0: #基础价格放在循环里的话,能快速反映当前价格,特保是激烈波动的时候;但是增加了请求次数 current_price = self.bibox_api.get_last_price( self.pair()) price = float(current_price) * ( 1 - self.arbitrage_percent * (step + count_buy_order)) print("----lower price order to buy--- " + str(price)) tmp = self.each_order_amount * self.amount_disguise() pay_amount = tmp * price # eth money 25 buy_amount = tmp # bix amount 0.05 new_orders.append( NewOrder(is_sell=False, price=Wad.from_number(price), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) # band_buy_order_gap = band_buy_order_gap - 1 count_buy_order = count_buy_order + 1 step = step + 1 self.place_orders(new_orders) # update local orders, 前面有更新模块,与这边不完全相同,尤其是有成交的情况下,必须要更新 # 是这样吗? 似乎也不是的,有成交的情况下,下一次订单也会让 set(local) - set(order book)=0的,集合相减的特殊之处 # 如果这样就没有必要了。 # 是这样简单的复制更新,还是本地自己维护一个 id list 好呢? 也就是把(1)确定成交的从 local 删除; # (2)确定提交的add 到本地; # 缩进到循环: if len(completed_orders) > 0:,在出现两者不一致的时候,同步更新订单; # 但是这个会导致一个问题,就是初始化的订单里,有price 为0,导致两者不一致的情况,怎么办?这里解决了,是通过 order id对比而不是 # 直接的 order 对比,所以应该是解决了才对 print("-----update local order------") self.local_orders = self.order_book_manager.get_order_book().orders # 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) # print("there is " + str(len(cancellable_orders)) + " orders should be cancelled") # 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 amount_symbol = self.token_sell() money = new_order_to_be_placed.buy_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.pay_amount money_symbol = self.token_buy() new_order_id = self.bibox_api.place_order( is_sell=new_order_to_be_placed.is_sell, amount=amount, amount_symbol=amount_symbol, money=money, money_symbol=money_symbol) return Order(new_order_id, 0, new_order_to_be_placed.is_sell, Wad(money / amount), amount, amount_symbol, money, money_symbol) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order)) def get_price(self, pair): self.bibox_api.get_all_trades()
class BiboxMarketSurfer: """Keeper acting as a market maker on Bibox.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='bibox-market-maker-keeper') parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the surfer will operate") parser.add_argument( "--output-path", type=str, required=True, help="output file path of the completed order result") # reserved for old program # 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.orderlog = Logger(self.arguments.output_path + "SurferResult_" + self.arguments.pair, level='info') try: f = open(self.arguments.config, 'r') config = json.loads(f.read()) f.close() self.bibox_api = BiboxApi(api_server=config["bibox_api_server"], api_key=config["bibox_api_key"], secret=config["bibox_secret"], timeout=config["bibox_timeout"]) for pair in config['pairs']: if pair['pair'] == self.arguments.pair: self.total_amount = pair["total_amount"] # percent of total amount of each transaction or each order self.each_order_percent = pair["each_order_percent"] self.arbitrage_percent = pair["arbitrage_percent"] # the order count of sell or buy bands must less than limit self.band_order_limit = pair["band_order_limit"] except Exception as e: logging.getLogger().warning( f"Config file is invalid ({e}). Treating the config file as it has no bands." ) self.history = History() 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.local_orders = [] self.each_order_amount = self.total_amount * self.each_order_percent # To implement abstract function with different exchanges API self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) 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.cancel_orders_with( lambda order: self.bibox_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): # Place new orders while initialize the whole surfer system self.initialize_orders(self.each_order_amount, self.arbitrage_percent, self.band_order_limit) time.sleep( 5 ) # wait for order book manager to get placed orders 足够时间保证系统稳定返回,这个时间还是不一定够 self.local_orders = self.order_book_manager.get_order_book().orders with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(10, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def initialize_orders(self, each_order_amount, arbitrage_percent, band_order_limit): orders = [] i = 1 base_price = self.get_last_price(self.pair()) while band_order_limit + 1 > i: # place sell order price = float(base_price) * (1 + arbitrage_percent * i) # pay_amount = Wad.min(band.avg_amount - total_amount, our_sell_balance, limit_amount) pay_amount = each_order_amount * self.amount_disguise( ) #bix amount pay_amount = pay_amount + self.suffix_amount_identify() buy_amount = pay_amount * price #eth money # print(pay_amount) orders.append( NewOrder(is_sell=True, price=Wad.from_number(price), amount=Wad.from_number(pay_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits.use_limit( time.time(), pay_amount))) # place buy order, pay attention to rotate bix - eth price = float(base_price) * (1 - arbitrage_percent * i) # pay_amount = Wad.min(band.avg_amount - total_amount, our_sell_balance, limit_amount) tmp = each_order_amount * self.amount_disguise() pay_amount = tmp * price #eth money 25 buy_amount = tmp #bix amount 0.05 buy_amount = buy_amount + self.suffix_amount_identify() # print(buy_amount) orders.append( NewOrder(is_sell=False, price=Wad.from_number(price), amount=Wad.from_number(buy_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits.use_limit( time.time(), pay_amount))) i = i + 1 self.place_orders(orders) # 偶尔有 bug,提交的完成慢,导致 local orders 比 下一次 获取回来少, initial_delay 加长时间到15秒,时间太长也麻烦, # 会导致一开始提交就成交的那部分订单不会存到 local orders # 需要换地方,order_book_manager更新不及时的情况下,会导致返回的订单数据不全,或者订单里的参数默认为0的情况 # self.local_orders = self.order_book_manager.get_order_book().orders @staticmethod def amount_disguise(): rand = [ 0.8, 0.84, 0.88, 0.92, 0.95, 0.99, 1.03, 1.06, 1.09, 1.12, 1.16, 1.2 ] return rand[random.randint(0, 11)] # suffix unique amount number to identify different buy/sell order pairs for result performance statics @staticmethod def suffix_amount_identify(): return round(random.random() / 10.0, 10) def get_last_price(self, pair): return self.bibox_api.ticker(pair)['last'] def shutdown(self): self.order_book_manager.cancel_all_orders(final_wait_time=30) def pair(self): return self.arguments.pair.upper() def token_sell(self) -> str: return self.arguments.pair.split('_')[0].upper() def token_buy(self) -> str: return self.arguments.pair.split('_')[1].upper() def our_available_balance(self, our_balances: list, token: str) -> Wad: return Wad.from_number( next(filter(lambda coin: coin['symbol'] == token, our_balances))['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 count_sell_orders(self, our_orders: list) -> int: return len(list(filter(lambda order: order.is_sell, our_orders))) def count_buy_orders(self, our_orders: list) -> int: return len(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() self.logger.info( "---**----synchronize_orders: The length of local_orders " + str(self.local_orders.__len__())) self.logger.info( "---**----synchronize_orders: The length of order_book.orders " + str(len(order_book.orders))) local_order_ids = set(order.order_id for order in self.local_orders) order_book_ids = set(order.order_id for order in order_book.orders) completed_order_ids = list(local_order_ids - order_book_ids) # 如果没有后续的更新 local orders,只有这里的更新模块,肯定有问题的,因为一旦有成交, # completed_order_ids 不为0,则永远更新不了local orders 了 # return if there none order be completed # 下面这种情况,只有在order_book订单完全"包含"local_order订单时,但是两者并不相等时,才会让本地订单等于远程订单; # 这种一般是远程订单比本地订单多,往往比如人工在系统提交了新的订单 # 这段是必须的,比如程序起步的时候,同时提交一批订单,导致速度慢,导致 local_orders 只有真正订单的一部分 if completed_order_ids.__len__() == 0: if local_order_ids.__len__() != order_book_ids.__len__(): self.logger.info( "---**---update local order with remote order while no completed order in past cycle" ) self.local_orders = order_book.orders return # completed_orders = list(filter(lambda order: order.order_id in completed_order_ids, self.local_orders)) completed_orders = [ order for order in self.local_orders if order.order_id in completed_order_ids ] # completed_orders = list(filter(lambda order: order.order_id in local_order_ids, order_book.orders)) self.logger.info("---**---The number of completed orders is " + str(len(completed_orders))) self.logger.info("---**---Below is/are completed orders : ") self.logger.info(completed_orders) # completed_orders_new = list(set(self.local_orders) - set(order_book.orders)) # print("---**---The lenght of completed new orders " + str(len(completed_orders_new))) # print(completed_orders_new) # completed_orders = [{'amount': Wad(2220000000000000000), # 'amount_symbol': 'BIX', # 'created_at': 1528203670000, # 'is_sell': True, # 'money': Wad(52779250000000000), # 'money_symbol': 'ETH', # 'order_id': 606026215, # 'price': Wad(2294750000000000)}, {'amount': Wad(2990000000000000000), # 'amount_symbol': 'BIX', # 'created_at': 1528203670000, # 'is_sell': False, # 'money': Wad(55779250000000000), # 'money_symbol': 'ETH', # 'order_id': 606026215, # 'price': Wad(2394750000000000)}] # our_buy_orders = self.our_buy_orders(order_book.orders) # our_sell_orders = self.our_sell_orders(order_book.orders) # print(our_buy_orders) # print(our_sell_orders) # 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 # order_book.orders 按照从低到高价格排序,获取价格最低(也就是最远)的1个订单作为要取消的订单; sorted_buy_orders = sorted(self.our_buy_orders(order_book.orders), key=lambda order: order.price) # order_book.orders 按照从高到低价格排序,获取价格最高(也就是最远)的 1 个订单作为要取消的订单; sorted_sell_orders = sorted(self.our_sell_orders(order_book.orders), key=lambda order: order.price, reverse=True) # if (self.local_orders.__len__() - len(order_book.orders) > 0): if len(completed_orders) > 0: self.logger.info( "---**---some orders have been completed, and new should be submitted" ) new_orders = [] cancellable_orders = [] step = 1 buy_index = 0 sell_index = 0 count_sell_order = self.count_sell_orders(order_book.orders) count_buy_order = self.count_buy_orders(order_book.orders) for cod in completed_orders: # print(type(cod)) # print(cod.is_sell) # the completed order is sell order, buy order should be placed cancel_order_count = 0 self.orderlog.logger.info(" - " + str(cod.is_sell) + " - " + str(cod.price) + " - " + str(cod.amount) + " - " + str(cod.price * cod.amount)) if cod.is_sell: #(1-1 buy) place buy order, pay attention to rotate bix - eth price = float(cod.price) * (1 - self.arbitrage_percent) self.logger.info("To submit a new buy order with price " + str(price)) pay_amount = float(cod.amount) * price # eth money 25 buy_amount = float(cod.amount) # bix amount 0.05 new_orders.append( NewOrder(is_sell=False, price=Wad.from_number(price), amount=Wad.from_number(buy_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) #(1-2 buy) 取消超出订单数量上限的buy订单,价格最远的订单--就是最低价格的订单 buyo = count_buy_order + 1 - self.band_order_limit self.logger.info( "Number of exceed the upper limit of buy order: " + str(buyo)) # 加上后面这个条件,防止buy订单也成交,当前的订单列表里实际已经没有那么多数量的 buy 订单供取消 # 比如同一周期,buy 订单被吃了1,sell 订单被吃了3个,那么剩下的buy订单就不会被取消3次了 if buyo > 0 and len(sorted_buy_orders) > buy_index: # order_book.orders 按照从低到高价格排序,获取价格最低(也就是最远)的1个订单作为要取消的订单; # sorted_buy_orders = sorted(self.our_buy_orders(order_book.orders), # key=lambda order: order.price) print(sorted_buy_orders) self.logger.info( "Cancel buy order which exceed band order limit ") self.logger.info( str(sorted_buy_orders[buy_index].order_id)) self.logger.info(str(sorted_buy_orders[buy_index])) print(type(sorted_buy_orders[buy_index])) cancellable_orders.append(sorted_buy_orders[buy_index]) # self.bibox_api.cancel_order(sorted_buy_orders[index].order_id) buy_index = buy_index + 1 else: #如果没有对应取消订单,则 buy 订单数量增加 count_buy_order = count_buy_order + 1 #(2 sell) 以当前价格为基数,重新submit一个高价格的 sell 订单,补充 sell list # place sell a new order with higher price # 需要判断订单的数量是否小于band order limits,并且按照差异补充订单 # count_sell_order = self.count_sell_orders(order_book.orders) band_sell_order_gap = self.band_order_limit - count_sell_order self.logger.info( "The number of sell order which need to submit " + str(band_sell_order_gap)) # while band_sell_order_gap > 0: # 外部已经有循环了,不需要这个循环了,否则在多订单被吃时,会加倍补充 # 这里只需要判断,控制数量就够了 if band_sell_order_gap > 0: current_price = self.get_last_price(self.pair()) self.logger.info("---**---current price is " + str(current_price)) price = float(current_price) * ( 1 + self.arbitrage_percent * (step + count_sell_order)) self.logger.info( "The higher price of new sell order is " + str(price)) pay_amount = self.each_order_amount * self.amount_disguise( ) # bix amount pay_amount = pay_amount + self.suffix_amount_identify( ) # add unique identify buy_amount = pay_amount * price # eth money new_orders.append( NewOrder(is_sell=True, price=Wad.from_number(price), amount=Wad.from_number(pay_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) count_sell_order = count_sell_order + 1 else: # buy order had been completed #(1-1 sell) to place a corresponding sell order price = float(cod.price) * (1 + self.arbitrage_percent) self.logger.info( "To submit new sell order with arbitrage price: " + str(price)) pay_amount = float(cod.amount) # bix amount buy_amount = pay_amount * price # eth money new_orders.append( NewOrder(is_sell=True, price=Wad.from_number(price), amount=Wad.from_number(pay_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) #(1-2 sell) 取消超出band order limits数量的 sell 订单 self.logger.info( "--------debug--------------Number of exceed the upper limit of sell order: " + str(count_sell_order + 1 - self.band_order_limit)) sello = count_sell_order + 1 - self.band_order_limit if sello > 0 and len(sorted_sell_orders) > sell_index: # order_book.orders 按照从高到低价格排序,获取价格最高(也就是最远)的 1 个订单作为要取消的订单; # sorted_sell_orders = sorted(self.our_sell_orders(order_book.orders), # key=lambda order: order.price, # reverse=True) print(sorted_sell_orders) self.logger.info( "Cancel sell order which exceed band order limit ") self.logger.info( str(sorted_sell_orders[sell_index].order_id)) self.logger.info(str(sorted_sell_orders[sell_index])) cancellable_orders.append( sorted_sell_orders[sell_index]) # self.bibox_api.cancel_order(sorted_sell_orders[index].order_id) sell_index = sell_index + 1 else: count_sell_order = count_sell_order + 1 #(2 buy) 以当前价格为基数,重新submit一个 buy 订单,补充 buy list # 需要判断订单的数量是否小于band order limits,并且按照差异补充订单 # count_buy_order = self.count_buy_orders(order_book.orders) band_buy_order_gap = self.band_order_limit - count_buy_order self.logger.info( "The number of buy order which need to submit " + str(band_buy_order_gap)) if band_buy_order_gap > 0: #基础价格放在循环里的话,能快速反映当前价格,特保是激烈波动的时候;但是增加了请求次数 current_price = self.get_last_price(self.pair()) price = float(current_price) * ( 1 - self.arbitrage_percent * (step + count_buy_order)) self.logger.info( "The lower price order of new buy order is " + str(price)) tmp = self.each_order_amount * self.amount_disguise() pay_amount = tmp * price # eth money 25 buy_amount = tmp # bix amount 0.05 buy_amount = buy_amount + self.suffix_amount_identify( ) # add unique identify new_orders.append( NewOrder(is_sell=False, price=Wad.from_number(price), amount=Wad.from_number(buy_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) # band_buy_order_gap = band_buy_order_gap - 1 count_buy_order = count_buy_order + 1 step = step + 1 # Cancel orders if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) time.sleep(1) while order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.info( "Sleep 1 s while order manager is in progress after cancelling exceed orders" ) time.sleep(1) # Submit new orders self.place_orders(new_orders) time.sleep(1) while order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.info( "Sleep 1 s while order manager is in progress after placing new orders" ) time.sleep(1) # update local orders, 前面有更新模块,与这边不完全相同,尤其是有成交的情况下,必须要更新 # 是这样吗? 似乎也不是的,有成交的情况下,下一次订单也会让 set(local) - set(order book)=0的,集合相减的特殊之处 # 如果这样就没有必要了。 # 是这样简单的复制更新,还是本地自己维护一个 id list 好呢? 也就是把(1)确定成交的从 local 删除; # (2)确定提交的add 到本地; # 缩进到循环: if len(completed_orders) > 0:,在出现两者不一致的时候,同步更新订单; # 但是这个会导致一个问题,就是初始化的订单里,有price 为0,导致两者不一致的情况,怎么办?这里解决了,是通过 order id对比而不是 # 直接的 order 对比,所以应该是解决了才对 # 默认有3秒的刷新周期,导致被成功取消的订单,不会马上出现在order_book_manager # 导致 local 比真实的多了,使得取消的订单在下一次被误认为是成交了 # 以上虽然 while 循环了,但是如果 while 循环的时候,有订单被成交了,这个订单就会丢掉,被误认为是撤销的订单了 self.logger.info( "Update local order with latest remote order at the end of synchronize cycle" ) self.local_orders = self.order_book_manager.get_order_book().orders # 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) # print("there is " + str(len(cancellable_orders)) + " orders should be cancelled") # 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 amount_symbol = self.token_sell() money = new_order_to_be_placed.buy_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.pay_amount money_symbol = self.token_buy() new_order_id = self.bibox_api.place_order( is_sell=new_order_to_be_placed.is_sell, amount=amount, amount_symbol=amount_symbol, money=money, money_symbol=money_symbol) return Order(new_order_id, 0, new_order_to_be_placed.is_sell, Wad(money / amount), amount, amount_symbol, money, money_symbol) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order)) def get_price(self, pair): self.bibox_api.get_all_trades()
class TethfinexMarketMakerKeeper: """Keeper acting as a market maker on Trustless Ethfinex.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='tethfinex-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to send transactions") parser.add_argument( "--eth-key", type=str, nargs='*', help= "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')" ) parser.add_argument( "--exchange-address", type=str, required=True, help="Ethereum address of the 0x Exchange contract") parser.add_argument("--tub-address", type=str, required=False, help="Ethereum address of the Tub contract") parser.add_argument( "--tethfinex-api-server", type=str, default='https://api.ethfinex.com', help= "Address of the Trustless Ethfinex API server (default: 'https://api.ethfinex.com')" ) parser.add_argument( "--tethfinex-timeout", type=float, default=9.5, help="Timeout for accessing the IDEX API (in seconds, default: 9.5)" ) parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--gas-price", type=int, default=0, help="Gas price (in Wei)") parser.add_argument( "--smart-gas-price", dest='smart_gas_price', action='store_true', help= "Use smart gas pricing strategy, based on the ethgasstation.info feed" ) parser.add_argument("--ethgasstation-api-key", type=str, default=None, help="ethgasstation API key") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") parser.set_defaults(cancel_on_shutdown=False, withdraw_on_shutdown=False) self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from self.our_address = Address(self.arguments.eth_from) register_keys(self.web3, self.arguments.eth_key) tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) \ if self.arguments.tub_address is not None else None self.sai = ERC20Token(web3=self.web3, address=tub.sai()) self.price_feed = PriceFeedFactory().create_price_feed( self.arguments, tub) self.bands_config = ReloadableConfig(self.arguments.config) self.gas_price = GasPriceFactory().create_gas_price( self.web3, self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.tethfinex_exchange = ZrxExchange( web3=self.web3, address=Address(self.arguments.exchange_address)) self.tethfinex_api = TEthfinexApi( self.tethfinex_exchange, self.arguments.tethfinex_api_server, timeout=self.arguments.tethfinex_timeout) config = self.tethfinex_api.get_config()['0x'] self.fee_address = Address(config['ethfinexAddress']) token_registry = config['tokenRegistry'] token_sell = self.token_sell() token_buy = self.token_buy() self.token_sell_wrapper = TEthfinexToken( self.web3, Address(token_registry[token_sell]['wrapperAddress']), token_sell) self.token_buy_wrapper = TEthfinexToken( self.web3, Address(token_registry[token_buy]['wrapperAddress']), token_buy) pair = self.pair() self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency, max_workers=1) self.order_book_manager.get_orders_with( lambda: self.tethfinex_api.get_orders(pair)) self.order_book_manager.cancel_orders_with( lambda order: self.tethfinex_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_block(self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def pair(self): # Trustless Ethfinex is inconsistent here. They call the pair `DAIETH`, but in reality all prices are # calculated like it was an `ETH/DAI` pair. return 'DAIETH' def token_sell(self) -> str: return self.arguments.pair[:3] def token_buy(self) -> str: return self.arguments.pair[3:] @retry(delay=5, logger=logger) def shutdown(self): self.order_book_manager.cancel_all_orders() def our_available_balance(self, token: TEthfinexToken) -> Wad: return Wad.from_number(token.balance_of(self.our_address)) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.control_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # Evaluate if we need to create new orders, and how much do we need to deposit new_orders, missing_buy_amount, missing_sell_amount = bands.new_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.our_available_balance(self.token_buy_wrapper), our_sell_balance=self.our_available_balance( self.token_sell_wrapper), target_price=target_price) # If deposited amount too low for placing buy orders, try to deposit. # If deposited amount too low for placing sell orders, try to deposit. made_deposit = False if missing_buy_amount > Wad(0): if self.deposit_for_buy_order(missing_buy_amount): made_deposit = True if missing_sell_amount > Wad(0): if missing_sell_amount > Wad(0): if self.deposit_for_sell_order(missing_sell_amount): made_deposit = True # If we managed to deposit something, do not do anything so we can reevaluate new orders to be placed. # Otherwise, place new orders. if not made_deposit: self.place_orders(new_orders) def cancel_orders(self, orders: list): for order in orders: self.tethfinex_api.cancel_order(order.order_id) def place_orders(self, new_orders): for new_order in new_orders: if new_order.is_sell: self.logger.info( f"Sell amount {float(new_order.pay_amount)} of ETH with {float(new_order.buy_amount)} DAI" ) self.tethfinex_api.place_order( True, pay_token=self.token_sell_wrapper.address, pay_amount=new_order.pay_amount, buy_token=self.token_buy_wrapper.address, buy_amount=new_order.buy_amount, fee_address=self.fee_address, pair=self.pair()) else: self.logger.info( f"Buy amount {float(new_order.buy_amount)} of ETH with {float(new_order.pay_amount)} DAI" ) self.tethfinex_api.place_order( False, pay_token=self.token_buy_wrapper.address, pay_amount=new_order.pay_amount, buy_token=self.token_sell_wrapper.address, buy_amount=new_order.buy_amount, fee_address=self.fee_address, pair=self.pair()) def deposit_for_sell_order(self, missing_sell_amount: Wad): # We can never lock more than our available ETH balance. depositable_eth = eth_balance(self.web3, self.our_address) missing_sell_amount = Wad.min(missing_sell_amount, depositable_eth) # If we still can deposit something, and it's at least `min_eth_deposit`, then we do deposit. if missing_sell_amount > Wad(0): receipt = self.token_sell_wrapper.deposit( missing_sell_amount).transact(gas_price=self.gas_price) return receipt is not None and receipt.successful else: return False def deposit_for_buy_order(self, missing_buy_amount: Wad): # We can never lock more than our available SAI balance. depositable_sai = self.sai.balance_of(self.our_address) missing_buy_amount = Wad.min(missing_buy_amount, depositable_sai) # If we still can deposit something, and it's at least `min_sai_deposit`, then we do deposit. if missing_buy_amount > Wad(0): receipt = self.token_buy_wrapper.deposit( missing_buy_amount).transact(gas_price=self.gas_price) return receipt is not None and receipt.successful else: return False
class LeverjMarketMakerKeeper: """Keeper acting as a market maker on leverj.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='leverj-market-maker-keeper') parser.add_argument( "--leverj-api-server", type=str, default="https://test.leverj.io", help= "Address of the leverj API server (default: 'https://test.leverj.io')" ) parser.add_argument("--account-id", type=str, default="", help="Address of leverj api account id") parser.add_argument("--api-key", type=str, default="", help="Address of leverj api key") parser.add_argument("--api-secret", type=str, default="", help="Address of leverj api secret") parser.add_argument( "--leverj-timeout", type=float, default=9.5, help= "Timeout for accessing the Leverj API (in seconds, default: 9.5)") parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--eth-from", type=str, required=True, help="Ethereum account from which to watch our trades") parser.add_argument( "--eth-key", type=str, nargs='*', help= "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')" ) parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) if "infura" in self.arguments.rpc_host: self.web3 = Web3( HTTPProvider( endpoint_uri=f"http://{self.arguments.rpc_host}", request_kwargs={"timeout": self.arguments.rpc_timeout})) else: self.web3 = Web3( HTTPProvider( endpoint_uri= f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.web3.eth.defaultAccount = self.arguments.eth_from 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.history = History() self.leverj_api = LeverjAPI( 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 = 1 / (self.leverj_api.get_product( self.arguments.pair)["ticksperpoint"]) self.precision = -(int(log10(float(quote_increment))) + 1) def shutdown(self): self.order_book_manager.cancel_all_orders() def pair(self): return self.arguments.pair.upper() def token_sell(self) -> str: if self.arguments.pair.startswith("USDC"): return "USDC" return self.arguments.pair[:3] def token_buy(self) -> str: if self.arguments.pair.startswith("USDC"): return self.arguments.pair[4:] return self.arguments.pair[3:] def our_available_balance(self, our_balances: dict, token: str) -> Wad: for key in our_balances: if our_balances[key]['symbol'] == token: if (token == "LEV") or (token == "FEE"): return Wad(int(our_balances[key]['available']) * 10**9) elif (token == "USDC") or (token == "USDT"): return Wad(int(our_balances[key]['available']) * 10**12) elif (token == "WBTC"): return Wad(int(our_balances[key]['available']) * 10**10) elif (token == "GUSD"): return Wad(int(our_balances[key]['available']) * 10**16) else: return Wad(int(our_balances[key]['available'])) return Wad(0) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): 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.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 order_id = str( self.leverj_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))
class OkexMarketSurfer: """Keeper acting as a market maker on OKEX.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='okex-market-maker-keeper') parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the surfer will operate") parser.add_argument( "--output-path", type=str, required=True, help="output file path of the completed order result") # reserved for old program # 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.orderlog = Logger(self.arguments.output_path + "SurferResult_" + self.arguments.pair, level='info') try: f = open(self.arguments.config, 'r') config = json.loads(f.read()) print(config) f.close() self.okex_api = OKEXApi(api_server=config["okex_api_server"], api_key=config["okex_api_key"], secret_key=config["okex_secret_key"], timeout=config["okex_timeout"]) print("--------------") print(config['pairs']) for pair in config['pairs']: print(pair) if pair['pair'] == self.arguments.pair: self.total_amount = pair["total_amount"] # percent of total amount of each transaction or each order self.each_order_percent = pair["each_order_percent"] self.arbitrage_percent = pair["arbitrage_percent"] # the order count of sell or buy bands must less than limit self.band_order_limit = pair["band_order_limit"] except Exception as e: logging.getLogger().warning( f"Config file is invalid ({e}). Treating the config file as it has no bands." ) self.history = History() self.bands_config = ReloadableConfig(self.arguments.config) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.local_orders = [] self.each_order_amount = self.total_amount * self.each_order_percent # To implement abstract function with different exchanges API self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.okex_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.okex_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.okex_api.cancel_order(self.pair(), order. order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): # Place new orders while initialize the whole surfer system self.initialize_orders(self.each_order_amount, self.arbitrage_percent, self.band_order_limit) time.sleep( 5) # wait for order book manager to get placed orders 足够时间保证系统稳定返回 self.local_orders = self.order_book_manager.get_order_book().orders with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(10, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def initialize_orders(self, each_order_amount, arbitrage_percent, band_order_limit): orders = [] i = 1 base_price = self.get_last_price(self.pair()) while band_order_limit + 1 > i: # place sell order price = float(base_price) * (1 + arbitrage_percent * i) # pay_amount = Wad.min(band.avg_amount - total_amount, our_sell_balance, limit_amount) pay_amount = each_order_amount * self.amount_disguise( ) # bix amount pay_amount = pay_amount + self.suffix_amount_identify() buy_amount = pay_amount * price # eth money # print(pay_amount) orders.append( NewOrder(is_sell=True, price=Wad.from_number(price), amount=Wad.from_number(pay_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits.use_limit( time.time(), pay_amount))) # place buy order, pay attention to rotate bix - eth price = float(base_price) * (1 - arbitrage_percent * i) # pay_amount = Wad.min(band.avg_amount - total_amount, our_sell_balance, limit_amount) tmp = each_order_amount * self.amount_disguise() pay_amount = tmp * price # eth money 25 buy_amount = tmp # bix amount 0.05 buy_amount = buy_amount + self.suffix_amount_identify() # print(buy_amount) orders.append( NewOrder(is_sell=False, price=Wad.from_number(price), amount=Wad.from_number(buy_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits.use_limit( time.time(), pay_amount))) i = i + 1 self.place_orders(orders) # 偶尔有 bug,提交的完成慢,导致 local orders 比 下一次 获取回来少, initial_delay 加长时间到15秒,时间太长也麻烦, # 会导致一开始提交就成交的那部分订单不会存到 local orders # 需要换地方,order_book_manager更新不及时的情况下,会导致返回的订单数据不全,或者订单里的参数默认为0的情况 # self.local_orders = self.order_book_manager.get_order_book().orders @staticmethod def amount_disguise(): rand = [ 0.8, 0.84, 0.88, 0.92, 0.95, 0.99, 1.03, 1.06, 1.09, 1.12, 1.16, 1.2 ] return rand[random.randint(0, 11)] # suffix unique amount number to identify different buy/sell order pairs for result performance statics @staticmethod def suffix_amount_identify(): return round(random.random() / 10.0, 10) def get_last_price(self, pair): return self.okex_api.ticker(pair)["ticker"]['last'] def shutdown(self): self.order_book_manager.cancel_all_orders() def pair(self): return self.arguments.pair.lower() def token_sell(self) -> str: return self.arguments.pair.split('_')[0].lower() def token_buy(self) -> str: return self.arguments.pair.split('_')[1].lower() def our_available_balance(self, our_balances: dict, token: str) -> Wad: return Wad.from_number(our_balances['free'][token]) 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 count_sell_orders(self, our_orders: list) -> int: return len(list(filter(lambda order: order.is_sell, our_orders))) def count_buy_orders(self, our_orders: list) -> int: return len(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() self.logger.info("---**---The length of local_orders " + str(self.local_orders.__len__())) self.logger.info("---**---The length of order_book.orders " + str(len(order_book.orders))) local_order_ids = set(order.order_id for order in self.local_orders) order_book_ids = set(order.order_id for order in order_book.orders) self.order_book_manager.get_order_book() completed_order_ids = list(local_order_ids - order_book_ids) # 如果没有后续的更新 local orders,只有这里的更新模块,肯定有问题的,因为一旦有成交, # completed_order_ids 不为0,则永远更新不了local orders 了 # return if there none order be completed # 下面这种情况,只有在order_book订单完全"包含"local_order订单时,但是两者并不相等时,才会让本地订单等于远程订单; # 这种一般是远程订单比本地订单多,往往比如人工在系统提交了新的订单 if completed_order_ids.__len__() == 0: if local_order_ids.__len__() != order_book_ids.__len__(): self.logger.info("---**---update local order") self.local_orders = order_book.orders return # completed_orders = list(filter(lambda order: order.order_id in completed_order_ids, self.local_orders)) completed_orders = [ order for order in self.local_orders if order.order_id in completed_order_ids ] # completed_orders = list(filter(lambda order: order.order_id in local_order_ids, order_book.orders)) self.logger.info("---**---The lenght of completed orders " + str(len(completed_orders))) self.logger.info(completed_orders) # 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 # if (self.local_orders.__len__() - len(order_book.orders) > 0): if len(completed_orders) > 0: self.logger.info("--------- some orders have been done --------") new_orders = [] step = 1 count_sell_order = self.count_sell_orders(order_book.orders) count_buy_order = self.count_buy_orders(order_book.orders) for cod in completed_orders: # print(type(cod)) # print(cod.is_sell) # the completed order is sell order, buy order should be placed self.orderlog.logger.info(" - " + str(cod.is_sell) + " - " + str(cod.price) + " - " + str(cod.amount) + " - " + str(cod.price * cod.amount)) if cod.is_sell: # place buy order, pay attention to rotate bix - eth price = float(cod.price) * (1 - self.arbitrage_percent) self.logger.info( "----to submit a new buy order with price " + str(price)) pay_amount = float(cod.amount) * price # eth money 25 buy_amount = float(cod.amount) # bix amount 0.05 new_orders.append( NewOrder(is_sell=False, price=Wad.from_number(price), amount=Wad.from_number(buy_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) # 以当前价格为基数,重新submit一个高价格的 sell 订单,补充 sell list # place sell a new order with higher price # 需要判断订单的数量是否小于band order limits,并且按照差异补充订单 # count_sell_order = self.count_sell_orders(order_book.orders) band_sell_order_gap = self.band_order_limit - count_sell_order self.logger.info("---sell band gap---- " + str(band_sell_order_gap)) # while band_sell_order_gap > 0: # 外部已经有循环了,不需要这个循环了,否则在多订单被吃时,会加倍补充 # 这里只需要判断,控制数量就够了 if band_sell_order_gap > 0: current_price = self.get_last_price(self.pair()) self.logger.info("------current price---- " + str(current_price)) price = float(current_price) * ( 1 + self.arbitrage_percent * (step + count_sell_order)) self.logger.info("----higher price to sell--- " + str(price)) pay_amount = self.each_order_amount * self.amount_disguise( ) # bix amount pay_amount = pay_amount + self.suffix_amount_identify( ) # add unique identify buy_amount = pay_amount * price # eth money new_orders.append( NewOrder(is_sell=True, price=Wad.from_number(price), amount=Wad.from_number(pay_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) # step = step + 1 # band_sell_order_gap = band_sell_order_gap - 1 count_sell_order = count_sell_order + 1 else: # buy order had been completed # to place a sell order price = float(cod.price) * (1 + self.arbitrage_percent) self.logger.info("----price--- sell--- " + str(price)) pay_amount = float(cod.amount) # bix amount buy_amount = pay_amount * price # eth money new_orders.append( NewOrder(is_sell=True, price=Wad.from_number(price), amount=Wad.from_number(pay_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) # 以当前价格为基数,重新submit一个 buy 订单,补充 buy list # 需要判断订单的数量是否小于band order limits,并且按照差异补充订单 # count_buy_order = self.count_buy_orders(order_book.orders) band_buy_order_gap = self.band_order_limit - count_buy_order self.logger.info("---buy band gap----" + str(band_buy_order_gap)) # while band_buy_order_gap > 0: if band_buy_order_gap > 0: # 基础价格放在循环里的话,能快速反映当前价格,特保是激烈波动的时候;但是增加了请求次数 current_price = self.get_last_price(self.pair()) price = float(current_price) * ( 1 - self.arbitrage_percent * (step + count_buy_order)) self.logger.info("----lower price order to buy--- " + str(price)) tmp = self.each_order_amount * self.amount_disguise() pay_amount = tmp * price # eth money 25 buy_amount = tmp # bix amount 0.05 buy_amount = buy_amount + self.suffix_amount_identify( ) # add unique identify new_orders.append( NewOrder(is_sell=False, price=Wad.from_number(price), amount=Wad.from_number(buy_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) # band_buy_order_gap = band_buy_order_gap - 1 count_buy_order = count_buy_order + 1 step = step + 1 self.place_orders(new_orders) # update local orders, 前面有更新模块,与这边不完全相同,尤其是有成交的情况下,必须要更新 # 是这样吗? 似乎也不是的,有成交的情况下,下一次订单也会让 set(local) - set(order book)=0的,集合相减的特殊之处 # 如果这样就没有必要了。 # 是这样简单的复制更新,还是本地自己维护一个 id list 好呢? 也就是把(1)确定成交的从 local 删除; # (2)确定提交的add 到本地; # 缩进到循环: if len(completed_orders) > 0:,在出现两者不一致的时候,同步更新订单; # 但是这个会导致一个问题,就是初始化的订单里,有price 为0,导致两者不一致的情况,怎么办?这里解决了,是通过 order id对比而不是 # 直接的 order 对比,所以应该是解决了才对 self.logger.info("-----update local order------") self.local_orders = self.order_book_manager.get_order_book().orders def place_orders(self, new_orders): def place_order_function(new_order_to_be_placed): amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount money = new_order_to_be_placed.buy_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.pay_amount order_id = self.okex_api.place_order( pair=self.pair(), is_sell=new_order_to_be_placed.is_sell, price=new_order_to_be_placed.price, amount=amount) return Order(order_id, 0, self.pair(), new_order_to_be_placed.is_sell, new_order_to_be_placed.price, amount, Wad(money)) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order))
class OkexMarketStats: """Keeper acting as a market maker on OKEX.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='okex-market-stats') parser.add_argument( "--okex-api-server", type=str, default="https://www.okex.com", help= "Address of the OKEX API server (default: 'https://www.okex.com')") parser.add_argument("--okex-api-key", type=str, required=True, help="API key for the OKEX API") parser.add_argument("--okex-secret-key", type=str, required=True, help="Secret key for the OKEX API") parser.add_argument("--okex-passphrase", type=str, required=True, help="Passphrase for the OKEX API") parser.add_argument( "--okex-timeout", type=float, default=9.5, help="Timeout for accessing the OKEX API (in seconds, default: 9.5)" ) parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--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.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.okex_api = OKEXApi(api_server=self.arguments.okex_api_server, api_key=self.arguments.okex_api_key, secret_key=self.arguments.okex_secret_key, passphrase=self.arguments.okex_passphrase, timeout=self.arguments.okex_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.okex_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.okex_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.okex_api.cancel_order(self.pair(), order. order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): balances = self.okex_api.get_balances() self.logger.info(f"Balances: {balances}") self.order_book_manager.cancel_all_orders() self.logger.info(f"Refresh balances: {balances}") def shutdown(self): self.order_book_manager.cancel_all_orders() def pair(self): return self.arguments.pair.upper() def token_sell(self) -> str: return self.arguments.pair.split('-')[0].upper() def token_buy(self) -> str: return self.arguments.pair.split('-')[1].upper() def our_available_balance(self, our_balances: list, token: str) -> Wad: for item in our_balances: if token == item['currency']: return Wad.from_number(item['available']) return Wad(0) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders))
class BitzlatoMarketMakerKeeper: """Keeper acting as a market maker on bitzlato.""" logger = logging.getLogger() def add_arguments(self, parser): """exchange settings""" parser.add_argument( "--bitzlato-api-server", type=str, default="https://bitzlato.com/api/", help= "Address of the bitzlato API server (default: 'https://bitzlato.com/api/')" ) # parser.add_argument("--bitzlato-api-key", type=str, required=True, # help="API key for the bitzlato API") parser.add_argument("--bitzlato-secret-key", type=str, required=True, help="Secret key for the bitzlato API") parser.add_argument( "--bitzlato-timeout", type=float, default=9.5, help= "Timeout for accessing the bitzlato 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") """price settings""" 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="bitzlato") 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 main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def shutdown(self): self.order_book_manager.cancel_all_orders() def pair(self): # Bittrex is inconsistent here. They call the pair `ETH-DAI`, but in reality all prices are # calculated like it was an `DAI-ETH` pair. Same for `ETH-MKR` return self.arguments.pair.upper().split( '-')[1] + "-" + self.arguments.pair.upper().split('-')[0] def token_sell(self) -> str: return self.arguments.pair.split('-')[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['Currency'].upper() == token, our_balances)) if token_balances: return Wad.from_number(token_balances[0]['Available']) else: return Wad(0) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.control_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # Place new orders new_orders = bands.new_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.our_available_balance(order_book.balances, self.token_buy()), our_sell_balance=self.our_available_balance( order_book.balances, self.token_sell()), target_price=target_price)[0] self.place_orders(new_orders) def place_orders(self, new_orders: List[NewOrder]): def place_order_function(new_order_to_be_placed): price = new_order_to_be_placed.price amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount order_id = self.bittrex_api.place_order( self.pair(), new_order_to_be_placed.is_sell, price, amount) return Order(order_id=order_id, pair=self.pair(), is_sell=new_order_to_be_placed.is_sell, price=price, amount=amount, remaining_amount=amount) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order))
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))
class CEXKeeperAPI: """ Define a common abstract API for keepers on centralized and hybrid exchanges """ def __init__(self, arguments: Namespace, pyex_api: PyexAPI): setup_logging(arguments) self.bands_config = ReloadableConfig(arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(arguments) self.spread_feed = create_spread_feed(arguments) self.control_feed = create_control_feed(arguments) self.order_history_reporter = create_order_history_reporter(arguments) self.history = History() self.init_order_book_manager(arguments, pyex_api) def init_order_book_manager(self, arguments: Namespace, pyex_api: PyexAPI): self.order_book_manager = OrderBookManager(refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with(lambda: pyex_api.get_orders(self.pair())) self.order_book_manager.get_balances_with(lambda: pyex_api.get_balances()) self.order_book_manager.cancel_orders_with(lambda order: pyex_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def shutdown(self): self.order_book_manager.cancel_all_orders() # Each exchange takes pair input as a different format def pair(self): raise NotImplementedError() def token_sell(self) -> str: raise NotImplementedError() def token_buy(self) -> str: raise NotImplementedError() # Different keys are used to access balance object for different exchanges def our_available_balance(self, our_balances: dict, token: str) -> Wad: raise NotImplementedError() def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.control_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders(our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug("Order book is in progress, not placing new orders") return # Place new orders self.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.our_available_balance(order_book.balances, self.token_buy()), our_sell_balance=self.our_available_balance(order_book.balances, self.token_sell()), target_price=target_price)[0]) def place_orders(self, new_orders: list): raise NotImplementedError()
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()
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 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 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 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))
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 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 OkexMarketTrading: """Keeper acting as a market maker on OKEX.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='okex-market-trading') parser.add_argument( "--okex-api-server", type=str, default="https://www.okex.com", help= "Address of the OKEX API server (default: 'https://www.okex.com')") parser.add_argument("--okex-api-key", type=str, required=True, help="API key for the OKEX API") parser.add_argument("--okex-secret-key", type=str, required=True, help="Secret key for the OKEX API") parser.add_argument("--okex-passphrase", type=str, required=True, help="Passphrase for the OKEX API") parser.add_argument( "--okex-timeout", type=float, default=9.5, help="Timeout for accessing the OKEX API (in seconds, default: 9.5)" ) parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) # TODO://这两个参数干嘛的 self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.okex_api = OKEXApi(api_server=self.arguments.okex_api_server, api_key=self.arguments.okex_api_key, secret_key=self.arguments.okex_secret_key, passphrase=self.arguments.okex_passphrase, timeout=self.arguments.okex_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.okex_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.okex_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.okex_api.cancel_order(self.pair(), order. order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): balances = self.okex_api.get_balances() self.logger.info(f"balances:{balances}") balances = self.okex_api.get_balances() self.order_book_manager.cancel_all_orders() self.logger.info(f"refresh balances:{balances}") with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(5, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def shutdown(self): self.order_book_manager.cancel_all_orders() def pair(self): return self.arguments.pair.upper() def token_sell(self) -> str: return self.arguments.pair.split('-')[0].upper() def token_buy(self) -> str: return self.arguments.pair.split('-')[1].upper() def our_available_balance(self, our_balances: list, token: str) -> Wad: for item in our_balances: if token == item['currency']: return Wad.from_number(item['available']) return Wad(0) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) # 在买一和卖一间隔中下单自动成交 def synchronize_orders(self): # 交易触发规则:随机触发。产生一个随机数,若命中概率则交易 current_time = time.strftime("%H") freq_dict = { '00': np.random.randint(50, 200), '01': np.random.randint(50, 100), '02': np.random.randint(10, 100), '03': np.random.randint(10, 50), '04': np.random.randint(10, 50), '05': np.random.randint(10, 50), '06': np.random.randint(50, 100), '07': np.random.randint(100, 200), '08': np.random.randint(100, 200), '09': np.random.randint(100, 300), '10': np.random.randint(100, 300), '11': np.random.randint(100, 300), '12': np.random.randint(100, 300), '13': np.random.randint(100, 300), '14': np.random.randint(100, 300), '15': np.random.randint(100, 300), '16': np.random.randint(100, 300), '17': np.random.randint(100, 300), '18': np.random.randint(100, 300), '19': np.random.randint(100, 300), '20': np.random.randint(100, 300), '21': np.random.randint(100, 200), '22': np.random.randint(100, 200), '23': np.random.randint(100, 200) } freq = freq_dict[current_time] hit_number = np.random.random() hit_range = freq / (12 * 60.0) do_trade = True if hit_number < hit_range else False if not do_trade: total_freq = reduce(lambda x, y: x + y, list(freq_dict.values())) logging.debug( f"NOT HIT. total freq is {total_freq} per day. hit_number={hit_number}, hit_range={hit_range}" ) return logging.info( f"[DO TRADING]hit_number={hit_number}, hit_range={hit_range}") order_book = self.order_book_manager.get_order_book() current_price = self.price_feed.get_price() if current_price.buy_price is None or current_price.sell_price is None: self.logger.warning( "Current_price:buy_price or sell_price is None") return logging.info(f"Current_price: {current_price}") # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # 只会使用到buy_bands的一个配置,同时应用于买和卖,买卖统一数量 bands = Bands.read(self.bands_config, self.spread_feed, self.control_feed, self.history) band = bands.buy_bands[0] price_gap = current_price.sell_price - current_price.buy_price # 确定交易的数量和价格 trade_price = current_price.buy_price + Wad.from_number( np.random.uniform(0, float(price_gap))) trade_amount = Wad.from_number( np.random.uniform(float(band.min_amount), float(band.max_amount))) # Place new orders new_orders = self.create_new_orders( trade_amount=trade_amount, trade_price=trade_price, our_buy_balance=self.our_available_balance(order_book.balances, self.token_buy()), our_sell_balance=self.our_available_balance( order_book.balances, self.token_sell()), band=band) self.place_orders(new_orders) def place_orders(self, new_orders): def place_order_function(new_order_to_be_placed): amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount order_id = self.okex_api.place_order( pair=self.pair(), is_sell=new_order_to_be_placed.is_sell, price=new_order_to_be_placed.price, amount=amount) return Order(order_id, 0.0, self.pair(), new_order_to_be_placed.is_sell, new_order_to_be_placed.price, amount, Wad(0)) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order)) def create_new_orders(self, trade_amount: Wad, trade_price: Wad, our_buy_balance: Wad, our_sell_balance: Wad, band: Band) -> list: assert (isinstance(our_buy_balance, Wad)) assert (isinstance(our_sell_balance, Wad)) assert (isinstance(trade_price, Wad)) # 1、构建需要创建的订单 new_buy_orders = [] buy_amount = trade_amount # pay_amount要付出的token数量, 买单时如usdt pay_amount = Wad.min(buy_amount * trade_price, our_buy_balance) if (trade_price > Wad(0)) and (pay_amount > Wad(0)) and (buy_amount > Wad(0)): new_buy_orders.append( NewOrder(is_sell=False, price=trade_price, amount=buy_amount, pay_amount=pay_amount, buy_amount=buy_amount, band=band, confirm_function=lambda: self.buy_limits.use_limit( time.time(), pay_amount))) logging.info( "Trading new_buy_order, price:%s, buy_amount:%s, pay_amount:%s" % (trade_price, buy_amount, pay_amount)) # 2、构建等量的卖出订单 new_sell_orders = [] # pay_amount要付出的token数量,卖单时如tokenx pay_amount = Wad.min(trade_amount, our_sell_balance) buy_amount = pay_amount * trade_price if (trade_price > Wad(0)) and (pay_amount > Wad(0)) and (buy_amount > Wad(0)): self.logger.info( f"Trading creating new sell order amount {pay_amount} with price {trade_price}" ) new_buy_orders.append( NewOrder(is_sell=True, price=trade_price, amount=pay_amount, pay_amount=pay_amount, buy_amount=buy_amount, band=band, confirm_function=lambda: self.buy_limits.use_limit( time.time(), pay_amount))) logging.info( "Trading new_sell_order, price:%s, buy_amount:%s, pay_amount:%s" % (trade_price, buy_amount, pay_amount)) # 先放卖单,再放买单 return new_sell_orders + new_buy_orders