Repository: liampauling/flumine
Pull request #424 (2021-05-08 20:14:19)

Iss423 get exposures double counting when replacing


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 3d9b43d..9c1ae21 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,13 +3,6 @@
 Release History
 ---------------
 
-1.18.7 (2021-05-10)
-+++++++++++++++++++
-
-**Bug Fixes**
-
-- #423 get_exposures() replace fix (@jsphon)
-
 1.18.6 (2021-05-06)
 +++++++++++++++++++
 
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 21cea79..3c014b3 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.18.7"
+__version__ = "1.18.6"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/controls/tradingcontrols.py b/flumine/controls/tradingcontrols.py
index 4de13de..6418a6d 100644
--- a/flumine/controls/tradingcontrols.py
+++ b/flumine/controls/tradingcontrols.py
@@ -147,8 +147,13 @@ class StrategyExposure(BaseControl):
 
             # per selection
             market = self.flumine.markets.markets[order.market_id]
+            if package_type == OrderPackageType.REPLACE:
+                exclusion = order
+            else:
+                exclusion = None
+
             current_exposures = market.blotter.get_exposures(
-                strategy, lookup=order.lookup
+                strategy, lookup=order.lookup, exclusion=exclusion
             )
             """
             We use -min(...) in the below, as "worst_possible_profit_on_X" will be negative if the position is
diff --git a/flumine/markets/blotter.py b/flumine/markets/blotter.py
index 1a3b0ce..86647df 100644
--- a/flumine/markets/blotter.py
+++ b/flumine/markets/blotter.py
@@ -90,13 +90,15 @@ class Blotter:
         )
         return max(exposure, 0.0)
 
-    def get_exposures(self, strategy, lookup: tuple) -> dict:
+    def get_exposures(self, strategy, lookup: tuple, exclusion=None) -> dict:
         """Returns strategy/selection exposures as a dict."""
         mb, ml = [], []  # matched bets, (price, size)
         ub, ul = [], []  # unmatched bets, (price, size)
         moc_win_liability = 0.0
         moc_lose_liability = 0.0
         for order in self.strategy_selection_orders(strategy, *lookup[1:]):
+            if order == exclusion:
+                continue
             if order.status == OrderStatus.VIOLATION:
                 continue
             if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:


Test patch:
diff --git a/tests/test_blotter.py b/tests/test_blotter.py
index 833850c..5f5b362 100644
--- a/tests/test_blotter.py
+++ b/tests/test_blotter.py
@@ -118,6 +118,43 @@ class BlotterTest(unittest.TestCase):
             },
         )
 
+    def test_get_exposures_with_exclusion(self):
+        mock_strategy = mock.Mock()
+        mock_trade = mock.Mock(strategy=mock_strategy)
+        mock_order = mock.Mock(
+            trade=mock_trade,
+            lookup=(self.blotter.market_id, 123, 0),
+            side="BACK",
+            average_price_matched=5.6,
+            size_matched=2.0,
+            size_remaining=0.0,
+            order_type=LimitOrder(price=5.6, size=2.0),
+        )
+        mock_order_excluded = mock.Mock(
+            trade=mock_trade,
+            lookup=(self.blotter.market_id, 123, 0),
+            side="BACK",
+            average_price_matched=5.6,
+            size_matched=2.0,
+            size_remaining=0.0,
+            order_type=LimitOrder(price=5.6, size=2.0),
+        )
+        self.blotter["12345"] = mock_order
+        self.blotter["67890"] = mock_order_excluded
+        self.assertEqual(
+            self.blotter.get_exposures(
+                mock_strategy, mock_order.lookup, exclusion=mock_order_excluded
+            ),
+            {
+                "matched_profit_if_lose": -2.0,
+                "matched_profit_if_win": 9.2,
+                "worst_possible_profit_on_lose": -2.0,
+                "worst_possible_profit_on_win": 9.2,
+                "worst_potential_unmatched_profit_if_lose": 0.0,
+                "worst_potential_unmatched_profit_if_win": 0.0,
+            },
+        )
+
     def test_get_exposures_value_error(self):
         mock_strategy = mock.Mock()
         mock_trade = mock.Mock(strategy=mock_strategy)
diff --git a/tests/test_tradingcontrols.py b/tests/test_tradingcontrols.py
index b0157a1..ec7be0d 100644
--- a/tests/test_tradingcontrols.py
+++ b/tests/test_tradingcontrols.py
@@ -509,3 +509,39 @@ class TestStrategyExposure(unittest.TestCase):
 
         self.trading_control._validate(mock_order, OrderPackageType.PLACE)
         mock_on_error.assert_not_called()
+
+    @mock.patch("flumine.controls.tradingcontrols.StrategyExposure._on_error")
+    def test_validate_replace(self, mock_on_error):
+        """
+        Check that validating a REPLACE order does not lead to double counting of exposures.
+
+        In this test, max_selection_exposure is 10.0, and the potential liability on the order is £9.
+        If exposures are double counted, the validation would fail.
+        If exposures aren't double counted, then the validation will succeed
+        """
+
+        strategy = mock.Mock()
+        strategy.max_order_exposure = 10
+        strategy.max_selection_exposure = 10
+
+        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"
+        order1.order_type.price = 2.0
+        order1.order_type.size = 9.0
+        order1.size_remaining = 9.0
+        order1.average_price_matched = 0.0
+        order1.size_matched = 0
+        order1.selection_id = 1234
+        order1.handicap = 0
+
+        self.market.blotter._strategy_selection_orders = {(strategy, 2, 3): [order1]}
+
+        # Show that the exposures aren't double counted when REPLACE is used
+        self.trading_control._validate(order1, OrderPackageType.REPLACE)
+        mock_on_error.assert_not_called()
+
+        # Just to be sure, check that the validation fails if we try to validate order1 as a PLACE
+        self.trading_control._validate(order1, OrderPackageType.PLACE)
+        mock_on_error.assert_called_once()