def add_data_client_factory(self, name: str, factory): """ Add the given data client factory to the builder. Parameters ---------- name : str The name of the client. factory : LiveDataClientFactory or LiveExecutionClientFactory The factory to add. Raises ------ ValueError If `name` is not a valid string. KeyError If `name` has already been added. """ PyCondition.valid_string(name, "name") PyCondition.not_none(factory, "factory") PyCondition.not_in(name, self._data_factories, "name", "self._data_factories") if not issubclass(factory, LiveDataClientFactory): self._log.error(f"Factory was not of type `LiveDataClientFactory` " f"was {factory}.") return self._data_factories[name] = factory
def test_condition_not_none_when_arg_not_none_does_nothing(self): # Arrange # Act PyCondition.not_none("something", "param") # Assert self.assertTrue(True) # ValueError not raised
def build_exec_clients(self, config: Dict): """ Build the execution clients with the given configuration. Parameters ---------- config : dict[str, object] The execution clients configuration. """ PyCondition.not_none(config, "config") if not config: self._log.warning("No `exec_clients` configuration found.") for name, options in config.items(): pieces = name.partition("-") factory = self._exec_factories[pieces[0]] client = factory.create( loop=self._loop, name=name, config=options, msgbus=self._msgbus, cache=self._cache, clock=self._clock, logger=self._logger, ) self._exec_engine.register_client(client)
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(self, command: SubmitOrder) -> None: PyCondition.not_none(command, "command") contract_details = self._instrument_provider.contract_details[command.instrument_id] order: IBOrder = nautilus_order_to_ib_order(order=command.order) trade: IBTrade = self._client.placeOrder(contract=contract_details.contract, order=order) self._venue_order_id_to_client_order_id[trade.order.orderId] = command.order.client_order_id self._client_order_id_to_strategy_id[command.order.client_order_id] = command.strategy_id self._ib_insync_orders[command.order.client_order_id] = trade
def default_fx_ccy(symbol: Symbol, leverage: Decimal=Decimal("50")) -> Instrument: """ Return a default FX currency pair instrument from the given symbol. Parameters ---------- symbol : Symbol The currency pair symbol. leverage : Decimal, optional The leverage for the instrument. Raises ------ ValueError If the symbol.code length is not in range [6, 7]. """ PyCondition.not_none(symbol, "symbol") PyCondition.in_range_int(len(symbol.code), 6, 7, "len(symbol)") base_currency = symbol.code[:3] quote_currency = symbol.code[-3:] # Check tick precision of quote currency if quote_currency == 'JPY': price_precision = 3 else: price_precision = 5 return Instrument( symbol=symbol, asset_class=AssetClass.FX, asset_type=AssetType.SPOT, base_currency=Currency.from_str(base_currency), quote_currency=Currency.from_str(quote_currency), settlement_currency=Currency.from_str(quote_currency), is_inverse=False, price_precision=price_precision, size_precision=0, tick_size=Decimal(f"{1 / 10 ** price_precision:.{price_precision}f}"), multiplier=Decimal("1"), leverage=leverage, lot_size=Quantity("1000"), max_quantity=Quantity("1e7"), min_quantity=Quantity("1000"), max_price=None, min_price=None, max_notional=Money(50000000.00, USD), min_notional=Money(1000.00, USD), margin_init=Decimal("0.03"), margin_maint=Decimal("0.03"), maker_fee=Decimal("0.00002"), taker_fee=Decimal("0.00002"), financing={}, timestamp=UNIX_EPOCH, )
def cancel_order(self, command: CancelOrder) -> None: """ ib_insync modifies orders by modifying the original order object and calling placeOrder again """ PyCondition.not_none(command, "command") # TODO - Can we just reconstruct the IBOrder object from the `command` ? trade: IBTrade = self._ib_insync_orders[command.client_order_id] order = trade.order new_trade: IBTrade = self._client.cancelOrder(order=order) self._ib_insync_orders[command.client_order_id] = new_trade
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")
def handle_trade_tick(self, tick: TradeTick): """ Update the indicator with the given trade tick. Parameters ---------- tick : TradeTick The update tick to handle. """ PyCondition.not_none(tick, "tick") self.update_raw(tick.price.as_double())
def handle_bar(self, bar: Bar): """ Update the indicator with the given bar. Parameters ---------- bar : Bar The update bar to handle. """ PyCondition.not_none(bar, "bar") self.update_raw(bar.close.as_double())
def handle_quote_tick(self, tick: QuoteTick): """ Update the indicator with the given quote tick. Parameters ---------- tick : QuoteTick The update tick to handle. """ PyCondition.not_none(tick, "tick") self.update_raw(tick.extract_price(self.price_type).as_double())
def subscribe_order_book_deltas( self, instrument_id: InstrumentId, book_type: BookType, depth: Optional[int] = None, kwargs=None, ): """ Subscribe to `OrderBook` data for the given instrument ID. Parameters ---------- instrument_id : InstrumentId The order book instrument to subscribe to. book_type : BookType {``L1_TBBO``, ``L2_MBP``, ``L3_MBO``} The order book type. depth : int, optional, default None The maximum depth for the subscription. kwargs : dict, optional The keyword arguments for exchange specific parameters. """ if kwargs is None: kwargs = {} PyCondition.not_none(instrument_id, "instrument_id") instrument: BettingInstrument = self._instrument_provider.find( instrument_id) if instrument.market_id in self._subscribed_market_ids: self._log.warning( f"Already subscribed to market_id: {instrument.market_id} " f"[Instrument: {instrument_id.symbol}] <OrderBook> data.", ) return # If this is the first subscription request we're receiving, schedule a # subscription after a short delay to allow other strategies to send # their subscriptions (every change triggers a full snapshot). self._subscribed_market_ids.add(instrument.market_id) self._subscribed_instrument_ids.add(instrument.id) if self.subscription_status == SubscriptionStatus.UNSUBSCRIBED: self._loop.create_task(self.delayed_subscribe(delay=5)) self.subscription_status = SubscriptionStatus.PENDING_STARTUP elif self.subscription_status == SubscriptionStatus.PENDING_STARTUP: pass elif self.subscription_status == SubscriptionStatus.RUNNING: self._loop.create_task(self.delayed_subscribe(delay=0)) self._log.info( f"Added market_id {instrument.market_id} for {instrument_id.symbol} <OrderBook> data." )
def modify_order(self, command: ModifyOrder) -> None: """ ib_insync modifies orders by modifying the original order object and calling placeOrder again """ PyCondition.not_none(command, "command") # TODO - Can we just reconstruct the IBOrder object from the `command` ? trade: IBTrade = self._ib_insync_orders[command.client_order_id] order = trade.order if order.totalQuantity != command.quantity: order.totalQuantity = command.quantity.as_double() if getattr(order, "lmtPrice", None) != command.price: order.lmtPrice = command.price.as_double() new_trade: IBTrade = self._client.placeOrder(contract=trade.contract, order=order) self._ib_insync_orders[command.client_order_id] = new_trade
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")
async def load_async(self, instrument_id: InstrumentId, filters: Optional[Dict] = None): """ Load the instrument for the given ID into the provider asynchronously, optionally applying the given filters. Parameters ---------- instrument_id: InstrumentId The instrument ID to load. filters : Dict, optional The venue specific instrument loading filters to apply. Raises ------ ValueError If `instrument_id.venue` is not equal to `self.venue`. """ PyCondition.not_none(instrument_id, "instrument_id") PyCondition.equal(instrument_id.venue, self.venue, "instrument_id.venue", "self.venue") filters_str = "..." if not filters else f" with filters {filters}..." self._log.debug(f"Loading instrument {instrument_id}{filters_str}.") symbol = instrument_id.symbol.value # Get current commission rates # try: # fees: Optional[Dict[str, str]] = None # except BinanceClientError: # self._log.error( # "Cannot load instruments: API key authentication failed " # "(this is needed to fetch the applicable account fee tier).", # ) # return # Get exchange info for all assets exchange_info: BinanceFuturesExchangeInfo = await self._market.exchange_info( symbol=symbol) for symbol_info in exchange_info.symbols: self._parse_instrument( symbol_info=symbol_info, fees=None, ts_event=millis_to_nanos(exchange_info.serverTime), )
def from_dict(values) -> "BSPOrderBookDelta": PyCondition.not_none(values, "values") action: BookAction = BookActionParser.from_str_py(values["action"]) order: Order = (Order.from_dict({ "price": values["order_price"], "size": values["order_size"], "side": values["order_side"], "id": values["order_id"], }) if values["action"] != "CLEAR" else None) return BSPOrderBookDelta( instrument_id=InstrumentId.from_str(values["instrument_id"]), book_type=BookTypeParser.from_str_py(values["book_type"]), action=action, order=order, ts_event=values["ts_event"], ts_init=values["ts_init"], )
def __init__( self, client: BinanceHttpClient, account_type: BinanceAccountType = BinanceAccountType.FUTURES_USDT, ): PyCondition.not_none(client, "client") self.client = client self.account_type = account_type if account_type == BinanceAccountType.FUTURES_USDT: self.BASE_ENDPOINT = "/fapi/v1/" elif account_type == BinanceAccountType.FUTURES_COIN: self.BASE_ENDPOINT = "/dapi/v1/" else: # pragma: no cover (design-time error) raise RuntimeError( f"invalid Binance account type, was {account_type}")
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)
def register_statistic(self, statistic: PortfolioStatistic) -> None: """ Register the given statistic with the analyzer. Parameters ---------- statistic : PortfolioStatistic The statistic to register. Raises ------ KeyError if `statistic` has already been registered. """ PyCondition.not_none(statistic, "statistic") PyCondition.not_in(statistic.name, self._statistics, "statistic.name", "_statistics") self._statistics[statistic.name] = statistic
def load(self, instrument_id: InstrumentId, details: Dict): """ Load the instrument for the given ID and details. Parameters ---------- instrument_id : InstrumentId The instrument ID. details : dict The instrument details. """ PyCondition.not_none(instrument_id, "instrument_id") PyCondition.not_none(details, "details") PyCondition.is_in("asset_type", details, "asset_type", "details") if not self._client.client.CONNECTED: self.connect() contract = ib_insync.contract.Contract( symbol=instrument_id.symbol.value, exchange=instrument_id.venue.value, multiplier=details.get("multiplier"), currency=details.get("currency"), ) contract_details: List[ContractDetails] = self._client.reqContractDetails(contract=contract) if not contract_details: raise ValueError( f"No contract details found for the given instrument ID {instrument_id}" ) elif len(contract_details) > 1: raise ValueError( f"Multiple contract details found for the given instrument ID {instrument_id}" ) instrument: Instrument = self._parse_instrument( asset_type=AssetTypeParser.from_str_py(details.get("asset_type")), instrument_id=instrument_id, details=details, contract_details=contract_details[0], ) self.add(instrument)
def __init__( self, client: BinanceHttpClient, account_type: BinanceAccountType = BinanceAccountType.FUTURES_USDT, ): PyCondition.not_none(client, "client") self.client = client self.account_type = account_type if self.account_type == BinanceAccountType.FUTURES_USDT: self.BASE_ENDPOINT = "/fapi/v1/" elif self.account_type == BinanceAccountType.FUTURES_COIN: self.BASE_ENDPOINT = "/dapi/v1/" else: # pragma: no cover (design-time error) raise RuntimeError( f"invalid Binance Futures account type, was {account_type}") self._decoder_exchange_info = msgspec.json.Decoder( BinanceFuturesExchangeInfo)
def build_exec_clients(self, config: Dict): """ Build the execution clients with the given configuration. Parameters ---------- config : dict[str, object] The execution clients configuration. """ PyCondition.not_none(config, "config") if not config: self._log.warning("No `exec_clients` configuration found.") for name, client_config in config.items(): pieces = name.partition("-") factory = self._exec_factories[pieces[0]] client = factory.create( loop=self._loop, name=name, config=client_config, msgbus=self._msgbus, cache=self._cache, clock=self._clock, logger=self._logger, ) self._exec_engine.register_client(client) # Default client config if client_config.routing.default: self._exec_engine.register_default_client(client) # Venue routing config venues = client_config.routing.venues or [] for venue in venues: if not isinstance(venue, Venue): venue = Venue(venue) self._exec_engine.register_venue_routing(client, venue)
async def load_async(self, instrument_id: InstrumentId, filters: Optional[Dict] = None): """ Load the instrument for the given ID into the provider asynchronously, optionally applying the given filters. Parameters ---------- instrument_id: InstrumentId The instrument ID to load. filters : Dict, optional The venue specific instrument loading filters to apply. Raises ------ ValueError If `instrument_id.venue` is not equal to `self.venue`. """ PyCondition.not_none(instrument_id, "instrument_id") PyCondition.equal(instrument_id.venue, self.venue, "instrument_id.venue", "self.venue") filters_str = "..." if not filters else f" with filters {filters}..." self._log.debug(f"Loading instrument {instrument_id}{filters_str}.") try: # Get current commission rates account_info: Dict[str, Any] = await self._client.get_account_info() except FTXClientError: self._log.error( "Cannot load instruments: API key authentication failed " "(this is needed to fetch the applicable account fee tier).", ) return data: Dict[str, Any] = await self._client.get_market(instrument_id.symbol.value) self._parse_instrument(data, account_info)
def __init__(self, client: BinanceHttpClient): PyCondition.not_none(client, "client") self.client = client
def __init__( self, strategies: List[TradingStrategy], config: Dict[str, object], ): """ Initialize a new instance of the TradingNode class. Parameters ---------- strategies : list[TradingStrategy] The list of strategies to run on the trading node. config : dict[str, object] The configuration for the trading node. Raises ------ ValueError If strategies is None or empty. ValueError If config is None or empty. """ PyCondition.not_none(strategies, "strategies") PyCondition.not_none(config, "config") PyCondition.not_empty(strategies, "strategies") PyCondition.not_empty(config, "config") # Extract configs config_trader = config.get("trader", {}) config_log = config.get("logging", {}) config_exec_db = config.get("exec_database", {}) config_strategy = config.get("strategy", {}) config_adapters = config.get("adapters", {}) self._uuid_factory = UUIDFactory() self._loop = asyncio.get_event_loop() self._executor = concurrent.futures.ThreadPoolExecutor() self._loop.set_default_executor(self._executor) self._clock = LiveClock(loop=self._loop) self.created_time = self._clock.utc_now() self._is_running = False # Uncomment for debugging # self._loop.set_debug(True) # Setup identifiers self.trader_id = TraderId( name=config_trader["name"], tag=config_trader["id_tag"], ) # Setup logging self._logger = LiveLogger( clock=self._clock, name=self.trader_id.value, level_console=LogLevelParser.from_str_py(config_log.get("log_level_console")), level_file=LogLevelParser.from_str_py(config_log.get("log_level_file")), level_store=LogLevelParser.from_str_py(config_log.get("log_level_store")), run_in_process=config_log.get("run_in_process", True), # Run logger in a separate process log_thread=config_log.get("log_thread_id", False), log_to_file=config_log.get("log_to_file", False), log_file_path=config_log.get("log_file_path", ""), ) self._log = LoggerAdapter(component_name=self.__class__.__name__, logger=self._logger) self._log_header() self._log.info("Building...") self._setup_loop() # Requires the logger to be initialized self.portfolio = Portfolio( clock=self._clock, logger=self._logger, ) self._data_engine = LiveDataEngine( loop=self._loop, portfolio=self.portfolio, clock=self._clock, logger=self._logger, config={"qsize": 10000}, ) self.portfolio.register_cache(self._data_engine.cache) self.analyzer = PerformanceAnalyzer() if config_exec_db["type"] == "redis": exec_db = RedisExecutionDatabase( trader_id=self.trader_id, logger=self._logger, command_serializer=MsgPackCommandSerializer(), event_serializer=MsgPackEventSerializer(), config={ "host": config_exec_db["host"], "port": config_exec_db["port"], } ) else: exec_db = BypassExecutionDatabase( trader_id=self.trader_id, logger=self._logger, ) self._exec_engine = LiveExecutionEngine( loop=self._loop, database=exec_db, portfolio=self.portfolio, clock=self._clock, logger=self._logger, config={"qsize": 10000}, ) self._exec_engine.load_cache() self._setup_adapters(config_adapters, self._logger) self.trader = Trader( trader_id=self.trader_id, strategies=strategies, portfolio=self.portfolio, data_engine=self._data_engine, exec_engine=self._exec_engine, clock=self._clock, logger=self._logger, ) self._check_residuals_delay = config_trader.get("check_residuals_delay", 5.0) self._load_strategy_state = config_strategy.get("load_state", True) self._save_strategy_state = config_strategy.get("save_state", True) if self._load_strategy_state: self.trader.load() self._log.info("state=INITIALIZED.") self.time_to_initialize = self._clock.delta(self.created_time) self._log.info(f"Initialized in {self.time_to_initialize.total_seconds():.3f}s.")
def __init__( self, strategies: List[TradingStrategy], config: Dict[str, object], ): """ Initialize a new instance of the TradingNode class. Parameters ---------- strategies : list[TradingStrategy] The list of strategies to run on the trading node. config : dict[str, object] The configuration for the trading node. Raises ------ ValueError If strategies is None or empty. ValueError If config is None or empty. """ PyCondition.not_none(strategies, "strategies") PyCondition.not_none(config, "config") PyCondition.not_empty(strategies, "strategies") PyCondition.not_empty(config, "config") self._config = config # Extract configs config_trader = config.get("trader", {}) config_system = config.get("system", {}) config_log = config.get("logging", {}) config_exec_db = config.get("exec_database", {}) config_risk = config.get("risk", {}) config_strategy = config.get("strategy", {}) # System config self._connection_timeout = config_system.get("connection_timeout", 5.0) self._disconnection_timeout = config_system.get( "disconnection_timeout", 5.0) self._check_residuals_delay = config_system.get( "check_residuals_delay", 5.0) self._load_strategy_state = config_strategy.get("load_state", True) self._save_strategy_state = config_strategy.get("save_state", True) # Setup loop self._loop = asyncio.get_event_loop() self._executor = concurrent.futures.ThreadPoolExecutor() self._loop.set_default_executor(self._executor) self._loop.set_debug(config_system.get("loop_debug", False)) # Components self._clock = LiveClock(loop=self._loop) self._uuid_factory = UUIDFactory() self.system_id = self._uuid_factory.generate() self.created_time = self._clock.utc_now() self._is_running = False # Setup identifiers self.trader_id = TraderId( name=config_trader["name"], tag=config_trader["id_tag"], ) # Setup logging level_stdout = LogLevelParser.from_str_py( config_log.get("level_stdout")) self._logger = LiveLogger( loop=self._loop, clock=self._clock, trader_id=self.trader_id, system_id=self.system_id, level_stdout=level_stdout, ) self._log = LoggerAdapter( component=self.__class__.__name__, logger=self._logger, ) self._log_header() self._log.info("Building...") if platform.system() != "Windows": # Requires the logger to be initialized # Windows does not support signal handling # https://stackoverflow.com/questions/45987985/asyncio-loops-add-signal-handler-in-windows self._setup_loop() # Build platform # ---------------------------------------------------------------------- self.portfolio = Portfolio( clock=self._clock, logger=self._logger, ) self._data_engine = LiveDataEngine( loop=self._loop, portfolio=self.portfolio, clock=self._clock, logger=self._logger, config={"qsize": 10000}, ) self.portfolio.register_cache(self._data_engine.cache) self.analyzer = PerformanceAnalyzer() if config_exec_db["type"] == "redis": exec_db = RedisExecutionDatabase( trader_id=self.trader_id, logger=self._logger, command_serializer=MsgPackCommandSerializer(), event_serializer=MsgPackEventSerializer(), config={ "host": config_exec_db["host"], "port": config_exec_db["port"], }, ) else: exec_db = BypassExecutionDatabase( trader_id=self.trader_id, logger=self._logger, ) self._exec_engine = LiveExecutionEngine( loop=self._loop, database=exec_db, portfolio=self.portfolio, clock=self._clock, logger=self._logger, config={"qsize": 10000}, ) self._risk_engine = LiveRiskEngine( loop=self._loop, exec_engine=self._exec_engine, portfolio=self.portfolio, clock=self._clock, logger=self._logger, config=config_risk, ) self._exec_engine.load_cache() self._exec_engine.register_risk_engine(self._risk_engine) self.trader = Trader( trader_id=self.trader_id, strategies=strategies, portfolio=self.portfolio, data_engine=self._data_engine, exec_engine=self._exec_engine, risk_engine=self._risk_engine, clock=self._clock, logger=self._logger, ) if self._load_strategy_state: self.trader.load() self._builder = TradingNodeBuilder( data_engine=self._data_engine, exec_engine=self._exec_engine, risk_engine=self._risk_engine, clock=self._clock, logger=self._logger, log=self._log, ) self._log.info("state=INITIALIZED.") self.time_to_initialize = self._clock.delta(self.created_time) self._log.info( f"Initialized in {self.time_to_initialize.total_seconds():.3f}s.") self._is_built = False
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")
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_order(self, command: CancelOrder) -> None: PyCondition.not_none(command, "command") self.create_task(self._cancel_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, )