Repository: liampauling/flumine
Pull request #545 (2022-01-06 11:19:31)

Release/1.21.0


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index e7e2f0c..98afe61 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,24 @@
 Release History
 ---------------
 
+1.21.0 (2022-01-06)
++++++++++++++++++++
+
+**Improvements**
+
+- #528 smart matching on passive orders
+- #544 market exposure refactor (*breaking change)
+
+**Bug Fixes**
+
+- #528 simulation processing on in flight requests
+- #541 handle betTargetSize
+- Example typo (@petercoles)
+
+**Libraries**
+
+- black upgraded to 21.12b0
+
 1.20.13 (2021-12-03)
 +++++++++++++++++++
 
diff --git a/docs/advanced.md b/docs/advanced.md
index b070094..5cdf800 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -112,6 +112,10 @@ logger.setLevel(logging.INFO)
 
 Updated to True when backtesting or paper trading
 
+#### simulated_strategy_isolation
+
+Defaults to True to match orders per strategy, when False prevents double counting of passive liquidity on all orders regardless of strategy.
+
 #### instance_id
 
 Store server id or similar (e.g. AWS ec2 instanceId)
diff --git a/docs/markets.md b/docs/markets.md
index 3f18ae4..fb2cbd1 100644
--- a/docs/markets.md
+++ b/docs/markets.md
@@ -59,7 +59,7 @@ The blotter is a simple and fast class to hold all orders for a particular marke
 - `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
-- `market_exposure(strategy, num_runners, num_winners)` Returns strategy/market exposure (default num_winners=1)
+- `market_exposure(strategy, market_book)` Returns strategy/market exposure
 
 ### Properties
 
diff --git a/examples/middleware/marketcatalogue.py b/examples/middleware/marketcatalogue.py
index 1fbab2a..df01eb3 100644
--- a/examples/middleware/marketcatalogue.py
+++ b/examples/middleware/marketcatalogue.py
@@ -8,7 +8,7 @@ MARKET_CATALOGUE_PATH = ""  # update to correct path
 
 
 # Will read and parse the market_catalogue file and add to the market object when backtesting
-# Usage framework.add_middleware(MarketCatalogueMiddleware())
+# Usage framework.add_market_middleware(MarketCatalogueMiddleware())
 class MarketCatalogueMiddleware(Middleware):
     def add_market(self, market) -> None:
         catalogue_file_path = os.path.join(MARKET_CATALOGUE_PATH, market.market_id)
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 0e4c53a..d642a99 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.20.13"
+__version__ = "1.21.0"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/config.py b/flumine/config.py
index ffedfbc..ad22ae7 100644
--- a/flumine/config.py
+++ b/flumine/config.py
@@ -2,6 +2,7 @@ import os
 import socket
 
 simulated = False
+simulated_strategy_isolation = True
 
 instance_id = None  # instance id (e.g. AWS ec2 instanceId)
 
diff --git a/flumine/controls/tradingcontrols.py b/flumine/controls/tradingcontrols.py
index 8487c37..423150c 100644
--- a/flumine/controls/tradingcontrols.py
+++ b/flumine/controls/tradingcontrols.py
@@ -38,11 +38,12 @@ class OrderValidation(BaseControl):
             self._on_error(order, "Unknown orderType")
 
     def _validate_betfair_size(self, order):
-        if order.order_type.size is None:
+        size = order.order_type.size or order.order_type.bet_target_size
+        if size is None:
             self._on_error(order, "Order size is None")
-        elif order.order_type.size <= 0:
+        elif size <= 0:
             self._on_error(order, "Order size is less than 0")
-        elif order.order_type.size != round(order.order_type.size, 2):
+        elif size != round(size, 2):
             self._on_error(order, "Order size has more than 2dp")
 
     def _validate_betfair_price(self, order):
@@ -64,10 +65,10 @@ class OrderValidation(BaseControl):
         if client.min_bet_validation is False:
             return  # some accounts do not have min bet restrictions
         if order_type == OrderTypes.LIMIT:
+            size = order.order_type.size or order.order_type.bet_target_size
             if (
-                order.order_type.size < client.min_bet_size
-                and (order.order_type.price * order.order_type.size)
-                < client.min_bet_payout
+                size < client.min_bet_size
+                and (order.order_type.price - 1) * size < client.min_bet_payout
             ):
                 self._on_error(
                     order,
@@ -146,12 +147,11 @@ class StrategyExposure(BaseControl):
         ):
             strategy = order.trade.strategy
             if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
+                size = order.order_type.size or order.order_type.bet_target_size
                 if order.side == "BACK":
-                    order_exposure = order.order_type.size
+                    order_exposure = size
                 else:
-                    order_exposure = (
-                        order.order_type.price - 1
-                    ) * order.order_type.size
+                    order_exposure = (order.order_type.price - 1) * size
             elif order.order_type.ORDER_TYPE == OrderTypes.LIMIT_ON_CLOSE:
                 order_exposure = order.order_type.liability  # todo correct?
             elif order.order_type.ORDER_TYPE == OrderTypes.MARKET_ON_CLOSE:
diff --git a/flumine/markets/blotter.py b/flumine/markets/blotter.py
index 867f0fe..00add47 100644
--- a/flumine/markets/blotter.py
+++ b/flumine/markets/blotter.py
@@ -110,7 +110,7 @@ class Blotter:
 
     """ position """
 
-    def market_exposure(self, strategy, num_runners, num_winners: int = 1) -> float:
+    def market_exposure(self, strategy, market_book) -> float:
         """Returns worst-case exposure for market, which is the maximum potential loss (negative),
         arising from the worst race outcome, or the minimum potential profit (positive).
         """
@@ -125,8 +125,8 @@ class Blotter:
         differences = [
             wpp["worst_possible_profit_on_win"] - wpp["worst_possible_profit_on_lose"]
             for wpp in worst_possible_profits
-        ] + (num_runners - len(runners)) * [0]
-        worst_differences = sorted(differences)[:num_winners]
+        ] + (market_book.number_of_active_runners - len(runners)) * [0]
+        worst_differences = sorted(differences)[: market_book.number_of_winners]
         return sum(worst_possible_profits_on_loses) + sum(worst_differences)
 
     def selection_exposure(self, strategy, lookup: tuple) -> float:
diff --git a/flumine/markets/middleware.py b/flumine/markets/middleware.py
index 3420544..85fee17 100644
--- a/flumine/markets/middleware.py
+++ b/flumine/markets/middleware.py
@@ -4,6 +4,7 @@ from betfairlightweight.resources.bettingresources import RunnerBook
 
 from ..order.order import OrderStatus, OrderTypes
 from ..utils import wap
+from .. import config
 
 logger = logging.getLogger(__name__)
 
@@ -180,14 +181,81 @@ class SimulatedMiddleware(Middleware):
         price_adjusted = round(price * (1 - (adjustment_factor / 100)), 2)
         return max(price_adjusted, 1.01)  # min: 1.01
 
-    @staticmethod
-    def _process_simulated_orders(market, market_analytics: dict) -> None:
-        for order in market.blotter.live_orders:
-            if order.status == OrderStatus.EXECUTABLE and order.simulated:
-                runner_analytics = market_analytics[
-                    (order.selection_id, order.handicap)
+    def _process_simulated_orders(self, market, market_analytics: dict) -> None:
+        """
+        #538 smart matching
+          - isolation per order
+            Potential double counting of passive liquidity, old logic no longer implemented
+          - isolation per strategy (default)
+            Prevent double counting of passive liquidity per strategy
+          - isolation per instance
+            Prevent double counting of passive liquidity on all orders regardless of strategy (interaction across strategies)
+        """
+        # isolation per strategy (default)
+        if config.simulated_strategy_isolation:
+            for strategy, orders in market.blotter._strategy_orders.items():
+                live_orders = [
+                    o
+                    for o in orders
+                    if o.status
+                    in [
+                        OrderStatus.EXECUTABLE,
+                        OrderStatus.CANCELLING,
+                        OrderStatus.UPDATING,
+                        OrderStatus.REPLACING,
+                    ]
+                    and o.simulated
                 ]
-                order.simulated(market.market_book, runner_analytics)
+                if live_orders:
+                    _lookup = {k: v.traded.copy() for k, v in market_analytics.items()}
+                    live_orders_sorted = self._sort_orders(live_orders)
+                    for order in live_orders_sorted:
+                        runner_traded = _lookup[(order.selection_id, order.handicap)]
+                        order.simulated(market.market_book, runner_traded)
+        else:  # isolation per instance
+            live_orders = list(market.blotter.live_orders)
+            if live_orders:
+                _lookup = {k: v.traded.copy() for k, v in market_analytics.items()}
+                live_orders_sorted = self._sort_orders(live_orders)
+                for order in live_orders_sorted:
+                    if (
+                        order.status
+                        in [
+                            OrderStatus.EXECUTABLE,
+                            OrderStatus.CANCELLING,
+                            OrderStatus.UPDATING,
+                            OrderStatus.REPLACING,
+                        ]
+                        and order.simulated
+                    ):
+                        runner_traded = _lookup[(order.selection_id, order.handicap)]
+                        order.simulated(market.market_book, runner_traded)
+
+    @staticmethod
+    def _sort_orders(orders: list) -> list:
+        # order by betId (default), side (Lay,Back) and then price
+        lay_orders = sorted(
+            [
+                o
+                for o in orders
+                if o.side == "LAY"
+                and o.order_type.ORDER_TYPE != OrderTypes.MARKET_ON_CLOSE
+            ],
+            key=lambda x: -x.order_type.price,
+        )
+        back_orders = sorted(
+            [
+                o
+                for o in orders
+                if o.side == "BACK"
+                and o.order_type.ORDER_TYPE != OrderTypes.MARKET_ON_CLOSE
+            ],
+            key=lambda x: x.order_type.price,
+        )
+        moc = [
+            o for o in orders if o.order_type.ORDER_TYPE == OrderTypes.MARKET_ON_CLOSE
+        ]
+        return lay_orders + back_orders + moc
 
     @staticmethod
     def _process_runner(
diff --git a/flumine/order/order.py b/flumine/order/order.py
index e65b80f..33ed927 100644
--- a/flumine/order/order.py
+++ b/flumine/order/order.py
@@ -425,12 +425,13 @@ class BetfairOrder(BaseOrder):
             return self.current_order.size_remaining or 0.0
         except AttributeError:
             if self.order_type.ORDER_TYPE == OrderTypes.LIMIT:
+                size = self.order_type.size or self.order_type.bet_target_size
                 if (
                     self.size_matched > 0
                 ):  # placeResponse does not include sizeRemaining
-                    return round(self.order_type.size - self.size_matched, 2)
+                    return round(size - self.size_matched, 2)
                 else:
-                    return self.order_type.size
+                    return size
             else:
                 return self.order_type.liability
 
diff --git a/flumine/order/ordertype.py b/flumine/order/ordertype.py
index 9ec9b59..0348638 100644
--- a/flumine/order/ordertype.py
+++ b/flumine/order/ordertype.py
@@ -35,7 +35,7 @@ class LimitOrder(BaseOrderType):
     def __init__(
         self,
         price: float,
-        size: float,
+        size: float = None,
         persistence_type: str = "LAPSE",
         time_in_force: str = None,
         min_fill_size: float = None,


Test patch:
diff --git a/flumine/backtest/simulated.py b/flumine/backtest/simulated.py
index 83313bd..e345f24 100644
--- a/flumine/backtest/simulated.py
+++ b/flumine/backtest/simulated.py
@@ -33,7 +33,7 @@ class Simulated:
         self._piq = 0.0
         self._bsp_reconciled = False
 
-    def __call__(self, market_book: MarketBook, runner_analytics) -> None:
+    def __call__(self, market_book: MarketBook, traded: dict) -> None:
         # simulates order matching
         if self._bsp_reconciled is False and market_book.bsp_reconciled:
             if self.take_sp:
@@ -52,10 +52,8 @@ class Simulated:
                         return
 
             # todo estimated piq cancellations
-            if runner_analytics.traded:
-                self._process_traded(
-                    market_book.publish_time_epoch, runner_analytics.traded
-                )
+            if traded:
+                self._process_traded(market_book.publish_time_epoch, traded)
 
     def place(
         self, order_package, market_book: MarketBook, instruction: dict, bet_id: int
@@ -94,11 +92,14 @@ class Simulated:
                 error_code="RUNNER_REMOVED",
             )
         if self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
-            available_to_back = get_price(runner.ex.available_to_back, 0) or 1.01
-            available_to_lay = get_price(runner.ex.available_to_lay, 0) or 1000
             price = self.order.order_type.price
             size = self.order.order_type.size
+            if size is None:
+                raise NotImplementedError(
+                    "Simulated betTargetSize placement not implemented"
+                )
             if self.order.side == "BACK":
+                available_to_back = get_price(runner.ex.available_to_back, 0) or 1.01
                 if (
                     not order_package.client.best_price_execution
                     and available_to_back > price
@@ -119,6 +120,7 @@ class Simulated:
                     return self._create_place_response(bet_id)
                 available = runner.ex.available_to_lay
             else:
+                available_to_lay = get_price(runner.ex.available_to_lay, 0) or 1000
                 if (
                     not order_package.client.best_price_execution
                     and available_to_lay < price
@@ -331,14 +333,18 @@ class Simulated:
                     )
                 )
             if side == "BACK" and traded_price >= price:
-                self._calculate_process_traded(publish_time, traded_size)
+                matched = self._calculate_process_traded(publish_time, traded_size)
+                if matched:
+                    traded[traded_price] = traded_size - matched
             elif side == "LAY" and traded_price <= price:
-                self._calculate_process_traded(publish_time, traded_size)
-
-    def _calculate_process_traded(self, publish_time: int, traded_size: float) -> None:
-        traded_size = traded_size / 2
-        if self._piq - traded_size < 0:
-            size = traded_size - self._piq
+                matched = self._calculate_process_traded(publish_time, traded_size)
+                if matched:
+                    traded[traded_price] = traded_size - matched
+
+    def _calculate_process_traded(self, publish_time: int, traded_size: float) -> float:
+        _traded_size = traded_size / 2
+        if self._piq - _traded_size < 0:
+            size = _traded_size - self._piq
             size = round(min(self.size_remaining, size), 2)
             if size:
                 self._update_matched(
@@ -346,15 +352,18 @@ class Simulated:
                         publish_time,
                         self.order.order_type.price,
                         size,
-                    ]  # todo takes the worst price, i.e what was asked
+                    ]
                 )
+            _matched = (self._piq + size) * 2
             self._piq = 0
+            return _matched
         else:
-            self._piq -= traded_size
+            self._piq -= _traded_size
             if logger.isEnabledFor(logging.DEBUG):
                 logger.debug(
                     "Simulated order {0} PIQ: {1}".format(self.order.id, self._piq)
                 )
+            return traded_size
 
     @property
     def take_sp(self) -> bool:
@@ -377,8 +386,9 @@ class Simulated:
     @property
     def size_remaining(self) -> float:
         if self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
+            size = self.order.order_type.size or self.order.order_type.bet_target_size
             return round(
-                self.order.order_type.size
+                size
                 - self.size_matched
                 - self.size_cancelled
                 - self.size_lapsed
diff --git a/requirements-test.txt b/requirements-test.txt
index d71d913..a1d84ae 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,5 +1,5 @@
 # Tests & Linting
-black==21.11b1
+black==21.12b0
 coverage
 
 # Documentation
diff --git a/tests/test_blotter.py b/tests/test_blotter.py
index 0a52661..ba3553b 100644
--- a/tests/test_blotter.py
+++ b/tests/test_blotter.py
@@ -481,18 +481,23 @@ class BlotterTest(unittest.TestCase):
                 order_type=order[6],
                 complete=order[7],
             )
+
+        mock_market_book = mock.Mock(number_of_active_runners=6, number_of_winners=1)
         self.assertEqual(  # single winner
-            self.blotter.market_exposure(mock_strategy, 6), -20.2
+            self.blotter.market_exposure(mock_strategy, mock_market_book), -20.2
         )
+        mock_market_book = mock.Mock(number_of_active_runners=6, number_of_winners=2)
         self.assertEqual(  # muliple winners
-            self.blotter.market_exposure(mock_strategy, 6, 2), -24.6
+            self.blotter.market_exposure(mock_strategy, mock_market_book), -24.6
         )
-        self.assertEqual(  # num winmers > num runners traded
-            self.blotter.market_exposure(mock_strategy, 20, 7), -28.6
+        mock_market_book = mock.Mock(number_of_active_runners=20, number_of_winners=7)
+        self.assertEqual(  # num winners > num runners traded
+            self.blotter.market_exposure(mock_strategy, mock_market_book), -28.6
         )
 
     def test_greened_market_position(self):
         mock_strategy = mock.Mock()
+        mock_market_book = mock.Mock(number_of_active_runners=6, number_of_winners=1)
         mock_trade = mock.Mock(strategy=mock_strategy)
         orders = [
             # (order_id, selection_id, side, average_price_matched, size_matched, size_remaining, order_type, complete)
@@ -515,7 +520,7 @@ class BlotterTest(unittest.TestCase):
                 complete=order[7],
             )
         self.assertEqual(  # single winner
-            self.blotter.market_exposure(mock_strategy, 6), 0.5
+            self.blotter.market_exposure(mock_strategy, mock_market_book), 0.5
         )
 
     def test_complete_order(self):
diff --git a/tests/test_config.py b/tests/test_config.py
index 0c5cfd6..a992cf2 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -6,6 +6,7 @@ from flumine import config
 class ConfigTest(unittest.TestCase):
     def test_init(self):
         self.assertFalse(config.simulated)
+        self.assertTrue(config.simulated_strategy_isolation)
         self.assertIsInstance(config.customer_strategy_ref, str)
         self.assertIsInstance(config.process_id, int)
         self.assertIsNone(config.current_time)
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
index 619d857..21d64c0 100644
--- a/tests/test_middleware.py
+++ b/tests/test_middleware.py
@@ -270,12 +270,43 @@ class SimulatedMiddlewareTest(unittest.TestCase):
         self.assertEqual(self.middleware._calculate_reduction_factor(10, 75.18), 2.48)
         self.assertEqual(self.middleware._calculate_reduction_factor(1.01, 75.18), 1.01)
 
-    def test__process_simulated_orders(self):
+    @mock.patch("flumine.markets.middleware.config")
+    def test__process_simulated_orders_strategy_isolation(self, mock_config):
+        mock_config.simulated_strategy_isolation = True
         mock_market_book = mock.Mock()
         mock_market = mock.Mock()
         mock_order = mock.Mock(
-            selection_id=123, handicap=1, status=OrderStatus.EXECUTABLE
+            selection_id=123, handicap=1, status=OrderStatus.EXECUTABLE, side="LAY"
         )
+        mock_order.order_type.price = 1.02
+        mock_order.order_type.ORDER_TYPE = OrderTypes.LIMIT
+        mock_order_two = mock.Mock(
+            selection_id=123, handicap=1, status=OrderStatus.PENDING
+        )
+        mock_order_three = mock.Mock(
+            selection_id=123, handicap=1, status=OrderStatus.EXECUTABLE, simulated=False
+        )
+        mock_market.blotter._strategy_orders = {
+            "test": [mock_order, mock_order_two, mock_order_three]
+        }
+        mock_market_analytics = {
+            (mock_order.selection_id, mock_order.handicap): mock.Mock(traded={1: 2})
+        }
+        mock_market.market_book = mock_market_book
+        self.middleware._process_simulated_orders(mock_market, mock_market_analytics)
+        mock_order.simulated.assert_called_with(mock_market_book, {1: 2})
+        mock_order_two.simulated.assert_not_called()
+
+    @mock.patch("flumine.markets.middleware.config")
+    def test__process_simulated_orders(self, mock_config):
+        mock_config.simulated_strategy_isolation = False
+        mock_market_book = mock.Mock()
+        mock_market = mock.Mock()
+        mock_order = mock.Mock(
+            selection_id=123, handicap=1, status=OrderStatus.EXECUTABLE, side="LAY"
+        )
+        mock_order.order_type.price = 1.02
+        mock_order.order_type.ORDER_TYPE = OrderTypes.LIMIT
         mock_order_two = mock.Mock(
             selection_id=123, handicap=1, status=OrderStatus.PENDING
         )
@@ -287,12 +318,52 @@ class SimulatedMiddlewareTest(unittest.TestCase):
             mock_order_two,
             mock_order_three,
         ]
-        mock_market_analytics = {(mock_order.selection_id, mock_order.handicap): "test"}
+        mock_market_analytics = {
+            (mock_order.selection_id, mock_order.handicap): mock.Mock(traded={1: 2})
+        }
         mock_market.market_book = mock_market_book
         self.middleware._process_simulated_orders(mock_market, mock_market_analytics)
-        mock_order.simulated.assert_called_with(mock_market_book, "test")
+        mock_order.simulated.assert_called_with(mock_market_book, {1: 2})
         mock_order_two.simulated.assert_not_called()
 
+    def test__sort_orders(self):
+        order_one = mock.Mock(side="LAY", bet_id=1)
+        order_one.order_type.price = 1.01
+        order_two = mock.Mock(side="LAY", bet_id=2)
+        order_two.order_type.price = 1.02
+        order_three = mock.Mock(side="LAY", bet_id=3)
+        order_three.order_type.price = 1.01
+        order_four = mock.Mock(side="BACK", bet_id=4)
+        order_four.order_type.price = 1.2
+        order_five = mock.Mock(side="BACK", bet_id=5)
+        order_five.order_type.price = 1.2
+        order_six = mock.Mock(side="BACK", bet_id=6)
+        order_six.order_type.price = 1.19
+        order_seven = mock.Mock(side="BACK", bet_id=6)
+        order_seven.order_type.price = "ERROR"
+        order_seven.order_type.ORDER_TYPE = OrderTypes.MARKET_ON_CLOSE
+        orders = [
+            order_one,
+            order_two,
+            order_three,
+            order_four,
+            order_five,
+            order_six,
+            order_seven,
+        ]
+        self.assertEqual(
+            self.middleware._sort_orders(orders),
+            [
+                order_two,
+                order_one,
+                order_three,
+                order_six,
+                order_four,
+                order_five,
+                order_seven,
+            ],
+        )
+
     @mock.patch("flumine.markets.middleware.RunnerAnalytics")
     def test__process_runner(self, mock_runner_analytics):
         market_analytics = {}
diff --git a/tests/test_order.py b/tests/test_order.py
index 7823dee..574e6c0 100644
--- a/tests/test_order.py
+++ b/tests/test_order.py
@@ -455,6 +455,7 @@ class BetfairOrderTest(unittest.TestCase):
     def test_size_remaining(self):
         self.mock_order_type.ORDER_TYPE = OrderTypes.LIMIT
         self.mock_order_type.size = 0
+        self.mock_order_type.bet_target_size = 0
         self.assertEqual(self.order.size_remaining, 0)
         self.mock_order_type.size = 10
         mock_current_order = mock.Mock(size_remaining=10)
diff --git a/tests/test_simulated.py b/tests/test_simulated.py
index 2a87142..1b8d0e3 100644
--- a/tests/test_simulated.py
+++ b/tests/test_simulated.py
@@ -39,13 +39,12 @@ class SimulatedTest(unittest.TestCase):
     def test_call_process_traded(
         self, mock__process_sp, mock__process_traded, _, mock__get_runner
     ):
-        mock_market_book = mock.Mock()
-        mock_market_book.bsp_reconciled = False
-        mock_runner_analytics = mock.Mock()
-        self.simulated(mock_market_book, mock_runner_analytics)
+        mock_market_book = mock.Mock(bsp_reconciled=False)
+        traded = {1: 2}
+        self.simulated(mock_market_book, traded)
         mock__process_sp.assert_not_called()
         mock__process_traded.assert_called_with(
-            mock_market_book.publish_time_epoch, mock_runner_analytics.traded
+            mock_market_book.publish_time_epoch, traded
         )
 
     @mock.patch("flumine.backtest.simulated.Simulated._get_runner")
@@ -136,6 +135,19 @@ class SimulatedTest(unittest.TestCase):
             self.simulated.matched, [[mock_market_book.publish_time_epoch, 12, 2]]
         )
 
+    @mock.patch("flumine.backtest.simulated.Simulated._get_runner")
+    def test_place_limit_back_target_size(self, mock__get_runner):
+        self.mock_order.order_type.size = None
+        mock_client = mock.Mock(best_price_execution=True)
+        mock_order_package = mock.Mock(client=mock_client, market_version=None)
+        mock_market_book = mock.Mock(status="OPEN")
+        mock_runner = mock.Mock()
+        mock_runner.ex.available_to_back = [{"price": 12, "size": 120}]
+        mock_runner.ex.available_to_lay = [{"price": 13, "size": 120}]
+        mock__get_runner.return_value = mock_runner
+        with self.assertRaises(NotImplementedError):
+            self.simulated.place(mock_order_package, mock_market_book, {}, 1)
+
     @mock.patch("flumine.backtest.simulated.Simulated._get_runner")
     def test_place_market_status(self, mock__get_runner):
         mock_client = mock.Mock(best_price_execution=False)
@@ -605,9 +617,9 @@ class SimulatedTest(unittest.TestCase):
         mock__calculate_process_traded.assert_not_called()
 
     def test__calculate_process_traded(self):
-        self.simulated._calculate_process_traded(1234582, 2.00)
-        self.simulated._calculate_process_traded(1234583, 2.00)
-        self.simulated._calculate_process_traded(1234584, 2.00)
+        self.assertEqual(self.simulated._calculate_process_traded(1234582, 2.00), 2)
+        self.assertEqual(self.simulated._calculate_process_traded(1234583, 2.00), 2)
+        self.assertEqual(self.simulated._calculate_process_traded(1234584, 2.00), 0)
         self.assertEqual(
             self.simulated.matched, [[1234582, 12, 1.00], [1234583, 12, 1.00]]
         )
@@ -615,12 +627,19 @@ class SimulatedTest(unittest.TestCase):
 
     def test__calculate_process_traded_piq(self):
         self.simulated._piq = 2.00
-        self.simulated._calculate_process_traded(1234585, 4.00)
+        self.assertEqual(self.simulated._calculate_process_traded(1234585, 4.00), 4)
         self.assertEqual(self.simulated.matched, [])
         self.assertEqual(self.simulated._piq, 0)
-        self.simulated._calculate_process_traded(1234586, 4.00)
+        self.assertEqual(self.simulated._calculate_process_traded(1234586, 4.00), 4)
         self.assertEqual(self.simulated.matched, [[1234586, 12, 2.00]])
         self.assertEqual(self.simulated._piq, 0)
+        self.assertEqual(self.simulated._calculate_process_traded(1234586, 4.00), 0)
+
+    def test__calculate_process_traded_piq_match(self):
+        self.simulated._piq = 4.00
+        self.assertEqual(self.simulated._calculate_process_traded(1234585, 20.00), 12)
+        self.assertEqual(self.simulated.matched, [[1234585, 12, 2.0]])
+        self.assertEqual(self.simulated._piq, 0)
 
     def test_take_sp(self):
         self.assertFalse(self.simulated.take_sp)
@@ -656,6 +675,13 @@ class SimulatedTest(unittest.TestCase):
         self.simulated._update_matched([1234, 1, 1])
         self.assertEqual(self.simulated.size_remaining, 1)
 
+    def test_size_remaining_target(self):
+        self.mock_order.order_type.size = None
+        self.mock_order.order_type.bet_target_size = 2
+        self.assertEqual(self.simulated.size_remaining, 2)
+        self.simulated._update_matched([1234, 1, 1])
+        self.assertEqual(self.simulated.size_remaining, 1)
+
     def test_size_remaining_multi(self):
         self.simulated._update_matched([1234, 1, 0.1])
         self.simulated.size_cancelled = 0.2
diff --git a/tests/test_tradingcontrols.py b/tests/test_tradingcontrols.py
index d714bea..0e9fc2e 100644
--- a/tests/test_tradingcontrols.py
+++ b/tests/test_tradingcontrols.py
@@ -116,6 +116,7 @@ class TestOrderValidation(unittest.TestCase):
     def test__validate_betfair_size_on_error(self, mock_on_error):
         order = mock.Mock()
         order.order_type.size = None
+        order.order_type.bet_target_size = None
         self.trading_control._validate_betfair_size(order)
         mock_on_error.assert_called_with(order, "Order size is None")
 
@@ -126,6 +127,14 @@ class TestOrderValidation(unittest.TestCase):
         self.trading_control._validate_betfair_size(order)
         mock_on_error.assert_called_with(order, "Order size is less than 0")
 
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_target_size_on_error_two(self, mock_on_error):
+        order = mock.Mock()
+        order.order_type.size = None
+        order.order_type.bet_target_size = -1
+        self.trading_control._validate_betfair_size(order)
+        mock_on_error.assert_called_with(order, "Order size is less than 0")
+
     @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
     def test__validate_betfair_size_on_error_three(self, mock_on_error):
         order = mock.Mock()
@@ -133,6 +142,14 @@ class TestOrderValidation(unittest.TestCase):
         self.trading_control._validate_betfair_size(order)
         mock_on_error.assert_called_with(order, "Order size has more than 2dp")
 
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_target_size_on_error_three(self, mock_on_error):
+        order = mock.Mock()
+        order.order_type.size = None
+        order.order_type.bet_target_size = 1.999
+        self.trading_control._validate_betfair_size(order)
+        mock_on_error.assert_called_with(order, "Order size has more than 2dp")
+
     @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
     def test__validate_betfair_price(self, mock_on_error):
         order = mock.Mock()
@@ -227,6 +244,21 @@ class TestOrderValidation(unittest.TestCase):
             "Order size is less than min bet size (2) or payout (10) for currency",
         )
 
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_min_target_size_limit_error(self, mock_on_error):
+        self.mock_flumine.client.min_bet_size = 2
+        self.mock_flumine.client.min_bet_payout = 10
+        order = mock.Mock()
+        order.side = "BACK"
+        order.order_type.size = None
+        order.order_type.bet_target_size = 1.99
+        order.order_type.price = 2
+        self.trading_control._validate_betfair_min_size(order, OrderTypes.LIMIT)
+        mock_on_error.assert_called_with(
+            order,
+            "Order size is less than min bet size (2) or payout (10) for currency",
+        )
+
     @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
     def test__validate_betfair_min_size(self, mock_on_error):
         self.mock_flumine.client.min_bet_size = 2
@@ -361,6 +393,21 @@ class TestStrategyExposure(unittest.TestCase):
             "Order exposure (12.0) is greater than strategy.max_order_exposure (10)",
         )
 
+    @mock.patch("flumine.controls.tradingcontrols.StrategyExposure._on_error")
+    def test_validate_limit_target(self, mock_on_error):
+        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 = None
+        mock_order.order_type.bet_target_size = 12.0
+        self.trading_control._validate(mock_order, OrderPackageType.PLACE)
+        mock_on_error.assert_called_with(
+            mock_order,
+            "Order exposure (12.0) is greater than strategy.max_order_exposure (10)",
+        )
+
     @mock.patch("flumine.controls.tradingcontrols.StrategyExposure._on_error")
     def test_validate_limit_with_multiple_strategies_succeeds(self, mock_on_error):
         """