Beispiel #1
0
    def __init__(
        self,
        loop: asyncio.AbstractEventLoop,
        client: BetfairClient,
        base_currency: Currency,
        msgbus: MessageBus,
        cache: Cache,
        clock: LiveClock,
        logger: Logger,
        market_filter: Dict,
        instrument_provider: BetfairInstrumentProvider,
    ):
        super().__init__(
            loop=loop,
            client_id=ClientId(BETFAIR_VENUE.value),
            venue=BETFAIR_VENUE,
            oms_type=OMSType.NETTING,
            account_type=AccountType.BETTING,
            base_currency=base_currency,
            instrument_provider=instrument_provider
            or BetfairInstrumentProvider(
                client=client, logger=logger, filters=market_filter),
            msgbus=msgbus,
            cache=cache,
            clock=clock,
            logger=logger,
        )

        self._client: BetfairClient = client
        self.stream = BetfairOrderStreamClient(
            client=self._client,
            logger=logger,
            message_handler=self.handle_order_stream_update,
        )

        self.venue_order_id_to_client_order_id: Dict[VenueOrderId,
                                                     ClientOrderId] = {}
        self.pending_update_order_client_ids: Set[Tuple[ClientOrderId,
                                                        VenueOrderId]] = set()
        self.published_executions: Dict[ClientOrderId,
                                        TradeId] = defaultdict(list)

        self._set_account_id(AccountId(BETFAIR_VENUE.value,
                                       "001"))  # TODO(cs): Temporary
        AccountFactory.register_calculated_account(BETFAIR_VENUE.value)
Beispiel #2
0
    def __init__(
        self,
        loop: asyncio.AbstractEventLoop,
        client: BetfairClient,
        account_id: AccountId,
        base_currency: Currency,
        msgbus: MessageBus,
        cache: Cache,
        clock: LiveClock,
        logger: Logger,
        market_filter: Dict,
        instrument_provider: BetfairInstrumentProvider,
    ):
        super().__init__(
            loop=loop,
            client_id=ClientId(BETFAIR_VENUE.value),
            instrument_provider=instrument_provider
            or BetfairInstrumentProvider(client=client, logger=logger, market_filter=market_filter),
            venue_type=VenueType.EXCHANGE,
            account_id=account_id,
            account_type=AccountType.BETTING,
            base_currency=base_currency,
            msgbus=msgbus,
            cache=cache,
            clock=clock,
            logger=logger,
            config={"name": "BetfairExecClient"},
        )

        self._client: BetfairClient = client
        self.stream = BetfairOrderStreamClient(
            client=self._client,
            logger=logger,
            message_handler=self.handle_order_stream_update,
        )

        self.venue_order_id_to_client_order_id: Dict[VenueOrderId, ClientOrderId] = {}
        self.pending_update_order_client_ids: Set[Tuple[ClientOrderId, VenueOrderId]] = set()
        self.published_executions: Dict[ClientOrderId, ExecutionId] = defaultdict(list)

        AccountFactory.register_calculated_account(account_id.issuer)
def test_unique_id(betfair_client, live_logger):
    clients = [
        BetfairMarketStreamClient(client=betfair_client,
                                  logger=live_logger,
                                  message_handler=len),
        BetfairOrderStreamClient(client=betfair_client,
                                 logger=live_logger,
                                 message_handler=len),
        BetfairMarketStreamClient(client=betfair_client,
                                  logger=live_logger,
                                  message_handler=len),
    ]
    result = [c.unique_id for c in clients]
    assert result == sorted(set(result))
Beispiel #4
0
class BetfairExecutionClient(LiveExecutionClient):
    """
    Provides an execution client for Betfair.

    Parameters
    ----------
    loop : asyncio.AbstractEventLoop
        The event loop for the client.
    client : BetfairClient
        The Betfair HttpClient.
    account_id : AccountId
        The account ID for the client.
    base_currency : Currency
        The account base currency for the client.
    msgbus : MessageBus
        The message bus for the client.
    cache : Cache
        The cache for the client.
    clock : LiveClock
        The clock for the client.
    logger : Logger
        The logger for the client.
    market_filter : dict
        The market filter.
    instrument_provider : BetfairInstrumentProvider, optional
        The instrument provider.
    """

    def __init__(
        self,
        loop: asyncio.AbstractEventLoop,
        client: BetfairClient,
        account_id: AccountId,
        base_currency: Currency,
        msgbus: MessageBus,
        cache: Cache,
        clock: LiveClock,
        logger: Logger,
        market_filter: Dict,
        instrument_provider: BetfairInstrumentProvider,
    ):
        super().__init__(
            loop=loop,
            client_id=ClientId(BETFAIR_VENUE.value),
            instrument_provider=instrument_provider
            or BetfairInstrumentProvider(client=client, logger=logger, market_filter=market_filter),
            venue_type=VenueType.EXCHANGE,
            account_id=account_id,
            account_type=AccountType.BETTING,
            base_currency=base_currency,
            msgbus=msgbus,
            cache=cache,
            clock=clock,
            logger=logger,
            config={"name": "BetfairExecClient"},
        )

        self._client: BetfairClient = client
        self.stream = BetfairOrderStreamClient(
            client=self._client,
            logger=logger,
            message_handler=self.handle_order_stream_update,
        )

        self.venue_order_id_to_client_order_id: Dict[VenueOrderId, ClientOrderId] = {}
        self.pending_update_order_client_ids: Set[Tuple[ClientOrderId, VenueOrderId]] = set()
        self.published_executions: Dict[ClientOrderId, ExecutionId] = defaultdict(list)

        AccountFactory.register_calculated_account(account_id.issuer)

    # -- CONNECTION HANDLERS -----------------------------------------------------------------------

    def connect(self):
        """
        Connect the client.
        """
        self._log.info("Connecting...")
        self._loop.create_task(self._connect())

    def disconnect(self):
        """
        Disconnect the client.
        """
        self._log.info("Disconnecting...")
        self._loop.create_task(self._disconnect())

    async def _connect(self):
        self._log.info("Connecting to BetfairClient...")
        await self._client.connect()
        self._log.info("BetfairClient login successful.", LogColor.GREEN)

        aws = [
            self.stream.connect(),
            self.connection_account_state(),
            self.check_account_currency(),
        ]
        await asyncio.gather(*aws)
        self.create_task(self.watch_stream())
        self._set_connected(True)
        assert self.is_connected
        self._log.info("Connected.")

    async def _disconnect(self) -> None:
        # Close socket
        self._log.info("Closing streaming socket...")
        await self.stream.disconnect()

        # Ensure client closed
        self._log.info("Closing BetfairClient...")
        await self._client.disconnect()

        self._set_connected(False)
        self._log.info("Disconnected.")

    async def watch_stream(self):
        """Ensure socket stream is connected"""
        while True:
            if not self.stream.is_connected:
                self.stream.connect()
            await asyncio.sleep(1)

    # -- ERROR HANDLING --------------------------------------------------------------------------
    async def on_api_exception(self, exc: BetfairAPIError):
        if exc.kind == "INVALID_SESSION_INFORMATION":
            # Session is invalid, need to reconnect
            self._log.warning("Invalid session error, reconnecting..")
            await self._client.disconnect()
            await self._connect()
            self._log.info("Reconnected.")

    # -- ACCOUNT HANDLERS --------------------------------------------------------------------------

    async def connection_account_state(self):
        account_details = await self._client.get_account_details()
        account_funds = await self._client.get_account_funds()
        timestamp = self._clock.timestamp_ns()
        account_state: AccountState = betfair_account_to_account_state(
            account_detail=account_details,
            account_funds=account_funds,
            event_id=self._uuid_factory.generate(),
            ts_event=timestamp,
            ts_init=timestamp,
        )
        self._log.debug(f"Received account state: {account_state}, sending")
        self._send_account_state(account_state)
        self._log.debug("Initial Account state completed")

    # -- COMMAND HANDLERS --------------------------------------------------------------------------

    def submit_order(self, command: SubmitOrder) -> None:
        PyCondition.not_none(command, "command")

        self.create_task(self._submit_order(command))

    async def _submit_order(self, command: SubmitOrder) -> None:
        self._log.debug(f"Received submit_order {command}")

        self.generate_order_submitted(
            instrument_id=command.instrument_id,
            strategy_id=command.strategy_id,
            client_order_id=command.order.client_order_id,
            ts_event=self._clock.timestamp_ns(),
        )
        self._log.debug("Generated _generate_order_submitted")

        instrument = self._cache.instrument(command.instrument_id)
        PyCondition.not_none(instrument, "instrument")
        client_order_id = command.order.client_order_id

        place_order = order_submit_to_betfair(command=command, instrument=instrument)
        try:
            result = await self._client.place_orders(**place_order)
        except Exception as exc:
            if isinstance(exc, BetfairAPIError):
                await self.on_api_exception(exc=exc)
            self._log.warning(f"Submit failed: {exc}")
            self.generate_order_rejected(
                strategy_id=command.strategy_id,
                instrument_id=command.instrument_id,
                client_order_id=client_order_id,
                reason="client error",  # type: ignore
                ts_event=self._clock.timestamp_ns(),
            )
            return

        self._log.debug(f"result={result}")
        for report in result["instructionReports"]:
            if result["status"] == "FAILURE":
                reason = f"{result['errorCode']}: {report['errorCode']}"
                self._log.warning(f"Submit failed - {reason}")
                self.generate_order_rejected(
                    strategy_id=command.strategy_id,
                    instrument_id=command.instrument_id,
                    client_order_id=client_order_id,
                    reason=reason,  # type: ignore
                    ts_event=self._clock.timestamp_ns(),
                )
                self._log.debug("Generated _generate_order_rejected")
                return
            else:
                venue_order_id = VenueOrderId(report["betId"])
                self._log.debug(
                    f"Matching venue_order_id: {venue_order_id} to client_order_id: {client_order_id}"
                )
                self.venue_order_id_to_client_order_id[venue_order_id] = client_order_id  # type: ignore
                self.generate_order_accepted(
                    strategy_id=command.strategy_id,
                    instrument_id=command.instrument_id,
                    client_order_id=client_order_id,
                    venue_order_id=venue_order_id,  # type: ignore
                    ts_event=self._clock.timestamp_ns(),
                )
                self._log.debug("Generated _generate_order_accepted")

    def submit_order_list(self, command: SubmitOrderList):
        """Abstract method (implement in subclass)."""
        raise NotImplementedError("method must be implemented in the subclass")  # pragma: no cover

    def modify_order(self, command: ModifyOrder) -> None:
        PyCondition.not_none(command, "command")

        self.create_task(self._modify_order(command))

    async def _modify_order(self, command: ModifyOrder) -> None:
        self._log.debug(f"Received modify_order {command}")
        client_order_id: ClientOrderId = command.client_order_id
        instrument = self._cache.instrument(command.instrument_id)
        PyCondition.not_none(instrument, "instrument")
        existing_order = self._cache.order(client_order_id)  # type: Order

        self.generate_order_pending_update(
            strategy_id=command.strategy_id,
            instrument_id=command.instrument_id,
            client_order_id=command.client_order_id,
            venue_order_id=command.venue_order_id,
            ts_event=self._clock.timestamp_ns(),
        )

        if existing_order is None:
            self._log.warning(
                f"Attempting to update order that does not exist in the cache: {command}"
            )
            self.generate_order_modify_rejected(
                strategy_id=command.strategy_id,
                instrument_id=command.instrument_id,
                client_order_id=client_order_id,
                venue_order_id=command.venue_order_id,
                reason="ORDER NOT IN CACHE",
                ts_event=self._clock.timestamp_ns(),
            )
            return
        if existing_order.venue_order_id is None:
            self._log.warning(f"Order found does not have `id` set: {existing_order}")
            PyCondition.not_none(command.strategy_id, "command.strategy_id")
            PyCondition.not_none(command.instrument_id, "command.instrument_id")
            PyCondition.not_none(client_order_id, "client_order_id")
            self.generate_order_modify_rejected(
                strategy_id=command.strategy_id,
                instrument_id=command.instrument_id,
                client_order_id=client_order_id,
                venue_order_id=VenueOrderId("-1"),
                reason="ORDER MISSING VENUE_ORDER_ID",
                ts_event=self._clock.timestamp_ns(),
            )
            return

        # Send order to client
        kw = order_update_to_betfair(
            command=command,
            venue_order_id=existing_order.venue_order_id,
            side=existing_order.side,
            instrument=instrument,
        )
        self.pending_update_order_client_ids.add(
            (command.client_order_id, existing_order.venue_order_id)
        )
        try:
            result = await self._client.replace_orders(**kw)
        except Exception as exc:
            if isinstance(exc, BetfairAPIError):
                await self.on_api_exception(exc=exc)
            self._log.warning(f"Modify failed: {exc}")
            self.generate_order_modify_rejected(
                strategy_id=command.strategy_id,
                instrument_id=command.instrument_id,
                client_order_id=command.client_order_id,
                venue_order_id=existing_order.venue_order_id,
                reason="client error",
                ts_event=self._clock.timestamp_ns(),
            )
            return

        self._log.debug(f"result={result}")

        for report in result["instructionReports"]:
            if report["status"] == "FAILURE":
                reason = f"{result['errorCode']}: {report['errorCode']}"
                self._log.warning(f"replace failed - {reason}")
                self.generate_order_rejected(
                    strategy_id=command.strategy_id,
                    instrument_id=command.instrument_id,
                    client_order_id=command.client_order_id,
                    reason=reason,
                    ts_event=self._clock.timestamp_ns(),
                )
                return

            # Check the venue_order_id that has been deleted currently exists on our order
            deleted_bet_id = report["cancelInstructionReport"]["instruction"]["betId"]
            self._log.debug(f"{existing_order}, {deleted_bet_id}")
            assert existing_order.venue_order_id == VenueOrderId(deleted_bet_id)

            update_instruction = report["placeInstructionReport"]
            venue_order_id = VenueOrderId(update_instruction["betId"])
            self.venue_order_id_to_client_order_id[venue_order_id] = client_order_id
            self.generate_order_updated(
                strategy_id=command.strategy_id,
                instrument_id=command.instrument_id,
                client_order_id=client_order_id,
                venue_order_id=VenueOrderId(update_instruction["betId"]),
                quantity=Quantity(
                    update_instruction["instruction"]["limitOrder"]["size"], precision=4
                ),
                price=price_to_probability(
                    str(update_instruction["instruction"]["limitOrder"]["price"])
                ),
                trigger=None,  # Not applicable for Betfair
                ts_event=self._clock.timestamp_ns(),
                venue_order_id_modified=True,
            )

    def cancel_order(self, command: CancelOrder) -> None:
        PyCondition.not_none(command, "command")

        self.create_task(self._cancel_order(command))

    async def _cancel_order(self, command: CancelOrder) -> None:
        self._log.debug(f"Received cancel order: {command}")
        self.generate_order_pending_cancel(
            strategy_id=command.strategy_id,
            instrument_id=command.instrument_id,
            client_order_id=command.client_order_id,
            venue_order_id=command.venue_order_id,
            ts_event=self._clock.timestamp_ns(),
        )

        instrument = self._cache.instrument(command.instrument_id)
        PyCondition.not_none(instrument, "instrument")

        # Format
        cancel_order = order_cancel_to_betfair(command=command, instrument=instrument)  # type: ignore
        self._log.debug(f"cancel_order {cancel_order}")

        # Send to client
        try:
            result = await self._client.cancel_orders(**cancel_order)
        except Exception as exc:
            if isinstance(exc, BetfairAPIError):
                await self.on_api_exception(exc=exc)
            self._log.warning(f"Cancel failed: {exc}")
            self.generate_order_cancel_rejected(
                strategy_id=command.strategy_id,
                instrument_id=command.instrument_id,
                client_order_id=command.client_order_id,
                venue_order_id=command.venue_order_id,
                reason="client error",
                ts_event=self._clock.timestamp_ns(),
            )
            return
        self._log.debug(f"result={result}")

        # Parse response
        for report in result["instructionReports"]:
            venue_order_id = VenueOrderId(report["instruction"]["betId"])
            if report["status"] == "FAILURE":
                reason = f"{result.get('errorCode', 'Error')}: {report['errorCode']}"
                self._log.warning(f"cancel failed - {reason}")
                self.generate_order_cancel_rejected(
                    strategy_id=command.strategy_id,
                    instrument_id=command.instrument_id,
                    client_order_id=command.client_order_id,
                    venue_order_id=venue_order_id,
                    reason=reason,
                    ts_event=self._clock.timestamp_ns(),
                )
                return

            self._log.debug(
                f"Matching venue_order_id: {venue_order_id} to client_order_id: {command.client_order_id}"
            )
            self.venue_order_id_to_client_order_id[venue_order_id] = command.client_order_id  # type: ignore
            self.generate_order_canceled(
                strategy_id=command.strategy_id,
                instrument_id=command.instrument_id,
                client_order_id=command.client_order_id,
                venue_order_id=venue_order_id,  # type: ignore
                ts_event=self._clock.timestamp_ns(),
            )
            self._log.debug("Sent order cancel")

    def cancel_all_orders(self, command: CancelAllOrders) -> None:
        PyCondition.not_none(command, "command")

        working_orders = self._cache.working_orders(instrument_id=command.instrument_id)

        # TODO(cs): Temporary solution generating individual cancels for all working orders
        for order in working_orders:
            command = CancelOrder(
                trader_id=command.trader_id,
                strategy_id=command.strategy_id,
                instrument_id=command.instrument_id,
                client_order_id=order.client_order_id,
                venue_order_id=order.venue_order_id,
                command_id=self._uuid_factory.generate(),
                ts_init=self._clock.timestamp_ns(),
            )

            self.cancel_order(command)

        # TODO(cs): Relates to below _cancel_all_orders
        # Format
        # cancel_orders = order_cancel_all_to_betfair(instrument=instrument)  # type: ignore
        # self._log.debug(f"cancel_orders {cancel_orders}")
        #
        # self.create_task(self._cancel_order(command))

    # TODO(cs): Currently not in use as old behavior restored to cancel orders individually
    async def _cancel_all_orders(self, command: CancelAllOrders) -> None:
        # TODO(cs): I've had to duplicate the logic as couldn't refactor and tease
        #  apart the cancel rejects and execution report. This will possibly fail
        #  badly if there are any API errors...
        self._log.debug(f"Received cancel all orders: {command}")

        instrument = self._cache.instrument(command.instrument_id)
        PyCondition.not_none(instrument, "instrument")

        # Format
        cancel_orders = order_cancel_all_to_betfair(instrument=instrument)  # type: ignore
        self._log.debug(f"cancel_orders {cancel_orders}")

        # Send to client
        try:
            result = await self._client.cancel_orders(**cancel_orders)
        except Exception as exc:
            if isinstance(exc, BetfairAPIError):
                await self.on_api_exception(exc=exc)
            self._log.error(f"Cancel failed: {exc}")
            # TODO(cs): Will probably just need to recover the client order ID
            #  and order ID from the execution report?
            # self.generate_order_cancel_rejected(
            #     strategy_id=command.strategy_id,
            #     instrument_id=command.instrument_id,
            #     client_order_id=command.client_order_id,
            #     venue_order_id=command.venue_order_id,
            #     reason="client error",
            #     ts_event=self._clock.timestamp_ns(),
            # )
            return
        self._log.debug(f"result={result}")

        # Parse response
        for report in result["instructionReports"]:
            venue_order_id = VenueOrderId(report["instruction"]["betId"])
            if report["status"] == "FAILURE":
                reason = f"{result.get('errorCode', 'Error')}: {report['errorCode']}"
                self._log.error(f"cancel failed - {reason}")
                # TODO(cs): Will probably just need to recover the client order ID
                #  and order ID from the execution report?
                # self.generate_order_cancel_rejected(
                #     strategy_id=command.strategy_id,
                #     instrument_id=command.instrument_id,
                #     client_order_id=command.client_order_id,
                #     venue_order_id=venue_order_id,
                #     reason=reason,
                #     ts_event=self._clock.timestamp_ns(),
                # )
                # return

            self._log.debug(
                f"Matching venue_order_id: {venue_order_id} to client_order_id: {command.client_order_id}"
            )
            self.venue_order_id_to_client_order_id[venue_order_id] = command.client_order_id  # type: ignore
            self.generate_order_canceled(
                strategy_id=command.strategy_id,
                instrument_id=command.instrument_id,
                client_order_id=command.client_order_id,
                venue_order_id=venue_order_id,  # type: ignore
                ts_event=self._clock.timestamp_ns(),
            )
            self._log.debug("Sent order cancel")

    # cpdef void bulk_submit_order(self, list commands):
    # betfair allows up to 200 inserts per request
    #     raise NotImplementedError

    # cpdef void bulk_submit_update(self, list commands):
    # betfair allows up to 60 updates per request
    #     raise NotImplementedError

    # cpdef void bulk_submit_delete(self, list commands):
    # betfair allows up to 60 cancels per request
    #     raise NotImplementedError

    # -- ACCOUNT -----------------------------------------------------------------------------------

    async def check_account_currency(self):
        """
        Check account currency against BetfairClient
        """
        self._log.debug("Checking account currency")
        PyCondition.not_none(self.base_currency, "self.base_currency")
        details = await self._client.get_account_details()
        currency_code = details["currencyCode"]
        self._log.debug(f"Account {currency_code=}, {self.base_currency.code=}")
        assert currency_code == self.base_currency.code
        self._log.debug("Base currency matches client details")

    # -- DEBUGGING ---------------------------------------------------------------------------------

    def create_task(self, coro):
        self._loop.create_task(self._check_task(coro))

    async def _check_task(self, coro):
        try:
            awaitable = await coro
            return awaitable
        except Exception as e:
            self._log.exception(f"Unhandled exception: {e}")

    def client(self) -> BetfairClient:
        return self._client

    # -- ORDER STREAM API --------------------------------------------------------------------------

    def handle_order_stream_update(self, raw: bytes) -> None:
        """Handle an update from the order stream socket"""
        update = orjson.loads(raw)
        self.create_task(self._handle_order_stream_update(update=update))

    async def _handle_order_stream_update(self, update: Dict):
        for market in update.get("oc", []):
            # market_id = market["id"]
            for selection in market.get("orc", []):
                if selection.get("fullImage", False):
                    # TODO (bm) - need to replace orders for this selection - probably via a recon
                    self._log.debug("Received full order image")
                for order_update in selection.get("uo", []):
                    await self._check_order_update(order_update)
                    if order_update["status"] == "E":
                        self._handle_stream_executable_order_update(update=order_update)
                    elif order_update["status"] == "EC":
                        self._handle_stream_execution_complete_order_update(update=order_update)
                    else:
                        self._log.warning(f"Unknown order state: {order_update}")

    async def _check_order_update(self, update: Dict):
        """
        Ensure we have a client_order_id, instrument and order for this venue order update
        """
        venue_order_id = VenueOrderId(str(update["id"]))
        client_order_id = await self.wait_for_order(
            venue_order_id=venue_order_id, timeout_seconds=10.0
        )
        if client_order_id is None:
            self._log.warning(f"Can't find client_order_id for {update}")
            return
        PyCondition.type(client_order_id, ClientOrderId, "client_order_id")
        order = self._cache.order(client_order_id)
        PyCondition.not_none(order, "order")
        instrument = self._cache.instrument(order.instrument_id)
        PyCondition.not_none(instrument, "instrument")

    def _handle_stream_executable_order_update(self, update: Dict) -> None:
        """
        Handle update containing "E" (executable) order update
        """
        venue_order_id = VenueOrderId(update["id"])
        client_order_id = self.venue_order_id_to_client_order_id[venue_order_id]
        order = self._cache.order(client_order_id)
        instrument = self._cache.instrument(order.instrument_id)

        # Check if this is the first time seeing this order (backtest or replay)
        if venue_order_id in self.venue_order_id_to_client_order_id:
            # We've already sent an accept for this order in self._submit_order
            self._log.debug(f"Skipping order_accept as order exists: venue_order_id={update['id']}")
        else:
            raise RuntimeError()
            # self.generate_order_accepted(
            #     strategy_id=order.strategy_id,
            #     instrument_id=instrument.id,
            #     client_order_id=client_order_id,
            #     venue_order_id=venue_order_id,
            #     ts_event=millis_to_nanos(order_update["pd"]),
            # )

        # Check for any portion executed
        if update["sm"] > 0 and update["sm"] > order.filled_qty:
            execution_id = create_execution_id(update)
            if execution_id not in self.published_executions[client_order_id]:
                fill_qty = update["sm"] - order.filled_qty
                fill_price = self._determine_fill_price(update=update, order=order)
                self.generate_order_filled(
                    strategy_id=order.strategy_id,
                    instrument_id=order.instrument_id,
                    client_order_id=client_order_id,
                    venue_order_id=venue_order_id,
                    venue_position_id=None,  # Can be None
                    execution_id=execution_id,
                    order_side=B2N_ORDER_STREAM_SIDE[update["side"]],
                    order_type=OrderType.LIMIT,
                    last_qty=Quantity(fill_qty, instrument.size_precision),
                    last_px=price_to_probability(str(fill_price)),
                    # avg_px=Decimal(order['avp']),
                    quote_currency=instrument.quote_currency,
                    commission=Money(0, self.base_currency),
                    liquidity_side=LiquiditySide.NONE,
                    ts_event=millis_to_nanos(update["md"]),
                )
                self.published_executions[client_order_id].append(execution_id)

    def _determine_fill_price(self, update: Dict, order: Order):
        if "avp" not in update:
            # We don't have any specifics about the fill, assume it was filled at our price
            return update["p"]
        if order.filled_qty == 0:
            # New fill, simply return average price
            return update["avp"]
        else:
            new_price = price_to_probability(str(update["avp"]))
            prev_price = order.avg_px
            if prev_price == new_price:
                # Matched at same price
                return update["avp"]
            else:
                prev_price = probability_to_price(order.avg_px)
                prev_size = order.filled_qty
                new_price = Price.from_str(str(update["avp"]))
                new_size = update["sm"] - prev_size
                total_size = prev_size + new_size
                price = (new_price - ((prev_price * (prev_size / total_size)))) / (
                    new_size / total_size
                )
                self._log.debug(
                    f"Calculating fill price {prev_price=} {prev_size=} {new_price=} {new_size=} == {price=}"
                )
                return price

    def _handle_stream_execution_complete_order_update(self, update: Dict) -> None:
        """
        Handle "EC" (execution complete) order updates
        """
        venue_order_id = VenueOrderId(str(update["id"]))
        client_order_id = self._cache.client_order_id(venue_order_id=venue_order_id)
        order = self._cache.order(client_order_id=client_order_id)
        instrument = self._cache.instrument(order.instrument_id)

        if update["sm"] > 0 and update["sm"] > order.filled_qty:
            self._log.debug("")
            execution_id = create_execution_id(update)
            if execution_id not in self.published_executions[client_order_id]:
                fill_qty = update["sm"] - order.filled_qty
                fill_price = self._determine_fill_price(update=update, order=order)
                # At least some part of this order has been filled
                self.generate_order_filled(
                    strategy_id=order.strategy_id,
                    instrument_id=instrument.id,
                    client_order_id=client_order_id,
                    venue_order_id=venue_order_id,
                    venue_position_id=None,  # Can be None
                    execution_id=execution_id,
                    order_side=B2N_ORDER_STREAM_SIDE[update["side"]],
                    order_type=OrderType.LIMIT,
                    last_qty=Quantity(fill_qty, instrument.size_precision),
                    last_px=price_to_probability(str(fill_price)),
                    quote_currency=instrument.quote_currency,
                    # avg_px=order['avp'],
                    commission=Money(0, self.base_currency),
                    liquidity_side=LiquiditySide.TAKER,  # TODO - Fix this?
                    ts_event=millis_to_nanos(update["md"]),
                )
                self.published_executions[client_order_id].append(execution_id)

        cancel_qty = update["sc"] + update["sl"] + update["sv"]
        if cancel_qty > 0 and not order.is_completed:
            assert (
                update["sm"] + cancel_qty == update["s"]
            ), f"Size matched + canceled != total: {update}"
            # If this is the result of a ModifyOrder, we don't want to emit a cancel

            key = (client_order_id, venue_order_id)
            self._log.debug(
                f"cancel key: {key}, pending_update_order_client_ids: {self.pending_update_order_client_ids}"
            )
            if key not in self.pending_update_order_client_ids:
                # The remainder of this order has been canceled
                cancelled_ts = update.get("cd") or update.get("ld") or update.get("md")
                if cancelled_ts is not None:
                    cancelled_ts = millis_to_nanos(cancelled_ts)
                else:
                    cancelled_ts = self._clock.timestamp_ns()
                self.generate_order_canceled(
                    strategy_id=order.strategy_id,
                    instrument_id=instrument.id,
                    client_order_id=client_order_id,
                    venue_order_id=venue_order_id,
                    ts_event=cancelled_ts,
                )
                if venue_order_id in self.venue_order_id_to_client_order_id:
                    del self.venue_order_id_to_client_order_id[venue_order_id]
        # Market order will not be in self.published_executions
        if client_order_id in self.published_executions:
            # This execution is complete - no need to track this anymore
            del self.published_executions[client_order_id]

    def _handle_stream_execution_matched_fills(self, selection: Dict) -> None:
        for _ in selection.get("mb", []):
            pass
        for _ in selection.get("ml", []):
            pass

    async def wait_for_order(
        self, venue_order_id: VenueOrderId, timeout_seconds=10.0
    ) -> Optional[ClientOrderId]:
        """
        We may get an order update from the socket before our submit_order
        response has come back (with our betId).

        As a precaution, wait up to `timeout_seconds` for the betId to be added
        to `self.order_id_to_client_order_id`.
        """
        assert isinstance(venue_order_id, VenueOrderId)
        start = self._clock.timestamp_ns()
        now = start
        while (now - start) < secs_to_nanos(timeout_seconds):
            # self._log.debug(
            #     f"checking venue_order_id={venue_order_id} in {self.venue_order_id_to_client_order_id}"
            # )
            if venue_order_id in self.venue_order_id_to_client_order_id:
                client_order_id = self.venue_order_id_to_client_order_id[venue_order_id]
                self._log.debug(
                    f"Found order in {nanos_to_secs(now - start)} sec: {client_order_id}"
                )
                return client_order_id
            now = self._clock.timestamp_ns()
            await asyncio.sleep(0.1)
        self._log.warning(
            f"Failed to find venue_order_id: {venue_order_id} "
            f"after {timeout_seconds} seconds"
            f"\nexisting: {self.venue_order_id_to_client_order_id})"
        )
        return None

    # -- RECONCILIATION -------------------------------------------------------------------------------

    async def generate_order_status_report(self, order: Order) -> Optional[OrderStatusReport]:
        self._log.debug(f"generate_order_status_report: {order}")
        return await generate_order_status_report(self, order)

    async def generate_exec_reports(
        self,
        venue_order_id: VenueOrderId,
        symbol: Symbol,
        since: Optional[datetime] = None,
    ) -> List[ExecutionReport]:
        self._log.debug(f"generate_exec_reports: {venue_order_id}, {symbol}, {since}")
        return await generate_trades_list(self, venue_order_id, symbol, since)
Beispiel #5
0
def betfair_order_socket(betfair_client, live_logger):
    return BetfairOrderStreamClient(client=betfair_client,
                                    logger=live_logger,
                                    message_handler=None)