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))
예제 #3
0
    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)