Repository: liampauling/flumine (python)

Issue:
Betting to target payout resulting in parameter requirement error due to erroneous 'size' requirement
"Unknown error __init__() missing 1 required positional argument: 'size' in process_market_book" error resulting from bet_target_type="BACKERS_PROFIT" orders.

order = trade.create_order(
    side="BACK",
    order_type=LimitOrder(price=get_price(runner.ex.available_to_back,0), bet_target_type="BACKERS_PROFIT", bet_target_size=50)
)

Pull request description:
handle None size and bet_target_size
close #541

Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 65dfe3b..98afe61 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -14,6 +14,7 @@ Release History
 **Bug Fixes**
 
 - #528 simulation processing on in flight requests
+- #541 handle betTargetSize
 - Example typo (@petercoles)
 
 **Libraries**
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/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 dbd48e3..e345f24 100644
--- a/flumine/backtest/simulated.py
+++ b/flumine/backtest/simulated.py
@@ -94,6 +94,10 @@ class Simulated:
         if self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
             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 (
@@ -382,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/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 842e3b8..1b8d0e3 100644
--- a/tests/test_simulated.py
+++ b/tests/test_simulated.py
@@ -135,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)
@@ -662,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):
         """