Repository: liampauling/flumine
Pull request #371 (2021-02-01 11:10:34)

#344 #303 order refactor to prevent unnecessary function calls and dy…
…namic order request within strategies

order package creation now at market level
minor change to allow backtesting to work with PendingPackages class reinstated


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index d319062..4d03f3f 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,18 @@
 Release History
 ---------------
 
+1.17.0 (2021-02-22)
++++++++++++++++++++
+
+**Improvements**
+
+- Major refactor to order placement using Transaction class to allow user control over order placement
+- Trading controls executed on place rather than OrderPackage level (Breaking change to controls)
+- strategy order placement to be depreciated (Breaking change from version 1.18.0)
+- OrderPackage no longer processed through the queue (quicker tick to trade)
+- Error correctly raised on duplicate place calls
+- Execution worker count bumped
+
 1.16.3 (2021-02-08)
 +++++++++++++++++++
 
diff --git a/README.md b/README.md
index a9d3c1e..5da6387 100644
--- a/README.md
+++ b/README.md
@@ -77,17 +77,17 @@ class ExampleStrategy(BaseStrategy):
                     side="LAY", 
                     order_type=LimitOrder(price=1.01, size=2.00)
                 )
-                self.place_order(market, order)
+                market.place_order(order)
 
     def process_orders(self, market: Market, orders: list) -> None:
         for order in orders:
             if order.status == OrderStatus.EXECUTABLE:
                 if order.size_remaining == 2.00:
-                    self.cancel_order(market, order, 0.02)  # reduce size to 1.98
-                if order.order_type.persistence_type == 'LAPSE':
-                    self.update_order(market, order, 'PERSIST')
+                    market.cancel_order(order, 0.02)  # reduce size to 1.98
+                if order.order_type.persistence_type == "LAPSE":
+                    market.update_order(order, "PERSIST")
                 if order.size_remaining > 0:
-                    self.replace_order(market, order, 1.02)  # move
+                    market.replace_order(order, 1.02)  # move
 
 
 strategy = ExampleStrategy(
diff --git a/docs/advanced.md b/docs/advanced.md
index b412869..336a61a 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -61,14 +61,6 @@ The following functions can be overridden dependant on the strategy:
 
 `finish()` Function called when framework ends
 
-`place_order()` Places an order by first validating using `validate_order`
-
-`cancel_order()` Cancel an order
-
-`update_order()` Updates an order
-
-`replace_order()` Replaces an order
-
 ### Runner Context
 
 
@@ -184,7 +176,7 @@ Store server id or similar (e.g. AWS ec2 instanceId)
 
 ### hostname
 
-Used as customerStrategyRefs so that only orders created by the running instance is returned.
+Used as customerStrategyRefs so that only orders created by the running instance are returned.
 
 ### process_id
 
diff --git a/docs/design.md b/docs/architecture.md
similarity index 100%
rename from docs/design.md
rename to docs/architecture.md
diff --git a/docs/index.md b/docs/index.md
index afb54a4..32a8fce 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -89,17 +89,17 @@ class ExampleStrategy(BaseStrategy):
                 order = trade.create_order(
                     side="LAY", order_type=LimitOrder(price=1.01, size=2.00)
                 )
-                self.place_order(market, order)
+                market.place_order(order)
 
     def process_orders(self, market: Market, orders: list) -> None:
         for order in orders:
             if order.status == OrderStatus.EXECUTABLE:
                 if order.size_remaining == 2.00:
-                    self.cancel_order(market, order, 0.02)  # reduce size to 1.98
+                    market.cancel_order(order, 0.02)  # reduce size to 1.98
                 if order.order_type.persistence_type == "LAPSE":
-                    self.update_order(market, order, "PERSIST")
+                    market.update_order(order, "PERSIST")
                 if order.size_remaining > 0:
-                    self.replace_order(market, order, 1.02)  # move
+                    market.replace_order(order, 1.02)  # move
 
 
 strategy = ExampleStrategy(
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 9498bef..57a4389 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -67,6 +67,48 @@ The framework can now be started:
 framework.run()
 ```
 
+### Order placement
+
+Orders can be placed as followed:
+
+```python
+from flumine.order.trade import Trade
+from flumine.order.order import LimitOrder
+
+
+class ExampleStrategy(BaseStrategy):
+    def process_market_book(self, market, market_book):
+        for runner in market_book.runners:
+            if runner.selection_id == 123:
+                trade = Trade(
+                    market_id=market_book.market_id, 
+                    selection_id=runner.selection_id,
+                    handicap=runner.handicap,
+                    strategy=self
+                )
+                order = trade.create_order(
+                    side="LAY", 
+                    order_type=LimitOrder(price=1.01, size=2.00)
+                )
+                market.place_order(order)
+```
+
+This order will be validated through controls, stored in the blotter and sent straight to the execution thread pool for execution. It is also possible to batch orders into transactions as follows:
+
+```python
+with market.transaction as t:
+    market.place_order(order)  # executed immediately in separate transaction
+    t.place_order(order)  # executed on transaction __exit__
+
+with market.transaction as t:
+    t.place_order(order)
+    
+    t.execute()  # above order executed
+    
+    t.cancel_order(order)
+    t.place_order(order)  # both executed on transaction __exit__
+```
+
 ### Stream class
 
 By default the stream class will be a MarketStream which provides a MarketBook python object, if collecting data this can be changed to a DataStream class however process_raw_data will be called and not process_market_book:
diff --git a/examples/example.py b/examples/example.py
index 33b68f2..d6f99ba 100644
--- a/examples/example.py
+++ b/examples/example.py
@@ -47,7 +47,7 @@ class ExampleStrategy(BaseStrategy):
                 order = trade.create_order(
                     side="LAY", order_type=LimitOrder(price=1.01, size=2.00)
                 )
-                self.place_order(market, order)
+                market.place_order(order)
 
     def process_orders(self, market, orders):
         for order in orders:
@@ -55,15 +55,14 @@ class ExampleStrategy(BaseStrategy):
                 if order.elapsed_seconds and order.elapsed_seconds > 5:
                     # print(order.bet_id, order.average_price_matched, order.size_matched)
                     if order.size_remaining == 2.00:
-                        self.cancel_order(market, order, size_reduction=1.51)
-                # self.update_order(market, order, "PERSIST")
+                        market.cancel_order(order, size_reduction=1.51)
+                # market.update_order(order, "PERSIST")
                 # if order.order_type.price == 1.01 and order.size_remaining == 0.49:
-                #     self.replace_order(market, order, 1.02)
+                #     market.replace_order(order, 1.02)
                 # if order.order_type.price == 1.02:
-                #     self.replace_order(market, order, 1.03)
+                #     market.replace_order(order, 1.03)
                 # if order.order_type.price == 1.03:
-                #     self.replace_order(market, order, 1.05)
-                pass
+                #     market.replace_order(order, 1.05)
 
 
 trading = betfairlightweight.APIClient("username")
diff --git a/examples/strategies/lowestlayer.py b/examples/strategies/lowestlayer.py
index ec73523..64ca9ff 100644
--- a/examples/strategies/lowestlayer.py
+++ b/examples/strategies/lowestlayer.py
@@ -11,6 +11,7 @@ class LowestLayer(BaseStrategy):
     will be placed on current lowest priced runner.
     - Market is active and inplay
     - Lay price below 3.0
+    - No live trades (EXECUTABLE orders in the market)
     - Order killed after 2 seconds if not matched
     """
 
@@ -33,19 +34,23 @@ class LowestLayer(BaseStrategy):
 
         for runner in market_book.runners:
             if runner.selection_id == selection_id:
-                # lay at current best lay price
-                lay = get_price(runner.ex.available_to_lay, 0)
-                trade = Trade(
-                    market_book.market_id,
-                    runner.selection_id,
-                    runner.handicap,
-                    self,
+                runner_context = self.get_runner_context(
+                    market.market_id, runner.selection_id
                 )
-                order = trade.create_order(
-                    side="LAY",
-                    order_type=LimitOrder(lay, self.context["stake"]),
-                )
-                self.place_order(market, order)
+                if runner_context.live_trade_count == 0:
+                    # lay at current best lay price
+                    lay = get_price(runner.ex.available_to_lay, 0)
+                    trade = Trade(
+                        market_book.market_id,
+                        runner.selection_id,
+                        runner.handicap,
+                        self,
+                    )
+                    order = trade.create_order(
+                        side="LAY",
+                        order_type=LimitOrder(lay, self.context["stake"]),
+                    )
+                    market.place_order(order)
 
     def process_orders(self, market, orders):
         # kill order if unmatched in market for greater than 2 seconds
@@ -53,4 +58,4 @@ class LowestLayer(BaseStrategy):
         for order in orders:
             if order.status == OrderStatus.EXECUTABLE:
                 if order.elapsed_seconds and order.elapsed_seconds > 2:
-                    self.cancel_order(market, order)
+                    market.cancel_order(order)
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 31ba5d7..9251e59 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.16.3"
+__version__ = "1.17.0"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/baseflumine.py b/flumine/baseflumine.py
index 09cbb4f..19cc1d0 100644
--- a/flumine/baseflumine.py
+++ b/flumine/baseflumine.py
@@ -64,7 +64,7 @@ class BaseFlumine:
         self._logging_controls = []
 
         # trading controls
-        self._trading_controls = []
+        self.trading_controls = []
         # add default controls (processed in order)
         self.add_trading_control(OrderValidation)
         self.add_trading_control(StrategyExposure)
@@ -93,7 +93,7 @@ class BaseFlumine:
 
     def add_trading_control(self, trading_control: Type[BaseControl], **kwargs) -> None:
         logger.info("Adding trading control {0}".format(trading_control.NAME))
-        self._trading_controls.append(trading_control(self, **kwargs))
+        self.trading_controls.append(trading_control(self, **kwargs))
 
     def add_market_middleware(self, middleware: Middleware) -> None:
         logger.info("Adding market middleware {0}".format(middleware))
@@ -150,32 +150,9 @@ class BaseFlumine:
                         strategy.process_market_book, market, market_book
                     )
 
-            self._process_market_orders()
-
-    def _process_market_orders(self) -> None:
-        for market in self.markets:
-            if market.blotter.pending_orders:
-                if market.market_book:
-                    bet_delay = market.market_book.bet_delay
-                else:
-                    bet_delay = None
-                for order_package in market.blotter.process_orders(
-                    self.client, bet_delay
-                ):
-                    self.handler_queue.put(order_package)
-
-    def _process_order_package(self, order_package) -> None:
-        """Validate trading controls and
-        then execute.
-        """
-        for control in self._trading_controls:
-            control(order_package)
-        for control in order_package.client.trading_controls:
-            control(order_package)
-        if order_package.orders:
-            order_package.client.execution.handler(order_package)
-        else:
-            logger.warning("Empty package, not executing", extra=order_package.info)
+    def process_order_package(self, order_package) -> None:
+        """Execute through client."""
+        order_package.client.execution.handler(order_package)
 
     def _add_market(self, market_id: str, market_book: resources.MarketBook) -> Market:
         logger.info("Adding: {0} to markets".format(market_id))
@@ -237,7 +214,6 @@ class BaseFlumine:
                 for strategy in self.strategies:
                     strategy_orders = market.blotter.strategy_orders(strategy)
                     strategy.process_orders(market, strategy_orders)
-        self._process_market_orders()
 
     def _process_custom_event(self, event: events.CustomEvent) -> None:
         try:
@@ -248,7 +224,6 @@ class BaseFlumine:
                     e, event.callback
                 )
             )
-        self._process_market_orders()
 
     def _process_close_market(self, event: events.CloseMarketEvent) -> None:
         market_book = event.event
diff --git a/flumine/controls/__init__.py b/flumine/controls/__init__.py
index 82720c3..cfe151d 100644
--- a/flumine/controls/__init__.py
+++ b/flumine/controls/__init__.py
@@ -1,7 +1,7 @@
 import logging
 
-from ..order.orderpackage import BaseOrderPackage
-from ..order.order import BaseOrder
+from ..order.orderpackage import OrderPackageType, BaseOrder
+from ..exceptions import ControlError
 
 logger = logging.getLogger(__name__)
 
@@ -13,10 +13,10 @@ class BaseControl:
     def __init__(self, flumine, *args, **kwargs):
         self.flumine = flumine
 
-    def __call__(self, order_package: BaseOrderPackage):
-        self._validate(order_package)
+    def __call__(self, order: BaseOrder, package_type: OrderPackageType):
+        self._validate(order, package_type)
 
-    def _validate(self, order_package: BaseOrderPackage) -> None:
+    def _validate(self, order: BaseOrder, package_type: OrderPackageType) -> None:
         raise NotImplementedError
 
     def _on_error(self, order: BaseOrder, error: str) -> None:
@@ -26,3 +26,4 @@ class BaseControl:
             violation_msg,
             extra={"control": self.NAME, "error": error, "order": order.info},
         )
+        raise ControlError(violation_msg)
diff --git a/flumine/controls/clientcontrols.py b/flumine/controls/clientcontrols.py
index 99f9e86..70f1e0e 100644
--- a/flumine/controls/clientcontrols.py
+++ b/flumine/controls/clientcontrols.py
@@ -3,7 +3,7 @@ import logging
 from typing import Optional
 from betfairlightweight.metadata import transaction_limit
 
-from ..order.orderpackage import OrderPackageType
+from ..order.orderpackage import BaseOrder, OrderPackageType
 from . import BaseControl
 from ..clients.baseclient import BaseClient
 
@@ -32,28 +32,27 @@ class MaxOrderCount(BaseControl):
         self._next_hour = None
         self.transaction_count = 0
 
-    def _validate(self, order_package) -> None:
+    def _validate(self, order: BaseOrder, package_type: OrderPackageType) -> None:
         if self._next_hour is None:
             self._set_next_hour()
         self._check_hour()
         self.total += 1
-        if order_package.package_type == OrderPackageType.PLACE:
-            self._check_transaction_count(len(order_package))
-            self.place_requests += len(order_package)
-            if not self.safe:  # and order.flumine_order_type == "initial"
-                for order in order_package:
-                    self._on_error(
-                        order,
-                        "Max Order Count has been reached ({0}) for current hour".format(
-                            self.transaction_count
-                        ),
-                    )
-        elif order_package.package_type == OrderPackageType.CANCEL:
-            self.cancel_requests += len(order_package)
-        elif order_package.package_type == OrderPackageType.UPDATE:
-            self.update_requests += len(order_package)
-        elif order_package.package_type == OrderPackageType.REPLACE:
-            self.replace_requests += len(order_package)
+        if package_type == OrderPackageType.PLACE:
+            self._check_transaction_count(1)
+            self.place_requests += 1
+            if not self.safe:
+                self._on_error(
+                    order,
+                    "Max Order Count has been reached ({0}) for current hour".format(
+                        self.transaction_count
+                    ),
+                )
+        elif package_type == OrderPackageType.CANCEL:
+            self.cancel_requests += 1
+        elif package_type == OrderPackageType.UPDATE:
+            self.update_requests += 1
+        elif package_type == OrderPackageType.REPLACE:
+            self.replace_requests += 1
 
     def _check_hour(self) -> None:
         if datetime.datetime.utcnow() > self._next_hour:
diff --git a/flumine/controls/tradingcontrols.py b/flumine/controls/tradingcontrols.py
index 8631a3b..00abb9c 100644
--- a/flumine/controls/tradingcontrols.py
+++ b/flumine/controls/tradingcontrols.py
@@ -2,7 +2,7 @@ import logging
 
 from ..clients.clients import ExchangeType
 from ..order.ordertype import OrderTypes
-from ..order.orderpackage import OrderPackageType
+from ..order.orderpackage import OrderPackageType, BaseOrder
 from . import BaseControl
 from .. import utils
 
@@ -18,10 +18,9 @@ class OrderValidation(BaseControl):
 
     NAME = "ORDER_VALIDATION"
 
-    def _validate(self, order_package):
-        for order in order_package:
-            if order.EXCHANGE == ExchangeType.BETFAIR:
-                self._validate_betfair_order(order)
+    def _validate(self, order: BaseOrder, package_type: OrderPackageType) -> None:
+        if order.EXCHANGE == ExchangeType.BETFAIR:
+            self._validate_betfair_order(order)
 
     def _validate_betfair_order(self, order):
         if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
@@ -106,52 +105,47 @@ class StrategyExposure(BaseControl):
 
     NAME = "STRATEGY_EXPOSURE"
 
-    def _validate(self, order_package):
-        if order_package.package_type in (
+    def _validate(self, order: BaseOrder, package_type: OrderPackageType) -> None:
+        if package_type == OrderPackageType.PLACE:
+            # strategy.validate_order
+            runner_context = order.trade.strategy.get_runner_context(*order.lookup)
+            if order.trade.strategy.validate_order(runner_context, order) is False:
+                return self._on_error(order, "strategy.validate_order failure")
+
+        if package_type in (
             OrderPackageType.PLACE,
             OrderPackageType.REPLACE,  # todo potential bug?
         ):
-
-            package_orders_by_strategy_lookup = {}
-            for order in order_package:
-                strategy = order.trade.strategy
-                package_orders_by_strategy_lookup.setdefault(
-                    (strategy, order.lookup), []
-                ).append(order)
-                if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
-                    if order.side == "BACK":
-                        exposure = order.order_type.size
-                    else:
-                        exposure = (order.order_type.price - 1) * order.order_type.size
-                elif order.order_type.ORDER_TYPE == OrderTypes.LIMIT_ON_CLOSE:
-                    exposure = order.order_type.liability  # todo correct?
-                elif order.order_type.ORDER_TYPE == OrderTypes.MARKET_ON_CLOSE:
-                    exposure = order.order_type.liability
+            strategy = order.trade.strategy
+            if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
+                if order.side == "BACK":
+                    exposure = order.order_type.size
                 else:
-                    continue
-
-                # per order
-                if exposure > strategy.max_order_exposure:
-                    self._on_error(
-                        order,
-                        "Order exposure ({0}) is greater than strategy.max_order_strategy ({1})".format(
-                            exposure, strategy.max_order_exposure
-                        ),
-                    )
-                    continue
-
-            market = self.flumine.markets.markets[order_package.market_id]
+                    exposure = (order.order_type.price - 1) * order.order_type.size
+            elif order.order_type.ORDER_TYPE == OrderTypes.LIMIT_ON_CLOSE:
+                exposure = order.order_type.liability  # todo correct?
+            elif order.order_type.ORDER_TYPE == OrderTypes.MARKET_ON_CLOSE:
+                exposure = order.order_type.liability
+            else:
+                return self._on_error(order, "Unknown order_type")
+
+            # per order
+            if exposure > strategy.max_order_exposure:
+                return self._on_error(
+                    order,
+                    "Order exposure ({0}) is greater than strategy.max_order_exposure ({1})".format(
+                        exposure, strategy.max_order_exposure
+                    ),
+                )
 
             # per selection
-            for (strategy, lookup), orders in package_orders_by_strategy_lookup.items():
-                exposure = market.blotter.selection_exposure(strategy, lookup=lookup)
-
-                if exposure > strategy.max_selection_exposure:
-                    for order in orders:
-                        self._on_error(
-                            order,
-                            "Potential selection exposure ({0}) is greater than strategy.max_selection_exposure ({1})".format(
-                                exposure,
-                                strategy.max_selection_exposure,
-                            ),
-                        )
+            market = self.flumine.markets.markets[order.market_id]
+            exposure = market.blotter.selection_exposure(strategy, lookup=order.lookup)
+            if exposure > strategy.max_selection_exposure:
+                return self._on_error(
+                    order,
+                    "Potential selection exposure ({0}) is greater than strategy.max_selection_exposure ({1})".format(
+                        exposure,
+                        strategy.max_selection_exposure,
+                    ),
+                )
diff --git a/flumine/exceptions.py b/flumine/exceptions.py
index 69b801d..078f65d 100644
--- a/flumine/exceptions.py
+++ b/flumine/exceptions.py
@@ -44,3 +44,12 @@ class OrderExecutionError(FlumineException):
     """
 
     pass
+
+
+class ControlError(FlumineException):
+    """Exception raised if order voilates
+    a control.
+    """
+
+    def __init__(self, message):
+        super(ControlError, self).__init__(message)
diff --git a/flumine/execution/baseexecution.py b/flumine/execution/baseexecution.py
index 4bc6509..92c81d1 100644
--- a/flumine/execution/baseexecution.py
+++ b/flumine/execution/baseexecution.py
@@ -8,7 +8,7 @@ from ..events.events import OrderEvent
 
 logger = logging.getLogger(__name__)
 
-MAX_WORKERS = 16
+MAX_WORKERS = 32
 MAX_SESSION_AGE = 200  # seconds since last request
 BET_ID_START = 100000000000  # simulated start betId->
 
@@ -88,6 +88,7 @@ class BaseExecution:
                 "session": session,
                 "session_time_created": session.time_created,
                 "session_time_returned": session.time_returned,
+                "live_sessions_count": len(self._sessions),
             },
         )
         return session
@@ -103,6 +104,7 @@ class BaseExecution:
                     "session": http_session,
                     "session_time_created": http_session.time_created,
                     "session_time_returned": http_session.time_returned,
+                    "live_sessions_count": len(self._sessions),
                     "err": err,
                 },
             )
@@ -137,10 +139,6 @@ class BaseExecution:
             order.bet_id = instruction_report.bet_id
             self.flumine.log_control(OrderEvent(order))
 
-    @property
-    def handler_queue(self):
-        return self.flumine.handler_queue
-
     def shutdown(self):
         logger.info("Shutting down Execution (%s)" % self.__class__.__name__)
         self._thread_pool.shutdown(wait=True)
diff --git a/flumine/execution/betfairexecution.py b/flumine/execution/betfairexecution.py
index f3fda72..c4849e6 100644
--- a/flumine/execution/betfairexecution.py
+++ b/flumine/execution/betfairexecution.py
@@ -205,7 +205,7 @@ class BetfairExecution(BaseExecution):
                     exc_info=True,
                 )
                 if order_package.retry():
-                    self.handler_queue.put(order_package)
+                    self.handler(order_package)
 
                 self._return_http_session(http_session, err=True)
                 return
diff --git a/flumine/execution/transaction.py b/flumine/execution/transaction.py
new file mode 100644
index 0000000..f9c8a55
--- /dev/null
+++ b/flumine/execution/transaction.py
@@ -0,0 +1,166 @@
+import logging
+from collections import defaultdict
+
+from ..order.orderpackage import OrderPackageType, BetfairOrderPackage
+from ..events import events
+from ..exceptions import ControlError, OrderError
+from ..utils import chunks
+
+logger = logging.getLogger(__name__)
+
+
+class Transaction:
+    """
+    Process place, cancel, update and replace requests.
+
+    Default behaviour is to execute immediately however
+    when it is used as a context manager requests can
+    be batched, for example:
+
+        with market.transaction as t:
+            market.place_order(order)  # executed immediately in separate transaction
+            t.place_order(order)  # executed on transaction __exit__
+
+        with market.transaction as t:
+            t.place_order(order)
+            ..
+            t.execute()  # above order executed
+            ..
+            t.cancel_order(order)
+            t.place_order(order)  # both executed on transaction __exit__
+    """
+
+    def __init__(self, market):
+        self.market = market
+        self._pending_orders = False
+        self._pending_place = []  # list of (<Order>, market_version)
+        self._pending_cancel = []  # list of (<Order>, None)
+        self._pending_update = []  # list of (<Order>, None)
+        self._pending_replace = []  # list of (<Order>, market_version)
+
+    def place_order(
+        self, order, market_version: int = None, execute: bool = True
+    ) -> bool:
+        if self._validate_controls(order, OrderPackageType.PLACE) is False:
+            return False
+        # place
+        order.place(self.market.market_book.publish_time)
+        if order.id not in self.market.blotter:
+            self.market.blotter[order.id] = order
+            if order.trade.market_notes is None:
+                order.trade.update_market_notes(self.market)
+            self.market.flumine.log_control(events.TradeEvent(order.trade))
+        else:
+            raise OrderError("Order %s has already been placed" % order.id)
+        if execute:  # handles replaceOrder
+            self._pending_place.append((order, market_version))
+            self._pending_orders = True
+        return True
+
+    def cancel_order(self, order, size_reduction: float = None) -> bool:
+        if self._validate_controls(order, OrderPackageType.CANCEL) is False:
+            return False
+        # cancel
+        order.cancel(size_reduction)
+        self._pending_cancel.append((order, None))
+        self._pending_orders = True
+        return True
+
+    def update_order(self, order, new_persistence_type: str) -> bool:
+        if self._validate_controls(order, OrderPackageType.UPDATE) is False:
+            return False
+        # update
+        order.update(new_persistence_type)
+        self._pending_update.append((order, None))
+        self._pending_orders = True
+        return True
+
+    def replace_order(
+        self, order, new_price: float, market_version: int = None
+    ) -> bool:
+        if self._validate_controls(order, OrderPackageType.REPLACE) is False:
+            return False
+        # replace
+        order.replace(new_price)
+        self._pending_replace.append((order, market_version))
+        self._pending_orders = True
+        return True
+
+    def execute(self) -> int:
+        packages = []
+        if self._pending_place:
+            packages += self._create_order_package(
+                self._pending_place,
+                OrderPackageType.PLACE,
+            )
+        if self._pending_cancel:
+            packages += self._create_order_package(
+                self._pending_cancel,
+                OrderPackageType.CANCEL,
+            )
+        if self._pending_update:
+            packages += self._create_order_package(
+                self._pending_update,
+                OrderPackageType.UPDATE,
+            )
+        if self._pending_replace:
+            packages += self._create_order_package(
+                self._pending_replace,
+                OrderPackageType.REPLACE,
+            )
+        if packages:
+            for package in packages:
+                self.market.flumine.process_order_package(package)
+            logger.info(
+                "%s order packages executed in transaction" % len(packages),
+                extra={
+                    "market_id": self.market.market_id,
+                    "order_packages": [o.info for o in packages],
+                },
+            )
+            self._pending_orders = False
+        return len(packages)
+
+    def _validate_controls(self, order, package_type: OrderPackageType) -> bool:
+        # return False on violation
+        try:
+            for control in self.market.flumine.trading_controls:
+                control(order, package_type)
+            for control in self.market.flumine.client.trading_controls:
+                control(order, package_type)
+        except ControlError:
+            return False
+        else:
+            return True
+
+    def _create_order_package(
+        self, orders: list, package_type: OrderPackageType
+    ) -> list:
+        # group orders by marketVersion
+        orders_grouped = defaultdict(list)
+        for o in orders:
+            orders_grouped[o[1]].append(o[0])
+        # create packages (chunked by limit)
+        limit = BetfairOrderPackage.order_limit(package_type)
+        packages = []
+        for market_version, package_orders in orders_grouped.items():
+            for chunked_orders in chunks(package_orders, limit):
+                packages.append(
+                    BetfairOrderPackage(
+                        client=self.market.flumine.client,
+                        market_id=self.market.market_id,
+                        orders=chunked_orders,
+                        package_type=package_type,
+                        bet_delay=self.market.market_book.bet_delay,
+                        market_version=market_version,
+                    )
+                )
+        orders.clear()
+        return packages
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        if self._pending_orders:
+            self.execute()
diff --git a/flumine/flumine.py b/flumine/flumine.py
index 99a2229..cbfd5a9 100644
--- a/flumine/flumine.py
+++ b/flumine/flumine.py
@@ -31,9 +31,6 @@ class Flumine(BaseFlumine):
                 elif event.EVENT_TYPE == EventType.CURRENT_ORDERS:
                     self._process_current_orders(event)
 
-                elif event.EVENT_TYPE == EventType.ORDER_PACKAGE:
-                    self._process_order_package(event)
-
                 elif event.EVENT_TYPE == EventType.CLEARED_MARKETS:
                     self._process_cleared_markets(event)
 
diff --git a/flumine/markets/blotter.py b/flumine/markets/blotter.py
index f68eff7..784dc2d 100644
--- a/flumine/markets/blotter.py
+++ b/flumine/markets/blotter.py
@@ -4,13 +4,10 @@ from collections import defaultdict
 
 from ..order.ordertype import OrderTypes
 from ..utils import (
-    batch_orders,
-    chunks,
     calculate_unmatched_exposure,
     calculate_matched_exposure,
 )
 from ..order.order import BaseOrder, OrderStatus
-from ..order.orderpackage import OrderPackageType, BetfairOrderPackage
 
 logger = logging.getLogger(__name__)
 
@@ -40,79 +37,11 @@ class Blotter:
         self._strategy_orders = defaultdict(
             list
         )  # cache list per strategy (faster lookup)
-        # pending orders, list of (<Order>, {..})
-        self.pending_place = []
-        self.pending_cancel = []
-        self.pending_update = []
-        self.pending_replace = []
 
     def strategy_orders(self, strategy) -> list:
         """Returns all orders related to a strategy."""
         return self._strategy_orders[strategy]
 
-    def process_orders(self, client, bet_delay: int = 0) -> list:
-        packages = []
-        if self.pending_place:
-            packages += self._create_packages(
-                client, self.pending_place, OrderPackageType.PLACE, bet_delay
-            )
-        if self.pending_cancel:
-            packages += self._create_packages(
-                client, self.pending_cancel, OrderPackageType.CANCEL, bet_delay
-            )
-        if self.pending_update:
-            packages += self._create_packages(
-                client, self.pending_update, OrderPackageType.UPDATE, bet_delay
-            )
-        if self.pending_replace:
-            packages += self._create_packages(
-                client, self.pending_replace, OrderPackageType.REPLACE, bet_delay
-            )
-        if packages:
-            logger.info(
-                "%s order packages created" % len(packages),
-                extra={
-                    "order_packages": [o.info for o in packages],
-                    "bet_delay": bet_delay,
-                },
-            )
-        return packages
-
-    def _create_packages(
-        self, client, orders: list, package_type: OrderPackageType, bet_delay: int
-    ) -> list:
-        packages = []
-        _package_cls = BetfairOrderPackage
-        limit = _package_cls.order_limit(package_type)
-        # batch based on request data dict
-        batched_orders = batch_orders(orders)
-        # create packages
-        for market_version, mv_batched_orders in batched_orders.items():
-            for _orders in mv_batched_orders:
-                for chunked_orders in chunks(_orders, limit):
-                    order_package = _package_cls(
-                        client=client,
-                        market_id=self.market_id,
-                        orders=chunked_orders,
-                        package_type=package_type,
-                        bet_delay=bet_delay,
-                        market_version=market_version,
-                    )
-                    packages.append(order_package)
-        orders.clear()
-        return packages
-
-    @property
-    def pending_orders(self) -> bool:
-        return any(
-            (
-                self.pending_place,
-                self.pending_cancel,
-                self.pending_update,
-                self.pending_replace,
-            )
-        )
-
     @property
     def live_orders(self):
         return iter(self._live_orders)
diff --git a/flumine/markets/market.py b/flumine/markets/market.py
index d350458..1ade5da 100644
--- a/flumine/markets/market.py
+++ b/flumine/markets/market.py
@@ -5,12 +5,19 @@ from collections import defaultdict
 from betfairlightweight.resources.bettingresources import MarketBook, MarketCatalogue
 
 from .blotter import Blotter
-from ..events import events
+from ..execution.transaction import Transaction
 
 logger = logging.getLogger(__name__)
 
 
 class Market:
+    """
+    Market data structure to hold latest marketBook,
+    marketCatalogue and various properties. Also
+    allows order placement through the Transaction
+    class.
+    """
+
     def __init__(
         self,
         flumine,
@@ -48,57 +55,32 @@ class Market:
             extra=self.info,
         )
 
+    def transaction(self) -> Transaction:
+        return Transaction(self)
+
     # order
     def place_order(
         self,
         order,
-        batch: bool = True,
         market_version: int = None,
         execute: bool = True,
-    ) -> None:
-        order.place(self.market_book.publish_time)
-        if order.id not in self.blotter:
-            self.blotter[order.id] = order
-            if order.trade.market_notes is None:
-                order.trade.update_market_notes(self)
-            self.flumine.log_control(events.TradeEvent(order.trade))  # todo dupes?
-        else:
-            return  # retry attempt so ignore?
-        if execute:  # handles replaceOrder
-            self.blotter.pending_place.append(
-                (order, {"batch": batch, "market_version": market_version})
-            )
-
-    def cancel_order(
-        self, order, size_reduction: float = None, batch: bool = True
-    ) -> None:
-        order.cancel(size_reduction)
-        self.blotter.pending_cancel.append(
-            (order, {"size_reduction": size_reduction, "batch": batch})
-        )
+    ) -> bool:
+        with self.transaction() as t:
+            return t.place_order(order, market_version, execute)
 
-    def update_order(
-        self, order, new_persistence_type: str, batch: bool = True
-    ) -> None:
-        order.update(new_persistence_type)
-        self.blotter.pending_update.append(
-            (order, {"new_persistence_type": new_persistence_type, "batch": batch})
-        )
+    def cancel_order(self, order, size_reduction: float = None) -> bool:
+        with self.transaction() as t:
+            return t.cancel_order(order, size_reduction)
+
+    def update_order(self, order, new_persistence_type: str) -> bool:
+        with self.transaction() as t:
+            return t.update_order(order, new_persistence_type)
 
     def replace_order(
-        self, order, new_price: float, batch: bool = True, market_version: int = None
-    ) -> None:
-        order.replace(new_price)
-        self.blotter.pending_replace.append(
-            (
-                order,
-                {
-                    "new_price": new_price,
-                    "batch": batch,
-                    "market_version": market_version,
-                },
-            )
-        )
+        self, order, new_price: float, market_version: int = None
+    ) -> bool:
+        with self.transaction() as t:
+            return t.replace_order(order, new_price, market_version)
 
     @property
     def event(self) -> dict:
diff --git a/flumine/markets/middleware.py b/flumine/markets/middleware.py
index 7d76c1e..c8eea79 100644
--- a/flumine/markets/middleware.py
+++ b/flumine/markets/middleware.py
@@ -41,8 +41,8 @@ class SimulatedMiddleware(Middleware):
         runner_updates = self._process_streaming_update(market.market_book)
 
         for runner in market.market_book.runners:
-            update = bool(runner.selection_id in runner_updates)
             if runner.status == "ACTIVE":
+                update = bool(runner.selection_id in runner_updates)
                 self._process_runner(market_analytics, runner, update)
             elif runner.status == "REMOVED":
                 _removal = (
@@ -169,28 +169,28 @@ class RunnerAnalytics:
         if update:
             self.middle = self._calculate_middle(self._runner)  # use last update
             self.matched = self._calculate_matched(runner)
-            self.traded = self._calculate_traded(runner)
+            self.traded = self._calculate_traded(runner.ex.traded_volume)
             self._traded_volume = runner.ex.traded_volume
             self._runner = runner
         else:
             self.matched = 0
             self.traded = {}
 
-    def _calculate_traded(self, runner: RunnerBook) -> dict:
-        if self._traded_volume == runner.ex.traded_volume:
+    def _calculate_traded(self, traded_volume: list) -> dict:
+        if self._traded_volume == traded_volume:
             return {}
         else:
-            c_v, p_v, traded = {}, self._p_v, {}
+            p_v, traded = self._p_v, {}
             # create dictionary
-            c_v = {i["price"]: i["size"] for i in runner.ex.traded_volume}
+            c_v = {i["price"]: i["size"] for i in traded_volume}
             # calculate difference
-            for key in c_v.keys():
+            for key, value in c_v.items():
                 if key in p_v:
-                    new_value = float(c_v[key]) - float(p_v[key])
+                    new_value = float(value) - float(p_v[key])
+                    if new_value > 0:
+                        traded[key] = round(new_value, 2)
                 else:
-                    new_value = float(c_v[key])
-                if new_value > 0:
-                    traded[key] = round(new_value, 2)
+                    traded[key] = value
             # cache for next update
             self._p_v = c_v
             return traded
diff --git a/flumine/order/order.py b/flumine/order/order.py
index e64d901..554d598 100644
--- a/flumine/order/order.py
+++ b/flumine/order/order.py
@@ -81,7 +81,7 @@ class BaseOrder:
         self.status_log.append(status)
         self.status = status
         logger.info("Order status update: %s" % self.status.value, extra=self.info)
-        if self.trade.complete:
+        if self.trade.complete and status != OrderStatus.VIOLATION:
             self.trade.complete_trade()
 
     def placing(self) -> None:
@@ -299,6 +299,8 @@ class BetfairOrder(BaseOrder):
     def place(self, publish_time: int) -> None:
         self.publish_time = publish_time
         self.placing()
+        runner_context = self.trade.strategy.get_runner_context(*self.lookup)
+        runner_context.place()
 
     def cancel(self, size_reduction: float = None) -> None:
         if self.order_type.ORDER_TYPE == OrderTypes.LIMIT:
diff --git a/flumine/order/orderpackage.py b/flumine/order/orderpackage.py
index f7aa7a8..f86c2b7 100644
--- a/flumine/order/orderpackage.py
+++ b/flumine/order/orderpackage.py
@@ -43,14 +43,16 @@ class BaseOrderPackage(BaseEvent):
         self.market_id = market_id
         self._orders = orders
         self.package_type = package_type
-        self.bet_delay = bet_delay  # used for simulated execution
         self.async_ = async_
         self._market_version = market_version
         self.customer_strategy_ref = config.hostname
-        self.processed = False  # used for simulated execution
         self._retry = True
         self._max_retries = 3  # will retry 3 times
         self._retry_count = 0
+        # following used for simulated execution
+        self.processed = False
+        self.bet_delay = bet_delay
+        self.simulated_delay = self.calc_simulated_delay()
 
     def retry(self):
         if self._retry and self._retry_count < self._max_retries:
@@ -59,6 +61,17 @@ class BaseOrderPackage(BaseEvent):
             return True
         return False
 
+    def calc_simulated_delay(self) -> float:
+        if self.client.execution.EXCHANGE == ExchangeType.SIMULATED:
+            if self.package_type == OrderPackageType.PLACE:
+                return self.client.execution.PLACE_LATENCY + self.bet_delay
+            elif self.package_type == OrderPackageType.CANCEL:
+                return self.client.execution.CANCEL_LATENCY
+            elif self.package_type == OrderPackageType.UPDATE:
+                return self.client.execution.UPDATE_LATENCY
+            elif self.package_type == OrderPackageType.REPLACE:
+                return self.client.execution.REPLACE_LATENCY + self.bet_delay
+
     @property
     def place_instructions(self) -> dict:
         raise NotImplementedError
@@ -90,6 +103,7 @@ class BaseOrderPackage(BaseEvent):
             "client": self.client,
             "market_id": self.market_id,
             "orders": [o.id for o in self._orders],
+            "order_count": len(self),
             "package_type": self.package_type.value,
             "customer_strategy_ref": self.customer_strategy_ref,
             "bet_delay": self.bet_delay,
diff --git a/flumine/strategy/strategy.py b/flumine/strategy/strategy.py
index c160022..019aa6c 100644
--- a/flumine/strategy/strategy.py
+++ b/flumine/strategy/strategy.py
@@ -1,9 +1,9 @@
-from typing import Type, Iterator
-
 import logging
-
+import warnings
+from typing import Type, Iterator
 from betfairlightweight import filters
 from betfairlightweight.resources import MarketBook
+
 from .runnercontext import RunnerContext
 from ..clients import BaseClient
 from ..markets.market import Market
@@ -131,47 +131,50 @@ class BaseStrategy:
         self,
         market: Market,
         order,
-        batch: bool = True,  # batch placeRequest with other orders
         market_version: int = None,
     ) -> bool:
-        """Returns True if passes
-        validate_order and placed.
-        """
-        runner_context = self.get_runner_context(*order.lookup)
-        if self.validate_order(runner_context, order):
-            runner_context.place()
-            market.place_order(order, batch, market_version)
-            return True
-        else:
-            return False
+        warnings.warn(
+            "strategy.place_order is deprecated and will be removed from v1.18 onwards, use market.place_order instead",
+            PendingDeprecationWarning,
+        )
+        return market.place_order(order, market_version)
 
     def cancel_order(
         self,
         market: Market,
         order,
         size_reduction: float = None,
-        batch: bool = True,  # batch cancelRequest with other orders
-    ) -> None:
-        market.cancel_order(order, size_reduction, batch)
+    ) -> bool:
+        warnings.warn(
+            "strategy.cancel_order is deprecated and will be removed from v1.18 onwards, use market.cancel_order instead",
+            PendingDeprecationWarning,
+        )
+        return market.cancel_order(order, size_reduction)
 
     def update_order(
         self,
         market: Market,
         order,
         new_persistence_type: str,
-        batch: bool = True,  # batch updateRequest with other orders
-    ) -> None:
-        market.update_order(order, new_persistence_type, batch)
+    ) -> bool:
+        warnings.warn(
+            "strategy.update_order is deprecated and will be removed from v1.18 onwards, use market.update_order instead",
+            PendingDeprecationWarning,
+        )
+        return market.update_order(order, new_persistence_type)
 
     def replace_order(
         self,
         market: Market,
         order,
         new_price: float,
-        batch: bool = True,  # batch replaceRequest with other orders
         market_version: int = None,
-    ) -> None:
-        market.replace_order(order, new_price, batch, market_version)
+    ) -> bool:
+        warnings.warn(
+            "strategy.replace_order is deprecated and will be removed from v1.18 onwards, use market.replace_order instead",
+            PendingDeprecationWarning,
+        )
+        return market.replace_order(order, new_price, market_version)
 
     def validate_order(self, runner_context: RunnerContext, order) -> bool:
         # validate context
diff --git a/flumine/streams/datastream.py b/flumine/streams/datastream.py
index 2401c36..4781d83 100644
--- a/flumine/streams/datastream.py
+++ b/flumine/streams/datastream.py
@@ -100,7 +100,15 @@ class DataStream(BaseStream):
 
     @retry(wait=wait_exponential(multiplier=1, min=2, max=20))
     def run(self) -> None:
-        logger.info("Starting DataStream")
+        logger.info(
+            "Starting DataStream {0}".format(self.stream_id),
+            extra={
+                "stream_id": self.stream_id,
+                "market_filter": self.market_filter,
+                "market_data_filter": self.market_data_filter,
+                "conflate_ms": self.conflate_ms,
+            },
+        )
 
         self._stream = self.betting_client.streaming.create_stream(
             unique_id=self.stream_id, listener=self._listener
@@ -115,9 +123,13 @@ class DataStream(BaseStream):
             )
             self._stream.start()
         except BetfairError:
-            logger.error("DataStream run error", exc_info=True)
+            logger.error(
+                "DataStream {0} run error".format(self.stream_id), exc_info=True
+            )
             raise
         except Exception:
-            logger.critical("DataStream run error", exc_info=True)
+            logger.critical(
+                "DataStream {0} run error".format(self.stream_id), exc_info=True
+            )
             raise
         logger.info("Stopped DataStream {0}".format(self.stream_id))
diff --git a/flumine/streams/marketstream.py b/flumine/streams/marketstream.py
index 7ad9927..bc3af27 100644
--- a/flumine/streams/marketstream.py
+++ b/flumine/streams/marketstream.py
@@ -12,10 +12,20 @@ logger = logging.getLogger(__name__)
 class MarketStream(BaseStream):
     @retry(wait=wait_exponential(multiplier=1, min=2, max=20))
     def run(self) -> None:
-        logger.info("Starting MarketStream")
-
+        logger.info(
+            "Starting MarketStream {0}".format(self.stream_id),
+            extra={
+                "stream_id": self.stream_id,
+                "market_filter": self.market_filter,
+                "market_data_filter": self.market_data_filter,
+                "conflate_ms": self.conflate_ms,
+                "streaming_timeout": self.streaming_timeout,
+            },
+        )
         if not self._output_thread.is_alive():
-            logger.info("Starting output_thread {0}".format(self._output_thread))
+            logger.info(
+                "Starting output_thread (MarketStream {0})".format(self.stream_id)
+            )
             self._output_thread.start()
 
         self._stream = self.betting_client.streaming.create_stream(
@@ -31,10 +41,14 @@ class MarketStream(BaseStream):
             )
             self._stream.start()
         except BetfairError:
-            logger.error("MarketStream run error", exc_info=True)
+            logger.error(
+                "MarketStream {0} run error".format(self.stream_id), exc_info=True
+            )
             raise
         except Exception:
-            logger.critical("MarketStream run error", exc_info=True)
+            logger.critical(
+                "MarketStream {0} run error".format(self.stream_id), exc_info=True
+            )
             raise
         logger.info("Stopped MarketStream {0}".format(self.stream_id))
 
diff --git a/flumine/streams/orderstream.py b/flumine/streams/orderstream.py
index 37be2fd..d7e8f56 100644
--- a/flumine/streams/orderstream.py
+++ b/flumine/streams/orderstream.py
@@ -13,10 +13,19 @@ logger = logging.getLogger(__name__)
 class OrderStream(BaseStream):
     @retry(wait=wait_exponential(multiplier=1, min=2, max=20))
     def run(self) -> None:
-        logger.info("Starting OrderStream")
-
+        logger.info(
+            "Starting OrderStream {0}".format(self.stream_id),
+            extra={
+                "stream_id": self.stream_id,
+                "customer_strategy_refs": config.hostname,
+                "conflate_ms": self.conflate_ms,
+                "streaming_timeout": self.streaming_timeout,
+            },
+        )
         if not self._output_thread.is_alive():
-            logger.info("Starting output_thread {0}".format(self._output_thread))
+            logger.info(
+                "Starting output_thread (OrderStream {0})".format(self.stream_id)
+            )
             self._output_thread.start()
 
         self._stream = self.betting_client.streaming.create_stream(
@@ -33,10 +42,14 @@ class OrderStream(BaseStream):
             )
             self._stream.start()
         except BetfairError:
-            logger.error("OrderStream run error", exc_info=True)
+            logger.error(
+                "OrderStream {0} run error".format(self.stream_id), exc_info=True
+            )
             raise
         except Exception:
-            logger.critical("OrderStream run error", exc_info=True)
+            logger.critical(
+                "OrderStream {0} run error".format(self.stream_id), exc_info=True
+            )
             raise
         logger.info("Stopped OrderStream {0}".format(self.stream_id))
 
diff --git a/flumine/streams/simulatedorderstream.py b/flumine/streams/simulatedorderstream.py
index 3ed6798..e51372f 100644
--- a/flumine/streams/simulatedorderstream.py
+++ b/flumine/streams/simulatedorderstream.py
@@ -15,7 +15,13 @@ class CurrentOrders:
 
 class SimulatedOrderStream(BaseStream):
     def run(self) -> None:
-        logger.info("Starting SimulatedOrderStream")
+        logger.info(
+            "Starting SimulatedOrderStream {0}".format(self.stream_id),
+            extra={
+                "stream_id": self.stream_id,
+                "streaming_timeout": self.streaming_timeout,
+            },
+        )
 
         while self.is_alive():
             if self.flumine.markets.live_orders:
diff --git a/flumine/utils.py b/flumine/utils.py
index c0b396c..e5f9705 100644
--- a/flumine/utils.py
+++ b/flumine/utils.py
@@ -43,31 +43,6 @@ def chunks(l: list, n: int) -> list:
         yield l[i : i + n]
 
 
-def batch_orders(orders: list) -> dict:
-    # batch by market_version -> batch
-    _return_orders = defaultdict(list)  # market_version: [[<Order>, ], .. ]
-    a = _split_orders_by(orders, "market_version")
-    for market_version, a_orders in a.items():
-        b = _split_orders_by(a_orders, "batch", True)
-        for batch, b_orders in b.items():
-            _temp = []
-            for order, _ in b_orders:
-                if batch is False:
-                    _return_orders[market_version].append([order])
-                else:
-                    _temp.append(order)
-            if _temp:
-                _return_orders[market_version].append(_temp)
-    return _return_orders
-
-
-def _split_orders_by(orders: list, by: str, default=None) -> dict:
-    res = defaultdict(list)
-    for order, request_data in orders:
-        res[request_data.get(by, default)].append((order, request_data))
-    return res
-
-
 def create_cheap_hash(txt: str, length: int = 15) -> str:
     # This is just a hash for debugging purposes.
     #    It does not need to be unique, just fast and short.
diff --git a/mkdocs.yml b/mkdocs.yml
index d3652ed..6c584ee 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -19,7 +19,7 @@ nav:
     - Clients / Execution: 'execution.md'
     - Trades / Orders: 'trades.md'
     - Advanced Usage: 'advanced.md'
-    - Design: 'design.md'
+    - Architecture: 'architecture.md'
     - Help: 'help.md'
 
 markdown_extensions:


Test patch:
diff --git a/flumine/backtest/backtest.py b/flumine/backtest/backtest.py
index 986c4fb..9b530d6 100644
--- a/flumine/backtest/backtest.py
+++ b/flumine/backtest/backtest.py
@@ -7,6 +7,7 @@ from .. import config, utils
 from ..clients import ExchangeType
 from ..exceptions import RunError
 from ..order.orderpackage import OrderPackageType
+from ..order.trade import TradeStatus
 from ..order import process
 
 logger = logging.getLogger(__name__)
@@ -23,7 +24,7 @@ class FlumineBacktest(BaseFlumine):
 
     def __init__(self, client):
         super(FlumineBacktest, self).__init__(client)
-        self._pending_packages = []
+        self.handler_queue = []
 
     def run(self) -> None:
         if self.client.EXCHANGE != ExchangeType.SIMULATED:
@@ -42,7 +43,7 @@ class FlumineBacktest(BaseFlumine):
                 for event in stream_gen():
                     self._process_market_books(events.MarketBookEvent(event))
 
-                self._pending_packages.clear()
+                self.handler_queue.clear()
 
                 logger.info(
                     "Completed historical market '{0}'".format(stream.market_filter)
@@ -61,7 +62,7 @@ class FlumineBacktest(BaseFlumine):
             config.current_time = market_book.publish_time
 
             # check if there are orders to process
-            if self._pending_packages:
+            if self.handler_queue:
                 self._check_pending_packages()
 
             if market_book.status == "CLOSED":
@@ -92,7 +93,9 @@ class FlumineBacktest(BaseFlumine):
                         strategy.process_market_book, market, market_book
                     )
 
-            self._process_market_orders()
+    def process_order_package(self, order_package) -> None:
+        # place in pending list (wait for latency+delay)
+        self.handler_queue.append(order_package)
 
     def _process_backtest_orders(self, market) -> None:
         """Remove order from blotter live
@@ -102,50 +105,20 @@ class FlumineBacktest(BaseFlumine):
         blotter = market.blotter
         for order in blotter.live_orders:
             process.process_current_order(order)
-            if order.trade.status.value == "Complete":
+            if order.trade.status == TradeStatus.COMPLETE:
                 blotter.complete_order(order)
         for strategy in self.strategies:
             strategy_orders = blotter.strategy_orders(strategy)
             strategy.process_orders(market, strategy_orders)
 
-    def _process_market_orders(self) -> None:
-        """Add any packages to pending
-        packages for later execution
-        """
-        for market in self.markets:
-            if market.blotter.pending_orders:
-                bet_delay = market.market_book.bet_delay
-                for order_package in market.blotter.process_orders(
-                    self.client, bet_delay
-                ):
-                    self._pending_packages.append(order_package)
-
     def _check_pending_packages(self):
         processed = []
-        for order_package in self._pending_packages:
-            client = order_package.client
-            if order_package.package_type == OrderPackageType.PLACE:
-                if order_package.elapsed_seconds > (
-                    client.execution.PLACE_LATENCY + order_package.bet_delay
-                ):
-                    self._process_order_package(order_package)
-                    processed.append(order_package)
-            elif order_package.package_type == OrderPackageType.CANCEL:
-                if order_package.elapsed_seconds > client.execution.CANCEL_LATENCY:
-                    self._process_order_package(order_package)
-                    processed.append(order_package)
-            elif order_package.package_type == OrderPackageType.UPDATE:
-                if order_package.elapsed_seconds > client.execution.UPDATE_LATENCY:
-                    self._process_order_package(order_package)
-                    processed.append(order_package)
-            elif order_package.package_type == OrderPackageType.REPLACE:
-                if order_package.elapsed_seconds > (
-                    client.execution.REPLACE_LATENCY + order_package.bet_delay
-                ):
-                    self._process_order_package(order_package)
-                    processed.append(order_package)
+        for order_package in self.handler_queue:
+            if order_package.elapsed_seconds > order_package.simulated_delay:
+                order_package.client.execution.handler(order_package)
+                processed.append(order_package)
         for p in processed:
-            self._pending_packages.remove(p)
+            self.handler_queue.remove(p)
 
     def _monkey_patch_datetime(self):
         config.current_time = datetime.datetime.utcnow()
diff --git a/tests/test_baseflumine.py b/tests/test_baseflumine.py
index 55c922b..634677d 100644
--- a/tests/test_baseflumine.py
+++ b/tests/test_baseflumine.py
@@ -15,7 +15,7 @@ class BaseFlumineTest(unittest.TestCase):
         self.assertFalse(self.base_flumine._running)
         self.assertEqual(self.base_flumine._market_middleware, [])
         self.assertEqual(self.base_flumine._logging_controls, [])
-        self.assertEqual(len(self.base_flumine._trading_controls), 2)
+        self.assertEqual(len(self.base_flumine.trading_controls), 2)
         self.assertEqual(self.base_flumine._workers, [])
 
     @mock.patch("flumine.baseflumine.SimulatedMiddleware")
@@ -67,7 +67,7 @@ class BaseFlumineTest(unittest.TestCase):
     def test_add_trading_control(self):
         mock_control = mock.Mock()
         self.base_flumine.add_trading_control(mock_control)
-        self.assertEqual(len(self.base_flumine._trading_controls), 3)
+        self.assertEqual(len(self.base_flumine.trading_controls), 3)
 
     def test_add_market_middleware(self):
         mock_middleware = mock.Mock()
@@ -97,59 +97,12 @@ class BaseFlumineTest(unittest.TestCase):
         mock_event.event = [mock_market_book]
         self.base_flumine._process_market_books(mock_event)
 
-    def test__process_market_orders(self):
-        mock_market = mock.Mock()
-        mock_market.blotter.pending_orders = True
-        mock_market.blotter.process_orders.return_value = [1, 2, 3]
-        mock_market_two = mock.Mock()
-        mock_market_two.blotter.pending_orders = False
-        self.base_flumine.markets = [mock_market, mock_market_two]
-        self.base_flumine._process_market_orders()
-        mock_market.blotter.process_orders.assert_called_with(
-            self.mock_client, mock_market.market_book.bet_delay
-        )
-        mock_market_two.blotter.process_orders.assert_not_called()
-
-    def test__process_order_package(self):
-        mock_trading_control = mock.Mock()
-        self.base_flumine._trading_controls = [mock_trading_control]
-        mock_client_trading_control = mock.Mock()
+    def test_process_order_package(self):
         mock_order_package = mock.Mock()
-        mock_order_package.market_id = "1.123"
-        mock_order_package.client.trading_controls = [mock_client_trading_control]
-        self.base_flumine._process_order_package(mock_order_package)
+        self.base_flumine.process_order_package(mock_order_package)
         mock_order_package.client.execution.handler.assert_called_with(
             mock_order_package
         )
-        mock_trading_control.assert_called_with(mock_order_package)
-        mock_client_trading_control.assert_called_with(mock_order_package)
-
-    def test__process_order_package_empty(self):
-        mock_client_trading_control = mock.Mock()
-        mock_order_package = mock.Mock()
-        mock_order_package.market_id = "1.123"
-        mock_order_package.client.trading_controls = [mock_client_trading_control]
-        mock_order_package.info = {}
-        mock_order_package.orders = []
-        self.base_flumine._trading_controls = []
-        self.base_flumine._process_order_package(mock_order_package)
-        mock_order_package.client.execution.handler.assert_not_called()
-
-    def test__process_order_package_controls(self):
-        mock_trading_control = mock.Mock()
-        mock_client_control = mock.Mock()
-        self.base_flumine._trading_controls = [mock_trading_control]
-        mock_client_trading_control = mock.Mock()
-        mock_order_package = mock.Mock()
-        mock_order_package.market_id = "1.123"
-        mock_order_package.client.trading_controls = [mock_client_trading_control]
-        mock_order_package.info = {}
-        mock_order_package.orders = []
-        mock_order_package.client.trading_controls = [mock_client_control]
-        self.base_flumine._process_order_package(mock_order_package)
-        mock_order_package.client.execution.handler.assert_not_called()
-        mock_trading_control.assert_called_with(mock_order_package)
-        mock_client_control.assert_called_with(mock_order_package)
 
     @mock.patch("flumine.baseflumine.Market")
     def test__add_market(self, mock_market):
@@ -230,14 +183,12 @@ class BaseFlumineTest(unittest.TestCase):
         mock_event.event = [mock_current_orders]
         self.base_flumine._process_current_orders(mock_event)
 
-    @mock.patch("flumine.baseflumine.BaseFlumine._process_market_orders")
-    def test__process_custom_event(self, mock__process_market_orders):
+    def test__process_custom_event(self):
         mock_market = mock.Mock()
         self.base_flumine.markets = [mock_market]
         mock_event = mock.Mock()
         self.base_flumine._process_custom_event(mock_event)
         mock_event.callback.assert_called_with(self.base_flumine, mock_event)
-        mock__process_market_orders.assert_called_with()
 
     @mock.patch("flumine.baseflumine.BaseFlumine.info")
     @mock.patch("flumine.baseflumine.BaseFlumine.log_control")
diff --git a/tests/test_blotter.py b/tests/test_blotter.py
index 3786df0..fee889f 100644
--- a/tests/test_blotter.py
+++ b/tests/test_blotter.py
@@ -1,7 +1,7 @@
 import unittest
 from unittest import mock
 
-from flumine.markets.blotter import Blotter, OrderPackageType
+from flumine.markets.blotter import Blotter
 from flumine.order.order import OrderStatus
 from flumine.order.ordertype import MarketOnCloseOrder, LimitOrder, LimitOnCloseOrder
 
@@ -13,10 +13,6 @@ class BlotterTest(unittest.TestCase):
     def test_init(self):
         self.assertEqual(self.blotter.market_id, "1.23")
         self.assertEqual(self.blotter._orders, {})
-        self.assertEqual(self.blotter.pending_place, [])
-        self.assertEqual(self.blotter.pending_cancel, [])
-        self.assertEqual(self.blotter.pending_update, [])
-        self.assertEqual(self.blotter.pending_replace, [])
         self.assertEqual(self.blotter._live_orders, [])
 
     def test_strategy_orders(self):
@@ -26,94 +22,6 @@ class BlotterTest(unittest.TestCase):
         self.assertEqual(self.blotter.strategy_orders(12), [])
         self.assertEqual(self.blotter.strategy_orders(69), [mock_order])
 
-    @mock.patch("flumine.markets.blotter.Blotter._create_packages")
-    def test_process_orders(self, mock__create_packages):
-        mock_client = mock.Mock()
-        self.blotter.pending_place = [1]
-        self.assertEqual(
-            self.blotter.process_orders(mock_client), mock__create_packages().__radd__()
-        )
-        self.blotter.pending_place = []
-        self.blotter.pending_cancel = [2]
-        self.assertEqual(
-            self.blotter.process_orders(mock_client), mock__create_packages().__radd__()
-        )
-        self.blotter.pending_cancel = []
-        self.blotter.pending_update = [3]
-        self.assertEqual(
-            self.blotter.process_orders(mock_client), mock__create_packages().__radd__()
-        )
-        self.blotter.pending_update = []
-        self.blotter.pending_replace = [4]
-        self.assertEqual(
-            self.blotter.process_orders(mock_client), mock__create_packages().__radd__()
-        )
-
-    @mock.patch("flumine.markets.blotter.BetfairOrderPackage")
-    def test___create_packages(self, mock_cls):
-        mock_cls.order_limit.return_value = 10
-        mock_client = mock.Mock()
-        mock_order_one = mock.Mock()
-        mock_order_two = mock.Mock()
-        mock_order_three = mock.Mock()
-        mock_order_four = mock.Mock()
-        mock_order_five = mock.Mock()
-        mock_orders = [
-            (mock_order_one, {}),
-            (mock_order_two, {"batch": False}),
-            (mock_order_three, {"market_version": 123}),
-            (mock_order_four, {"market_version": 123}),
-            (mock_order_five, {"market_version": 123, "batch": False}),
-        ]
-        packages = self.blotter._create_packages(
-            mock_client, mock_orders, OrderPackageType.PLACE, 0
-        )
-        self.assertEqual(len(packages), 4)
-        mock_cls.order_limit.assert_called_with(OrderPackageType.PLACE)
-        self.assertEqual(
-            mock_cls.call_args_list,
-            [
-                mock.call(
-                    client=mock_client,
-                    market_id=self.blotter.market_id,
-                    orders=[mock_order_one],
-                    package_type=OrderPackageType.PLACE,
-                    bet_delay=0,
-                    market_version=None,
-                ),
-                mock.call(
-                    client=mock_client,
-                    market_id=self.blotter.market_id,
-                    orders=[mock_order_two],
-                    package_type=OrderPackageType.PLACE,
-                    bet_delay=0,
-                    market_version=None,
-                ),
-                mock.call(
-                    client=mock_client,
-                    market_id=self.blotter.market_id,
-                    orders=[mock_order_three, mock_order_four],
-                    package_type=OrderPackageType.PLACE,
-                    bet_delay=0,
-                    market_version=123,
-                ),
-                mock.call(
-                    client=mock_client,
-                    market_id=self.blotter.market_id,
-                    orders=[mock_order_five],
-                    package_type=OrderPackageType.PLACE,
-                    bet_delay=0,
-                    market_version=123,
-                ),
-            ],
-        )
-        self.assertEqual(mock_orders, [])
-
-    def test_pending_orders(self):
-        self.assertFalse(self.blotter.pending_orders)
-        self.blotter.pending_update = [1]
-        self.assertTrue(self.blotter.pending_orders)
-
     def test_live_orders(self):
         self.assertEqual(list(self.blotter.live_orders), [])
         mock_order = mock.Mock(complete=False)
diff --git a/tests/test_clientcontrols.py b/tests/test_clientcontrols.py
index f795497..b00bb96 100644
--- a/tests/test_clientcontrols.py
+++ b/tests/test_clientcontrols.py
@@ -7,6 +7,7 @@ from flumine.controls.clientcontrols import (
     MaxOrderCount,
     OrderPackageType,
 )
+from flumine.exceptions import ControlError
 
 
 class TestBaseControl(unittest.TestCase):
@@ -21,25 +22,19 @@ class TestBaseControl(unittest.TestCase):
 
     @mock.patch("flumine.controls.BaseControl._validate")
     def test_call(self, mock_validate):
-        order_package = mock.Mock()
-        self.control(order_package)
-
-        mock_validate.assert_called_with(order_package)
+        order = mock.Mock()
+        self.control(order, OrderPackageType.PLACE)
+        mock_validate.assert_called_with(order, OrderPackageType.PLACE)
 
     def test_validate(self):
         with self.assertRaises(NotImplementedError):
-            self.control._validate(None)
+            self.control._validate(None, None)
 
     def test_on_error(self):
         order = mock.Mock()
         order.info = {"hello": "world"}
-        self.control._on_error(order, "test")
-        order.violation.assert_called_with("Order has violated: None Error: test")
-
-    def test_on_error_offset(self):
-        order = mock.Mock()
-        order.info = {"hello": "world"}
-        self.control._on_error(order, "test")
+        with self.assertRaises(ControlError):
+            self.control._on_error(order, "test")
         order.violation.assert_called_with("Order has violated: None Error: test")
 
 
@@ -71,12 +66,8 @@ class TestMaxOrderCount(unittest.TestCase):
     def test_validate_place(
         self, mock_check_hour, mock__check_transaction_count, mock__set_next_hour
     ):
-        mock_package = mock.Mock()
-        mock_package.package_type = OrderPackageType.PLACE
-        mock_package.__len__ = mock.Mock()
-        mock_package.__len__.return_value = 1
-        self.trading_control._validate(mock_package)
-
+        mock_order = mock.Mock()
+        self.trading_control._validate(mock_order, OrderPackageType.PLACE)
         mock_check_hour.assert_called()
         mock__check_transaction_count.assert_called_with(1)
         mock__set_next_hour.assert_called()
@@ -84,34 +75,22 @@ class TestMaxOrderCount(unittest.TestCase):
 
     @mock.patch("flumine.controls.clientcontrols.MaxOrderCount._check_hour")
     def test_validate_cancel(self, mock_check_hour):
-        mock_package = mock.Mock()
-        mock_package.package_type = OrderPackageType.CANCEL
-        mock_package.__len__ = mock.Mock()
-        mock_package.__len__.return_value = 1
-        self.trading_control._validate(mock_package)
-
+        mock_order = mock.Mock()
+        self.trading_control._validate(mock_order, OrderPackageType.CANCEL)
         mock_check_hour.assert_called_with()
         self.assertEqual(self.trading_control.cancel_requests, 1)
 
     @mock.patch("flumine.controls.clientcontrols.MaxOrderCount._check_hour")
     def test_validate_update(self, mock_check_hour):
-        mock_package = mock.Mock()
-        mock_package.package_type = OrderPackageType.UPDATE
-        mock_package.__len__ = mock.Mock()
-        mock_package.__len__.return_value = 1
-        self.trading_control._validate(mock_package)
-
+        mock_order = mock.Mock()
+        self.trading_control._validate(mock_order, OrderPackageType.UPDATE)
         mock_check_hour.assert_called_with()
         self.assertEqual(self.trading_control.update_requests, 1)
 
     @mock.patch("flumine.controls.clientcontrols.MaxOrderCount._check_hour")
     def test_validate_replace(self, mock_check_hour):
-        mock_package = mock.Mock()
-        mock_package.package_type = OrderPackageType.REPLACE
-        mock_package.__len__ = mock.Mock()
-        mock_package.__len__.return_value = 1
-        self.trading_control._validate(mock_package)
-
+        mock_order = mock.Mock()
+        self.trading_control._validate(mock_order, OrderPackageType.REPLACE)
         mock_check_hour.assert_called_with()
         self.assertEqual(self.trading_control.replace_requests, 1)
 
diff --git a/tests/test_execution.py b/tests/test_execution.py
index f734e0d..66ff6d9 100644
--- a/tests/test_execution.py
+++ b/tests/test_execution.py
@@ -23,7 +23,7 @@ class BaseExecutionTest(unittest.TestCase):
         self.execution = BaseExecution(self.mock_flumine, max_workers=2)
 
     def test_init(self):
-        self.assertEqual(MAX_WORKERS, 16)
+        self.assertEqual(MAX_WORKERS, 32)
         self.assertEqual(MAX_SESSION_AGE, 200)
         self.assertEqual(BET_ID_START, 100000000000)
         self.assertEqual(self.execution.flumine, self.mock_flumine)
@@ -190,9 +190,6 @@ 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_handler_queue(self):
-        self.assertEqual(self.execution.handler_queue, self.mock_flumine.handler_queue)
-
     def test_shutdown(self):
         self.execution.shutdown()
         self.assertTrue(self.execution._thread_pool._shutdown)
@@ -694,13 +691,11 @@ class BetfairExecutionTest(unittest.TestCase):
         mock_trading_function.assert_not_called()
         mock__return_http_session.assert_called_with(mock_session)
 
-    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.handler_queue")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.handler")
     @mock.patch(
         "flumine.execution.betfairexecution.BetfairExecution._return_http_session"
     )
-    def test__execution_helper_error(
-        self, mock__return_http_session, mock_handler_queue
-    ):
+    def test__execution_helper_error(self, mock__return_http_session, mock_handler):
         mock_trading_function = mock.Mock()
         mock_trading_function.__name__ = "test"
         mock_trading_function.side_effect = BetfairError()
@@ -715,14 +710,14 @@ class BetfairExecutionTest(unittest.TestCase):
         )
         mock_trading_function.assert_called_with(mock_order_package, mock_session)
         mock__return_http_session.assert_called_with(mock_session, err=True)
-        mock_handler_queue.put.assert_called_with(mock_order_package)
+        mock_handler.assert_called_with(mock_order_package)
 
-    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.handler_queue")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.handler")
     @mock.patch(
         "flumine.execution.betfairexecution.BetfairExecution._return_http_session"
     )
     def test__execution_helper_error_no_retry(
-        self, mock__return_http_session, mock_handler_queue
+        self, mock__return_http_session, mock_handler
     ):
         mock_trading_function = mock.Mock()
         mock_trading_function.__name__ = "test"
@@ -738,7 +733,7 @@ class BetfairExecutionTest(unittest.TestCase):
         )
         mock_trading_function.assert_called_with(mock_order_package, mock_session)
         mock__return_http_session.assert_called_with(mock_session, err=True)
-        mock_handler_queue.put.assert_not_called()
+        mock_handler.put.assert_not_called()
 
     @mock.patch(
         "flumine.execution.betfairexecution.BetfairExecution._return_http_session"
diff --git a/tests/test_flumine.py b/tests/test_flumine.py
index ab9c3b9..7022306 100644
--- a/tests/test_flumine.py
+++ b/tests/test_flumine.py
@@ -3,7 +3,6 @@ from unittest import mock
 
 from flumine import Flumine, worker
 from flumine.events import events
-from flumine.order.orderpackage import BaseOrderPackage
 
 
 class FlumineTest(unittest.TestCase):
@@ -16,7 +15,6 @@ class FlumineTest(unittest.TestCase):
     @mock.patch("flumine.flumine.Flumine._process_cleared_orders")
     @mock.patch("flumine.flumine.Flumine._process_cleared_markets")
     @mock.patch("flumine.flumine.Flumine._process_close_market")
-    @mock.patch("flumine.flumine.Flumine._process_order_package")
     @mock.patch("flumine.flumine.Flumine._process_current_orders")
     @mock.patch("flumine.flumine.Flumine._process_end_flumine")
     @mock.patch("flumine.flumine.Flumine._process_market_catalogues")
@@ -29,7 +27,6 @@ class FlumineTest(unittest.TestCase):
         mock__process_market_catalogues,
         mock__process_end_flumine,
         mock__process_current_orders,
-        mock__process_order_package,
         mock__process_close_market,
         mock__process_cleared_markets,
         mock__process_cleared_orders,
@@ -41,7 +38,6 @@ class FlumineTest(unittest.TestCase):
             events.MarketBookEvent(None),
             events.RawDataEvent(None),
             events.CurrentOrdersEvent(None),
-            BaseOrderPackage(None, "1.123", [], "12", None),
             events.ClearedMarketsEvent(None),
             events.ClearedOrdersEvent(None),
             events.CloseMarketEvent(None),
@@ -58,11 +54,10 @@ class FlumineTest(unittest.TestCase):
         mock__process_market_catalogues.assert_called_with(mock_events[0])
         mock__process_end_flumine.assert_called_with()
         mock__process_current_orders.assert_called_with(mock_events[3])
-        mock__process_order_package.assert_called_with(mock_events[4])
-        mock__process_close_market.assert_called_with(mock_events[7])
-        mock__process_cleared_markets.assert_called_with(mock_events[5])
-        mock__process_cleared_orders.assert_called_with(mock_events[6])
-        mock__process_custom_event.assert_called_with(mock_events[8])
+        mock__process_close_market.assert_called_with(mock_events[6])
+        mock__process_cleared_markets.assert_called_with(mock_events[4])
+        mock__process_cleared_orders.assert_called_with(mock_events[5])
+        mock__process_custom_event.assert_called_with(mock_events[7])
         mock__add_default_workers.assert_called()
 
     @mock.patch("flumine.worker.BackgroundWorker")
diff --git a/tests/test_fluminebacktest.py b/tests/test_fluminebacktest.py
index d54e36f..2191af8 100644
--- a/tests/test_fluminebacktest.py
+++ b/tests/test_fluminebacktest.py
@@ -6,6 +6,7 @@ from flumine.clients import ExchangeType
 from flumine.order.orderpackage import OrderPackageType
 from flumine import config
 from flumine.exceptions import RunError
+from flumine.order.trade import TradeStatus
 
 
 class FlumineBacktestTest(unittest.TestCase):
@@ -47,16 +48,14 @@ class FlumineBacktestTest(unittest.TestCase):
         mock__process_end_flumine.assert_called_with()
         mock__unpatch_datetime.assert_called_with()
 
-    @mock.patch("flumine.backtest.backtest.FlumineBacktest._process_market_orders")
     @mock.patch("flumine.backtest.backtest.FlumineBacktest._process_backtest_orders")
     @mock.patch("flumine.backtest.backtest.FlumineBacktest._check_pending_packages")
     def test__process_market_books(
         self,
         mock__check_pending_packages,
         mock__process_backtest_orders,
-        mock__process_market_orders,
     ):
-        self.flumine._pending_packages = [mock.Mock()]
+        self.flumine.handler_queue.append(mock.Mock())
         mock_event = mock.Mock()
         mock_market_book = mock.Mock(market_id="1.23")
         mock_market_book.runners = []
@@ -67,13 +66,17 @@ class FlumineBacktestTest(unittest.TestCase):
         self.flumine._process_market_books(mock_event)
         mock__check_pending_packages.assert_called_with()
         mock__process_backtest_orders.assert_called_with(mock_market)
-        mock__process_market_orders.assert_called_with()
+
+    def test_process_order_package(self):
+        mock_order_package = mock.Mock()
+        self.flumine.process_order_package(mock_order_package)
+        self.assertEqual(self.flumine.handler_queue, [mock_order_package])
 
     @mock.patch("flumine.backtest.backtest.process.process_current_order")
     def test__process_backtest_orders(self, mock_process_current_order):
         mock_market = mock.Mock(context={})
         mock_order = mock.Mock()
-        mock_order.trade.status.value = "Complete"
+        mock_order.trade.status = TradeStatus.COMPLETE
         mock_market.blotter.live_orders = [mock_order]
         self.flumine._process_backtest_orders(mock_market)
         mock_process_current_order.assert_called_with(mock_order)
@@ -89,117 +92,93 @@ class FlumineBacktestTest(unittest.TestCase):
             mock_market, mock_market.blotter.strategy_orders(mock_strategy)
         )
 
-    def test__process_market_orders(self):
-        self.flumine._pending_packages = []
-        mock_order_package = mock.Mock()
-        mock_market = mock.Mock()
-        mock_market.blotter.process_orders.return_value = [mock_order_package]
-        self.flumine.markets = [mock_market]
-        self.flumine._process_market_orders()
-        mock_market.blotter.process_orders.assert_called_with(
-            self.flumine.client, mock_market.market_book.bet_delay
-        )
-        self.assertEqual(self.flumine._pending_packages, [mock_order_package])
-
-    @mock.patch("flumine.backtest.backtest.FlumineBacktest._process_order_package")
-    def test__check_pending_packages_place(self, mock__process_order_package):
+    def test__check_pending_packages_place(self):
         mock_client = mock.Mock()
-        mock_client.execution.PLACE_LATENCY = 2.0
         mock_order_package = mock.Mock(
             package_type=OrderPackageType.PLACE,
             elapsed_seconds=5,
             bet_delay=1,
             client=mock_client,
+            simulated_delay=1.2,
         )
-        self.flumine._pending_packages = [mock_order_package]
+        self.flumine.handler_queue = [mock_order_package]
         self.flumine._check_pending_packages()
-        mock__process_order_package.assert_called_with(mock_order_package)
+        mock_client.execution.handler(mock_order_package)
 
-    @mock.patch("flumine.backtest.backtest.FlumineBacktest._process_order_package")
-    def test__check_pending_packages_place_pending(self, mock__process_order_package):
+    def test__check_pending_packages_place_pending(self):
         mock_client = mock.Mock()
-        mock_client.execution.PLACE_LATENCY = 2.0
         mock_order_package = mock.Mock(
             package_type=OrderPackageType.PLACE,
             elapsed_seconds=2,
             bet_delay=1,
             client=mock_client,
+            simulated_delay=1.2,
         )
-        self.flumine._pending_packages = [mock_order_package]
+        self.flumine.handler_queue = [mock_order_package]
         self.flumine._check_pending_packages()
-        mock__process_order_package.assert_not_called()
+        mock_client.execution.handler(mock_order_package)
 
-    @mock.patch("flumine.backtest.backtest.FlumineBacktest._process_order_package")
-    def test__check_pending_packages_cancel(self, mock__process_order_package):
+    def test__check_pending_packages_cancel(self):
         mock_client = mock.Mock()
-        mock_client.execution.CANCEL_LATENCY = 2.0
         mock_order_package = mock.Mock(
-            package_type=OrderPackageType.CANCEL, elapsed_seconds=3, client=mock_client
+            elapsed_seconds=3, client=mock_client, simulated_delay=0.2
         )
-        self.flumine._pending_packages = [mock_order_package]
+        self.flumine.handler_queue = [mock_order_package]
         self.flumine._check_pending_packages()
-        mock__process_order_package.assert_called_with(mock_order_package)
+        mock_client.execution.handler(mock_order_package)
 
-    @mock.patch("flumine.backtest.backtest.FlumineBacktest._process_order_package")
-    def test__check_pending_packages_cancel_pending(self, mock__process_order_package):
+    def test__check_pending_packages_cancel_pending(self):
         mock_client = mock.Mock()
-        mock_client.execution.CANCEL_LATENCY = 2.0
         mock_order_package = mock.Mock(
-            package_type=OrderPackageType.CANCEL, elapsed_seconds=2, client=mock_client
+            elapsed_seconds=2, client=mock_client, simulated_delay=0.2
         )
-        self.flumine._pending_packages = [mock_order_package]
+        self.flumine.handler_queue = [mock_order_package]
         self.flumine._check_pending_packages()
-        mock__process_order_package.assert_not_called()
+        mock_client.execution.handler(mock_order_package)
 
-    @mock.patch("flumine.backtest.backtest.FlumineBacktest._process_order_package")
-    def test__check_pending_packages_update(self, mock__process_order_package):
+    def test__check_pending_packages_update(self):
         mock_client = mock.Mock()
-        mock_client.execution.UPDATE_LATENCY = 2.0
         mock_order_package = mock.Mock(
-            package_type=OrderPackageType.UPDATE, elapsed_seconds=3, client=mock_client
+            elapsed_seconds=3, client=mock_client, simulated_delay=0.2
         )
-        self.flumine._pending_packages = [mock_order_package]
+        self.flumine.handler_queue = [mock_order_package]
         self.flumine._check_pending_packages()
-        mock__process_order_package.assert_called_with(mock_order_package)
+        mock_client.execution.handler(mock_order_package)
 
-    @mock.patch("flumine.backtest.backtest.FlumineBacktest._process_order_package")
-    def test__check_pending_packages_update_pending(self, mock__process_order_package):
+    def test__check_pending_packages_update_pending(self):
         mock_client = mock.Mock()
-        mock_client.execution.UPDATE_LATENCY = 2.0
         mock_order_package = mock.Mock(
-            package_type=OrderPackageType.UPDATE, elapsed_seconds=2, client=mock_client
+            elapsed_seconds=2, client=mock_client, simulated_delay=0.2
         )
-        self.flumine._pending_packages = [mock_order_package]
+        self.flumine.handler_queue = [mock_order_package]
         self.flumine._check_pending_packages()
-        mock__process_order_package.assert_not_called()
+        mock_client.execution.handler(mock_order_package)
 
-    @mock.patch("flumine.backtest.backtest.FlumineBacktest._process_order_package")
-    def test__check_pending_packages_replace(self, mock__process_order_package):
+    def test__check_pending_packages_replace(self):
         mock_client = mock.Mock()
-        mock_client.execution.REPLACE_LATENCY = 2.0
         mock_order_package = mock.Mock(
             package_type=OrderPackageType.REPLACE,
             elapsed_seconds=5,
             bet_delay=1,
             client=mock_client,
+            simulated_delay=1.2,
         )
-        self.flumine._pending_packages = [mock_order_package]
+        self.flumine.handler_queue = [mock_order_package]
         self.flumine._check_pending_packages()
-        mock__process_order_package.assert_called_with(mock_order_package)
+        mock_client.execution.handler(mock_order_package)
 
-    @mock.patch("flumine.backtest.backtest.FlumineBacktest._process_order_package")
-    def test__check_pending_packages_replace_pending(self, mock__process_order_package):
+    def test__check_pending_packages_replace_pending(self):
         mock_client = mock.Mock()
-        mock_client.execution.REPLACE_LATENCY = 2.0
         mock_order_package = mock.Mock(
             package_type=OrderPackageType.REPLACE,
             elapsed_seconds=2,
             bet_delay=1,
             client=mock_client,
+            simulated_delay=1.2,
         )
-        self.flumine._pending_packages = [mock_order_package]
+        self.flumine.handler_queue.append(mock_order_package)
         self.flumine._check_pending_packages()
-        mock__process_order_package.assert_not_called()
+        mock_client.execution.handler(mock_order_package)
 
     @mock.patch("flumine.baseflumine.BaseFlumine.info")
     @mock.patch("flumine.baseflumine.BaseFlumine.log_control")
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 264a28d..f31d4c4 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -46,13 +46,13 @@ class IntegrationTest(unittest.TestCase):
                             side="LAY",
                             order_type=LimitOrder(lay, 2.00),
                         )
-                        self.place_order(market, order)
+                        market.place_order(order)
 
             def process_orders(self, market, orders):
                 for order in orders:
                     if order.status == OrderStatus.EXECUTABLE:
                         if order.elapsed_seconds and order.elapsed_seconds > 2:
-                            self.cancel_order(market, order)
+                            market.cancel_order(order)
 
         class MarketOnCloseOrders(BaseStrategy):
             def check_market_book(self, market, market_book):
@@ -76,7 +76,7 @@ class IntegrationTest(unittest.TestCase):
                                 side="LAY",
                                 order_type=MarketOnCloseOrder(100.00),
                             )
-                            self.place_order(market, order)
+                            market.place_order(order)
 
         client = clients.BacktestClient()
         framework = FlumineBacktest(client=client)
diff --git a/tests/test_markets.py b/tests/test_markets.py
index d15211f..ae2ed43 100644
--- a/tests/test_markets.py
+++ b/tests/test_markets.py
@@ -130,79 +130,33 @@ class MarketTest(unittest.TestCase):
         self.assertTrue(self.market.closed)
         self.assertIsNotNone(self.market.date_time_closed)
 
-    @mock.patch("flumine.markets.market.events")
-    def test_place_order(self, mock_events):
+    @mock.patch("flumine.markets.market.Market.transaction")
+    def test_place_order(self, mock_transaction):
+        mock_transaction.return_value.__enter__.return_value = mock_transaction
         mock_order = mock.Mock()
-        mock_order.id = "123"
-        mock_order.trade.market_notes = None
-        self.market.place_order(mock_order)
-        mock_order.place.assert_called_with(self.market.market_book.publish_time)
-        self.assertEqual(
-            self.market.blotter.pending_place,
-            [(mock_order, {"batch": True, "market_version": None})],
-        )
-        self.mock_flumine.log_control.assert_called_with(mock_events.TradeEvent())
-        mock_order.trade.update_market_notes.assert_called_with(self.market)
+        self.assertTrue(self.market.place_order(mock_order, 2, False))
+        mock_transaction.place_order.assert_called_with(mock_order, 2, False)
 
-    @mock.patch("flumine.markets.market.events")
-    def test_place_order_not_executed(self, mock_events):
+    @mock.patch("flumine.markets.market.Market.transaction")
+    def test_cancel_order(self, mock_transaction):
+        mock_transaction.return_value.__enter__.return_value = mock_transaction
         mock_order = mock.Mock()
-        mock_order.id = "123"
-        self.market.place_order(mock_order, execute=False)
-        mock_order.place.assert_called_with(self.market.market_book.publish_time)
-        self.assertEqual(self.market.blotter.pending_place, [])
-        self.mock_flumine.log_control.assert_called_with(mock_events.TradeEvent())
+        self.assertTrue(self.market.cancel_order(mock_order, 2.02))
+        mock_transaction.cancel_order.assert_called_with(mock_order, 2.02)
 
-    def test_place_order_retry(self):
-        mock_order = mock.Mock()
-        self.market.blotter._orders = {mock_order.id: mock_order}
-        self.market.place_order(mock_order)
-        self.assertEqual(self.market.blotter.pending_place, [])
-
-    def test_cancel_order(self):
-        mock_blotter = mock.Mock()
-        mock_blotter.pending_cancel = []
-        self.market.blotter = mock_blotter
+    @mock.patch("flumine.markets.market.Market.transaction")
+    def test_update_order(self, mock_transaction):
+        mock_transaction.return_value.__enter__.return_value = mock_transaction
         mock_order = mock.Mock()
-        self.market.cancel_order(mock_order, 0.01)
-        mock_order.cancel.assert_called_with(0.01)
-        self.assertEqual(
-            mock_blotter.pending_cancel,
-            [(mock_order, {"size_reduction": 0.01, "batch": True})],
-        )
+        self.assertTrue(self.market.update_order(mock_order, "test"))
+        mock_transaction.update_order.assert_called_with(mock_order, "test")
 
-    def test_update_order(self):
-        mock_blotter = mock.Mock()
-        mock_blotter.pending_update = []
-        self.market.blotter = mock_blotter
+    @mock.patch("flumine.markets.market.Market.transaction")
+    def test_replace_order(self, mock_transaction):
+        mock_transaction.return_value.__enter__.return_value = mock_transaction
         mock_order = mock.Mock()
-        self.market.update_order(mock_order, "PERSIST")
-        mock_order.update.assert_called_with("PERSIST")
-        self.assertEqual(
-            mock_blotter.pending_update,
-            [(mock_order, {"new_persistence_type": "PERSIST", "batch": True})],
-        )
-
-    def test_replace_order(self):
-        mock_blotter = mock.Mock()
-        mock_blotter.pending_replace = []
-        self.market.blotter = mock_blotter
-        mock_order = mock.Mock()
-        self.market.replace_order(mock_order, 1.01, True, 321)
-        mock_order.replace.assert_called_with(1.01)
-        self.assertEqual(
-            mock_blotter.pending_replace,
-            [
-                (
-                    mock_order,
-                    {
-                        "new_price": 1.01,
-                        "batch": True,
-                        "market_version": 321,
-                    },
-                )
-            ],
-        )
+        self.assertTrue(self.market.replace_order(mock_order, 2, False))
+        mock_transaction.replace_order.assert_called_with(mock_order, 2, False)
 
     def test_event(self):
         mock_market_catalogue = mock.Mock()
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
index 9d3d649..2ae65b2 100644
--- a/tests/test_middleware.py
+++ b/tests/test_middleware.py
@@ -225,7 +225,7 @@ class RunnerAnalyticsTest(unittest.TestCase):
     ):
         mock_runner = mock.Mock()
         self.runner_analytics(mock_runner, True)
-        mock__calculate_traded.assert_called_with(mock_runner)
+        mock__calculate_traded.assert_called_with(mock_runner.ex.traded_volume)
         mock__calculate_middle.assert_called_with(self.mock_runner)
         mock__calculate_matched.assert_called_with(mock_runner)
         self.assertEqual(
@@ -252,37 +252,32 @@ class RunnerAnalyticsTest(unittest.TestCase):
 
     def test__calculate_traded_dict_empty(self):
         self.runner_analytics._traded_volume = []
-        mock_runner = mock.Mock()
-        mock_runner.ex.traded_volume = []
-        self.assertEqual(self.runner_analytics._calculate_traded(mock_runner), {})
+        self.assertEqual(self.runner_analytics._calculate_traded([]), {})
 
     def test__calculate_traded_dict_same(self):
-        mock_runner = mock.Mock()
-        mock_runner.ex.traded_volume = [{"price": 1.01, "size": 69}]
+        traded_volume = [{"price": 1.01, "size": 69}]
         self.runner_analytics._traded_volume = [{"price": 1.01, "size": 69}]
         self.runner_analytics._p_v = {1.01: 69}
-        self.assertEqual(self.runner_analytics._calculate_traded(mock_runner), {})
+        self.assertEqual(self.runner_analytics._calculate_traded(traded_volume), {})
         self.assertEqual(self.runner_analytics._p_v, {1.01: 69})
 
     def test__calculate_traded_dict_new(self):
-        mock_runner = mock.Mock()
-        mock_runner.ex.traded_volume = [{"price": 1.01, "size": 69}]
+        traded_volume = [{"price": 1.01, "size": 69}]
         self.runner_analytics._traded_volume = []
         self.assertEqual(
-            self.runner_analytics._calculate_traded(mock_runner), {1.01: 67.0}
+            self.runner_analytics._calculate_traded(traded_volume), {1.01: 67.0}
         )
         self.assertEqual(self.runner_analytics._p_v, {1.01: 69})
 
     def test__calculate_traded_dict_new_multi(self):
-        mock_runner = mock.Mock()
-        mock_runner.ex.traded_volume = [
+        traded_volume = [
             {"price": 1.01, "size": 69},
             {"price": 10, "size": 32},
         ]
         self.runner_analytics._traded_volume = [{"price": 1.01, "size": 30}]
         self.runner_analytics._p_v = {1.01: 30}
         self.assertEqual(
-            self.runner_analytics._calculate_traded(mock_runner),
+            self.runner_analytics._calculate_traded(traded_volume),
             {1.01: 39.0, 10: 32},
         )
         self.assertEqual(self.runner_analytics._p_v, {1.01: 69, 10: 32})
diff --git a/tests/test_order.py b/tests/test_order.py
index c6aa74f..0014e06 100644
--- a/tests/test_order.py
+++ b/tests/test_order.py
@@ -274,6 +274,9 @@ class BetfairOrderTest(unittest.TestCase):
         self.order.place(123)
         mock_placing.assert_called_with()
         self.assertEqual(self.order.publish_time, 123)
+        self.order.trade.strategy.get_runner_context.assert_called_with(
+            *self.order.lookup
+        )
 
     @mock.patch(
         "flumine.order.order.BetfairOrder.size_remaining",
diff --git a/tests/test_orderpackage.py b/tests/test_orderpackage.py
index d180bfe..40a319a 100644
--- a/tests/test_orderpackage.py
+++ b/tests/test_orderpackage.py
@@ -42,6 +42,7 @@ class OrderPackageTest(unittest.TestCase):
         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)
 
     def test_retry(self):
         self.assertTrue(self.order_package.retry())
@@ -59,6 +60,22 @@ class OrderPackageTest(unittest.TestCase):
         self.assertEqual(self.order_package._retry_count, 3)
         mock_time.sleep.assert_called()
 
+    def test_calc_simulated_delay(self):
+        self.assertIsNone(self.order_package.calc_simulated_delay())
+        self.order_package.client.execution.EXCHANGE = ExchangeType.SIMULATED
+        self.order_package.client.execution.PLACE_LATENCY = 0.1
+        self.order_package.client.execution.CANCEL_LATENCY = 0.2
+        self.order_package.client.execution.UPDATE_LATENCY = 0.3
+        self.order_package.client.execution.REPLACE_LATENCY = 0.4
+        self.order_package.package_type = OrderPackageType.PLACE
+        self.assertEqual(self.order_package.calc_simulated_delay(), 1.1)
+        self.order_package.package_type = OrderPackageType.CANCEL
+        self.assertEqual(self.order_package.calc_simulated_delay(), 0.2)
+        self.order_package.package_type = OrderPackageType.UPDATE
+        self.assertEqual(self.order_package.calc_simulated_delay(), 0.3)
+        self.order_package.package_type = OrderPackageType.REPLACE
+        self.assertEqual(self.order_package.calc_simulated_delay(), 1.4)
+
     def test_place_instructions(self):
         with self.assertRaises(NotImplementedError):
             assert self.order_package.place_instructions
@@ -96,6 +113,7 @@ class OrderPackageTest(unittest.TestCase):
                 "client": self.order_package.client,
                 "market_id": self.order_package.market_id,
                 "orders": [self.mock_order.id],
+                "order_count": 1,
                 "package_type": self.order_package.package_type.value,
                 "customer_strategy_ref": self.order_package.customer_strategy_ref,
                 "bet_delay": self.order_package.bet_delay,
diff --git a/tests/test_release.py b/tests/test_release.py
index 4726bfa..1966ddc 100644
--- a/tests/test_release.py
+++ b/tests/test_release.py
@@ -5,6 +5,7 @@ from flumine import __version__
 
 
 class ReleaseTests(unittest.TestCase):
+    @unittest.skipIf("b" in __version__, "test_version: beta version")
     def test_version(self):
         with open("HISTORY.rst", "r") as f:
             version = re.findall(r"(?:(\d+\.(?:\d+\.)*\d+))", f.read())[0]
diff --git a/tests/test_strategy.py b/tests/test_strategy.py
index 5fe8bcc..2e88b0b 100644
--- a/tests/test_strategy.py
+++ b/tests/test_strategy.py
@@ -146,45 +146,29 @@ class BaseStrategyTest(unittest.TestCase):
         self.strategy.remove_market("1.23")
         self.assertEqual(self.strategy._invested, {("1.24", 112, 7): 3})
 
-    @mock.patch(
-        "flumine.strategy.strategy.BaseStrategy.validate_order", return_value=True
-    )
-    def test_place_order(self, mock_validate_order):
-        mock_order = mock.Mock()
-        mock_order.lookup = ("1", 2, 3)
-        mock_market = mock.Mock()
-        self.assertTrue(self.strategy.place_order(mock_market, mock_order, False, 123))
-        mock_market.place_order.assert_called_with(mock_order, False, 123)
-        self.assertIn(mock_order.lookup, self.strategy._invested)
-
-    @mock.patch(
-        "flumine.strategy.strategy.BaseStrategy.validate_order", return_value=False
-    )
-    def test_place_order_false(self, mock_validate_order):
+    def test_place_order(self):
         mock_order = mock.Mock()
-        mock_order.lookup = ("1", 2, 3)
         mock_market = mock.Mock()
-        self.assertFalse(self.strategy.place_order(mock_market, mock_order))
-        mock_market.place_order.assert_not_called()
-        self.assertIn(mock_order.lookup, self.strategy._invested)
+        self.assertTrue(self.strategy.place_order(mock_market, mock_order, 123))
+        mock_market.place_order.assert_called_with(mock_order, 123)
 
     def test_cancel_order(self):
         mock_order = mock.Mock()
         mock_market = mock.Mock()
         self.strategy.cancel_order(mock_market, mock_order, 0.01)
-        mock_market.cancel_order.assert_called_with(mock_order, 0.01, True)
+        mock_market.cancel_order.assert_called_with(mock_order, 0.01)
 
     def test_update_order(self):
         mock_order = mock.Mock()
         mock_market = mock.Mock()
         self.strategy.update_order(mock_market, mock_order, "PERSIST")
-        mock_market.update_order.assert_called_with(mock_order, "PERSIST", True)
+        mock_market.update_order.assert_called_with(mock_order, "PERSIST")
 
     def test_replace_order(self):
         mock_order = mock.Mock()
         mock_market = mock.Mock()
-        self.strategy.replace_order(mock_market, mock_order, 1.01, True, 123)
-        mock_market.replace_order.assert_called_with(mock_order, 1.01, True, 123)
+        self.strategy.replace_order(mock_market, mock_order, 1.01, 123)
+        mock_market.replace_order.assert_called_with(mock_order, 1.01, 123)
 
     def test_validate_order(self):
         mock_order = mock.Mock()
diff --git a/tests/test_tradingcontrols.py b/tests/test_tradingcontrols.py
index b5bb024..de59274 100644
--- a/tests/test_tradingcontrols.py
+++ b/tests/test_tradingcontrols.py
@@ -26,14 +26,12 @@ class TestOrderValidation(unittest.TestCase):
         "flumine.controls.tradingcontrols.OrderValidation._validate_betfair_order"
     )
     def test_validate_betfair(self, mock_validate_betfair_order):
-        order = mock.Mock()
-        order.order_type.ORDER_TYPE = OrderTypes.LIMIT
-        order.EXCHANGE = ExchangeType.BETFAIR
-        order.order_type.size = 12
-        order_package = [order]
-
-        self.trading_control._validate(order_package)
-        mock_validate_betfair_order.assert_called_with(order)
+        mock_order = mock.Mock()
+        mock_order.order_type.ORDER_TYPE = OrderTypes.LIMIT
+        mock_order.EXCHANGE = ExchangeType.BETFAIR
+        mock_order.order_type.size = 12
+        self.trading_control._validate(mock_order, OrderPackageType.PLACE)
+        mock_validate_betfair_order.assert_called_with(mock_order)
 
     @mock.patch(
         "flumine.controls.tradingcontrols.OrderValidation._validate_betfair_min_size"
@@ -281,22 +279,38 @@ class TestStrategyExposure(unittest.TestCase):
         self.assertEqual(self.trading_control.NAME, "STRATEGY_EXPOSURE")
         self.assertEqual(self.trading_control.flumine, self.mock_flumine)
 
+    @mock.patch("flumine.controls.tradingcontrols.StrategyExposure._on_error")
+    def test_validate_strategy_validate_order(self, mock_on_error):
+        mock_order = mock.Mock(market_id="market_id", lookup=(1, 2, 3))
+        mock_order.trade.strategy.validate_order.return_value = True
+        mock_runner_context = mock.Mock()
+        mock_order.trade.strategy.get_runner_context.return_value = mock_runner_context
+        self.trading_control._validate(mock_order, OrderPackageType.PLACE)
+
+    @mock.patch("flumine.controls.tradingcontrols.StrategyExposure._on_error")
+    def test_validate_strategy_validate_order_error(self, mock_on_error):
+        mock_order = mock.Mock(market_id="market_id", lookup=(1, 2, 3))
+        mock_order.trade.strategy.validate_order.return_value = False
+        mock_runner_context = mock.Mock()
+        mock_order.trade.strategy.get_runner_context.return_value = mock_runner_context
+        self.trading_control._validate(mock_order, OrderPackageType.PLACE)
+        mock_on_error.assert_called_with(
+            mock_order,
+            "strategy.validate_order failure",
+        )
+
     @mock.patch("flumine.controls.tradingcontrols.StrategyExposure._on_error")
     def test_validate_limit(self, mock_on_error):
-        order = mock.Mock()
-        order.trade.strategy.max_order_exposure = 10
-        order.trade.strategy.max_selection_exposure = 100
-        order.order_type.ORDER_TYPE = OrderTypes.LIMIT
-        order.side = "BACK"
-        order.order_type.size = 12.0
-        order_package = mock.Mock()
-        order_package.package_type = OrderPackageType.PLACE
-        order_package.market_id = "market_id"
-        order_package.__iter__ = mock.Mock(return_value=iter([order]))
-        self.trading_control._validate(order_package)
+        mock_order = mock.Mock(market_id="market_id", lookup=(1, 2, 3))
+        mock_order.trade.strategy.max_order_exposure = 10
+        mock_order.trade.strategy.max_selection_exposure = 100
+        mock_order.order_type.ORDER_TYPE = OrderTypes.LIMIT
+        mock_order.side = "BACK"
+        mock_order.order_type.size = 12.0
+        self.trading_control._validate(mock_order, OrderPackageType.PLACE)
         mock_on_error.assert_called_with(
-            order,
-            "Order exposure (12.0) is greater than strategy.max_order_strategy (10)",
+            mock_order,
+            "Order exposure (12.0) is greater than strategy.max_order_exposure (10)",
         )
 
     @mock.patch("flumine.controls.tradingcontrols.StrategyExposure._on_error")
@@ -305,12 +319,11 @@ class TestStrategyExposure(unittest.TestCase):
         The 2 orders would exceed selection_exposure limits if they were for the same strategy. But they are
         for different strategies. Assert that they do not cause a validation failure.
         """
-
         strategy = mock.Mock()
         strategy.max_order_exposure = 10
         strategy.max_selection_exposure = 10
 
-        order1 = mock.Mock()
+        order1 = mock.Mock(market_id="market_id", lookup=(1, 2, 3))
         order1.trade.strategy.max_order_exposure = 10
         order1.trade.strategy.max_selection_exposure = 10
         order1.order_type.ORDER_TYPE = OrderTypes.LIMIT
@@ -337,12 +350,7 @@ class TestStrategyExposure(unittest.TestCase):
 
         self.market.blotter._orders = {"order1": order1, "order2": order2}
 
-        order_package = mock.Mock()
-        order_package.package_type = OrderPackageType.PLACE
-        order_package.market_id = "market_id"
-        order_package.__iter__ = mock.Mock(return_value=iter([order1, order2]))
-        self.trading_control._validate(order_package)
-
+        self.trading_control._validate(order1, OrderPackageType.PLACE)
         mock_on_error.assert_not_called()
 
     @mock.patch("flumine.controls.tradingcontrols.StrategyExposure._on_error")
@@ -355,7 +363,7 @@ class TestStrategyExposure(unittest.TestCase):
         strategy.max_order_exposure = 10
         strategy.max_selection_exposure = 10
 
-        order1 = mock.Mock()
+        order1 = mock.Mock(market_id="market_id", lookup=(1, 2, 3))
         order1.trade.strategy = strategy
         order1.order_type.ORDER_TYPE = OrderTypes.LIMIT
         order1.side = "BACK"
@@ -380,58 +388,43 @@ class TestStrategyExposure(unittest.TestCase):
         self.market.blotter["order1"] = order1
         self.market.blotter["order2"] = order2
 
-        order_package = mock.Mock()
-        order_package.package_type = OrderPackageType.PLACE
-        order_package.market_id = "market_id"
-        order_package.__iter__ = mock.Mock(return_value=iter([order1, order2]))
-        self.trading_control._validate(order_package)
-
-        self.assertEqual(2, mock_on_error.call_count)
-        # This asserts the 2nd call, using order2
+        self.trading_control._validate(order1, OrderPackageType.PLACE)
+        self.assertEqual(1, mock_on_error.call_count)
         mock_on_error.assert_called_with(
-            order2,
+            order1,
             "Potential selection exposure (14.0) is greater than strategy.max_selection_exposure (10)",
         )
 
     @mock.patch("flumine.controls.tradingcontrols.StrategyExposure._on_error")
     def test_validate_limit_on_close(self, mock_on_error):
-        order = mock.Mock()
-        order.trade.strategy.max_order_exposure = 10
-        order.trade.strategy.max_selection_exposure = 100
-        order.order_type.ORDER_TYPE = OrderTypes.LIMIT_ON_CLOSE
-        order.side = "BACK"
-        order.order_type.liability = 12
-        order_package = mock.Mock()
-        order_package.package_type = OrderPackageType.PLACE
-        order_package.market_id = "market_id"
-        order_package.__iter__ = mock.Mock(return_value=iter([order]))
-        self.trading_control._validate(order_package)
+        mock_order = mock.Mock(market_id="market_id", lookup=(1, 2, 3))
+        mock_order.trade.strategy.max_order_exposure = 10
+        mock_order.trade.strategy.max_selection_exposure = 100
+        mock_order.order_type.ORDER_TYPE = OrderTypes.LIMIT_ON_CLOSE
+        mock_order.side = "BACK"
+        mock_order.order_type.liability = 12
+        self.trading_control._validate(mock_order, OrderPackageType.PLACE)
         mock_on_error.assert_called_with(
-            order,
-            "Order exposure (12) is greater than strategy.max_order_strategy (10)",
+            mock_order,
+            "Order exposure (12) is greater than strategy.max_order_exposure (10)",
         )
 
     @mock.patch("flumine.controls.tradingcontrols.StrategyExposure._on_error")
     def test_validate_market_on_close(self, mock_on_error):
-
         mock_market = mock.Mock()
         mock_market.blotter.selection_exposure.return_value = 10.0
         self.mock_flumine.markets.markets = {"1.234": mock_market}
 
-        order = mock.Mock()
-        order.trade.strategy.max_order_exposure = 10
-        order.trade.strategy.max_selection_exposure = 100
-        order.order_type.ORDER_TYPE = OrderTypes.MARKET_ON_CLOSE
-        order.side = "BACK"
-        order.order_type.liability = 12
-        order_package = mock.Mock()
-        order_package.market_id = "1.234"
-        order_package.package_type = OrderPackageType.PLACE
-        order_package.__iter__ = mock.Mock(return_value=iter([order]))
-        self.trading_control._validate(order_package)
+        mock_order = mock.Mock(market_id="1.234", lookup=(1, 2, 3))
+        mock_order.trade.strategy.max_order_exposure = 10
+        mock_order.trade.strategy.max_selection_exposure = 100
+        mock_order.order_type.ORDER_TYPE = OrderTypes.MARKET_ON_CLOSE
+        mock_order.side = "BACK"
+        mock_order.order_type.liability = 12
+        self.trading_control._validate(mock_order, OrderPackageType.PLACE)
         mock_on_error.assert_called_with(
-            order,
-            "Order exposure (12) is greater than strategy.max_order_strategy (10)",
+            mock_order,
+            "Order exposure (12) is greater than strategy.max_order_exposure (10)",
         )
 
     @mock.patch("flumine.controls.tradingcontrols.StrategyExposure._on_error")
@@ -439,19 +432,15 @@ class TestStrategyExposure(unittest.TestCase):
         mock_market = mock.Mock()
         mock_market.blotter.selection_exposure.return_value = 12.0
         self.mock_flumine.markets.markets = {"1.234": mock_market}
-
-        order = mock.Mock()
-        order.trade.strategy.max_order_exposure = 10
-        order.trade.strategy.max_selection_exposure = 10
-        order_package = mock.Mock()
-        order_package.market_id = "1.234"
-        order_package.package_type = OrderPackageType.PLACE
-        order_package.__iter__ = mock.Mock(return_value=iter([order]))
-
-        mock_market.blotter._live_orders = [order]
-
-        self.trading_control._validate(order_package)
+        mock_order = mock.Mock(market_id="1.234", lookup=(1, 2, 3))
+        mock_order.trade.strategy.max_order_exposure = 10
+        mock_order.trade.strategy.max_selection_exposure = 10
+        mock_order.order_type.ORDER_TYPE = OrderTypes.LIMIT
+        mock_order.order_type.size = 12.0
+        mock_order.order_type.price = 1.01
+        mock_market.blotter._live_orders = [mock_order]
+        self.trading_control._validate(mock_order, OrderPackageType.PLACE)
         mock_on_error.assert_called_with(
-            order,
+            mock_order,
             "Potential selection exposure (12.0) is greater than strategy.max_selection_exposure (10)",
         )
diff --git a/tests/test_transaction.py b/tests/test_transaction.py
new file mode 100644
index 0000000..6560870
--- /dev/null
+++ b/tests/test_transaction.py
@@ -0,0 +1,257 @@
+import unittest
+from unittest import mock
+from unittest.mock import call
+
+from flumine.execution.transaction import Transaction, OrderPackageType
+from flumine.exceptions import ControlError, OrderError
+
+
+class TransactionTest(unittest.TestCase):
+    def setUp(self) -> None:
+        mock_blotter = {}
+        self.mock_market = mock.Mock(blotter=mock_blotter)
+        self.transaction = Transaction(self.mock_market)
+
+    def test_init(self):
+        self.assertEqual(self.transaction.market, self.mock_market)
+        self.assertFalse(self.transaction._pending_orders)
+        self.assertEqual(self.transaction._pending_place, [])
+        self.assertEqual(self.transaction._pending_cancel, [])
+        self.assertEqual(self.transaction._pending_update, [])
+        self.assertEqual(self.transaction._pending_replace, [])
+
+    @mock.patch(
+        "flumine.execution.transaction.Transaction._validate_controls",
+        return_value=True,
+    )
+    @mock.patch("flumine.execution.transaction.events")
+    def test_place_order(self, mock_events, mock__validate_controls):
+        self.transaction.market.blotter = {}
+        mock_order = mock.Mock(id="123")
+        mock_order.trade.market_notes = None
+        self.assertTrue(self.transaction.place_order(mock_order))
+        mock_order.place.assert_called_with(
+            self.transaction.market.market_book.publish_time
+        )
+        self.transaction.market.flumine.log_control.assert_called_with(
+            mock_events.TradeEvent()
+        )
+        mock_order.trade.update_market_notes.assert_called_with(self.mock_market)
+        mock__validate_controls.assert_called_with(mock_order, OrderPackageType.PLACE)
+        mock__validate_controls.assert_called_with(mock_order, OrderPackageType.PLACE)
+        self.transaction._pending_place = [(mock_order, None)]
+        self.assertTrue(self.transaction._pending_orders)
+
+    @mock.patch(
+        "flumine.execution.transaction.Transaction._validate_controls",
+        return_value=True,
+    )
+    @mock.patch("flumine.execution.transaction.events")
+    def test_place_order_not_executed(self, mock_events, mock__validate_controls):
+        mock_order = mock.Mock(id="123")
+        self.assertTrue(self.transaction.place_order(mock_order, execute=False))
+        mock_order.place.assert_called_with(
+            self.transaction.market.market_book.publish_time
+        )
+        self.transaction.market.flumine.log_control.assert_called_with(
+            mock_events.TradeEvent()
+        )
+        mock__validate_controls.assert_called_with(mock_order, OrderPackageType.PLACE)
+        self.transaction._pending_place = []
+        self.assertFalse(self.transaction._pending_orders)
+
+    @mock.patch(
+        "flumine.execution.transaction.Transaction._validate_controls",
+        return_value=True,
+    )
+    def test_place_order_retry(self, mock__validate_controls):
+        mock_order = mock.Mock()
+        self.transaction.market.blotter = {mock_order.id: mock_order}
+        with self.assertRaises(OrderError):
+            self.transaction.place_order(mock_order)
+        mock__validate_controls.assert_called_with(mock_order, OrderPackageType.PLACE)
+        self.transaction._pending_place = []
+        self.assertFalse(self.transaction._pending_orders)
+
+    @mock.patch(
+        "flumine.execution.transaction.Transaction._validate_controls",
+        return_value=False,
+    )
+    def test_place_order_violation(self, mock__validate_controls):
+        mock_order = mock.Mock()
+        self.assertFalse(self.transaction.place_order(mock_order))
+        mock__validate_controls.assert_called_with(mock_order, OrderPackageType.PLACE)
+        self.transaction._pending_place = []
+        self.assertFalse(self.transaction._pending_orders)
+
+    @mock.patch(
+        "flumine.execution.transaction.Transaction._validate_controls",
+        return_value=True,
+    )
+    def test_cancel_order(self, mock__validate_controls):
+        mock_order = mock.Mock()
+        self.assertTrue(self.transaction.cancel_order(mock_order, 0.01))
+        mock_order.cancel.assert_called_with(0.01)
+        mock__validate_controls.assert_called_with(mock_order, OrderPackageType.CANCEL)
+        self.transaction._pending_cancel = [(mock_order, None)]
+        self.assertTrue(self.transaction._pending_orders)
+
+    @mock.patch(
+        "flumine.execution.transaction.Transaction._validate_controls",
+        return_value=False,
+    )
+    def test_cancel_order_violation(self, mock__validate_controls):
+        mock_order = mock.Mock()
+        self.assertFalse(self.transaction.cancel_order(mock_order))
+        mock__validate_controls.assert_called_with(mock_order, OrderPackageType.CANCEL)
+        self.transaction._pending_cancel = []
+        self.assertFalse(self.transaction._pending_orders)
+
+    @mock.patch(
+        "flumine.execution.transaction.Transaction._validate_controls",
+        return_value=True,
+    )
+    def test_update_order(self, mock__validate_controls):
+        mock_order = mock.Mock()
+        self.assertTrue(self.transaction.update_order(mock_order, "PERSIST"))
+        mock_order.update.assert_called_with("PERSIST")
+        mock__validate_controls.assert_called_with(mock_order, OrderPackageType.UPDATE)
+        self.transaction._pending_update = [(mock_order, None)]
+        self.assertTrue(self.transaction._pending_orders)
+
+    @mock.patch(
+        "flumine.execution.transaction.Transaction._validate_controls",
+        return_value=False,
+    )
+    def test_update_order_violation(self, mock__validate_controls):
+        mock_order = mock.Mock()
+        self.assertFalse(self.transaction.update_order(mock_order, "test"))
+        mock__validate_controls.assert_called_with(mock_order, OrderPackageType.UPDATE)
+        self.transaction._pending_update = []
+        self.assertFalse(self.transaction._pending_orders)
+
+    @mock.patch(
+        "flumine.execution.transaction.Transaction._validate_controls",
+        return_value=True,
+    )
+    def test_replace_order(self, mock__validate_controls):
+        mock_order = mock.Mock()
+        self.assertTrue(self.transaction.replace_order(mock_order, 1.01, 321))
+        mock_order.replace.assert_called_with(1.01)
+        mock__validate_controls.assert_called_with(mock_order, OrderPackageType.REPLACE)
+        self.transaction._pending_replace = [(mock_order, None)]
+        self.assertTrue(self.transaction._pending_orders)
+
+    @mock.patch(
+        "flumine.execution.transaction.Transaction._validate_controls",
+        return_value=False,
+    )
+    def test_replace_order_violation(self, mock__validate_controls):
+        mock_order = mock.Mock()
+        self.assertFalse(self.transaction.replace_order(mock_order, 2.02))
+        mock__validate_controls.assert_called_with(mock_order, OrderPackageType.REPLACE)
+        self.transaction._pending_replace = []
+        self.assertFalse(self.transaction._pending_orders)
+
+    @mock.patch("flumine.execution.transaction.Transaction._create_order_package")
+    def test_execute(self, mock__create_order_package):
+        self.transaction._pending_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),
+                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)
+
+    def test__validate_controls(self):
+        mock_trading_control = mock.Mock()
+        mock_client_control = mock.Mock()
+        self.transaction.market.flumine.trading_controls = [mock_trading_control]
+        self.transaction.market.flumine.client.trading_controls = [mock_client_control]
+        mock_order = mock.Mock()
+        mock_package_type = mock.Mock()
+        self.assertTrue(
+            self.transaction._validate_controls(mock_order, mock_package_type)
+        )
+        mock_trading_control.assert_called_with(mock_order, mock_package_type)
+        mock_client_control.assert_called_with(mock_order, mock_package_type)
+
+    def test__validate_controls_violation(self):
+        mock_trading_control = mock.Mock()
+        mock_trading_control.side_effect = ControlError("test")
+        mock_client_control = mock.Mock()
+        self.transaction.market.flumine.trading_controls = [mock_trading_control]
+        self.transaction.market.flumine.client.trading_controls = [mock_client_control]
+        mock_order = mock.Mock()
+        mock_package_type = mock.Mock()
+        self.assertFalse(
+            self.transaction._validate_controls(mock_order, mock_package_type)
+        )
+        mock_trading_control.assert_called_with(mock_order, mock_package_type)
+        mock_client_control.assert_not_called()
+
+    @mock.patch("flumine.execution.transaction.BetfairOrderPackage")
+    def test__create_order_package(self, mock_betfair_order_package):
+        mock_betfair_order_package.order_limit.return_value = 2
+        packages = self.transaction._create_order_package(
+            [(1, None), (2, None), (3, 123), (4, 123), (5, 123)], OrderPackageType.PLACE
+        )
+        mock_betfair_order_package.assert_has_calls(
+            [
+                call(
+                    client=self.transaction.market.flumine.client,
+                    market_id=self.transaction.market.market_id,
+                    orders=[1, 2],
+                    package_type=OrderPackageType.PLACE,
+                    bet_delay=self.transaction.market.market_book.bet_delay,
+                    market_version=None,
+                ),
+                call(
+                    client=self.transaction.market.flumine.client,
+                    market_id=self.transaction.market.market_id,
+                    orders=[3, 4],
+                    package_type=OrderPackageType.PLACE,
+                    bet_delay=self.transaction.market.market_book.bet_delay,
+                    market_version=123,
+                ),
+                call(
+                    client=self.transaction.market.flumine.client,
+                    market_id=self.transaction.market.market_id,
+                    orders=[5],
+                    package_type=OrderPackageType.PLACE,
+                    bet_delay=self.transaction.market.market_book.bet_delay,
+                    market_version=123,
+                ),
+            ]
+        )
+        self.assertEqual(len(packages), 3)
+        mock_betfair_order_package.order_limit.assert_called_with(
+            OrderPackageType.PLACE
+        )
+
+    @mock.patch("flumine.execution.transaction.Transaction.execute")
+    def test_enter_exit(self, mock_execute):
+        with self.transaction as t:
+            self.assertEqual(self.transaction, t)
+            t._pending_orders = True
+        mock_execute.assert_called()
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 6804594..610a30a 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -18,31 +18,6 @@ class UtilsTest(unittest.TestCase):
     def test_chunks(self):
         self.assertEqual([i for i in utils.chunks([1, 2, 3], 1)], [[1], [2], [3]])
 
-    def test_batch_orders(self):
-        order_one = (mock.Mock(), {})
-        order_two = (mock.Mock(), {"market_version": 123})
-        order_three = (mock.Mock(), {"batch": False})
-        order_four = (mock.Mock(), {"market_version": 123, "batch": True})
-        self.assertEqual(
-            utils.batch_orders([order_one, order_two, order_three, order_four]),
-            {
-                None: [[order_one[0]], [order_three[0]]],
-                123: [[order_two[0], order_four[0]]],
-            },
-        )
-
-    def test__split_orders_by(self):
-        order_one = (mock.Mock(), {})
-        order_four = (mock.Mock(), {"market_version": 123, "batch": True})
-        order_eight = (mock.Mock(), {"market_version": 456, "batch": True})
-        order_ten = (mock.Mock(), {"market_version": 456, "batch": True})
-        self.assertEqual(
-            utils._split_orders_by(
-                [order_one, order_four, order_eight, order_ten], "market_version", 321
-            ),
-            {321: [order_one], 123: [order_four], 456: [order_eight, order_ten]},
-        )
-
     def test_create_cheap_hash(self):
         self.assertEqual(
             utils.create_cheap_hash("test"),