Repository: liampauling/flumine
Pull request #394 (2021-03-12 08:57:05)

release


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 989af21..0354133 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,18 @@
 Release History
 ---------------
 
+1.17.10 (2021-03-12)
++++++++++++++++++++
+
+**Improvements**
+
+- async placeOrder handling added, defaults to False via config.py
+- Execution logging improvements
+
+**Bug Fixes**
+
+- Handle race condition (seen daily) where cancel is not correctly update to execution complete
+
 1.17.9 (2021-03-09)
 +++++++++++++++++++
 
diff --git a/docs/advanced.md b/docs/advanced.md
index f012b55..17f6802 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -194,3 +194,7 @@ Raises errors on strategy functions, see [Error Handling](/advanced/#error-handl
 ### max_execution_workers
 
 Max number of workers in execution thread pool
+
+### async_place_orders
+
+Place orders sent with place orders flag, prevents waiting for bet delay
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 9cde18a..3e092d1 100644
--- a/flumine/__version__.py
+++ b/flumine/__version__.py
@@ -1,6 +1,6 @@
 __title__ = "flumine"
 __description__ = "Betfair trading framework"
 __url__ = "https://github.com/liampauling/flumine"
-__version__ = "1.17.9"
+__version__ = "1.17.10"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/baseflumine.py b/flumine/baseflumine.py
index 37ea2e1..fd2f9de 100644
--- a/flumine/baseflumine.py
+++ b/flumine/baseflumine.py
@@ -209,7 +209,8 @@ class BaseFlumine:
                 market.update_market_catalogue = False
 
     def _process_current_orders(self, event: events.CurrentOrdersEvent) -> None:
-        process_current_orders(self.markets, self.strategies, event)  # update state
+        # update state
+        process_current_orders(self.markets, self.strategies, event, self.log_control)
         for market in self.markets:
             if market.closed is False:
                 for strategy in self.strategies:
diff --git a/flumine/config.py b/flumine/config.py
index dbc7a91..085a23d 100644
--- a/flumine/config.py
+++ b/flumine/config.py
@@ -16,3 +16,5 @@ current_time = None  # used for backtesting
 raise_errors = False  # used for call_check_market / call_process_market_book
 
 max_execution_workers = 32  # max number of workers in execution thread pool
+
+async_place_orders = False  # async place orders
diff --git a/flumine/execution/baseexecution.py b/flumine/execution/baseexecution.py
index ca2b68e..18b3aaf 100644
--- a/flumine/execution/baseexecution.py
+++ b/flumine/execution/baseexecution.py
@@ -126,8 +126,9 @@ class BaseExecution:
         )
         if package_type == OrderPackageType.PLACE:
             order.responses.placed(instruction_report)
-            order.bet_id = instruction_report.bet_id
-            self.flumine.log_control(OrderEvent(order))
+            if instruction_report.bet_id:
+                order.bet_id = instruction_report.bet_id
+                self.flumine.log_control(OrderEvent(order))
         elif package_type == OrderPackageType.CANCEL:
             order.responses.cancelled(instruction_report)
         elif package_type == OrderPackageType.UPDATE:
diff --git a/flumine/execution/betfairexecution.py b/flumine/execution/betfairexecution.py
index fde4654..3367d53 100644
--- a/flumine/execution/betfairexecution.py
+++ b/flumine/execution/betfairexecution.py
@@ -28,7 +28,10 @@ class BetfairExecution(BaseExecution):
                         order, instruction_report, OrderPackageType.PLACE
                     )
                     if instruction_report.status == "SUCCESS":
-                        order.executable()
+                        if instruction_report.order_status == "PENDING":
+                            pass  # async request pending processing
+                        else:
+                            order.executable()  # let process.py pick it up
                     elif instruction_report.status == "FAILURE":
                         order.lapsed()  # todo correct?
                     elif instruction_report.status == "TIMEOUT":
@@ -66,7 +69,9 @@ class BetfairExecution(BaseExecution):
                     if instruction_report.status == "SUCCESS":
                         if (
                             instruction_report.size_cancelled == order.size_remaining
-                        ):  # todo what if?
+                            or order.size_remaining
+                            == 0  # handle orders stream update / race condition
+                        ):
                             order.execution_complete()
                         else:
                             order.executable()
@@ -219,7 +224,7 @@ class BetfairExecution(BaseExecution):
         order_package: BaseOrderPackage,
         http_session: requests.Session,
     ):
-        if order_package.elapsed_seconds > 0.1:
+        if order_package.elapsed_seconds > 0.1 and order_package.retry_count == 0:
             logger.warning(
                 "High latency between current time and OrderPackage creation time, it is likely that the thread pool is currently exhausted",
                 extra={
diff --git a/flumine/execution/transaction.py b/flumine/execution/transaction.py
index fad0068..469b414 100644
--- a/flumine/execution/transaction.py
+++ b/flumine/execution/transaction.py
@@ -30,9 +30,10 @@ class Transaction:
             t.place_order(order)  # both executed on transaction __exit__
     """
 
-    def __init__(self, market, id_: int):
+    def __init__(self, market, id_: int, async_place_orders: bool):
         self.market = market
         self._id = id_  # unique per market only
+        self._async_place_orders = async_place_orders
         self._pending_orders = False
         self._pending_place = []  # list of (<Order>, market_version)
         self._pending_cancel = []  # list of (<Order>, None)
@@ -96,6 +97,7 @@ class Transaction:
             packages += self._create_order_package(
                 self._pending_place,
                 OrderPackageType.PLACE,
+                async_=self._async_place_orders,
             )
         if self._pending_cancel:
             packages += self._create_order_package(
@@ -139,7 +141,7 @@ class Transaction:
             return True
 
     def _create_order_package(
-        self, orders: list, package_type: OrderPackageType
+        self, orders: list, package_type: OrderPackageType, async_: bool = False
     ) -> list:
         # group orders by marketVersion
         orders_grouped = defaultdict(list)
@@ -158,6 +160,7 @@ class Transaction:
                         package_type=package_type,
                         bet_delay=self.market.market_book.bet_delay,
                         market_version=market_version,
+                        async_=async_,
                     )
                 )
         orders.clear()
diff --git a/flumine/markets/market.py b/flumine/markets/market.py
index d0a62ee..f966936 100644
--- a/flumine/markets/market.py
+++ b/flumine/markets/market.py
@@ -4,6 +4,7 @@ from typing import Optional
 from collections import defaultdict
 from betfairlightweight.resources.bettingresources import MarketBook, MarketCatalogue
 
+from .. import config
 from .blotter import Blotter
 from ..execution.transaction import Transaction
 
@@ -60,9 +61,13 @@ class Market:
             extra=self.info,
         )
 
-    def transaction(self) -> Transaction:
+    def transaction(
+        self, async_place_orders: bool = config.async_place_orders
+    ) -> Transaction:
         self._transaction_id += 1
-        return Transaction(self, id_=self._transaction_id)
+        return Transaction(
+            self, id_=self._transaction_id, async_place_orders=async_place_orders
+        )
 
     # order
     def place_order(
diff --git a/flumine/order/order.py b/flumine/order/order.py
index 30fd1a0..bcc4821 100644
--- a/flumine/order/order.py
+++ b/flumine/order/order.py
@@ -302,7 +302,9 @@ class BetfairOrder(BaseOrder):
         self.placing()
 
     def cancel(self, size_reduction: float = None) -> None:
-        if self.order_type.ORDER_TYPE == OrderTypes.LIMIT:
+        if self.bet_id is None:
+            raise OrderUpdateError("Order does not currently have a betId")
+        elif self.order_type.ORDER_TYPE == OrderTypes.LIMIT:
             if size_reduction and self.size_remaining - size_reduction < 0:
                 raise OrderUpdateError("Size reduction too large")
             if self.status != OrderStatus.EXECUTABLE:
@@ -311,11 +313,13 @@ class BetfairOrder(BaseOrder):
             self.cancelling()
         else:
             raise OrderUpdateError(
-                "Only LIMIT orders can be cancelled or partially cancelled once placed."
+                "Only LIMIT orders can be cancelled or partially cancelled once placed"
             )
 
     def update(self, new_persistence_type: str) -> None:
-        if self.order_type.ORDER_TYPE == OrderTypes.LIMIT:
+        if self.bet_id is None:
+            raise OrderUpdateError("Order does not currently have a betId")
+        elif self.order_type.ORDER_TYPE == OrderTypes.LIMIT:
             if self.order_type.persistence_type == new_persistence_type:
                 raise OrderUpdateError("Persistence types match")
             elif self.status != OrderStatus.EXECUTABLE:
@@ -323,10 +327,15 @@ class BetfairOrder(BaseOrder):
             self.order_type.persistence_type = new_persistence_type
             self.updating()
         else:
-            raise OrderUpdateError("Only LIMIT orders can be updated.")
+            raise OrderUpdateError("Only LIMIT orders can be updated")
 
     def replace(self, new_price: float) -> None:
-        if self.order_type.ORDER_TYPE in [OrderTypes.LIMIT, OrderTypes.LIMIT_ON_CLOSE]:
+        if self.bet_id is None:
+            raise OrderUpdateError("Order does not currently have a betId")
+        elif self.order_type.ORDER_TYPE in [
+            OrderTypes.LIMIT,
+            OrderTypes.LIMIT_ON_CLOSE,
+        ]:
             if self.order_type.price == new_price:
                 raise OrderUpdateError("Prices match")
             elif self.status != OrderStatus.EXECUTABLE:
@@ -335,7 +344,7 @@ class BetfairOrder(BaseOrder):
             self.replacing()
         else:
             raise OrderUpdateError(
-                "Only LIMIT or LIMIT_ON_CLOSE orders can be replaced."
+                "Only LIMIT or LIMIT_ON_CLOSE orders can be replaced"
             )
 
     # instructions
diff --git a/flumine/order/orderpackage.py b/flumine/order/orderpackage.py
index f86c2b7..3edb62c 100644
--- a/flumine/order/orderpackage.py
+++ b/flumine/order/orderpackage.py
@@ -96,6 +96,10 @@ class BaseOrderPackage(BaseEvent):
     def orders(self) -> list:
         return [o for o in self._orders if o.status != OrderStatus.VIOLATION]
 
+    @property
+    def retry_count(self) -> int:
+        return self._retry_count
+
     @property
     def info(self) -> dict:
         return {
diff --git a/flumine/order/process.py b/flumine/order/process.py
index fea23f6..087dd3f 100644
--- a/flumine/order/process.py
+++ b/flumine/order/process.py
@@ -4,19 +4,31 @@ from ..markets.markets import Markets
 from ..order.order import BaseOrder, OrderStatus, OrderTypes
 from ..order.trade import Trade
 from ..strategy.strategy import Strategies, STRATEGY_NAME_HASH_LENGTH
+from ..events.events import OrderEvent
 
 logger = logging.getLogger(__name__)
 
 """
-Handles trade fillkill / green etc.
-Handles orphan orders by creating empty trade and order data from CurrentOrder object/
-"""
-
+Various functions to update current order status
+and update status.
 
-# todo handle fillkill/green/
+Loop through each current order:
+    order = Lookup order in market using marketId and orderId
+    if order is None (not present locally):
+        create local order using data and make executable #todo!!!
+    if betId is None (async placement):
+        Update betId and log through logging_control
+    if order betId != current_order betId:
+        Get order using current_order betId due to replace request (new betId)
+    if order:
+        update current status
+        process
+"""
 
 
-def process_current_orders(markets: Markets, strategies: Strategies, event):
+def process_current_orders(
+    markets: Markets, strategies: Strategies, event, log_control
+) -> None:
     for current_orders in event.event:
         for current_order in current_orders.orders:
             order_id = current_order.customer_order_ref[STRATEGY_NAME_HASH_LENGTH + 1 :]
@@ -24,7 +36,7 @@ def process_current_orders(markets: Markets, strategies: Strategies, event):
                 market_id=current_order.market_id,
                 order_id=order_id,
             )
-            if not order:
+            if order is None:
                 logger.warning(
                     "Order %s not present in blotter" % current_order.bet_id,
                     extra={
@@ -43,7 +55,14 @@ def process_current_orders(markets: Markets, strategies: Strategies, event):
                     order.executable()  # todo correct?
                 else:
                     continue
-            if order.bet_id != current_order.bet_id:  # replaceOrder handling (hacky)
+
+            if (
+                order.bet_id is None and current_order.bet_id
+            ):  # async bet pending processing
+                order.bet_id = current_order.bet_id
+                log_control(OrderEvent(order))
+                order.executable()
+            elif order.bet_id != current_order.bet_id:  # replaceOrder handling (hacky)
                 order = markets.get_order_from_bet_id(
                     market_id=current_order.market_id,
                     bet_id=current_order.bet_id,
@@ -57,6 +76,7 @@ def process_current_orders(markets: Markets, strategies: Strategies, event):
 def process_current_order(order: BaseOrder):
     if order.status == OrderStatus.EXECUTABLE:
         if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
+            # todo use `order.current_order.status` ?
             if order.size_voided:
                 order.voided()
             elif order.size_lapsed:


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
index 472bf32..414a072 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -11,3 +11,4 @@ class ConfigTest(unittest.TestCase):
         self.assertIsNone(config.current_time)
         self.assertFalse(config.raise_errors)
         self.assertEqual(config.max_execution_workers, 32)
+        self.assertFalse(config.async_place_orders)
diff --git a/tests/test_execution.py b/tests/test_execution.py
index abc8ecb..e942829 100644
--- a/tests/test_execution.py
+++ b/tests/test_execution.py
@@ -159,6 +159,16 @@ class BaseExecutionTest(unittest.TestCase):
         mock_order.responses.placed.assert_called_with(mock_instruction_report)
         self.mock_flumine.log_control.assert_called_with(mock_order_event(mock_order))
 
+    def test__order_logger_place_no_bet_id(self):
+        mock_order = mock.Mock(bet_id=123)
+        mock_instruction_report = mock.Mock(bet_id=None)
+        self.execution._order_logger(
+            mock_order, mock_instruction_report, OrderPackageType.PLACE
+        )
+        self.assertEqual(mock_order.bet_id, 123)
+        mock_order.responses.placed.assert_called_with(mock_instruction_report)
+        self.mock_flumine.log_control.assert_not_called()
+
     def test__order_logger_cancel(self):
         mock_order = mock.Mock()
         mock_instruction_report = mock.Mock()
@@ -230,6 +240,36 @@ class BetfairExecutionTest(unittest.TestCase):
         mock_order.trade.__exit__.assert_called_with(None, None, None)
         mock_order_package.client.add_transaction.assert_called_with(1)
 
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.place")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_place_success_pending(
+        self, mock__execution_helper, mock_place, mock__order_logger
+    ):
+        mock_session = mock.Mock()
+        mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
+        mock_order_package = mock.MagicMock()
+        mock_order_package.__len__.return_value = 1
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.info = {}
+        mock_report = mock.Mock()
+        mock_instruction_report = mock.Mock(status="SUCCESS", order_status="PENDING")
+        mock_report.place_instruction_reports = [mock_instruction_report]
+        mock__execution_helper.return_value = mock_report
+        self.execution.execute_place(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_place, mock_order_package, mock_session
+        )
+        mock__order_logger.assert_called_with(
+            mock_order, mock_instruction_report, OrderPackageType.PLACE
+        )
+        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_called_with(1)
+
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.place")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
@@ -345,6 +385,39 @@ class BetfairExecutionTest(unittest.TestCase):
         mock_order.trade.__enter__.assert_called_with()
         mock_order.trade.__exit__.assert_called_with(None, None, None)
 
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.cancel")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_cancel_success_race(
+        self, mock__execution_helper, mock_cancel, mock__order_logger
+    ):
+        mock_session = mock.Mock()
+        mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
+        mock_order.bet_id = 123
+        mock_order.size_remaining = 0
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.info = {}
+        mock_report = mock.Mock()
+        mock_instruction_report = mock.Mock()
+        mock_instruction_report.status = "SUCCESS"
+        mock_instruction_report.instruction.bet_id = 123
+        mock_instruction_report.size_cancelled = 2
+        mock_report.cancel_instruction_reports = [mock_instruction_report]
+        mock__execution_helper.return_value = mock_report
+        self.execution.execute_cancel(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_cancel, mock_order_package, mock_session
+        )
+        mock__order_logger.assert_called_with(
+            mock_order, mock_instruction_report, OrderPackageType.CANCEL
+        )
+        mock_order.execution_complete.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
+
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.cancel")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
@@ -682,6 +755,21 @@ class BetfairExecutionTest(unittest.TestCase):
         mock_trading_function.assert_called_with(mock_order_package, mock_session)
         mock__return_http_session.assert_called_with(mock_session)
 
+    @mock.patch(
+        "flumine.execution.betfairexecution.BetfairExecution._return_http_session"
+    )
+    def test__execution_helper_warning(self, mock__return_http_session):
+        mock_trading_function = mock.Mock()
+        mock_trading_function.__name__ = "test"
+        mock_session = mock.Mock()
+        mock_order_package = mock.Mock(elapsed_seconds=0.2, retry_count=0)
+        mock_order_package.info = {}
+        self.execution._execution_helper(
+            mock_trading_function, mock_order_package, mock_session
+        )
+        mock_trading_function.assert_called_with(mock_order_package, mock_session)
+        mock__return_http_session.assert_called_with(mock_session)
+
     @mock.patch(
         "flumine.execution.betfairexecution.BetfairExecution._return_http_session"
     )
diff --git a/tests/test_markets.py b/tests/test_markets.py
index 7c57433..cb3fd3a 100644
--- a/tests/test_markets.py
+++ b/tests/test_markets.py
@@ -141,7 +141,7 @@ class MarketTest(unittest.TestCase):
     def test_transaction(self, mock_transaction):
         transaction = self.market.transaction()
         mock_transaction.assert_called_with(
-            self.market, id_=self.market._transaction_id
+            self.market, id_=self.market._transaction_id, async_place_orders=False
         )
         self.assertEqual(transaction, mock_transaction())
 
diff --git a/tests/test_order.py b/tests/test_order.py
index 2e976b0..b8b54f8 100644
--- a/tests/test_order.py
+++ b/tests/test_order.py
@@ -283,6 +283,7 @@ class BetfairOrderTest(unittest.TestCase):
     @mock.patch("flumine.order.order.BetfairOrder.cancelling")
     def test_cancel(self, mock_cancelling, mock_size_remaining):
         mock_size_remaining.return_value = 20
+        self.order.bet_id = 123
         self.order.status = OrderStatus.EXECUTABLE
         with self.assertRaises(OrderUpdateError):
             self.order.cancel(12)
@@ -294,6 +295,11 @@ class BetfairOrderTest(unittest.TestCase):
         self.order.cancel()
         self.assertEqual(self.order.update_data, {"size_reduction": None})
 
+    def test_cancel_bet_id(self):
+        self.order.status = OrderStatus.EXECUTABLE
+        with self.assertRaises(OrderUpdateError):
+            self.order.cancel(12)
+
     @mock.patch(
         "flumine.order.order.BetfairOrder.size_remaining",
         new_callable=mock.PropertyMock,
@@ -322,6 +328,7 @@ class BetfairOrderTest(unittest.TestCase):
 
     @mock.patch("flumine.order.order.BetfairOrder.updating")
     def test_update(self, mock_updating):
+        self.order.bet_id = 123
         self.order.status = OrderStatus.EXECUTABLE
         with self.assertRaises(OrderUpdateError):
             self.order.update("PERSIST")
@@ -335,6 +342,11 @@ class BetfairOrderTest(unittest.TestCase):
         with self.assertRaises(OrderUpdateError):
             self.order.update("PERSIST")
 
+    def test_update_bet_id(self):
+        self.order.status = OrderStatus.EXECUTABLE
+        with self.assertRaises(OrderUpdateError):
+            self.order.update("PERSIST")
+
     def test_update_error(self):
         self.mock_order_type.ORDER_TYPE = OrderTypes.LIMIT
         self.mock_order_type.persistence_type = "LAPSE"
@@ -344,6 +356,7 @@ class BetfairOrderTest(unittest.TestCase):
 
     @mock.patch("flumine.order.order.BetfairOrder.replacing")
     def test_replace(self, mock_replacing):
+        self.order.bet_id = 123
         self.order.status = OrderStatus.EXECUTABLE
         with self.assertRaises(OrderUpdateError):
             self.order.replace(1.01)
@@ -357,6 +370,11 @@ class BetfairOrderTest(unittest.TestCase):
         with self.assertRaises(OrderUpdateError):
             self.order.replace(2.02)
 
+    def test_replace_bet_id(self):
+        self.order.status = OrderStatus.EXECUTABLE
+        with self.assertRaises(OrderUpdateError):
+            self.order.replace(1.01)
+
     def test_replace_error(self):
         self.mock_order_type.ORDER_TYPE = OrderTypes.LIMIT
         self.order.status = OrderStatus.PENDING
diff --git a/tests/test_orderpackage.py b/tests/test_orderpackage.py
index 40a319a..fa912d6 100644
--- a/tests/test_orderpackage.py
+++ b/tests/test_orderpackage.py
@@ -105,6 +105,10 @@ class OrderPackageTest(unittest.TestCase):
         ]
         self.assertEqual(len(self.order_package.orders), 2)
 
+    def test_retry_count(self):
+        self.order_package._retry_count = 1
+        self.assertEqual(self.order_package.retry_count, 1)
+
     def test_info(self):
         self.assertEqual(
             self.order_package.info,
diff --git a/tests/test_process.py b/tests/test_process.py
index 3c00fcd..4ac5fbf 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -29,6 +29,7 @@ class BaseOrderTest(unittest.TestCase):
         config.simulated = False
 
     def test_process_current_orders_with_default_sep(self):
+        mock_log_control = mock.Mock()
         market_book = mock.Mock()
         markets = Markets()
         market = Market(
@@ -54,7 +55,12 @@ class BaseOrderTest(unittest.TestCase):
 
         event = mock.Mock(event=[mock.Mock(orders=[current_order])])
 
-        process_current_orders(markets=markets, strategies=strategies, event=event)
+        process_current_orders(
+            markets=markets,
+            strategies=strategies,
+            event=event,
+            log_control=mock_log_control,
+        )
 
         self.assertEqual(current_order, betfair_order.responses.current_order)
 
diff --git a/tests/test_transaction.py b/tests/test_transaction.py
index 21fd7e7..cbc24d2 100644
--- a/tests/test_transaction.py
+++ b/tests/test_transaction.py
@@ -10,11 +10,12 @@ class TransactionTest(unittest.TestCase):
     def setUp(self) -> None:
         mock_blotter = {}
         self.mock_market = mock.Mock(blotter=mock_blotter)
-        self.transaction = Transaction(self.mock_market, 1)
+        self.transaction = Transaction(self.mock_market, 1, False)
 
     def test_init(self):
         self.assertEqual(self.transaction.market, self.mock_market)
         self.assertEqual(self.transaction._id, 1)
+        self.assertFalse(self.transaction._async_place_orders)
         self.assertFalse(self.transaction._pending_orders)
         self.assertEqual(self.transaction._pending_place, [])
         self.assertEqual(self.transaction._pending_cancel, [])
@@ -171,7 +172,38 @@ class TransactionTest(unittest.TestCase):
         self.assertEqual(self.transaction.execute(), 4)
         mock__create_order_package.assert_has_calls(
             [
-                call([(mock_order, 1234)], OrderPackageType.PLACE),
+                call([(mock_order, 1234)], OrderPackageType.PLACE, async_=False),
+                call([(mock_order, None)], OrderPackageType.CANCEL),
+                call([(mock_order, None)], OrderPackageType.UPDATE),
+                call([(mock_order, 1234)], OrderPackageType.REPLACE),
+            ]
+        )
+        self.transaction.market.flumine.process_order_package.assert_has_calls(
+            [
+                call(mock__create_order_package()[0]),
+                call(mock__create_order_package()[0]),
+                call(mock__create_order_package()[0]),
+                call(mock__create_order_package()[0]),
+            ]
+        )
+        self.assertFalse(self.transaction._pending_orders)
+
+    @mock.patch("flumine.execution.transaction.Transaction._create_order_package")
+    def test_execute_async(self, mock__create_order_package):
+        self.transaction._pending_orders = True
+        self.transaction._async_place_orders = True
+        mock_package = mock.Mock()
+        mock__create_order_package.return_value = [mock_package]
+        self.assertEqual(self.transaction.execute(), 0)
+        mock_order = mock.Mock()
+        self.transaction._pending_place = [(mock_order, 1234)]
+        self.transaction._pending_cancel = [(mock_order, None)]
+        self.transaction._pending_update = [(mock_order, None)]
+        self.transaction._pending_replace = [(mock_order, 1234)]
+        self.assertEqual(self.transaction.execute(), 4)
+        mock__create_order_package.assert_has_calls(
+            [
+                call([(mock_order, 1234)], OrderPackageType.PLACE, async_=True),
                 call([(mock_order, None)], OrderPackageType.CANCEL),
                 call([(mock_order, None)], OrderPackageType.UPDATE),
                 call([(mock_order, 1234)], OrderPackageType.REPLACE),
@@ -229,6 +261,7 @@ class TransactionTest(unittest.TestCase):
                     package_type=OrderPackageType.PLACE,
                     bet_delay=self.transaction.market.market_book.bet_delay,
                     market_version=None,
+                    async_=False,
                 ),
                 call(
                     client=self.transaction.market.flumine.client,
@@ -237,6 +270,7 @@ class TransactionTest(unittest.TestCase):
                     package_type=OrderPackageType.PLACE,
                     bet_delay=self.transaction.market.market_book.bet_delay,
                     market_version=123,
+                    async_=False,
                 ),
                 call(
                     client=self.transaction.market.flumine.client,
@@ -245,6 +279,7 @@ class TransactionTest(unittest.TestCase):
                     package_type=OrderPackageType.PLACE,
                     bet_delay=self.transaction.market.market_book.bet_delay,
                     market_version=123,
+                    async_=False,
                 ),
             ]
         )