class ErisXMarketMakerKeeper(CEXKeeperAPI): """ Keeper acting as a market maker on ErisX. """ logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='erisx-market-maker-keeper') parser.add_argument("--erisx-clearing-url", type=str, required=True, help="Address of the ErisX clearing server") parser.add_argument("--fix-trading-endpoint", type=str, required=True, help="FIX endpoint for ErisX trading") parser.add_argument("--fix-trading-user", type=str, required=True, help="Account ID for ErisX trading") parser.add_argument("--fix-marketdata-endpoint", type=str, required=True, help="FIX endpoint for ErisX market data") parser.add_argument("--fix-marketdata-user", type=str, required=True, help="Account ID for ErisX market data") parser.add_argument("--erisx-password", type=str, required=True, help="password for FIX account") parser.add_argument("--erisx-api-key", type=str, required=True, help="API key for ErisX REST API") parser.add_argument("--erisx-api-secret", type=str, required=True, help="API secret for ErisX REST API") parser.add_argument("--erisx-certs", type=str, default=None, help="Client key pair used to authenticate to Production FIX endpoints") parser.add_argument("--account-id", type=int, default=0, help="ErisX account ID index") 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.erisx_api = ErisxApi(fix_trading_endpoint=self.arguments.fix_trading_endpoint, fix_trading_user=self.arguments.fix_trading_user, fix_marketdata_endpoint=self.arguments.fix_marketdata_endpoint, fix_marketdata_user=self.arguments.fix_marketdata_user, password=self.arguments.erisx_password, clearing_url=self.arguments.erisx_clearing_url, api_key=self.arguments.erisx_api_key, api_secret=self.arguments.erisx_api_secret, certs=self.arguments.erisx_certs, account_id=self.arguments.account_id) termination_time = time.time() + 15 while self.erisx_api.fix_trading.connection_state != FixConnectionState.LOGGED_IN and self.erisx_api.fix_marketdata.connection_state != FixConnectionState.LOGGED_IN: time.sleep(0.3) if time.time() > termination_time: raise RuntimeError("Timed out while waiting to log in") self.market_info = self.erisx_api.get_markets() self.orders = self.erisx_api.get_orders(self.pair()) super().__init__(self.arguments, self.erisx_api) def init_order_book_manager(self, arguments, erisx_api): self.order_book_manager = ErisXOrderBookManager(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.erisx_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.erisx_api.cancel_order(order.order_id, self.pair(), order.is_sell)) self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.pair = self.pair() self.order_book_manager.start() def main(self): with ErisXLifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def pair(self): return self.arguments.pair 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: if 'newrelease' in self.arguments.erisx_clearing_url: if token == 'ETH': token = 'TETH' if token == 'BTC': token = 'TBTC' token_balances = list(filter(lambda asset: asset['asset_type'].upper() == token, our_balances)) if token_balances: return Wad.from_number(float(token_balances[0]['available_to_trade'])) else: return Wad(0) def get_orders(self) -> List[Order]: """ Check the list of orders for cancellations or fills. If an order has been partially filled, the order amount will be updated. If an application message has been received from ErisX, update the Keeper orderbook accordingly. """ existing_orders = self.orders self.orders = self.erisx_api.sync_orders(existing_orders) return self.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 # automatically retrive qty precision round_lot = str(self.market_info[self.pair()]["RoundLot"]) price_increment = str(self.market_info[self.pair()]["MinPriceIncrement"]) price_precision = abs(Decimal(price_increment).as_tuple().exponent) order_qty_precision = abs(Decimal(round_lot).as_tuple().exponent) rounded_amount = round(Wad.__float__(amount), order_qty_precision) rounded_price = round(Wad.__float__(new_order_to_be_placed.price), price_precision) order_id = self.erisx_api.place_order(pair=self.pair().upper(), is_sell=new_order_to_be_placed.is_sell, price=rounded_price, amount=rounded_amount) # check that order was placed properly if len(order_id) > 0: placed_order = Order(str(order_id), int(time.time()), self.pair(), new_order_to_be_placed.is_sell, new_order_to_be_placed.price, amount) self.orders.append(placed_order) return placed_order else: return None for new_order in new_orders: # check if new order is greater than exchange minimums amount = new_order.pay_amount if new_order.is_sell else new_order.buy_amount side = 'Sell' if new_order.is_sell else 'Buy' round_lot = str(self.market_info[self.pair()]["RoundLot"]) order_qty_precision = abs(Decimal(round_lot).as_tuple().exponent) min_amount = float(self.market_info[self.pair()]["MinTradeVol"]) rounded_amount = round(Wad.__float__(amount), order_qty_precision) if min_amount < rounded_amount: self.order_book_manager.place_order(lambda new_order=new_order: place_order_function(new_order)) else: logging.info(f"New {side} Order below size minimum of {min_amount}. Order of amount {amount} ignored.") 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])
class ErisXMarketMakerKeeper(CEXKeeperAPI): """ Keeper acting as a market maker on ErisX. """ logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='erisx-market-maker-keeper') parser.add_argument("--erisx-clearing-url", type=str, required=True, help="Address of the ErisX clearing server") parser.add_argument("--fix-trading-endpoint", type=str, required=True, help="FIX endpoint for ErisX trading") parser.add_argument("--fix-trading-user", type=str, required=True, help="Account ID for ErisX trading") parser.add_argument("--fix-marketdata-endpoint", type=str, required=True, help="FIX endpoint for ErisX market data") parser.add_argument("--fix-marketdata-user", type=str, required=True, help="Account ID for ErisX market data") parser.add_argument("--erisx-password", type=str, required=True, help="password for FIX account") parser.add_argument("--erisx-api-key", type=str, required=True, help="API key for ErisX REST API") parser.add_argument("--erisx-api-secret", type=str, required=True, help="API secret for ErisX REST API") parser.add_argument( "--erisx-certs", type=str, default=None, help= "Client key pair used to authenticate to Production FIX endpoints") parser.add_argument("--account-id", type=int, default=0, help="ErisX account ID index") 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) logging.basicConfig( format='%(asctime)-15s %(levelname)-8s %(message)s', level=(logging.DEBUG if self.arguments.debug else logging.INFO)) self.erisx_api = ErisxApi( fix_trading_endpoint=self.arguments.fix_trading_endpoint, fix_trading_user=self.arguments.fix_trading_user, fix_marketdata_endpoint=self.arguments.fix_marketdata_endpoint, fix_marketdata_user=self.arguments.fix_marketdata_user, password=self.arguments.erisx_password, clearing_url=self.arguments.erisx_clearing_url, api_key=self.arguments.erisx_api_key, api_secret=self.arguments.erisx_api_secret, certs=self.arguments.erisx_certs, account_id=self.arguments.account_id) self.market_info = self.erisx_api.get_markets() super().__init__(self.arguments, self.erisx_api) def init_order_book_manager(self, arguments, erisx_api): self.order_book_manager = ErisXOrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.erisx_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.erisx_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.erisx_api.cancel_order( order.order_id, self.pair(), order.is_sell)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.pair = self.pair() self.order_book_manager.start() def main(self): with ErisXLifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def pair(self): return self.arguments.pair 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: if 'newrelease' in self.arguments.erisx_clearing_url: if token == 'ETH': token = 'TETH' if token == 'BTC': token = 'TBTC' token_balances = list( filter(lambda asset: asset['asset_type'].upper() == token, our_balances)) if token_balances: return Wad.from_number( float(token_balances[0]['available_to_trade'])) else: return Wad(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 # automatically retrive qty precision round_lot = str(self.market_info[self.pair()]["RoundLot"]) price_increment = str( self.market_info[self.pair()]["MinPriceIncrement"]) order_qty_precision = abs(Decimal(round_lot).as_tuple().exponent) price_precision = abs(Decimal(price_increment).as_tuple().exponent) order_id = self.erisx_api.place_order( pair=self.pair().upper(), is_sell=new_order_to_be_placed.is_sell, price=round(Wad.__float__(new_order_to_be_placed.price), price_precision), amount=round(Wad.__float__(amount), order_qty_precision)) return Order(str(order_id), int(time.time()), 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))
level=logging.DEBUG) client = ErisxApi(fix_trading_endpoint=sys.argv[1], fix_trading_user=sys.argv[2], fix_marketdata_endpoint=sys.argv[3], fix_marketdata_user=sys.argv[4], password=sys.argv[5], clearing_url="https://clearing.newrelease.erisx.com/api/v1/", api_key=sys.argv[6], api_secret=sys.argv[7], account_id=0) # print(sys.argv) print("ErisxApi created\n") # print(client.get_balances()) # time.sleep(30) securities = client.get_markets() print(f"Received {len(securities)} securities:") pprint(securities) time.sleep(2) pair = client.get_pair("ETH/USD") pprint(pair) time.sleep(1) new_order = client.place_order('ETH/USD', False, 185.1, 0.2) print(f"Placed new order: {new_order}") time.sleep(1) orders = client.get_orders("ETH/USD") print(f"Received {len(orders)} orders:") pprint(orders)