Repository: liampauling/flumine
Pull request #377 (2021-02-24 08:12:37)

history and version update


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 4d03f3f..2cdc7d3 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,18 @@
 Release History
 ---------------
 
+1.17.1 (2021-02-24)
++++++++++++++++++++
+
+**Improvements**
+
+- Current and total transactions available from client
+- `blotter.strategy_selection_orders` func added (speed improvement on exposure calc)
+
+**Bug Fixes**
+
+- Refactor of client transaction control to correctly apply the 5000 limit
+
 1.17.0 (2021-02-22)
 +++++++++++++++++++
 
diff --git a/docs/markets.md b/docs/markets.md
index 1c8a278..41feecd 100644
--- a/docs/markets.md
+++ b/docs/markets.md
@@ -32,7 +32,17 @@ Within markets you have market objects which contains current up to date market
 - `elapsed_seconds_closed` Seconds since market was closed (543.21)
 - `market_start_datetime` Market scheduled start time
 
-### Middleware
+## Blotter
+
+The blotter is a simple and fast class to hold all orders for a particular market.
+
+### Functions
+
+- `strategy_orders(strategy)` Returns all orders related to a strategy
+- `strategy_selection_orders(strategy, selection_id, handicap)` Returns all orders related to a strategy selection
+- `selection_exposure(strategy, lookup)` Returns strategy/selection exposure
+
+## Middleware
 
 It is common that you want to carry about analysis on a market before passing through to strategies, similar to Django's middleware design flumine allows middleware to be executed.
 
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 57a4389..93071f3 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -96,11 +96,11 @@ class ExampleStrategy(BaseStrategy):
 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:
+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:
+with market.transaction() as t:
     t.place_order(order)
     
     t.execute()  # above order executed
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 9251e59..eee2122 100644
--- a/flumine/__version__.py
+++ b/flumine/__version__.py
@@ -1,6 +1,6 @@
 __title__ = "flumine"
 __description__ = "Betfair trading framework"
 __url__ = "https://github.com/liampauling/flumine"
-__version__ = "1.17.0"
+__version__ = "1.17.1"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/baseflumine.py b/flumine/baseflumine.py
index 19cc1d0..0039efc 100644
--- a/flumine/baseflumine.py
+++ b/flumine/baseflumine.py
@@ -16,7 +16,7 @@ from .markets.middleware import Middleware, SimulatedMiddleware
 from .execution.betfairexecution import BetfairExecution
 from .execution.simulatedexecution import SimulatedExecution
 from .order.process import process_current_orders
-from .controls.clientcontrols import BaseControl, MaxOrderCount
+from .controls.clientcontrols import BaseControl, MaxTransactionCount
 from .controls.tradingcontrols import OrderValidation, StrategyExposure
 from .controls.loggingcontrols import LoggingControl
 from . import config, utils
@@ -69,7 +69,7 @@ class BaseFlumine:
         self.add_trading_control(OrderValidation)
         self.add_trading_control(StrategyExposure)
         # register default client controls (processed in order)
-        self.add_client_control(MaxOrderCount)
+        self.add_client_control(MaxTransactionCount)
 
         # workers
         self._workers = []
diff --git a/flumine/clients/baseclient.py b/flumine/clients/baseclient.py
index efea439..a6d8111 100644
--- a/flumine/clients/baseclient.py
+++ b/flumine/clients/baseclient.py
@@ -42,7 +42,6 @@ class BaseClient:
         self.account_details = None
         self.account_funds = None
         self.commission_paid = 0
-        self.chargeable_transaction_count = 0
 
         self.execution = None  # set during flumine init
         self.trading_controls = []
@@ -65,6 +64,25 @@ class BaseClient:
         elif self.EXCHANGE == ExchangeType.BETFAIR:
             self.execution = flumine.betfair_execution
 
+    def add_transaction(self, count: int, failed: bool = False) -> None:
+        for control in self.trading_controls:
+            if hasattr(control, "add_transaction"):
+                control.add_transaction(count, failed)
+
+    @property
+    def current_transaction_count_total(self) -> Optional[int]:
+        # current hours total transaction count
+        for control in self.trading_controls:
+            if control.NAME == "MAX_TRANSACTION_COUNT":
+                return control.current_transaction_count_total
+
+    @property
+    def transaction_count_total(self) -> Optional[int]:
+        # total transaction count
+        for control in self.trading_controls:
+            if control.NAME == "MAX_TRANSACTION_COUNT":
+                return control.transaction_count_total
+
     @property
     def min_bet_size(self) -> Optional[float]:
         raise NotImplementedError
@@ -83,7 +101,8 @@ class BaseClient:
             "id": self.id,
             "exchange": self.EXCHANGE.value if self.EXCHANGE else None,
             "betting_client": self.betting_client,
-            "chargeable_transaction_count": self.chargeable_transaction_count,
+            "current_transaction_count_total": self.current_transaction_count_total,
+            "transaction_count_total": self.transaction_count_total,
             "trading_controls": self.trading_controls,
             "order_stream": self.order_stream,
             "best_price_execution": self.best_price_execution,
diff --git a/flumine/controls/clientcontrols.py b/flumine/controls/clientcontrols.py
index 70f1e0e..ddaa6e8 100644
--- a/flumine/controls/clientcontrols.py
+++ b/flumine/controls/clientcontrols.py
@@ -1,7 +1,7 @@
 import datetime
 import logging
+import threading
 from typing import Optional
-from betfairlightweight.metadata import transaction_limit
 
 from ..order.orderpackage import BaseOrder, OrderPackageType
 from . import BaseControl
@@ -10,106 +10,104 @@ from ..clients.baseclient import BaseClient
 logger = logging.getLogger(__name__)
 
 
-class MaxOrderCount(BaseControl):
+class MaxTransactionCount(BaseControl):
 
     """
     Counts and limits orders based on max
-    order count.
-    Only prevents placeOrders if limit is
-    reached.
+    transaction count as per:
+        - https://www.betfair.com/aboutUs/Betfair.Charges/#TranCharges2
+        - 5000 transactions per hour
+        - `A ‘transaction’ shall include all bets placed and all failed transactions`
+    Counts are updated after an execution,
+    thread safe due to the execution pool.
     """
 
-    NAME = "MAX_ORDER_COUNT"
+    NAME = "MAX_TRANSACTION_COUNT"
 
     def __init__(self, flumine, client: BaseClient):
-        super(MaxOrderCount, self).__init__(flumine)
+        super(MaxTransactionCount, self).__init__(flumine)
         self.client = client
-        self.total = 0
-        self.place_requests = 0
-        self.cancel_requests = 0
-        self.update_requests = 0
-        self.replace_requests = 0
         self._next_hour = None
+        # this hour
+        self.current_transaction_count = 0
+        self.current_failed_transaction_count = 0
+        # total since start
         self.transaction_count = 0
+        self.failed_transaction_count = 0
+        # thread lock
+        self._lock = threading.Lock()
+
+    def add_transaction(self, count: int, failed: bool = False) -> None:
+        with self._lock:
+            if failed:
+                self.failed_transaction_count += count
+                self.current_failed_transaction_count += count
+            else:
+                self.transaction_count += count
+                self.current_transaction_count += count
 
     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 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
+        if not self.safe:
+            self._on_error(
+                order,
+                "Max Transaction Count has been reached ({0}) for current hour".format(
+                    self.current_transaction_count_total
+                ),
+            )
 
     def _check_hour(self) -> None:
-        if datetime.datetime.utcnow() > self._next_hour:
+        if self._next_hour is None:
+            self._set_next_hour()
+        elif datetime.datetime.utcnow() > self._next_hour:
             logger.info(
                 "Execution new hour",
                 extra={
-                    "transaction_count": self.transaction_count,
-                    "place_requests": self.place_requests,
-                    "cancel_requests": self.cancel_requests,
-                    "update_requests": self.update_requests,
-                    "replace_requests": self.replace_requests,
+                    "current_transaction_count_total": self.current_transaction_count_total,
+                    "current_transaction_count": self.current_transaction_count,
+                    "current_failed_transaction_count": self.current_failed_transaction_count,
+                    "total_transaction_count": self.transaction_count,
+                    "total_failed_transaction_count": self.failed_transaction_count,
                     "client": self.client,
                 },
             )
             self._set_next_hour()
-            if self.transaction_count > transaction_limit:
-                self.client.chargeable_transaction_count += (
-                    self.transaction_count - transaction_limit
-                )
-            self.transaction_count = 0
-
-    def _check_transaction_count(self, transaction_count: int) -> None:
-        self.transaction_count += transaction_count
-        if self.transaction_limit and self.transaction_count > self.transaction_limit:
-            logger.error(
-                "Transaction limit reached",
-                extra={
-                    "transaction_count": self.transaction_count,
-                    "transaction_limit": self.transaction_limit,
-                    "client": self.client,
-                },
-            )
 
     def _set_next_hour(self) -> None:
         now = datetime.datetime.utcnow()
         self._next_hour = (now + datetime.timedelta(hours=1)).replace(
             minute=0, second=0, microsecond=0
         )
+        self.current_transaction_count = 0
+        self.current_failed_transaction_count = 0
 
     @property
     def safe(self) -> bool:
-        self._check_hour()
         if self.transaction_limit is None:
             return True
-        elif self.transaction_count < self.transaction_limit:
+        elif self.current_transaction_count_total <= self.transaction_limit:
             return True
         else:
             logger.error(
                 "Transaction limit reached",
                 extra={
-                    "transaction_count": self.transaction_count,
+                    "current_transaction_count_total": self.current_transaction_count_total,
+                    "current_transaction_count": self.current_transaction_count,
+                    "current_failed_transaction_count": self.current_failed_transaction_count,
                     "transaction_limit": self.transaction_limit,
                     "client": self.client,
                 },
             )
             return False
 
+    @property
+    def current_transaction_count_total(self) -> int:
+        return self.current_transaction_count + self.current_failed_transaction_count
+
+    @property
+    def transaction_count_total(self) -> int:
+        return self.transaction_count + self.failed_transaction_count
+
     @property
     def transaction_limit(self) -> Optional[int]:
         return self.client.transaction_limit
diff --git a/flumine/execution/betfairexecution.py b/flumine/execution/betfairexecution.py
index c4849e6..c83792c 100644
--- a/flumine/execution/betfairexecution.py
+++ b/flumine/execution/betfairexecution.py
@@ -35,6 +35,9 @@ class BetfairExecution(BaseExecution):
                         # https://docs.developer.betfair.com/display/1smk3cen4v3lu3yomq5qye0ni/Betting+Enums#BettingEnums-ExecutionReportStatus
                         pass
 
+            # update transaction counts
+            order_package.client.add_transaction(len(order_package))
+
     def place(self, order_package: OrderPackageType, session: requests.Session):
         return order_package.client.betting_client.betting.place_orders(
             market_id=order_package.market_id,
@@ -51,6 +54,7 @@ class BetfairExecution(BaseExecution):
     ) -> None:
         response = self._execution_helper(self.cancel, order_package, http_session)
         if response:
+            failed_transaction_count = 0
             order_lookup = {o.bet_id: o for o in order_package}
             for instruction_report in response.cancel_instruction_reports:
                 # get order (can't rely on the order they are returned)
@@ -68,6 +72,7 @@ class BetfairExecution(BaseExecution):
                             order.executable()
                     elif instruction_report.status == "FAILURE":
                         order.executable()
+                        failed_transaction_count += 1
                     elif instruction_report.status == "TIMEOUT":
                         order.executable()
 
@@ -76,6 +81,12 @@ class BetfairExecution(BaseExecution):
                 with order.trade:
                     order.executable()
 
+            # update transaction counts
+            if failed_transaction_count:
+                order_package.client.add_transaction(
+                    failed_transaction_count, failed=True
+                )
+
     def cancel(self, order_package: OrderPackageType, session: requests.Session):
         # temp copy to prevent an empty list of instructions sent
         # this can occur if order is matched during the execution
@@ -96,6 +107,7 @@ class BetfairExecution(BaseExecution):
     ) -> None:
         response = self._execution_helper(self.update, order_package, http_session)
         if response:
+            failed_transaction_count = 0
             for (order, instruction_report) in zip(
                 order_package, response.update_instruction_reports
             ):
@@ -107,9 +119,16 @@ class BetfairExecution(BaseExecution):
                         order.executable()
                     elif instruction_report.status == "FAILURE":
                         order.executable()
+                        failed_transaction_count += 1
                     elif instruction_report.status == "TIMEOUT":
                         order.executable()
 
+            # update transaction counts
+            if failed_transaction_count:
+                order_package.client.add_transaction(
+                    failed_transaction_count, failed=True
+                )
+
     def update(self, order_package: OrderPackageType, session: requests.Session):
         return order_package.client.betting_client.betting.update_orders(
             market_id=order_package.market_id,
@@ -123,6 +142,7 @@ class BetfairExecution(BaseExecution):
     ) -> None:
         response = self._execution_helper(self.replace, order_package, http_session)
         if response:
+            failed_transaction_count = 0
             market = self.flumine.markets.markets[order_package.market_id]
             for (order, instruction_report) in zip(
                 order_package, response.replace_instruction_reports
@@ -144,6 +164,7 @@ class BetfairExecution(BaseExecution):
                         == "FAILURE"
                     ):
                         order.executable()
+                        failed_transaction_count += 1
                     elif (
                         instruction_report.cancel_instruction_reports.status
                         == "TIMEOUT"
@@ -175,6 +196,13 @@ class BetfairExecution(BaseExecution):
                     ):
                         pass  # todo
 
+            # update transaction counts
+            order_package.client.add_transaction(len(order_package))
+            if failed_transaction_count:
+                order_package.client.add_transaction(
+                    failed_transaction_count, failed=True
+                )
+
     def replace(self, order_package: OrderPackageType, session: requests.Session):
         return order_package.client.betting_client.betting.replace_orders(
             market_id=order_package.market_id,
diff --git a/flumine/execution/simulatedexecution.py b/flumine/execution/simulatedexecution.py
index 8c3f75d..4a72267 100644
--- a/flumine/execution/simulatedexecution.py
+++ b/flumine/execution/simulatedexecution.py
@@ -53,11 +53,15 @@ class SimulatedExecution(BaseExecution):
                 elif simulated_response.status == "FAILURE":
                     order.lapsed()
 
+        # update transaction counts
+        order_package.client.add_transaction(len(order_package))
+
     def execute_cancel(
         self, order_package, http_session: Optional[requests.Session]
     ) -> None:
         if order_package.client.paper_trade:
             time.sleep(self.CANCEL_LATENCY)
+        failed_transaction_count = 0
         for order in order_package:
             with order.trade:
                 simulated_response = order.simulated.cancel()
@@ -71,12 +75,18 @@ class SimulatedExecution(BaseExecution):
                         order.executable()
                 elif simulated_response.status == "FAILURE":
                     order.executable()
+                    failed_transaction_count += 1
+
+        # update transaction counts
+        if failed_transaction_count:
+            order_package.client.add_transaction(failed_transaction_count, failed=True)
 
     def execute_update(
         self, order_package, http_session: Optional[requests.Session]
     ) -> None:
         if order_package.client.paper_trade:
             time.sleep(self.UPDATE_LATENCY)
+        failed_transaction_count = 0
         for order, instruction in zip(order_package, order_package.update_instructions):
             with order.trade:
                 simulated_response = order.simulated.update(instruction)
@@ -87,6 +97,11 @@ class SimulatedExecution(BaseExecution):
                     order.executable()
                 elif simulated_response.status == "FAILURE":
                     order.executable()
+                    failed_transaction_count += 1
+
+        # update transaction counts
+        if failed_transaction_count:
+            order_package.client.add_transaction(failed_transaction_count, failed=True)
 
     def execute_replace(
         self, order_package, http_session: Optional[requests.Session]
@@ -95,6 +110,7 @@ class SimulatedExecution(BaseExecution):
             order_package.client.paper_trade
         ):  # todo should the cancel happen without a delay?
             time.sleep(order_package.bet_delay + self.REPLACE_LATENCY)
+        failed_transaction_count = 0
         market = self.flumine.markets.markets[order_package.market_id]
         for order, instruction in zip(
             order_package, order_package.replace_instructions
@@ -106,6 +122,7 @@ class SimulatedExecution(BaseExecution):
                     order.execution_complete()
                 elif cancel_instruction_report.status == "FAILURE":
                     order.executable()  # todo do not carry out replace
+                    failed_transaction_count += 1
                 else:
                     order.lapsed()  # todo do not carry out replace
                 self._order_logger(
@@ -135,3 +152,8 @@ class SimulatedExecution(BaseExecution):
                     replacement_order.executable()
                 elif place_instruction_report.status == "FAILURE":
                     order.executable()
+
+        # update transaction counts
+        order_package.client.add_transaction(len(order_package))
+        if failed_transaction_count:
+            order_package.client.add_transaction(failed_transaction_count, failed=True)
diff --git a/flumine/execution/transaction.py b/flumine/execution/transaction.py
index f9c8a55..83a96ea 100644
--- a/flumine/execution/transaction.py
+++ b/flumine/execution/transaction.py
@@ -17,11 +17,11 @@ class Transaction:
     when it is used as a context manager requests can
     be batched, for example:
 
-        with market.transaction as t:
+        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:
+        with market.transaction() as t:
             t.place_order(order)
             ..
             t.execute()  # above order executed
diff --git a/flumine/markets/blotter.py b/flumine/markets/blotter.py
index 784dc2d..7a53873 100644
--- a/flumine/markets/blotter.py
+++ b/flumine/markets/blotter.py
@@ -33,15 +33,21 @@ class Blotter:
     def __init__(self, market_id: str):
         self.market_id = market_id
         self._orders = {}  # {Order.id: Order}
-        self._live_orders = []  # cached list of live orders
-        self._strategy_orders = defaultdict(
-            list
-        )  # cache list per strategy (faster lookup)
+        # cached lists/dicts for faster lookup
+        self._live_orders = []
+        self._strategy_orders = defaultdict(list)
+        self._strategy_selection_orders = defaultdict(list)
 
     def strategy_orders(self, strategy) -> list:
         """Returns all orders related to a strategy."""
         return self._strategy_orders[strategy]
 
+    def strategy_selection_orders(
+        self, strategy, selection_id: int, handicap: float = 0
+    ) -> list:
+        """Returns all orders related to a strategy selection."""
+        return self._strategy_selection_orders[(strategy, selection_id, handicap)]
+
     @property
     def live_orders(self):
         return iter(self._live_orders)
@@ -75,37 +81,34 @@ class Blotter:
         ub, ul = [], []  # unmatched bets, (price, size)
         moc_win_liability = 0.0
         moc_lose_liability = 0.0
-        for order in self.strategy_orders(strategy):
-            if order.lookup == lookup:
-                if order.status == OrderStatus.VIOLATION:
-                    continue
-
-                if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
-                    _size_matched = order.size_matched  # cache
-                    if _size_matched:
-                        if order.side == "BACK":
-                            mb.append((order.average_price_matched, _size_matched))
-                        else:
-                            ml.append((order.average_price_matched, _size_matched))
-                    _size_remaining = order.size_remaining  # cache
-                    if order.order_type.price and _size_remaining:
-                        if order.side == "BACK":
-                            ub.append((order.order_type.price, _size_remaining))
-                        else:
-                            ul.append((order.order_type.price, _size_remaining))
-                elif order.order_type.ORDER_TYPE in (
-                    OrderTypes.LIMIT_ON_CLOSE,
-                    OrderTypes.MARKET_ON_CLOSE,
-                ):
+        for order in self.strategy_selection_orders(strategy, *lookup[1:]):
+            if order.status == OrderStatus.VIOLATION:
+                continue
+            if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
+                _size_matched = order.size_matched  # cache
+                if _size_matched:
                     if order.side == "BACK":
-                        moc_lose_liability -= order.order_type.liability
+                        mb.append((order.average_price_matched, _size_matched))
                     else:
-                        moc_win_liability -= order.order_type.liability
+                        ml.append((order.average_price_matched, _size_matched))
+                _size_remaining = order.size_remaining  # cache
+                if order.order_type.price and _size_remaining:
+                    if order.side == "BACK":
+                        ub.append((order.order_type.price, _size_remaining))
+                    else:
+                        ul.append((order.order_type.price, _size_remaining))
+            elif order.order_type.ORDER_TYPE in (
+                OrderTypes.LIMIT_ON_CLOSE,
+                OrderTypes.MARKET_ON_CLOSE,
+            ):
+                if order.side == "BACK":
+                    moc_lose_liability -= order.order_type.liability
                 else:
-                    raise ValueError(
-                        "Unexpected order type: %s" % order.order_type.ORDER_TYPE
-                    )
-
+                    moc_win_liability -= order.order_type.liability
+            else:
+                raise ValueError(
+                    "Unexpected order type: %s" % order.order_type.ORDER_TYPE
+                )
         matched_exposure = calculate_matched_exposure(mb, ml)
         unmatched_exposure = calculate_unmatched_exposure(ub, ul)
 
@@ -132,6 +135,9 @@ class Blotter:
         self._orders[customer_order_ref] = order
         self._live_orders.append(order)
         self._strategy_orders[order.trade.strategy].append(order)
+        self._strategy_selection_orders[
+            (order.trade.strategy, *order.lookup[1:])
+        ].append(order)
 
     def __getitem__(self, customer_order_ref: str):
         return self._orders[customer_order_ref]


Test patch:
diff --git a/tests/test_blotter.py b/tests/test_blotter.py
index fee889f..ab42f00 100644
--- a/tests/test_blotter.py
+++ b/tests/test_blotter.py
@@ -14,14 +14,23 @@ class BlotterTest(unittest.TestCase):
         self.assertEqual(self.blotter.market_id, "1.23")
         self.assertEqual(self.blotter._orders, {})
         self.assertEqual(self.blotter._live_orders, [])
+        self.assertEqual(self.blotter._strategy_orders, {})
+        self.assertEqual(self.blotter._strategy_selection_orders, {})
 
     def test_strategy_orders(self):
-        mock_order = mock.Mock()
+        mock_order = mock.Mock(lookup=(1, 2, 3))
         mock_order.trade.strategy = 69
         self.blotter["12345"] = mock_order
         self.assertEqual(self.blotter.strategy_orders(12), [])
         self.assertEqual(self.blotter.strategy_orders(69), [mock_order])
 
+    def test_strategy_selection_orders(self):
+        mock_order = mock.Mock(lookup=(1, 2, 3))
+        mock_order.trade.strategy = 69
+        self.blotter["12345"] = mock_order
+        self.assertEqual(self.blotter.strategy_selection_orders(12, 2, 3), [])
+        self.assertEqual(self.blotter.strategy_selection_orders(69, 2, 3), [mock_order])
+
     def test_live_orders(self):
         self.assertEqual(list(self.blotter.live_orders), [])
         mock_order = mock.Mock(complete=False)
@@ -247,13 +256,17 @@ class BlotterTest(unittest.TestCase):
         self.assertNotIn("321", self.blotter)
 
     def test__setitem(self):
-        mock_order = mock.Mock()
+        mock_order = mock.Mock(lookup=(1, 2, 3))
         self.blotter["123"] = mock_order
         self.assertEqual(self.blotter._orders, {"123": mock_order})
         self.assertEqual(self.blotter._live_orders, [mock_order])
         self.assertEqual(
             self.blotter._strategy_orders, {mock_order.trade.strategy: [mock_order]}
         )
+        self.assertEqual(
+            self.blotter._strategy_selection_orders,
+            {(mock_order.trade.strategy, 2, 3): [mock_order]},
+        )
 
     def test__getitem(self):
         self.blotter._orders = {"12345": "test", "54321": "test2"}
diff --git a/tests/test_clientcontrols.py b/tests/test_clientcontrols.py
index b00bb96..5436cb1 100644
--- a/tests/test_clientcontrols.py
+++ b/tests/test_clientcontrols.py
@@ -4,7 +4,7 @@ from unittest import mock
 
 from flumine.controls.clientcontrols import (
     BaseControl,
-    MaxOrderCount,
+    MaxTransactionCount,
     OrderPackageType,
 )
 from flumine.exceptions import ControlError
@@ -38,82 +38,61 @@ class TestBaseControl(unittest.TestCase):
         order.violation.assert_called_with("Order has violated: None Error: test")
 
 
-class TestMaxOrderCount(unittest.TestCase):
+class TestMaxTransactionCount(unittest.TestCase):
     def setUp(self):
         self.mock_client = mock.Mock()
         self.mock_client.transaction_limit = 1000
         self.mock_client.chargeable_transaction_count = 0
         self.mock_flumine = mock.Mock()
-        self.trading_control = MaxOrderCount(self.mock_flumine, self.mock_client)
+        self.trading_control = MaxTransactionCount(self.mock_flumine, self.mock_client)
 
     def test_init(self):
         self.assertEqual(self.trading_control.client, self.mock_client)
-        self.assertEqual(self.trading_control.NAME, "MAX_ORDER_COUNT")
-        self.assertEqual(self.trading_control.total, 0)
-        self.assertEqual(self.trading_control.place_requests, 0)
-        self.assertEqual(self.trading_control.update_requests, 0)
-        self.assertEqual(self.trading_control.cancel_requests, 0)
-        self.assertEqual(self.trading_control.replace_requests, 0)
+        self.assertEqual(self.trading_control.NAME, "MAX_TRANSACTION_COUNT")
         self.assertIsNone(self.trading_control._next_hour)
+        self.assertEqual(self.trading_control.current_transaction_count, 0)
+        self.assertEqual(self.trading_control.current_failed_transaction_count, 0)
         self.assertEqual(self.trading_control.transaction_count, 0)
-        self.assertEqual(self.trading_control.transaction_limit, 1000)
-
-    @mock.patch("flumine.controls.clientcontrols.MaxOrderCount._set_next_hour")
-    @mock.patch(
-        "flumine.controls.clientcontrols.MaxOrderCount._check_transaction_count"
-    )
-    @mock.patch("flumine.controls.clientcontrols.MaxOrderCount._check_hour")
-    def test_validate_place(
-        self, mock_check_hour, mock__check_transaction_count, mock__set_next_hour
-    ):
+        self.assertEqual(self.trading_control.failed_transaction_count, 0)
+
+    def test_add_transaction(self):
+        self.trading_control.add_transaction(123)
+        self.assertEqual(self.trading_control.transaction_count, 123)
+        self.assertEqual(self.trading_control.current_transaction_count, 123)
+        self.trading_control.add_transaction(123, True)
+        self.assertEqual(self.trading_control.failed_transaction_count, 123)
+        self.assertEqual(self.trading_control.current_failed_transaction_count, 123)
+        self.trading_control.add_transaction(123)
+        self.assertEqual(self.trading_control.transaction_count, 246)
+        self.assertEqual(self.trading_control.current_transaction_count, 246)
+
+    @mock.patch("flumine.controls.clientcontrols.MaxTransactionCount._check_hour")
+    def test_validate(self, mock_check_hour):
         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()
-        self.assertEqual(self.trading_control.place_requests, 1)
 
-    @mock.patch("flumine.controls.clientcontrols.MaxOrderCount._check_hour")
-    def test_validate_cancel(self, mock_check_hour):
-        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_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_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)
-
-    @mock.patch("flumine.controls.clientcontrols.MaxOrderCount._set_next_hour")
+    @mock.patch("flumine.controls.clientcontrols.MaxTransactionCount._set_next_hour")
     def test_check_hour(self, mock_set_next_hour):
         self.trading_control._next_hour = datetime.datetime.utcnow()
         self.trading_control._check_hour()
 
-        self.trading_control.transaction_count = 5069
         now = datetime.datetime.now()
         self.trading_control._next_hour = (now + datetime.timedelta(hours=-1)).replace(
             minute=0, second=0, microsecond=0
         )
         self.trading_control._check_hour()
         mock_set_next_hour.assert_called_with()
-        self.assertEqual(self.trading_control.transaction_count, 0)
-        self.assertEqual(self.mock_client.chargeable_transaction_count, 69)
 
-    def test_check_transaction_count(self):
-        self.trading_control._check_transaction_count(10)
-        self.assertEqual(self.trading_control.transaction_count, 10)
+    @mock.patch("flumine.controls.clientcontrols.MaxTransactionCount._set_next_hour")
+    def test_check_hour_none(self, mock_set_next_hour):
+        self.trading_control._next_hour = None
+        self.trading_control._check_hour()
+        mock_set_next_hour.assert_called_with()
 
     def test_set_next_hour(self):
+        self.trading_control.current_transaction_count = 5069
+        self.trading_control.current_failed_transaction_count = 5069
         self.trading_control._next_hour = None
 
         self.trading_control._set_next_hour()
@@ -122,19 +101,27 @@ class TestMaxOrderCount(unittest.TestCase):
             minute=0, second=0, microsecond=0
         )
         self.assertEqual(self.trading_control._next_hour, now_1)
+        self.assertEqual(self.trading_control.current_transaction_count, 0)
+        self.assertEqual(self.trading_control.current_failed_transaction_count, 0)
 
-    @mock.patch("flumine.controls.clientcontrols.MaxOrderCount._check_hour")
-    def test_safe(self, mock_check_hour):
+    def test_safe(self):
         self.assertTrue(self.trading_control.safe)
-        mock_check_hour.assert_called_with()
 
         self.trading_control.client.transaction_limit = -10
         self.assertFalse(self.trading_control.safe)
-        mock_check_hour.assert_called_with()
 
         self.trading_control.client.transaction_limit = None
         self.assertTrue(self.trading_control.safe)
-        mock_check_hour.assert_called_with()
+
+    def test_current_transaction_count_total(self):
+        self.trading_control.current_transaction_count = 1
+        self.trading_control.current_failed_transaction_count = 2
+        self.assertEqual(self.trading_control.current_transaction_count_total, 3)
+
+    def test_transaction_count_total(self):
+        self.trading_control.transaction_count = 1
+        self.trading_control.failed_transaction_count = 2
+        self.assertEqual(self.trading_control.transaction_count_total, 3)
 
     def test_transaction_limit(self):
         self.assertEqual(
diff --git a/tests/test_clients.py b/tests/test_clients.py
index 5e6cc11..9e33d18 100644
--- a/tests/test_clients.py
+++ b/tests/test_clients.py
@@ -24,7 +24,6 @@ class BaseClientTest(unittest.TestCase):
         self.assertIsNone(self.base_client.account_details)
         self.assertIsNone(self.base_client.account_funds)
         self.assertEqual(self.base_client.commission_paid, 0)
-        self.assertEqual(self.base_client.chargeable_transaction_count, 0)
         self.assertIsNone(self.base_client.execution)
         self.assertEqual(self.base_client.trading_controls, [])
         self.assertTrue(self.base_client.order_stream)
@@ -64,6 +63,28 @@ class BaseClientTest(unittest.TestCase):
         self.base_client.add_execution(mock_flumine)
         self.assertEqual(self.base_client.execution, mock_flumine.simulated_execution)
 
+    def test_add_transaction(self):
+        mock_trading_control = mock.Mock()
+        self.base_client.trading_controls.append(mock_trading_control)
+        self.base_client.add_transaction(123, True)
+        mock_trading_control.add_transaction.assert_called_with(123, True)
+
+    def test_current_transaction_count_total(self):
+        self.assertIsNone(self.base_client.current_transaction_count_total)
+        mock_trading_control = mock.Mock(
+            NAME="MAX_TRANSACTION_COUNT", current_transaction_count_total=123
+        )
+        self.base_client.trading_controls.append(mock_trading_control)
+        self.assertEqual(self.base_client.current_transaction_count_total, 123)
+
+    def test_transaction_count_total(self):
+        self.assertIsNone(self.base_client.transaction_count_total)
+        mock_trading_control = mock.Mock(
+            NAME="MAX_TRANSACTION_COUNT", transaction_count_total=123
+        )
+        self.base_client.trading_controls.append(mock_trading_control)
+        self.assertEqual(self.base_client.transaction_count_total, 123)
+
     def test_min_bet_size(self):
         with self.assertRaises(NotImplementedError):
             assert self.base_client.min_bet_size
diff --git a/tests/test_execution.py b/tests/test_execution.py
index 66ff6d9..278fc8d 100644
--- a/tests/test_execution.py
+++ b/tests/test_execution.py
@@ -213,7 +213,8 @@ class BetfairExecutionTest(unittest.TestCase):
         mock_order = mock.Mock()
         mock_order.trade.__enter__ = mock.Mock()
         mock_order.trade.__exit__ = mock.Mock()
-        mock_order_package = mock.Mock()
+        mock_order_package = mock.MagicMock()
+        mock_order_package.__len__.return_value = 1
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_order_package.info = {}
         mock_report = mock.Mock()
@@ -231,6 +232,7 @@ class BetfairExecutionTest(unittest.TestCase):
         mock_order.executable.assert_called_with()
         mock_order.trade.__enter__.assert_called_with()
         mock_order.trade.__exit__.assert_called_with(None, None, None)
+        mock_order_package.client.add_transaction.assert_called_with(1)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.place")
@@ -242,7 +244,8 @@ class BetfairExecutionTest(unittest.TestCase):
         mock_order = mock.Mock()
         mock_order.trade.__enter__ = mock.Mock()
         mock_order.trade.__exit__ = mock.Mock()
-        mock_order_package = mock.Mock()
+        mock_order_package = mock.MagicMock()
+        mock_order_package.__len__.return_value = 1
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_order_package.info = {}
         mock_report = mock.Mock()
@@ -260,6 +263,7 @@ class BetfairExecutionTest(unittest.TestCase):
         mock_order.lapsed.assert_called_with()
         mock_order.trade.__enter__.assert_called_with()
         mock_order.trade.__exit__.assert_called_with(None, None, None)
+        mock_order_package.client.add_transaction.assert_called_with(1)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.place")
@@ -271,7 +275,8 @@ class BetfairExecutionTest(unittest.TestCase):
         mock_order = mock.Mock()
         mock_order.trade.__enter__ = mock.Mock()
         mock_order.trade.__exit__ = mock.Mock()
-        mock_order_package = mock.Mock()
+        mock_order_package = mock.MagicMock()
+        mock_order_package.__len__.return_value = 1
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_order_package.info = {}
         mock_report = mock.Mock()
@@ -289,6 +294,7 @@ class BetfairExecutionTest(unittest.TestCase):
         # mock_order.lapsed.assert_called_with()
         mock_order.trade.__enter__.assert_called_with()
         mock_order.trade.__exit__.assert_called_with(None, None, None)
+        mock_order_package.client.add_transaction.assert_called_with(1)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.place")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
@@ -406,6 +412,7 @@ class BetfairExecutionTest(unittest.TestCase):
         mock_order.executable.assert_called_with()
         mock_order.trade.__enter__.assert_called_with()
         mock_order.trade.__exit__.assert_called_with(None, None, None)
+        mock_order_package.client.add_transaction.assert_called_with(1, failed=True)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.cancel")
@@ -551,6 +558,7 @@ class BetfairExecutionTest(unittest.TestCase):
         mock_order.executable.assert_called_with()
         mock_order.trade.__enter__.assert_called_with()
         mock_order.trade.__exit__.assert_called_with(None, None, None)
+        mock_order_package.client.add_transaction.assert_called_with(1, failed=True)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.update")
@@ -614,7 +622,8 @@ class BetfairExecutionTest(unittest.TestCase):
         mock_order = mock.Mock(market_id="1.23", bet_id=123)
         mock_order.trade.__enter__ = mock.Mock()
         mock_order.trade.__exit__ = mock.Mock()
-        mock_order_package = mock.Mock(market_id="1.23", info={})
+        mock_order_package = mock.MagicMock(market_id="1.23", info={})
+        mock_order_package.__len__.return_value = 1
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_report = mock.Mock()
         mock_instruction_report = mock.Mock()
@@ -640,6 +649,7 @@ class BetfairExecutionTest(unittest.TestCase):
         )
         mock_order.trade.__enter__.assert_called_with()
         mock_order.trade.__exit__.assert_called_with(None, None, None)
+        mock_order_package.client.add_transaction.assert_called_with(1)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.replace")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
@@ -820,7 +830,8 @@ class SimulatedExecutionTest(unittest.TestCase):
         mock_order = mock.Mock()
         mock_order.trade.__enter__ = mock.Mock()
         mock_order.trade.__exit__ = mock.Mock()
-        mock_order_package = mock.Mock(market_id="1.23")
+        mock_order_package = mock.MagicMock(market_id="1.23")
+        mock_order_package.__len__.return_value = 1
         mock_order_package.client.paper_trade = False
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_order_package.place_instructions = [1]
@@ -841,13 +852,15 @@ class SimulatedExecutionTest(unittest.TestCase):
         mock_order.executable.assert_called_with()
         mock_order.trade.__enter__.assert_called_with()
         mock_order.trade.__exit__.assert_called_with(None, None, None)
+        mock_order_package.client.add_transaction.assert_called_with(1)
 
     @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger")
     def test_execute_place_failure(self, mock__order_logger):
         mock_order = mock.Mock()
         mock_order.trade.__enter__ = mock.Mock()
         mock_order.trade.__exit__ = mock.Mock()
-        mock_order_package = mock.Mock(market_id="1.23")
+        mock_order_package = mock.MagicMock(market_id="1.23")
+        mock_order_package.__len__.return_value = 1
         mock_order_package.client.paper_trade = False
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_order_package.place_instructions = [1]
@@ -868,23 +881,27 @@ class SimulatedExecutionTest(unittest.TestCase):
         mock_order.lapsed.assert_called_with()
         mock_order.trade.__enter__.assert_called_with()
         mock_order.trade.__exit__.assert_called_with(None, None, None)
+        mock_order_package.client.add_transaction.assert_called_with(1)
 
     @mock.patch("flumine.execution.simulatedexecution.time")
     def test_execute_place_paper_trade(self, mock_time):
-        mock_order_package = mock.Mock(
+        mock_order_package = mock.MagicMock(
             market_id="1.23", place_instructions=[], bet_delay=1
         )
+        mock_order_package.__len__.return_value = 1
         mock_order_package.__iter__ = mock.Mock(return_value=iter([]))
         mock_order_package.client.paper_trade = True
         self.execution.execute_place(mock_order_package, None)
         mock_time.sleep.assert_called_with(self.execution.PLACE_LATENCY + 1)
+        mock_order_package.client.add_transaction.assert_called_with(1)
 
     @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger")
     def test_execute_cancel(self, mock__order_logger):
         mock_order = mock.Mock(size_cancelled=2, size_remaining=0)
         mock_order.trade.__enter__ = mock.Mock()
         mock_order.trade.__exit__ = mock.Mock()
-        mock_order_package = mock.Mock()
+        mock_order_package = mock.MagicMock()
+        mock_order_package.__len__.return_value = 1
         mock_order_package.client.paper_trade = False
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_sim_resp = mock.Mock()
@@ -904,7 +921,8 @@ class SimulatedExecutionTest(unittest.TestCase):
         mock_order = mock.Mock(size_cancelled=2, size_remaining=2)
         mock_order.trade.__enter__ = mock.Mock()
         mock_order.trade.__exit__ = mock.Mock()
-        mock_order_package = mock.Mock()
+        mock_order_package = mock.MagicMock(market_id="1.23")
+        mock_order_package.__len__.return_value = 1
         mock_order_package.client.paper_trade = False
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_sim_resp = mock.Mock()
@@ -924,7 +942,8 @@ class SimulatedExecutionTest(unittest.TestCase):
         mock_order = mock.Mock()
         mock_order.trade.__enter__ = mock.Mock()
         mock_order.trade.__exit__ = mock.Mock()
-        mock_order_package = mock.Mock()
+        mock_order_package = mock.MagicMock()
+        mock_order_package.__len__.return_value = 1
         mock_order_package.client.paper_trade = False
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_sim_resp = mock.Mock()
@@ -938,6 +957,7 @@ class SimulatedExecutionTest(unittest.TestCase):
         mock_order.executable.assert_called_with()
         mock_order.trade.__enter__.assert_called_with()
         mock_order.trade.__exit__.assert_called_with(None, None, None)
+        mock_order_package.client.add_transaction.assert_called_with(1, failed=True)
 
     @mock.patch("flumine.execution.simulatedexecution.time")
     def test_execute_cancel_paper_trade(self, mock_time):
@@ -990,6 +1010,7 @@ class SimulatedExecutionTest(unittest.TestCase):
         mock_order.executable.assert_called_with()
         mock_order.trade.__enter__.assert_called_with()
         mock_order.trade.__exit__.assert_called_with(None, None, None)
+        mock_order_package.client.add_transaction.assert_called_with(1, failed=True)
 
     @mock.patch("flumine.execution.simulatedexecution.time")
     def test_execute_update_paper_trade(self, mock_time):
@@ -1005,7 +1026,8 @@ class SimulatedExecutionTest(unittest.TestCase):
         mock_order.trade.__enter__ = mock.Mock()
         mock_order.trade.__exit__ = mock.Mock()
         mock_order.market_id = "1.234"
-        mock_order_package = mock.Mock(market_id="1.23")
+        mock_order_package = mock.MagicMock(market_id="1.23")
+        mock_order_package.__len__.return_value = 1
         mock_order_package.client.paper_trade = False
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_order_package.replace_instructions = [{"newPrice": 2.03}]
@@ -1041,13 +1063,15 @@ class SimulatedExecutionTest(unittest.TestCase):
         mock_replacement_order.executable.assert_called_with()
         mock_order.trade.__enter__.assert_called_with()
         mock_order.trade.__exit__.assert_called_with(None, None, None)
+        mock_order_package.client.add_transaction.assert_called_with(1)
 
     @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger")
     def test_execute_replace_failure(self, mock__order_logger):
         mock_order = mock.Mock()
         mock_order.trade.__enter__ = mock.Mock()
         mock_order.trade.__exit__ = mock.Mock()
-        mock_order_package = mock.Mock(market_id="1.23")
+        mock_order_package = mock.MagicMock(market_id="1.23")
+        mock_order_package.__len__.return_value = 1
         mock_order_package.client.paper_trade = False
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_order_package.replace_instructions = [{"newPrice": 2.54}]
@@ -1079,9 +1103,10 @@ class SimulatedExecutionTest(unittest.TestCase):
 
     @mock.patch("flumine.execution.simulatedexecution.time")
     def test_execute_replace_paper_trade(self, mock_time):
-        mock_order_package = mock.Mock(
+        mock_order_package = mock.MagicMock(
             market_id="1.23", replace_instructions=[], bet_delay=1
         )
+        mock_order_package.__len__.return_value = 1
         mock_order_package.__iter__ = mock.Mock(return_value=iter([]))
         mock_order_package.client.paper_trade = True
         self.execution.execute_replace(mock_order_package, None)
diff --git a/tests/test_tradingcontrols.py b/tests/test_tradingcontrols.py
index de59274..9d82705 100644
--- a/tests/test_tradingcontrols.py
+++ b/tests/test_tradingcontrols.py
@@ -331,11 +331,10 @@ class TestStrategyExposure(unittest.TestCase):
         order1.order_type.price = 2.0
         order1.order_type.size = 9.0
         order1.size_remaining = 9.0
-        order1.lookup = "lookup"
         order1.average_price_matched = 0.0
         order1.size_matched = 0
 
-        order2 = mock.Mock()
+        order2 = mock.Mock(lookup=(1, 2, 3))
         order2.trade.strategy.max_order_exposure = 10
         order2.trade.strategy.max_selection_exposure = 10
         order2.trade.strategy = strategy
@@ -344,7 +343,6 @@ class TestStrategyExposure(unittest.TestCase):
         order2.order_type.price = 3.0
         order2.order_type.size = 9.0
         order2.size_remaining = 5.0
-        order2.lookup = "lookup"
         order2.average_price_matched = 0.0
         order2.size_matched = 0
 
@@ -370,18 +368,16 @@ class TestStrategyExposure(unittest.TestCase):
         order1.order_type.price = 2.0
         order1.order_type.size = 9.0
         order1.size_remaining = 9.0
-        order1.lookup = "lookup"
         order1.average_price_matched = 0.0
         order1.size_matched = 0
 
-        order2 = mock.Mock()
+        order2 = mock.Mock(lookup=(1, 2, 3))
         order2.trade.strategy = strategy
         order2.order_type.ORDER_TYPE = OrderTypes.LIMIT
         order2.side = "BACK"
         order2.order_type.price = 3.0
         order2.order_type.size = 9.0
         order2.size_remaining = 5.0
-        order2.lookup = "lookup"
         order2.average_price_matched = 0.0
         order2.size_matched = 0