From c870e2547662d72c60f9d0ddbf4e82ca78cf726a Mon Sep 17 00:00:00 2001 From: liampauling Date: Mon, 16 Dec 2024 10:42:35 +0000 Subject: [PATCH 1/5] initial poc for 'zero delay passive orders' --- flumine/execution/simulatedexecution.py | 12 +- flumine/order/orderpackage.py | 26 ++- flumine/simulation/simulatedorder.py | 19 +++ flumine/simulation/simulation.py | 7 +- tests/test_execution.py | 36 ++++- tests/test_fluminesimulation.py | 46 ++++-- tests/test_orderpackage.py | 40 ++++- tests/test_simulatedorder.py | 206 +++++++++++++++++++++--- 8 files changed, 341 insertions(+), 51 deletions(-) diff --git a/flumine/execution/simulatedexecution.py b/flumine/execution/simulatedexecution.py index c04c1337..93b26b9d 100644 --- a/flumine/execution/simulatedexecution.py +++ b/flumine/execution/simulatedexecution.py @@ -35,12 +35,17 @@ def execute_place( if order_package.client.paper_trade: time.sleep(order_package.bet_delay + config.place_latency) market = self.flumine.markets.markets[order_package.market_id] - for order, instruction in zip(order_package, order_package.place_instructions): + for order, instruction in zip( + order_package.orders_pending, order_package.place_instructions + ): with order.trade: self._bet_id += 1 simulated_response = order.simulated.place( order_package, market.market_book, instruction, self._bet_id ) + if simulated_response.status == "DELAY": # PENDING delay + self._bet_id -= 1 + continue self._order_logger( order, simulated_response, order_package.package_type ) @@ -48,9 +53,8 @@ def execute_place( order.executable() elif simulated_response.status == "FAILURE": order.execution_complete() - - # update transaction counts - order_package.client.add_transaction(len(order_package)) + # update transaction counts + order_package.client.add_transaction(1) def execute_cancel( self, order_package, http_session: Optional[requests.Session] diff --git a/flumine/order/orderpackage.py b/flumine/order/orderpackage.py index f802cd9f..41a7d7d0 100644 --- a/flumine/order/orderpackage.py +++ b/flumine/order/orderpackage.py @@ -53,7 +53,8 @@ def __init__( # following used for simulated execution self.processed = False self.bet_delay = bet_delay - self.simulated_delay = self.calc_simulated_delay() + self.simulated_latency = self.calc_simulated_latency() + self.simulated_latency_plus_delay = self.calc_simulated_latency_delay() def retry(self): if self._retry and self._retry_count < self._max_retries: @@ -70,7 +71,18 @@ def reset_orders(self, complete: bool = False) -> None: else: order.executable() - def calc_simulated_delay(self) -> float: + def calc_simulated_latency(self) -> float: + if self.client.execution.EXCHANGE == ExchangeType.SIMULATED: + if self.package_type == OrderPackageType.PLACE: + return config.place_latency + elif self.package_type == OrderPackageType.CANCEL: + return config.cancel_latency + elif self.package_type == OrderPackageType.UPDATE: + return config.update_latency + elif self.package_type == OrderPackageType.REPLACE: + return config.replace_latency + + def calc_simulated_latency_delay(self) -> float: if self.client.execution.EXCHANGE == ExchangeType.SIMULATED: if self.package_type == OrderPackageType.PLACE: return config.place_latency + self.bet_delay @@ -105,6 +117,14 @@ def order_limit(cls, package_type: OrderPackageType) -> int: def orders(self) -> list: return [o for o in self._orders if o.status != OrderStatus.VIOLATION] + @property + def orders_pending(self) -> list: + return [ + o + for o in self._orders + if o.status in [OrderStatus.PENDING, OrderStatus.REPLACING] + ] + @property def retry_count(self) -> int: return self._retry_count @@ -147,7 +167,7 @@ class BetfairOrderPackage(BaseOrderPackage): @property def place_instructions(self): - return [order.create_place_instruction() for order in self] + return [order.create_place_instruction() for order in self.orders_pending] @property def cancel_instructions(self): diff --git a/flumine/simulation/simulatedorder.py b/flumine/simulation/simulatedorder.py index ae82c074..7d0acb11 100644 --- a/flumine/simulation/simulatedorder.py +++ b/flumine/simulation/simulatedorder.py @@ -2,6 +2,7 @@ import datetime from typing import List, Optional from betfairlightweight.resources.bettingresources import MarketBook, RunnerBook +from docutils.nodes import status from .utils import ( SimulatedPlaceResponse, @@ -109,8 +110,22 @@ def place( raise NotImplementedError( "Simulated betTargetSize placement not implemented" ) + # calc current delay + current_bet_delay = order_package.elapsed_seconds + bet_delay_complete = False + if current_bet_delay > order_package.simulated_latency_plus_delay: + bet_delay_complete = True + + # todo is this going to be a market level setting? + # if order_package.client.passive_bet_delay and bet_delay_complete is False: + # return False + if self.order.side == "BACK": available_to_back = get_price(runner.ex.available_to_back, 0) or 1.01 + if bet_delay_complete is False and price <= available_to_back: + # order would be 'aggressive' + return SimulatedPlaceResponse(status="DELAY") + if ( not order_package.client.best_price_execution and available_to_back > price @@ -157,6 +172,10 @@ def place( available = runner.ex.available_to_lay else: available_to_lay = get_price(runner.ex.available_to_lay, 0) or 1000 + if bet_delay_complete is False and price >= available_to_lay: + # order would be 'aggressive' + return SimulatedPlaceResponse(status="DELAY") + if ( not order_package.client.best_price_execution and available_to_lay < price diff --git a/flumine/simulation/simulation.py b/flumine/simulation/simulation.py index e2703bb5..c0052fad 100644 --- a/flumine/simulation/simulation.py +++ b/flumine/simulation/simulation.py @@ -151,7 +151,7 @@ def _process_market_books(self, event: events.MarketBookEvent) -> None: ) def process_order_package(self, order_package) -> None: - # place in pending list (wait for latency+delay) + # place in pending list (wait for latency) self.handler_queue.append(order_package) def _process_simulated_orders(self, market) -> None: @@ -187,10 +187,11 @@ def _check_pending_packages(self, market_id: str) -> None: for order_package in self.handler_queue: if ( order_package.market_id == market_id - and order_package.elapsed_seconds > order_package.simulated_delay + and order_package.elapsed_seconds > order_package.simulated_latency ): order_package.client.execution.handler(order_package) - processed.append(order_package) + if len(order_package.orders_pending) == 0: + processed.append(order_package) for p in processed: self.handler_queue.remove(p) diff --git a/tests/test_execution.py b/tests/test_execution.py index d8134284..3b70ffd0 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -1051,9 +1051,8 @@ def test_execute_place_success(self, mock__order_logger): mock_order.trade.__enter__ = mock.Mock() mock_order.trade.__exit__ = mock.Mock() mock_order_package = mock.MagicMock(market_id="1.23") - mock_order_package.__len__.return_value = 1 mock_order_package.client.paper_trade = False - mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order])) + mock_order_package.orders_pending = [mock_order] mock_order_package.place_instructions = [1] mock_order_package.info = {} mock_sim_resp = mock.Mock() @@ -1080,9 +1079,8 @@ def test_execute_place_failure(self, mock__order_logger): mock_order.trade.__enter__ = mock.Mock() mock_order.trade.__exit__ = mock.Mock() mock_order_package = mock.MagicMock(market_id="1.23") - mock_order_package.__len__.return_value = 1 mock_order_package.client.paper_trade = False - mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order])) + mock_order_package.orders_pending = [mock_order] mock_order_package.place_instructions = [1] mock_order_package.info = {} mock_sim_resp = mock.Mock() @@ -1103,17 +1101,41 @@ def test_execute_place_failure(self, mock__order_logger): mock_order.trade.__exit__.assert_called_with(None, None, None) mock_order_package.client.add_transaction.assert_called_with(1) + @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger") + def test_execute_place_delay(self, mock__order_logger): + mock_order = mock.Mock() + mock_order.trade.__enter__ = mock.Mock() + mock_order.trade.__exit__ = mock.Mock() + mock_order_package = mock.MagicMock(market_id="1.23") + mock_order_package.client.paper_trade = False + mock_order_package.orders_pending = [mock_order] + mock_order_package.place_instructions = [1] + mock_order_package.info = {} + mock_sim_resp = mock.Mock() + mock_sim_resp.status = "DELAY" + mock_order.simulated.place.return_value = mock_sim_resp + self.execution.execute_place(mock_order_package, None) + mock_order.simulated.place.assert_called_with( + mock_order_package, + self.mock_market.market_book, + 1, + self.execution._bet_id + 1, + ) + mock__order_logger.assert_not_called() + mock_order.executable.assert_not_called() + mock_order.trade.__enter__.assert_called_with() + mock_order.trade.__exit__.assert_called_with(None, None, None) + mock_order_package.client.add_transaction.assert_not_called() + @mock.patch("flumine.execution.simulatedexecution.time") def test_execute_place_paper_trade(self, mock_time): mock_order_package = mock.MagicMock( market_id="1.23", place_instructions=[], bet_delay=1 ) - mock_order_package.__len__.return_value = 1 - mock_order_package.__iter__ = mock.Mock(return_value=iter([])) + mock_order_package.orders_pending = [] mock_order_package.client.paper_trade = True self.execution.execute_place(mock_order_package, None) mock_time.sleep.assert_called_with(config.place_latency + 1) - mock_order_package.client.add_transaction.assert_called_with(1) @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger") def test_execute_cancel(self, mock__order_logger): diff --git a/tests/test_fluminesimulation.py b/tests/test_fluminesimulation.py index cd6be596..f71c1b53 100644 --- a/tests/test_fluminesimulation.py +++ b/tests/test_fluminesimulation.py @@ -190,11 +190,13 @@ def test__check_pending_packages_place(self): elapsed_seconds=5, bet_delay=1, client=mock_client, - simulated_delay=1.2, + orders_pending=[], + simulated_latency=1.2, ) self.flumine.handler_queue = [mock_order_package] self.flumine._check_pending_packages("1.23") mock_client.execution.handler.assert_called_with(mock_order_package) + self.assertEqual(self.flumine.handler_queue, []) def test__check_pending_packages_place_pending(self): mock_client = mock.Mock() @@ -204,11 +206,12 @@ def test__check_pending_packages_place_pending(self): elapsed_seconds=0.2, bet_delay=1, client=mock_client, - simulated_delay=1.2, + simulated_latency=1.2, ) self.flumine.handler_queue = [mock_order_package] self.flumine._check_pending_packages("1.23") mock_client.execution.handler.assert_not_called() + self.assertEqual(self.flumine.handler_queue, [mock_order_package]) def test__check_pending_packages_place_diff_market_id(self): mock_client = mock.Mock() @@ -218,47 +221,68 @@ def test__check_pending_packages_place_diff_market_id(self): elapsed_seconds=2, bet_delay=1, client=mock_client, - simulated_delay=1.2, + simulated_latency=1.2, ) self.flumine.handler_queue = [mock_order_package] self.flumine._check_pending_packages("1.24") mock_client.execution.handler.assert_not_called() + self.assertEqual(self.flumine.handler_queue, [mock_order_package]) def test__check_pending_packages_cancel(self): mock_client = mock.Mock() mock_order_package = mock.Mock( - market_id="1.23", elapsed_seconds=3, client=mock_client, simulated_delay=0.2 + market_id="1.23", + elapsed_seconds=3, + client=mock_client, + simulated_latency=0.2, + orders_pending=[], ) self.flumine.handler_queue = [mock_order_package] self.flumine._check_pending_packages("1.23") mock_client.execution.handler.assert_called_with(mock_order_package) + self.assertEqual(self.flumine.handler_queue, []) def test__check_pending_packages_cancel_pending(self): mock_client = mock.Mock() mock_order_package = mock.Mock( - market_id="1.23", elapsed_seconds=2, client=mock_client, simulated_delay=0.2 + market_id="1.23", + elapsed_seconds=2, + client=mock_client, + simulated_latency=0.2, + orders_pending=[], ) self.flumine.handler_queue = [mock_order_package] self.flumine._check_pending_packages("1.23") mock_client.execution.handler.assert_called_with(mock_order_package) + self.assertEqual(self.flumine.handler_queue, []) def test__check_pending_packages_update(self): mock_client = mock.Mock() mock_order_package = mock.Mock( - market_id="1.23", elapsed_seconds=3, client=mock_client, simulated_delay=0.2 + market_id="1.23", + elapsed_seconds=3, + client=mock_client, + orders_pending=[], + simulated_latency=0.2, ) self.flumine.handler_queue = [mock_order_package] self.flumine._check_pending_packages("1.23") mock_client.execution.handler.assert_called_with(mock_order_package) + self.assertEqual(self.flumine.handler_queue, []) def test__check_pending_packages_update_pending(self): mock_client = mock.Mock() mock_order_package = mock.Mock( - market_id="1.23", elapsed_seconds=2, client=mock_client, simulated_delay=0.2 + market_id="1.23", + elapsed_seconds=2, + client=mock_client, + orders_pending=[], + simulated_latency=0.2, ) self.flumine.handler_queue = [mock_order_package] self.flumine._check_pending_packages("1.23") mock_client.execution.handler.assert_called_with(mock_order_package) + self.assertEqual(self.flumine.handler_queue, []) def test__check_pending_packages_replace(self): mock_client = mock.Mock() @@ -268,11 +292,13 @@ def test__check_pending_packages_replace(self): elapsed_seconds=5, bet_delay=1, client=mock_client, - simulated_delay=1.2, + orders_pending=[], + simulated_latency=1.2, ) self.flumine.handler_queue = [mock_order_package] self.flumine._check_pending_packages("1.23") mock_client.execution.handler.assert_called_with(mock_order_package) + self.assertEqual(self.flumine.handler_queue, []) def test__check_pending_packages_replace_pending(self): mock_client = mock.Mock() @@ -282,11 +308,13 @@ def test__check_pending_packages_replace_pending(self): elapsed_seconds=2, bet_delay=1, client=mock_client, - simulated_delay=1.2, + orders_pending=[], + simulated_latency=1.2, ) self.flumine.handler_queue.append(mock_order_package) self.flumine._check_pending_packages("1.23") mock_client.execution.handler.assert_called_with(mock_order_package) + self.assertEqual(self.flumine.handler_queue, []) @mock.patch("flumine.baseflumine.BaseFlumine.info") @mock.patch("flumine.baseflumine.BaseFlumine.log_control") diff --git a/tests/test_orderpackage.py b/tests/test_orderpackage.py index 47b41076..b23144c9 100644 --- a/tests/test_orderpackage.py +++ b/tests/test_orderpackage.py @@ -43,7 +43,8 @@ def test_init(self): self.assertTrue(self.order_package._retry) self.assertEqual(self.order_package._max_retries, 3) self.assertEqual(self.order_package._retry_count, 0) - self.assertIsNone(self.order_package.simulated_delay) + self.assertIsNone(self.order_package.simulated_latency) + self.assertIsNone(self.order_package.simulated_latency_plus_delay) def test_retry(self): self.assertTrue(self.order_package.retry()) @@ -70,22 +71,37 @@ def test_reset_orders(self): self.order_package.reset_orders(True) mock_order.execution_complete.assert_called() - def test_calc_simulated_delay(self): + def test_calc_simulated_latency(self): config.place_latency = 0.1 config.cancel_latency = 0.2 config.update_latency = 0.3 config.replace_latency = 0.4 + self.assertIsNone(self.order_package.calc_simulated_latency()) + self.order_package.client.execution.EXCHANGE = ExchangeType.SIMULATED + self.order_package.package_type = OrderPackageType.PLACE + self.assertEqual(self.order_package.calc_simulated_latency(), 0.1) + self.order_package.package_type = OrderPackageType.CANCEL + self.assertEqual(self.order_package.calc_simulated_latency(), 0.2) + self.order_package.package_type = OrderPackageType.UPDATE + self.assertEqual(self.order_package.calc_simulated_latency(), 0.3) + self.order_package.package_type = OrderPackageType.REPLACE + self.assertEqual(self.order_package.calc_simulated_latency(), 0.4) - self.assertIsNone(self.order_package.calc_simulated_delay()) + def test_calc_simulated_latency_delay(self): + config.place_latency = 0.1 + config.cancel_latency = 0.2 + config.update_latency = 0.3 + config.replace_latency = 0.4 + self.assertIsNone(self.order_package.calc_simulated_latency_delay()) self.order_package.client.execution.EXCHANGE = ExchangeType.SIMULATED self.order_package.package_type = OrderPackageType.PLACE - self.assertEqual(self.order_package.calc_simulated_delay(), 1.1) + self.assertEqual(self.order_package.calc_simulated_latency_delay(), 1.1) self.order_package.package_type = OrderPackageType.CANCEL - self.assertEqual(self.order_package.calc_simulated_delay(), 0.2) + self.assertEqual(self.order_package.calc_simulated_latency_delay(), 0.2) self.order_package.package_type = OrderPackageType.UPDATE - self.assertEqual(self.order_package.calc_simulated_delay(), 0.3) + self.assertEqual(self.order_package.calc_simulated_latency_delay(), 0.3) self.order_package.package_type = OrderPackageType.REPLACE - self.assertEqual(self.order_package.calc_simulated_delay(), 1.4) + self.assertEqual(self.order_package.calc_simulated_latency_delay(), 1.4) def test_place_instructions(self): with self.assertRaises(NotImplementedError): @@ -116,6 +132,16 @@ def test_orders(self): ] self.assertEqual(len(self.order_package.orders), 2) + def test_orders_pending(self): + self.assertEqual(self.order_package.orders, [self.mock_order]) + self.order_package._orders = [ + mock.Mock(status=OrderStatus.PENDING), + mock.Mock(status=OrderStatus.PENDING), + mock.Mock(status=OrderStatus.VIOLATION), + mock.Mock(status=OrderStatus.REPLACING), + ] + self.assertEqual(len(self.order_package.orders_pending), 3) + def test_retry_count(self): self.order_package._retry_count = 1 self.assertEqual(self.order_package.retry_count, 1) diff --git a/tests/test_simulatedorder.py b/tests/test_simulatedorder.py index e46e8b40..959dee72 100644 --- a/tests/test_simulatedorder.py +++ b/tests/test_simulatedorder.py @@ -182,7 +182,12 @@ def test_call_bsp_reconciled( @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_back(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() mock_runner.ex.available_to_back = [{"price": 12, "size": 120}] @@ -196,6 +201,46 @@ def test_place_limit_back(self, mock__get_runner): self.simulated.matched, [[mock_market_book.publish_time_epoch, 12, 2]] ) + @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") + def test_place_limit_back_delay_aggressive(self, mock__get_runner): + mock_client = mock.Mock(best_price_execution=True) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=1.1, + ) + mock_market_book = mock.Mock(status="OPEN") + mock_runner = mock.Mock() + mock_runner.ex.available_to_back = [{"price": 12, "size": 120}] + mock_runner.ex.available_to_lay = [{"price": 13, "size": 120}] + mock__get_runner.return_value = mock_runner + resp = self.simulated.place(mock_order_package, mock_market_book, {}, 1) + self.assertEqual(self.simulated.market_version, mock_market_book.version) + self.assertEqual(resp.status, "DELAY") + self.assertEqual(self.simulated.matched, []) + + @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") + def test_place_limit_back_delay_passive(self, mock__get_runner): + mock_client = mock.Mock(best_price_execution=True) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) + mock_market_book = mock.Mock(status="OPEN") + mock_runner = mock.Mock() + mock_runner.ex.available_to_back = [{"price": 11, "size": 120}] + mock_runner.ex.available_to_lay = [{"price": 12, "size": 120}] + mock__get_runner.return_value = mock_runner + resp = self.simulated.place(mock_order_package, mock_market_book, {}, 1) + self.assertEqual(self.simulated.market_version, mock_market_book.version) + self.assertEqual(resp.status, "SUCCESS") + self.assertEqual(resp.average_price_matched, 0) + self.assertEqual(resp.size_matched, 0) + self.assertEqual(self.simulated.matched, []) + @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_back_target_size(self, mock__get_runner): self.mock_order.order_type.size = None @@ -212,7 +257,12 @@ def test_place_limit_back_target_size(self, mock__get_runner): @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_back_fill_or_kill_matched(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() mock_runner.ex.available_to_back = [{"price": 12, "size": 2}] @@ -234,7 +284,12 @@ def test_place_limit_back_fill_or_kill_matched(self, mock__get_runner): @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_back_fill_or_kill_lapsed(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() mock_runner.ex.available_to_back = [{"price": 12, "size": 1}] @@ -255,7 +310,12 @@ def test_place_limit_back_fill_or_kill_lapsed(self, mock__get_runner): @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_back_fill_or_kill_no_price(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() mock_runner.ex.available_to_back = [] @@ -278,7 +338,12 @@ def test_place_limit_back_fill_or_kill_min_fill_size_matched( self, mock__get_runner ): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() mock_runner.ex.available_to_back = [{"price": 12, "size": 1}] @@ -301,7 +366,12 @@ def test_place_limit_back_fill_or_kill_min_fill_size_matched( @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_back_fill_or_kill_min_fill_size_lapsed(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() mock_runner.ex.available_to_back = [{"price": 12, "size": 1}] @@ -324,7 +394,12 @@ def test_place_limit_back_fill_or_kill_min_fill_size_lapsed(self, mock__get_runn @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_back_fill_or_kill_vwap(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() mock_runner.ex.available_to_back = [ @@ -380,7 +455,12 @@ def test_place_market_version(self, mock__get_runner): @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_back_unmatched(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() mock_runner.ex.available_to_back = [{"price": 10, "size": 120}] @@ -400,7 +480,12 @@ def test_place_limit_back_unmatched(self, mock__get_runner): @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_back_bpe(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=False) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() mock_runner.ex.available_to_back = [{"price": 15, "size": 120}] @@ -415,7 +500,12 @@ def test_place_limit_back_bpe(self, mock__get_runner): @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_lay(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) self.simulated.order.side = "LAY" mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() @@ -429,10 +519,55 @@ def test_place_limit_lay(self, mock__get_runner): self.simulated.matched, [[mock_market_book.publish_time_epoch, 12, 2]] ) + @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") + def test_place_limit_lay_delay_aggressive(self, mock__get_runner): + mock_client = mock.Mock(best_price_execution=True) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=1.1, + ) + self.simulated.order.side = "LAY" + mock_market_book = mock.Mock(status="OPEN") + mock_runner = mock.Mock() + mock_runner.ex.available_to_back = [{"price": 11, "size": 120}] + mock_runner.ex.available_to_lay = [{"price": 12, "size": 120}] + mock__get_runner.return_value = mock_runner + resp = self.simulated.place(mock_order_package, mock_market_book, {}, 1) + self.assertEqual(resp.status, "DELAY") + self.assertEqual(self.simulated.matched, []) + + @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") + def test_place_limit_lay_delay_passive(self, mock__get_runner): + mock_client = mock.Mock(best_price_execution=True) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) + self.simulated.order.side = "LAY" + mock_market_book = mock.Mock(status="OPEN") + mock_runner = mock.Mock() + mock_runner.ex.available_to_back = [{"price": 12, "size": 120}] + mock_runner.ex.available_to_lay = [{"price": 13, "size": 120}] + mock__get_runner.return_value = mock_runner + resp = self.simulated.place(mock_order_package, mock_market_book, {}, 1) + self.assertEqual(resp.status, "SUCCESS") + self.assertEqual(resp.average_price_matched, 0) + self.assertEqual(resp.size_matched, 0) + self.assertEqual(self.simulated.matched, []) + @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_lay_fill_or_kill_matched(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() mock_runner.ex.available_to_back = [{"price": 11, "size": 120}] @@ -454,7 +589,12 @@ def test_place_limit_lay_fill_or_kill_matched(self, mock__get_runner): @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_lay_fill_or_kill_lapsed(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() mock_runner.ex.available_to_back = [{"price": 11, "size": 120}] @@ -474,7 +614,12 @@ def test_place_limit_lay_fill_or_kill_lapsed(self, mock__get_runner): @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_lay_fill_or_kill_no_price(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() mock_runner.ex.available_to_back = [] @@ -496,7 +641,12 @@ def test_place_limit_lay_fill_or_kill_no_price(self, mock__get_runner): @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_lay_fill_or_kill_min_fill_size_matched(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() mock_runner.ex.available_to_back = [{"price": 11, "size": 120}] @@ -520,7 +670,12 @@ def test_place_limit_lay_fill_or_kill_min_fill_size_matched(self, mock__get_runn @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_lay_fill_or_kill_min_fill_size_lapsed(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() mock_runner.ex.available_to_back = [{"price": 11, "size": 120}] @@ -542,7 +697,12 @@ def test_place_limit_lay_fill_or_kill_min_fill_size_lapsed(self, mock__get_runne @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_lay_fill_or_kill_vwap(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() mock_runner.ex.available_to_back = [{"price": 11, "size": 120}] @@ -573,7 +733,12 @@ def test_place_limit_lay_fill_or_kill_vwap(self, mock__get_runner): @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_lay_unmatched(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=True) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) self.simulated.order.side = "LAY" mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() @@ -594,7 +759,12 @@ def test_place_limit_lay_unmatched(self, mock__get_runner): @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_lay_bpe(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=False) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.15, + simulated_latency_plus_delay=0.1, + ) self.simulated.order.side = "LAY" mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock() From e736d3b53a4f8ce6341044acb0f7e8d0359295f5 Mon Sep 17 00:00:00 2001 From: liampauling Date: Wed, 15 Apr 2026 11:33:13 +0100 Subject: [PATCH 2/5] refactor using bet_delay_models --- flumine/__version__.py | 2 +- flumine/execution/simulatedexecution.py | 33 ++++++++++-- flumine/order/orderpackage.py | 2 + flumine/simulation/simulatedorder.py | 9 +--- tests/test_execution.py | 67 ++++++++++++++++++------- tests/test_orderpackage.py | 10 ++-- tests/test_simulatedorder.py | 7 ++- 7 files changed, 92 insertions(+), 38 deletions(-) diff --git a/flumine/__version__.py b/flumine/__version__.py index 7fdc9cfb..1699ee0f 100644 --- a/flumine/__version__.py +++ b/flumine/__version__.py @@ -1,6 +1,6 @@ __title__ = "flumine" __description__ = "Betting trading framework" __url__ = "https://github.com/betcode-org/flumine" -__version__ = "2.13.9" +__version__ = "2.14.0b0" __author__ = "Liam Pauling" __license__ = "MIT" diff --git a/flumine/execution/simulatedexecution.py b/flumine/execution/simulatedexecution.py index 5961f8ac..e559cf07 100644 --- a/flumine/execution/simulatedexecution.py +++ b/flumine/execution/simulatedexecution.py @@ -33,8 +33,20 @@ def execute_place( self, order_package, http_session: Optional[requests.Session] ) -> None: if order_package.client.paper_trade: - time.sleep(order_package.bet_delay + config.place_latency) + time.sleep(order_package.bet_delay + config.place_latency) # todo PASSIVE market = self.flumine.markets.markets[order_package.market_id] + + # calc current delay + elapsed_seconds = order_package.elapsed_seconds + if elapsed_seconds < order_package.simulated_latency_plus_delay: + # check if PASSIVE execution available + if ( + market.market_book.market_definition.bet_delay_models is None + or "PASSIVE" + not in market.market_book.market_definition.bet_delay_models + ): + return + for order, instruction in zip( order_package.orders_pending, order_package.place_instructions ): @@ -110,11 +122,22 @@ def execute_update( def execute_replace( self, order_package, http_session: Optional[requests.Session] ) -> None: - if ( - order_package.client.paper_trade - ): # todo should the cancel happen without a delay? - time.sleep(order_package.bet_delay + config.replace_latency) + if order_package.client.paper_trade: # todo the cancel happens without a delay! + time.sleep(order_package.bet_delay + config.replace_latency) # todo PASSIVE market = self.flumine.markets.markets[order_package.market_id] + + # todo cancel after latency -> check passive models / place execution + # calc current delay + elapsed_seconds = order_package.elapsed_seconds + if elapsed_seconds < order_package.simulated_latency_plus_delay: + # check if PASSIVE execution available + if ( + market.market_book.market_definition.bet_delay_models is None + or "PASSIVE" + not in market.market_book.market_definition.bet_delay_models + ): + return + failed_transaction_count = 0 for order, instruction in zip( order_package, order_package.replace_instructions diff --git a/flumine/order/orderpackage.py b/flumine/order/orderpackage.py index 7cbc7d3b..1cbf428d 100644 --- a/flumine/order/orderpackage.py +++ b/flumine/order/orderpackage.py @@ -82,6 +82,7 @@ def calc_simulated_latency(self) -> float: return config.update_latency elif self.package_type == OrderPackageType.REPLACE: return config.replace_latency + return -1 def calc_simulated_latency_delay(self) -> float: if self.client.execution.VENUE == VenueType.SIMULATED: @@ -93,6 +94,7 @@ def calc_simulated_latency_delay(self) -> float: return config.update_latency elif self.package_type == OrderPackageType.REPLACE: return config.replace_latency + self.bet_delay + return -1 @property def place_instructions(self) -> list: diff --git a/flumine/simulation/simulatedorder.py b/flumine/simulation/simulatedorder.py index cadf0cba..992ad2a8 100644 --- a/flumine/simulation/simulatedorder.py +++ b/flumine/simulation/simulatedorder.py @@ -2,7 +2,6 @@ import datetime from typing import List, Optional from betfairlightweight.resources.bettingresources import MarketBook, RunnerBook -from docutils.nodes import status from .utils import ( SimulatedPlaceResponse, @@ -125,15 +124,11 @@ def place( "Simulated betTargetSize placement not implemented" ) # calc current delay - current_bet_delay = order_package.elapsed_seconds + elapsed_seconds = order_package.elapsed_seconds bet_delay_complete = False - if current_bet_delay > order_package.simulated_latency_plus_delay: + if elapsed_seconds > order_package.simulated_latency_plus_delay: bet_delay_complete = True - # todo is this going to be a market level setting? - # if order_package.client.passive_bet_delay and bet_delay_complete is False: - # return False - # Validate that min_fill_size <= size for fill or kill orders is_fill_or_kill_order = time_in_force == "FILL_OR_KILL" if is_fill_or_kill_order and min_fill_size > size: diff --git a/tests/test_execution.py b/tests/test_execution.py index 599aa2ed..c19a099b 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -1075,11 +1075,15 @@ def test_execute_place_success(self, mock__order_logger): mock_order = mock.Mock() mock_order.trade.__enter__ = mock.Mock() mock_order.trade.__exit__ = mock.Mock() - mock_order_package = mock.MagicMock(market_id="1.23") + mock_order_package = mock.MagicMock( + market_id="1.23", + orders_pending=[mock_order], + place_instructions=[1], + info={}, + elapsed_seconds=0.25, + simulated_latency_plus_delay=0.2, + ) mock_order_package.client.paper_trade = False - mock_order_package.orders_pending = [mock_order] - mock_order_package.place_instructions = [1] - mock_order_package.info = {} mock_sim_resp = mock.Mock() mock_sim_resp.status = "SUCCESS" mock_order.simulated.place.return_value = mock_sim_resp @@ -1103,11 +1107,15 @@ def test_execute_place_failure(self, mock__order_logger): mock_order = mock.Mock() mock_order.trade.__enter__ = mock.Mock() mock_order.trade.__exit__ = mock.Mock() - mock_order_package = mock.MagicMock(market_id="1.23") + mock_order_package = mock.MagicMock( + market_id="1.23", + orders_pending=[mock_order], + place_instructions=[1], + info={}, + elapsed_seconds=0.25, + simulated_latency_plus_delay=0.2, + ) mock_order_package.client.paper_trade = False - mock_order_package.orders_pending = [mock_order] - mock_order_package.place_instructions = [1] - mock_order_package.info = {} mock_sim_resp = mock.Mock() mock_sim_resp.status = "FAILURE" mock_order.simulated.place.return_value = mock_sim_resp @@ -1131,11 +1139,15 @@ def test_execute_place_delay(self, mock__order_logger): mock_order = mock.Mock() mock_order.trade.__enter__ = mock.Mock() mock_order.trade.__exit__ = mock.Mock() - mock_order_package = mock.MagicMock(market_id="1.23") + mock_order_package = mock.MagicMock( + market_id="1.23", + place_instructions=[1], + info={}, + orders_pending=[mock_order], + elapsed_seconds=0.25, + simulated_latency_plus_delay=0.2, + ) mock_order_package.client.paper_trade = False - mock_order_package.orders_pending = [mock_order] - mock_order_package.place_instructions = [1] - mock_order_package.info = {} mock_sim_resp = mock.Mock() mock_sim_resp.status = "DELAY" mock_order.simulated.place.return_value = mock_sim_resp @@ -1155,9 +1167,13 @@ def test_execute_place_delay(self, mock__order_logger): @mock.patch("flumine.execution.simulatedexecution.time") def test_execute_place_paper_trade(self, mock_time): mock_order_package = mock.MagicMock( - market_id="1.23", place_instructions=[], bet_delay=1 + market_id="1.23", + place_instructions=[], + bet_delay=1, + orders_pending=[], + elapsed_seconds=0.25, + simulated_latency_plus_delay=0.2, ) - mock_order_package.orders_pending = [] mock_order_package.client.paper_trade = True self.execution.execute_place(mock_order_package, None) mock_time.sleep.assert_called_with(config.place_latency + 1) @@ -1301,11 +1317,15 @@ def test_execute_update_paper_trade(self, mock_time): @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger") def test_execute_replace(self, mock__order_logger): mock_client = mock.Mock() - mock_order = mock.Mock(client=mock_client) + mock_order = mock.Mock(market_id="1.234", client=mock_client) mock_order.trade.__enter__ = mock.Mock() mock_order.trade.__exit__ = mock.Mock() - mock_order.market_id = "1.234" - mock_order_package = mock.MagicMock(market_id="1.23", client=mock_client) + mock_order_package = mock.MagicMock( + market_id="1.23", + client=mock_client, + elapsed_seconds=0.25, + simulated_latency_plus_delay=0.2, + ) mock_order_package.__len__.return_value = 1 mock_order_package.client.paper_trade = False mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order])) @@ -1350,7 +1370,12 @@ def test_execute_replace_failure(self, mock__order_logger): mock_order = mock.Mock(client=mock_client) mock_order.trade.__enter__ = mock.Mock() mock_order.trade.__exit__ = mock.Mock() - mock_order_package = mock.MagicMock(market_id="1.23", client=mock_client) + mock_order_package = mock.MagicMock( + market_id="1.23", + client=mock_client, + elapsed_seconds=0.25, + simulated_latency_plus_delay=0.2, + ) mock_order_package.__len__.return_value = 1 mock_order_package.client.paper_trade = False mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order])) @@ -1379,7 +1404,11 @@ def test_execute_replace_failure(self, mock__order_logger): @mock.patch("flumine.execution.simulatedexecution.time") def test_execute_replace_paper_trade(self, mock_time): mock_order_package = mock.MagicMock( - market_id="1.23", replace_instructions=[], bet_delay=1 + market_id="1.23", + replace_instructions=[], + bet_delay=1, + elapsed_seconds=0.25, + simulated_latency_plus_delay=0.2, ) mock_order_package.__len__.return_value = 1 mock_order_package.__iter__ = mock.Mock(return_value=iter([])) diff --git a/tests/test_orderpackage.py b/tests/test_orderpackage.py index 603edcd4..59d0de90 100644 --- a/tests/test_orderpackage.py +++ b/tests/test_orderpackage.py @@ -45,8 +45,8 @@ def test_init(self): self.assertTrue(self.order_package._retry) self.assertEqual(self.order_package._max_retries, 3) self.assertEqual(self.order_package._retry_count, 0) - self.assertIsNone(self.order_package.simulated_latency) - self.assertIsNone(self.order_package.simulated_latency_plus_delay) + self.assertEqual(self.order_package.simulated_latency, -1) + self.assertEqual(self.order_package.simulated_latency_plus_delay, -1) self.assertEqual(self.order_package.customer_strategy_ref, "test") def test_retry(self): @@ -79,8 +79,8 @@ def test_calc_simulated_latency(self): config.cancel_latency = 0.2 config.update_latency = 0.3 config.replace_latency = 0.4 - self.assertIsNone(self.order_package.calc_simulated_latency()) - self.order_package.client.execution.EXCHANGE = VenueType.SIMULATED + self.assertEqual(self.order_package.calc_simulated_latency(), -1) + self.order_package.client.execution.VENUE = VenueType.SIMULATED self.order_package.package_type = OrderPackageType.PLACE self.assertEqual(self.order_package.calc_simulated_latency(), 0.1) self.order_package.package_type = OrderPackageType.CANCEL @@ -95,7 +95,7 @@ def test_calc_simulated_latency_delay(self): config.cancel_latency = 0.2 config.update_latency = 0.3 config.replace_latency = 0.4 - self.assertIsNone(self.order_package.calc_simulated_latency_delay()) + self.assertEqual(self.order_package.calc_simulated_latency_delay(), -1) self.order_package.client.execution.VENUE = VenueType.SIMULATED self.order_package.package_type = OrderPackageType.PLACE self.assertEqual(self.order_package.calc_simulated_latency_delay(), 1.1) diff --git a/tests/test_simulatedorder.py b/tests/test_simulatedorder.py index 40fd3185..e558fcf9 100644 --- a/tests/test_simulatedorder.py +++ b/tests/test_simulatedorder.py @@ -276,7 +276,12 @@ def test_place_limit_back_target_size(self, mock__get_runner): @mock.patch("flumine.simulation.simulatedorder.SimulatedOrder._get_runner") def test_place_limit_fill_or_kill_invalid_min_fill_size(self, mock__get_runner): mock_client = mock.Mock(best_price_execution=False) - mock_order_package = mock.Mock(client=mock_client, market_version=None) + mock_order_package = mock.Mock( + client=mock_client, + market_version=None, + elapsed_seconds=0.25, + simulated_latency_plus_delay=0.2, + ) mock_market_book = mock.Mock(status="OPEN") instruction = self.mock_betfair_place_instruction_fill_or_kill( size=1, min_fill_size=2 From 9fdf98a671c0f51526c14742aee4be1a7356a37b Mon Sep 17 00:00:00 2001 From: liampauling Date: Wed, 15 Apr 2026 11:48:41 +0100 Subject: [PATCH 3/5] minor cleanups --- flumine/execution/simulatedexecution.py | 33 +++++++++++++------------ tests/test_execution.py | 17 +++++++++---- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/flumine/execution/simulatedexecution.py b/flumine/execution/simulatedexecution.py index e559cf07..57bb9057 100644 --- a/flumine/execution/simulatedexecution.py +++ b/flumine/execution/simulatedexecution.py @@ -3,7 +3,6 @@ from typing import Optional from .baseexecution import BaseExecution -from .. import config from ..clients.clients import VenueType from ..order.orderpackage import BaseOrderPackage, OrderPackageType @@ -33,17 +32,18 @@ def execute_place( self, order_package, http_session: Optional[requests.Session] ) -> None: if order_package.client.paper_trade: - time.sleep(order_package.bet_delay + config.place_latency) # todo PASSIVE + # todo PASSIVE matching + time.sleep(order_package.simulated_latency_plus_delay) market = self.flumine.markets.markets[order_package.market_id] + market_book = market.market_book - # calc current delay + # calc current elapsed seconds elapsed_seconds = order_package.elapsed_seconds if elapsed_seconds < order_package.simulated_latency_plus_delay: # check if PASSIVE execution available if ( - market.market_book.market_definition.bet_delay_models is None - or "PASSIVE" - not in market.market_book.market_definition.bet_delay_models + market_book.market_definition.bet_delay_models is None + or "PASSIVE" not in market_book.market_definition.bet_delay_models ): return @@ -53,7 +53,7 @@ def execute_place( with order.trade: self._bet_id += 1 simulated_response = order.simulated.place( - order_package, market.market_book, instruction, self._bet_id + order_package, market_book, instruction, self._bet_id ) if simulated_response.status == "DELAY": # PENDING delay self._bet_id -= 1 @@ -72,7 +72,7 @@ def execute_cancel( self, order_package, http_session: Optional[requests.Session] ) -> None: if order_package.client.paper_trade: - time.sleep(config.cancel_latency) + time.sleep(order_package.simulated_latency_plus_delay) market = self.flumine.markets.markets[order_package.market_id] failed_transaction_count = 0 for order in order_package: @@ -98,7 +98,7 @@ def execute_update( self, order_package, http_session: Optional[requests.Session] ) -> None: if order_package.client.paper_trade: - time.sleep(config.update_latency) + time.sleep(order_package.simulated_latency_plus_delay) market = self.flumine.markets.markets[order_package.market_id] failed_transaction_count = 0 for order, instruction in zip(order_package, order_package.update_instructions): @@ -123,18 +123,19 @@ def execute_replace( self, order_package, http_session: Optional[requests.Session] ) -> None: if order_package.client.paper_trade: # todo the cancel happens without a delay! - time.sleep(order_package.bet_delay + config.replace_latency) # todo PASSIVE + # todo PASSIVE matching + time.sleep(order_package.simulated_latency_plus_delay) market = self.flumine.markets.markets[order_package.market_id] + market_book = market.market_book # todo cancel after latency -> check passive models / place execution - # calc current delay + # calc current elapsed seconds elapsed_seconds = order_package.elapsed_seconds if elapsed_seconds < order_package.simulated_latency_plus_delay: # check if PASSIVE execution available if ( - market.market_book.market_definition.bet_delay_models is None - or "PASSIVE" - not in market.market_book.market_definition.bet_delay_models + market_book.market_definition.bet_delay_models is None + or "PASSIVE" not in market_book.market_definition.bet_delay_models ): return @@ -145,7 +146,7 @@ def execute_replace( with order.trade: # cancel current order err = False - cancel_instruction_report = order.simulated.cancel(market.market_book) + cancel_instruction_report = order.simulated.cancel(market_book) if cancel_instruction_report.status == "SUCCESS": order.execution_complete() elif cancel_instruction_report.status == "FAILURE": @@ -169,7 +170,7 @@ def execute_replace( order_package.date_time_created, ) place_instruction_report = replacement_order.simulated.place( - order_package, market.market_book, instruction, self._bet_id + order_package, market_book, instruction, self._bet_id ) if place_instruction_report.status == "SUCCESS": self._order_logger( diff --git a/tests/test_execution.py b/tests/test_execution.py index c19a099b..bf18e99a 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -6,7 +6,6 @@ from betdaq import BetdaqError from betfairlightweight import BetfairError -from flumine import config from flumine.clients.clients import VenueType from flumine.exceptions import OrderExecutionError from flumine.execution.baseexecution import ( @@ -1176,7 +1175,9 @@ def test_execute_place_paper_trade(self, mock_time): ) mock_order_package.client.paper_trade = True self.execution.execute_place(mock_order_package, None) - mock_time.sleep.assert_called_with(config.place_latency + 1) + mock_time.sleep.assert_called_with( + mock_order_package.simulated_latency_plus_delay + ) @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger") def test_execute_cancel(self, mock__order_logger): @@ -1251,7 +1252,9 @@ def test_execute_cancel_paper_trade(self, mock_time): mock_order_package.__iter__ = mock.Mock(return_value=iter([])) mock_order_package.client.paper_trade = True self.execution.execute_cancel(mock_order_package, None) - mock_time.sleep.assert_called_with(config.cancel_latency) + mock_time.sleep.assert_called_with( + mock_order_package.simulated_latency_plus_delay + ) @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger") def test_execute_update(self, mock__order_logger): @@ -1312,7 +1315,9 @@ def test_execute_update_paper_trade(self, mock_time): mock_order_package.__iter__ = mock.Mock(return_value=iter([])) mock_order_package.client.paper_trade = True self.execution.execute_update(mock_order_package, None) - mock_time.sleep.assert_called_with(config.update_latency) + mock_time.sleep.assert_called_with( + mock_order_package.simulated_latency_plus_delay + ) @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger") def test_execute_replace(self, mock__order_logger): @@ -1414,7 +1419,9 @@ def test_execute_replace_paper_trade(self, mock_time): mock_order_package.__iter__ = mock.Mock(return_value=iter([])) mock_order_package.client.paper_trade = True self.execution.execute_replace(mock_order_package, None) - mock_time.sleep.assert_called_with(config.replace_latency + 1) + mock_time.sleep.assert_called_with( + mock_order_package.simulated_latency_plus_delay + ) class BetdaqExecutionTest(unittest.TestCase): From 26bbc5827d87e74db5280152cab43127691a95f6 Mon Sep 17 00:00:00 2001 From: liampauling Date: Wed, 15 Apr 2026 14:35:20 +0100 Subject: [PATCH 4/5] replace logic refactor correctly cancel without betDelay --- flumine/execution/simulatedexecution.py | 117 +++++++++++++----------- flumine/order/trade.py | 5 +- tests/test_execution.py | 37 ++++++-- 3 files changed, 98 insertions(+), 61 deletions(-) diff --git a/flumine/execution/simulatedexecution.py b/flumine/execution/simulatedexecution.py index 57bb9057..7d57871f 100644 --- a/flumine/execution/simulatedexecution.py +++ b/flumine/execution/simulatedexecution.py @@ -2,8 +2,10 @@ import requests from typing import Optional +from .. import config from .baseexecution import BaseExecution from ..clients.clients import VenueType +from ..order.order import OrderStatus from ..order.orderpackage import BaseOrderPackage, OrderPackageType @@ -122,71 +124,82 @@ def execute_update( def execute_replace( self, order_package, http_session: Optional[requests.Session] ) -> None: - if order_package.client.paper_trade: # todo the cancel happens without a delay! + if order_package.client.paper_trade: + # todo the cancel happens without a bet delay! # todo PASSIVE matching time.sleep(order_package.simulated_latency_plus_delay) market = self.flumine.markets.markets[order_package.market_id] market_book = market.market_book - # todo cancel after latency -> check passive models / place execution # calc current elapsed seconds elapsed_seconds = order_package.elapsed_seconds - if elapsed_seconds < order_package.simulated_latency_plus_delay: - # check if PASSIVE execution available - if ( - market_book.market_definition.bet_delay_models is None - or "PASSIVE" not in market_book.market_definition.bet_delay_models - ): - return + if elapsed_seconds < config.cancel_latency: + return failed_transaction_count = 0 - for order, instruction in zip( - order_package, order_package.replace_instructions - ): + for order in order_package.orders_pending: with order.trade: - # cancel current order - err = False - cancel_instruction_report = order.simulated.cancel(market_book) - if cancel_instruction_report.status == "SUCCESS": - order.execution_complete() - elif cancel_instruction_report.status == "FAILURE": - order.executable() - failed_transaction_count += 1 - err = True - self._order_logger( - order, - cancel_instruction_report, - OrderPackageType.CANCEL, - ) - if err: - continue - - # place new order - self._bet_id += 1 - replacement_order = order.trade.create_order_replacement( - order, - instruction.get("newPrice"), - cancel_instruction_report.size_cancelled, - order_package.date_time_created, - ) - place_instruction_report = replacement_order.simulated.place( - order_package, market_book, instruction, self._bet_id - ) - if place_instruction_report.status == "SUCCESS": + if order.status == OrderStatus.REPLACING: + # we can cancel after the latency (no bet delay) + instruction = order.create_replace_instruction() + # cancel current order + err = False + cancel_instruction_report = order.simulated.cancel(market_book) + if cancel_instruction_report.status == "SUCCESS": + order.execution_complete() + elif cancel_instruction_report.status == "FAILURE": + order.executable() + failed_transaction_count += 1 + err = True self._order_logger( - replacement_order, - place_instruction_report, - order_package.package_type, + order, + cancel_instruction_report, + OrderPackageType.CANCEL, ) - # add to blotter - market.place_order( - replacement_order, execute=False, client=order.client + if err: + continue + + # create the replacement order + replacement_order = order.trade.create_order_replacement( + order, + instruction.get("newPrice"), + cancel_instruction_report.size_cancelled, + order_package.date_time_created, ) - replacement_order.executable() - elif place_instruction_report.status == "FAILURE": - order.executable() + replacement_order.placing() + order_package._orders.append(replacement_order) + + elif order.status == OrderStatus.PENDING: + # we can re'place' after the bet delay latency or PASSIVE + if elapsed_seconds < config.place_latency + order_package.bet_delay: + # check if PASSIVE execution available + if ( + market_book.market_definition.bet_delay_models is None + or "PASSIVE" + not in market_book.market_definition.bet_delay_models + ): + return + instruction = order.create_place_instruction() + self._bet_id += 1 + place_instruction_report = order.simulated.place( + order_package, market_book, instruction, self._bet_id + ) + if place_instruction_report.status == "DELAY": # PENDING delay + self._bet_id -= 1 + continue + if place_instruction_report.status == "SUCCESS": + self._order_logger( + order, + place_instruction_report, + order_package.package_type, + ) + # add to blotter + market.place_order(order, execute=False, client=order.client) + order.executable() + elif place_instruction_report.status == "FAILURE": + order.execution_complete() + # update transaction counts + order_package.client.add_transaction(1) - # update transaction counts - order_package.client.add_transaction(len(order_package)) if failed_transaction_count: order_package.client.add_transaction(failed_transaction_count, failed=True) diff --git a/flumine/order/trade.py b/flumine/order/trade.py index 1b876e54..7c80a5cb 100644 --- a/flumine/order/trade.py +++ b/flumine/order/trade.py @@ -58,8 +58,8 @@ def __init__( def _update_status(self, status: TradeStatus) -> None: self.status_log.append(status) self.status = status - if logger.isEnabledFor(logging.INFO): - logger.info("Trade status update: %s" % self.status.value, extra=self.info) + if logger.isEnabledFor(logging.DEBUG): + logger.debug("Trade status update: %s" % self.status.value, extra=self.info) if self.complete: self.complete_trade() @@ -71,6 +71,7 @@ def complete_trade(self) -> None: self.market_id, self.selection_id, self.handicap ) runner_context.reset(self.id) + logger.info("Trade completed: %s" % self.status.value, extra=self.info) @property def complete(self) -> bool: diff --git a/tests/test_execution.py b/tests/test_execution.py index bf18e99a..95d81307 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -17,6 +17,7 @@ from flumine.execution.betdaqexecution import BetdaqExecution from flumine.execution.betfairexecution import BetfairExecution from flumine.execution.simulatedexecution import SimulatedExecution +from flumine.order.order import OrderStatus class BaseExecutionTest(unittest.TestCase): @@ -1322,14 +1323,20 @@ def test_execute_update_paper_trade(self, mock_time): @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger") def test_execute_replace(self, mock__order_logger): mock_client = mock.Mock() - mock_order = mock.Mock(market_id="1.234", client=mock_client) + mock_order = mock.Mock( + market_id="1.234", client=mock_client, status=OrderStatus.REPLACING + ) mock_order.trade.__enter__ = mock.Mock() mock_order.trade.__exit__ = mock.Mock() mock_order_package = mock.MagicMock( market_id="1.23", client=mock_client, + bet_delay=0, elapsed_seconds=0.25, + simulated_latency=0.2, simulated_latency_plus_delay=0.2, + orders_pending=[mock_order], + _orders=[mock_order], ) mock_order_package.__len__.return_value = 1 mock_order_package.client.paper_trade = False @@ -1340,14 +1347,26 @@ def test_execute_replace(self, mock__order_logger): mock_replacement_order_package.__iter__ = mock.Mock( return_value=iter([mock_order]) ) - mock_sim_resp = mock.Mock() - mock_sim_resp.status = "SUCCESS" + mock_sim_resp = mock.Mock(status="SUCCESS") mock_order.simulated.cancel.return_value = mock_sim_resp mock_replacement_order.simulated.place.return_value = mock_sim_resp mock_order.trade.create_order_replacement.return_value = mock_replacement_order + + # test order is cancelled self.execution.execute_replace(mock_order_package, None) + mock_replacement_order.placing.assert_called() mock_order.simulated.cancel.assert_called_with(self.mock_market.market_book) - mock_replacement_order.simulated.place.assert_called_with( + self.assertEqual(len(mock_order_package._orders), 2) + self.assertEqual(len(mock_order_package.orders_pending), 1) + + # now test if it can be replaced + r_order = mock_order_package.orders_pending[0] + r_order.status = OrderStatus.PENDING + r_order.create_place_instruction.return_value = {"newPrice": 2.03} + r_order.simulated.place.return_value = mock_sim_resp + self.execution.execute_replace(mock_order_package, None) + + r_order.simulated.place.assert_called_with( mock_order_package, self.mock_market.market_book, {"newPrice": 2.03}, @@ -1357,14 +1376,14 @@ def test_execute_replace(self, mock__order_logger): [ call(mock_order, mock_sim_resp, OrderPackageType.CANCEL), call( - mock_replacement_order, + r_order, mock_sim_resp, mock_order_package.package_type, ), ] ) mock_order.execution_complete.assert_called_with() - # mock_replacement_order.executable.assert_called_with() + r_order.executable.assert_called_with() mock_order.trade.__enter__.assert_called_with() mock_order.trade.__exit__.assert_called_with(None, None, None) mock_order_package.client.add_transaction.assert_called_with(1) @@ -1372,14 +1391,18 @@ def test_execute_replace(self, mock__order_logger): @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger") def test_execute_replace_failure(self, mock__order_logger): mock_client = mock.Mock() - mock_order = mock.Mock(client=mock_client) + mock_order = mock.Mock(client=mock_client, status=OrderStatus.REPLACING) mock_order.trade.__enter__ = mock.Mock() mock_order.trade.__exit__ = mock.Mock() mock_order_package = mock.MagicMock( market_id="1.23", client=mock_client, + bet_delay=0, elapsed_seconds=0.25, + simulated_latency=0.2, simulated_latency_plus_delay=0.2, + orders_pending=[mock_order], + _orders=[mock_order], ) mock_order_package.__len__.return_value = 1 mock_order_package.client.paper_trade = False From d40f3fa7e548b83ad3cb87626f9c732396f52889 Mon Sep 17 00:00:00 2001 From: liampauling Date: Mon, 6 Jul 2026 11:38:48 +0100 Subject: [PATCH 5/5] only simulate matching when market is open --- flumine/markets/middleware.py | 2 +- tests/test_middleware.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flumine/markets/middleware.py b/flumine/markets/middleware.py index 01c7b882..f5b01c42 100644 --- a/flumine/markets/middleware.py +++ b/flumine/markets/middleware.py @@ -74,7 +74,7 @@ def __call__(self, market) -> None: market.context["simulated"] = market_analytics # process simulated orders - if market.blotter.active: + if market.blotter.active and market.market_book.status == "OPEN": self._process_simulated_orders(market, market_analytics) def remove_market(self, market) -> None: diff --git a/tests/test_middleware.py b/tests/test_middleware.py index b7decb56..1579b1a0 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -57,7 +57,7 @@ def test_init(self): @mock.patch("flumine.markets.middleware.SimulatedMiddleware._process_runner") def test_call(self, mock__process_runner, mock__process_simulated_orders): mock_market = mock.Mock(context={}) - mock_market_book = mock.Mock() + mock_market_book = mock.Mock(status="OPEN") mock_runner = mock.Mock(status="ACTIVE") mock_market_book.runners = [mock_runner] mock_market.market_book = mock_market_book