async def execution_client(betfair_client, account_id, exec_engine, clock, live_logger) -> BetfairExecutionClient: client = BetfairExecutionClient( client=betfair_client, account_id=account_id, engine=exec_engine, clock=clock, logger=live_logger, ) client.instrument_provider().load_all() exec_engine.register_client(client) return client
async def execution_client(betfair_client, account_id, exec_engine, clock, live_logger) -> BetfairExecutionClient: client = BetfairExecutionClient( client=betfair_client, account_id=account_id, base_currency=AUD, engine=exec_engine, clock=clock, logger=live_logger, market_filter={}, load_instruments=False, ) client.instrument_provider().load_all() exec_engine.register_client(client) return client
def setup(self): # Fixture Setup self.loop = asyncio.get_event_loop() self.loop.set_debug(True) self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.trader_id = TestStubs.trader_id() self.venue = BETFAIR_VENUE self.account_id = AccountId(self.venue.value, "001") # Setup logging self.logger = LiveLogger(loop=self.loop, clock=self.clock, level_stdout=LogLevel.DEBUG) self._log = LoggerAdapter("TestBetfairExecutionClient", self.logger) self.msgbus = MessageBus( trader_id=self.trader_id, clock=self.clock, logger=self.logger, ) self.cache = TestStubs.cache() self.cache.add_instrument(BetfairTestStubs.betting_instrument()) self.cache.add_account( TestStubs.betting_account(account_id=self.account_id)) self.portfolio = Portfolio( msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.exec_engine = LiveExecutionEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.betfair_client: BetfairClient = BetfairTestStubs.betfair_client( loop=self.loop, logger=self.logger) assert self.betfair_client.session_token self.instrument_provider = BetfairTestStubs.instrument_provider( betfair_client=self.betfair_client) self.client = BetfairExecutionClient( loop=asyncio.get_event_loop(), client=self.betfair_client, account_id=self.account_id, base_currency=GBP, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, instrument_provider=self.instrument_provider, market_filter={}, ) self.exec_engine.register_client(self.client) # Re-route exec engine messages through `handler` self.messages = [] def handler(func): def inner(x): self.messages.append(x) return func(x) return inner def listener(x): print(x) self.msgbus.subscribe("*", listener) self.msgbus.deregister(endpoint="ExecEngine.execute", handler=self.exec_engine.execute) self.msgbus.register(endpoint="ExecEngine.execute", handler=handler(self.exec_engine.execute)) self.msgbus.deregister(endpoint="ExecEngine.process", handler=self.exec_engine.process) self.msgbus.register(endpoint="ExecEngine.process", handler=handler(self.exec_engine.process)) self.msgbus.deregister(endpoint="Portfolio.update_account", handler=self.portfolio.update_account) self.msgbus.register(endpoint="Portfolio.update_account", handler=handler(self.portfolio.update_account))
class TestBetfairExecutionClient: def setup(self): # Fixture Setup self.loop = asyncio.get_event_loop() self.loop.set_debug(True) self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.trader_id = TestStubs.trader_id() self.venue = BETFAIR_VENUE self.account_id = AccountId(self.venue.value, "001") # Setup logging self.logger = LiveLogger(loop=self.loop, clock=self.clock, level_stdout=LogLevel.DEBUG) self._log = LoggerAdapter("TestBetfairExecutionClient", self.logger) self.msgbus = MessageBus( trader_id=self.trader_id, clock=self.clock, logger=self.logger, ) self.cache = TestStubs.cache() self.cache.add_instrument(BetfairTestStubs.betting_instrument()) self.cache.add_account( TestStubs.betting_account(account_id=self.account_id)) self.portfolio = Portfolio( msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.exec_engine = LiveExecutionEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.betfair_client: BetfairClient = BetfairTestStubs.betfair_client( loop=self.loop, logger=self.logger) assert self.betfair_client.session_token self.instrument_provider = BetfairTestStubs.instrument_provider( betfair_client=self.betfair_client) self.client = BetfairExecutionClient( loop=asyncio.get_event_loop(), client=self.betfair_client, account_id=self.account_id, base_currency=GBP, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, instrument_provider=self.instrument_provider, market_filter={}, ) self.exec_engine.register_client(self.client) # Re-route exec engine messages through `handler` self.messages = [] def handler(func): def inner(x): self.messages.append(x) return func(x) return inner def listener(x): print(x) self.msgbus.subscribe("*", listener) self.msgbus.deregister(endpoint="ExecEngine.execute", handler=self.exec_engine.execute) self.msgbus.register(endpoint="ExecEngine.execute", handler=handler(self.exec_engine.execute)) self.msgbus.deregister(endpoint="ExecEngine.process", handler=self.exec_engine.process) self.msgbus.register(endpoint="ExecEngine.process", handler=handler(self.exec_engine.process)) self.msgbus.deregister(endpoint="Portfolio.update_account", handler=self.portfolio.update_account) self.msgbus.register(endpoint="Portfolio.update_account", handler=handler(self.portfolio.update_account)) def _prefill_venue_order_id_to_client_order_id(self, update): order_ids = [ update["id"] for market in update.get("oc", []) for order in market.get("orc", []) for update in order.get("uo", []) ] return { VenueOrderId(oid): ClientOrderId(str(i + 1)) for i, oid in enumerate(order_ids) } async def _setup_account(self): await self.client.connection_account_state() def _setup_exec_client_and_cache(self, update): """ Called before processing a test streaming update - ensure all orders are in the cache in `update`. """ venue_order_ids = self._prefill_venue_order_id_to_client_order_id( update) venue_order_id_to_client_order_id = {} for c_id, v_id in enumerate(venue_order_ids): client_order_id = ClientOrderId(str(c_id)) venue_order_id = VenueOrderId(str(v_id)) self._log.debug( f"Adding client_order_id=[{c_id}], venue_order_id=[{v_id}] ") order = BetfairTestStubs.make_accepted_order( venue_order_id=venue_order_id, client_order_id=client_order_id) self._log.debug(f"created order: {order}") venue_order_id_to_client_order_id[v_id] = order.client_order_id cache_order = self.cache.order( client_order_id=order.client_order_id) self._log.debug(f"Cached order: {order}") if cache_order is None: self._log.debug("Adding order to cache") self.cache.add_order(order, position_id=PositionId(v_id.value)) assert self.cache.order( client_order_id).venue_order_id == venue_order_id self.cache.update_order(order) self.client.venue_order_id_to_client_order_id = venue_order_id_to_client_order_id async def _account_state(self): account_details = await self.betfair_client.get_account_details() account_funds = await self.betfair_client.get_account_funds() timestamp = self.clock.timestamp_ns() account_state = 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, ) return account_state @pytest.mark.asyncio async def test_submit_order_success(self): # Arrange command = BetfairTestStubs.submit_order_command() mock_betfair_request(self.betfair_client, BetfairResponses.betting_place_order_success()) # Act self.client.submit_order(command) await asyncio.sleep(0) # Assert submitted, accepted = self.messages assert isinstance(submitted, OrderSubmitted) assert isinstance(accepted, OrderAccepted) assert accepted.venue_order_id == VenueOrderId("228302937743") @pytest.mark.asyncio async def test_submit_order_error(self): # Arrange command = BetfairTestStubs.submit_order_command() mock_betfair_request(self.betfair_client, BetfairResponses.betting_place_order_error()) # Act self.client.submit_order(command) await asyncio.sleep(0) # Assert submitted, rejected = self.messages assert isinstance(submitted, OrderSubmitted) assert isinstance(rejected, OrderRejected) assert rejected.reason == "PERMISSION_DENIED: ERROR_IN_ORDER" @pytest.mark.asyncio async def test_modify_order_success(self): # Arrange venue_order_id = VenueOrderId("240808576108") order = BetfairTestStubs.make_accepted_order( venue_order_id=venue_order_id) command = BetfairTestStubs.modify_order_command( instrument_id=order.instrument_id, client_order_id=order.client_order_id, venue_order_id=venue_order_id, ) mock_betfair_request(self.betfair_client, BetfairResponses.betting_replace_orders_success()) # Act self.cache.add_order(order, PositionId("1")) self.client.modify_order(command) await asyncio.sleep(0) # Assert pending_update, updated = self.messages assert isinstance(pending_update, OrderPendingUpdate) assert isinstance(updated, OrderUpdated) assert updated.price == Price.from_str("0.02000") @pytest.mark.asyncio async def test_modify_order_error_order_doesnt_exist(self): # Arrange venue_order_id = VenueOrderId("229435133092") order = BetfairTestStubs.make_accepted_order( venue_order_id=venue_order_id) command = BetfairTestStubs.modify_order_command( instrument_id=order.instrument_id, client_order_id=order.client_order_id, venue_order_id=venue_order_id, ) mock_betfair_request(self.betfair_client, BetfairResponses.betting_replace_orders_success()) # Act self.client.modify_order(command) await asyncio.sleep(0) # Assert pending_update, rejected = self.messages assert isinstance(pending_update, OrderPendingUpdate) assert isinstance(rejected, OrderModifyRejected) assert rejected.reason == "ORDER NOT IN CACHE" @pytest.mark.asyncio async def test_modify_order_error_no_venue_id(self): # Arrange order = BetfairTestStubs.make_submitted_order() self.cache.add_order(order, position_id=BetfairTestStubs.position_id()) command = BetfairTestStubs.modify_order_command( instrument_id=order.instrument_id, client_order_id=order.client_order_id, venue_order_id="", ) mock_betfair_request(self.betfair_client, BetfairResponses.betting_replace_orders_success()) # Act self.client.modify_order(command) await asyncio.sleep(0) # Assert pending_update, rejected = self.messages assert isinstance(pending_update, OrderPendingUpdate) assert isinstance(rejected, OrderModifyRejected) assert rejected.reason == "ORDER MISSING VENUE_ORDER_ID" @pytest.mark.asyncio async def test_cancel_order_success(self): # Arrange order = BetfairTestStubs.make_submitted_order() self.cache.add_order(order, position_id=BetfairTestStubs.position_id()) command = BetfairTestStubs.cancel_order_command( instrument_id=order.instrument_id, client_order_id=order.client_order_id, venue_order_id=VenueOrderId("240564968665"), ) mock_betfair_request(self.betfair_client, BetfairResponses.betting_cancel_orders_success()) # Act self.client.cancel_order(command) await asyncio.sleep(0) # Assert pending_cancel, cancelled = self.messages assert isinstance(pending_cancel, OrderPendingCancel) assert isinstance(cancelled, OrderCanceled) @pytest.mark.asyncio async def test_cancel_order_fail(self): # Arrange order = BetfairTestStubs.make_submitted_order() self.cache.add_order(order, position_id=BetfairTestStubs.position_id()) command = BetfairTestStubs.cancel_order_command( instrument_id=order.instrument_id, client_order_id=order.client_order_id, venue_order_id=VenueOrderId("228302937743"), ) mock_betfair_request(self.betfair_client, BetfairResponses.betting_cancel_orders_error()) # Act self.client.cancel_order(command) await asyncio.sleep(0) # Assert pending_cancel, cancelled = self.messages assert isinstance(pending_cancel, OrderPendingCancel) assert isinstance(cancelled, OrderCancelRejected) @pytest.mark.asyncio async def test_order_multiple_fills(self): # Arrange self.exec_engine.start() client_order_id = ClientOrderId("1") venue_order_id = VenueOrderId("246938411724") submitted = BetfairTestStubs.make_submitted_order( client_order_id=client_order_id, quantity=Quantity.from_int(20)) self.cache.add_order(submitted, position_id=BetfairTestStubs.position_id()) self.client.venue_order_id_to_client_order_id[ venue_order_id] = client_order_id # Act for update in BetfairStreaming.ocm_multiple_fills(): await self.client._handle_order_stream_update(update) await asyncio.sleep(0.1) # Assert result = [fill.last_qty for fill in self.messages] expected = [ Quantity.from_str("16.1900"), Quantity.from_str("0.77"), Quantity.from_str("0.77"), ] assert result == expected @pytest.mark.asyncio async def test_connection_account_state(self): # Arrange, Act, Assert await self.client.connection_account_state() # Assert assert self.cache.account(self.account_id) @pytest.mark.asyncio async def test_check_account_currency(self): # Arrange, Act, Assert await self.client.check_account_currency() @pytest.mark.asyncio async def test_order_stream_full_image(self): # Arrange update = BetfairStreaming.ocm_FULL_IMAGE() await self._setup_account() self._setup_exec_client_and_cache(update=update) # Act await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert assert len(self.messages) == 7 @pytest.mark.asyncio async def test_order_stream_empty_image(self): # Arrange update = BetfairStreaming.ocm_EMPTY_IMAGE() await self._setup_account() self._setup_exec_client_and_cache(update=update) # Act await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert assert len(self.messages) == 1 @pytest.mark.asyncio async def test_order_stream_new_full_image(self): update = BetfairStreaming.ocm_NEW_FULL_IMAGE() await self._setup_account() self._setup_exec_client_and_cache(update=update) await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) assert len(self.messages) == 4 @pytest.mark.asyncio async def test_order_stream_sub_image(self): # Arrange update = BetfairStreaming.ocm_SUB_IMAGE() await self._setup_account() self._setup_exec_client_and_cache(update=update) # Act await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert assert len(self.messages) == 1 @pytest.mark.asyncio async def test_order_stream_update(self): # Arrange update = BetfairStreaming.ocm_UPDATE() await self._setup_account() self._setup_exec_client_and_cache(update=update) # Act await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert assert len(self.messages) == 2 @pytest.mark.asyncio async def test_order_stream_filled(self): # Arrange update = BetfairStreaming.ocm_FILLED() self._setup_exec_client_and_cache(update) await self._setup_account() # Act await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert assert len(self.messages) == 2 assert isinstance(self.messages[1], OrderFilled) assert self.messages[1].last_px == Price.from_str("0.9090909") @pytest.mark.asyncio async def test_order_stream_filled_multiple_prices(self): # Arrange await self._setup_account() update1 = BetfairStreaming.generate_order_update( price="1.50", size=20, side="B", status="E", sm=10, avp="1.60", ) self._setup_exec_client_and_cache(update1) await self.client._handle_order_stream_update(update=update1) await asyncio.sleep(0) order = self.cache.order(client_order_id=ClientOrderId("0")) event = self.messages[-1] order.apply(event) # Act update2 = BetfairStreaming.generate_order_update( price="1.50", size=20, side="B", status="EC", sm=20, avp="1.55", ) self._setup_exec_client_and_cache(update2) await self.client._handle_order_stream_update(update=update2) await asyncio.sleep(0) # Assert assert len(self.messages) == 3 assert isinstance(self.messages[1], OrderFilled) assert isinstance(self.messages[2], OrderFilled) assert self.messages[1].last_px == price_to_probability("1.60") assert self.messages[2].last_px == price_to_probability("1.50") @pytest.mark.asyncio async def test_order_stream_mixed(self): # Arrange update = BetfairStreaming.ocm_MIXED() self._setup_exec_client_and_cache(update) await self._setup_account() # Act await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert _, fill1, fill2, cancel = self.messages assert isinstance( fill1, OrderFilled) and fill1.venue_order_id.value == "229430281341" assert isinstance( fill2, OrderFilled) and fill2.venue_order_id.value == "229430281339" assert isinstance( cancel, OrderCanceled) and cancel.venue_order_id.value == "229430281339" @pytest.mark.asyncio @pytest.mark.skip(reason="Not implemented") async def test_generate_order_status_report(self): # Betfair client login orders = await self.betfair_client.list_current_orders() for order in orders: result = await self.client.generate_order_status_report(order=order ) assert result raise NotImplementedError() @pytest.mark.asyncio @pytest.mark.skip async def test_generate_trades_list(self): patch( "betfairlightweight.endpoints.betting.Betting.list_cleared_orders", return_value=BetfairDataProvider.list_cleared_orders( order_id="226125004209"), ) patch.object( self.client, "venue_order_id_to_client_order_id", {"226125004209": ClientOrderId("1")}, ) result = await generate_trades_list(self=self.client, venue_order_id="226125004209", symbol=None, since=None) assert result @pytest.mark.asyncio async def test_duplicate_execution_id(self): # Arrange await self._setup_account() for update in BetfairStreaming.ocm_DUPLICATE_EXECUTION(): self._setup_exec_client_and_cache(update) # # Load submitted orders # for client_order_id in (ClientOrderId('0'), ClientOrderId('1')): # order = BetfairTestStubs.make_order( # price=Price.from_str("0.5"), quantity=Quantity.from_int(10), client_order_id=client_order_id # ) # command = BetfairTestStubs.submit_order_command(order=order) # self.client.submit_order(command) # await asyncio.sleep(0) # Act for update in BetfairStreaming.ocm_DUPLICATE_EXECUTION(): self._setup_exec_client_and_cache(update=update) await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert _, fill1, cancel, fill2, fill3 = self.messages # First order example, partial fill followed by remainder canceled assert isinstance(fill1, OrderFilled) assert isinstance(cancel, OrderCanceled) # Second order example, partial fill followed by remainder filled assert (isinstance(fill2, OrderFilled) and fill2.execution_id.value == "4721ad7594e7a4a4dffb1bacb0cb45ccdec0747a") assert (isinstance(fill3, OrderFilled) and fill3.execution_id.value == "8b3e65be779968a3fdf2d72731c848c5153e88cd") @pytest.mark.asyncio async def test_betfair_order_reduces_balance(self): # Arrange self.client.stream = MagicMock() self.exec_engine.start() await asyncio.sleep(1) balance = self.cache.account_for_venue(self.venue).balances()[GBP] order = BetfairTestStubs.make_order(price=Price.from_str("0.5"), quantity=Quantity.from_int(10)) self.cache.add_order(order=order, position_id=None) mock_betfair_request(self.betfair_client, BetfairResponses.betting_place_order_success()) command = BetfairTestStubs.submit_order_command(order=order) self.client.submit_order(command) await asyncio.sleep(0.01) # Act balance_order = self.cache.account_for_venue( BETFAIR_VENUE).balances()[GBP] # Cancel the order, balance should return command = BetfairTestStubs.cancel_order_command( client_order_id=order.client_order_id, venue_order_id=order.venue_order_id) mock_betfair_request(self.betfair_client, BetfairResponses.betting_cancel_orders_success()) self.client.cancel_order(command) await asyncio.sleep(0.1) balance_cancel = self.cache.account_for_venue( BETFAIR_VENUE).balances()[GBP] # Assert assert balance.free == Money(1000.0, GBP) assert balance_order.free == Money(990.0, GBP) assert balance_cancel.free == Money(1000.0, GBP) self.exec_engine.kill() await asyncio.sleep(1) @pytest.mark.asyncio async def test_betfair_order_cancelled_no_timestamp(self): update = BetfairStreaming.ocm_error_fill() self._setup_exec_client_and_cache(update) for upd in update["oc"][0]["orc"][0]["uo"]: self.client._handle_stream_execution_complete_order_update( update=upd) await asyncio.sleep(1) @pytest.mark.asyncio @pytest.mark.parametrize( "price,size,side,status,updates", [ ("1.50", "50", "B", "EC", [{ "sm": 50 }]), ("1.50", "50", "B", "E", [{ "sm": 10 }, { "sm": 15 }]), ], ) async def test_various_betfair_order_fill_scenarios( self, price, size, side, status, updates): # Arrange update = BetfairStreaming.ocm_filled_different_price() self._setup_exec_client_and_cache(update) await self._setup_account() # Act for raw in updates: update = BetfairStreaming.generate_order_update(price=price, size=size, side=side, status=status, **raw) await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert assert len(self.messages) == 1 + len(updates) for msg, raw in zip(self.messages[1:], updates): assert isinstance(msg, OrderFilled) assert msg.last_qty == raw["sm"] @pytest.mark.asyncio async def test_order_filled_avp_update(self): # Arrange update = BetfairStreaming.ocm_filled_different_price() self._setup_exec_client_and_cache(update) await self._setup_account() # Act update = BetfairStreaming.generate_order_update(price="1.50", size=20, side="B", status="E", avp="1.50", sm=10) await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) update = BetfairStreaming.generate_order_update(price="1.30", size=20, side="B", status="E", avp="1.50", sm=10) await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0)
def create( loop: asyncio.AbstractEventLoop, name: str, config: Dict[str, Any], msgbus: MessageBus, cache: Cache, clock: LiveClock, logger: LiveLogger, client_cls=None, ) -> BetfairExecutionClient: """ Create a new Betfair execution client. Parameters ---------- loop : asyncio.AbstractEventLoop The event loop for the client. name : str The client name. config : dict[str, Any] The configuration 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 : LiveLogger The logger for the client. client_cls : class, optional The internal client constructor. This allows external library and testing dependency injection. Returns ------- BetfairExecutionClient """ market_filter = config.get("market_filter", {}) client = get_cached_betfair_client( username=config.get("username"), password=config.get("password"), app_key=config.get("app_key"), cert_dir=config.get("cert_dir"), loop=loop, logger=logger, ) provider = get_cached_betfair_instrument_provider( client=client, logger=logger, market_filter=tuple(market_filter.items())) # Get account ID env variable or set default account_id_env_var = os.getenv(config.get("account_id", ""), "001") # Set account ID account_id = AccountId(BETFAIR_VENUE.value, account_id_env_var) # Create client exec_client = BetfairExecutionClient( loop=loop, client=client, account_id=account_id, base_currency=Currency.from_str(config.get("base_currency")), msgbus=msgbus, cache=cache, clock=clock, logger=logger, market_filter=market_filter, instrument_provider=provider, ) return exec_client
def create( loop: asyncio.AbstractEventLoop, name: str, config: Dict[str, Any], msgbus: MessageBus, cache: Cache, clock: LiveClock, logger: LiveLogger, ) -> BetfairExecutionClient: """ Create a new Betfair execution client. Parameters ---------- loop : asyncio.AbstractEventLoop The event loop for the client. name : str The client name. config : dict[str, Any] The configuration 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 : LiveLogger The logger for the client. Returns ------- BetfairExecutionClient """ market_filter = config.get("market_filter", {}) client = get_cached_betfair_client( username=config.get("username"), password=config.get("password"), app_key=config.get("app_key"), cert_dir=config.get("cert_dir"), loop=loop, logger=logger, ) provider = get_cached_betfair_instrument_provider( client=client, logger=logger, market_filter=tuple(market_filter.items())) # Create client exec_client = BetfairExecutionClient( loop=loop, client=client, base_currency=Currency.from_str(config.get("base_currency")), msgbus=msgbus, cache=cache, clock=clock, logger=logger, market_filter=market_filter, instrument_provider=provider, ) return exec_client