Repository: liampauling/flumine
Pull request #425 (2021-05-10 07:32:15)

Release/1.18.7


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 9c1ae21..3d9b43d 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,13 @@
 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/docs/controls.md b/docs/controls.md
index f63e233..4715053 100644
--- a/docs/controls.md
+++ b/docs/controls.md
@@ -1,7 +1,5 @@
 # Controls
 
-## BaseStrategy
-
 ## Trading Controls
 
 Before placing an order flumine will check the client and trading controls, this allows validation to occur before execution. If an order does not meet any of these validations it is not executed and status is updated to `Violation`.
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 3c014b3..21cea79 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.6"
+__version__ = "1.18.7"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/controls/tradingcontrols.py b/flumine/controls/tradingcontrols.py
index 4de13de..49f5be7 100644
--- a/flumine/controls/tradingcontrols.py
+++ b/flumine/controls/tradingcontrols.py
@@ -119,7 +119,7 @@ class StrategyExposure(BaseControl):
 
         if package_type in (
             OrderPackageType.PLACE,
-            OrderPackageType.REPLACE,  # todo potential bug?
+            OrderPackageType.REPLACE,
         ):
             strategy = order.trade.strategy
             if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
@@ -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()