Repository: liampauling/flumine
Pull request #316 (2020-11-04 17:38:16)

history and version update


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index e724902..a656a5f 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,21 @@
 Release History
 ---------------
 
+1.14.4 (2020-11-05)
++++++++++++++++++++
+
+**Improvements**
+
+- #310 typing update and bool return added on stream
+- add min_bet_validation flag to prevent control checking min size
+
+**Bug Fixes**
+
+- filters out violated orders from being used to calculate the selection exposure (@lunswor)
+- handle simulated cancel when size reduction is larger than size remaining
+- pass correct size into create replace order based on api response
+- #314 Calculates size_remaining from size and size_matched when not set from placeResponse
+
 1.14.3 (2020-11-02)
 +++++++++++++++++++
 
diff --git a/flumine/__version__.py b/flumine/__version__.py
index f60c1a1..7dd489f 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.14.3"
+__version__ = "1.14.4"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/clients/baseclient.py b/flumine/clients/baseclient.py
index 21fa30b..efea439 100644
--- a/flumine/clients/baseclient.py
+++ b/flumine/clients/baseclient.py
@@ -25,6 +25,7 @@ class BaseClient:
         id_: str = None,
         order_stream: bool = True,
         best_price_execution: bool = True,
+        min_bet_validation: bool = True,
         paper_trade: bool = False,
     ):
         self.id = id_ or create_short_uuid()
@@ -35,6 +36,7 @@ class BaseClient:
         self.interactive_login = interactive_login
         self.order_stream = order_stream
         self.best_price_execution = best_price_execution
+        self.min_bet_validation = min_bet_validation  # used in OrderValidation control
         self.paper_trade = paper_trade
 
         self.account_details = None
diff --git a/flumine/controls/tradingcontrols.py b/flumine/controls/tradingcontrols.py
index eb92a3c..8631a3b 100644
--- a/flumine/controls/tradingcontrols.py
+++ b/flumine/controls/tradingcontrols.py
@@ -60,6 +60,8 @@ class OrderValidation(BaseControl):
 
     def _validate_betfair_min_size(self, order, order_type):
         client = self.flumine.client
+        if client.min_bet_validation is False:
+            return  # some accounts do not have min bet restrictions
         if order_type == OrderTypes.LIMIT:
             if (
                 order.order_type.size < client.min_bet_size
diff --git a/flumine/execution/betfairexecution.py b/flumine/execution/betfairexecution.py
index e1416ba..f3fda72 100644
--- a/flumine/execution/betfairexecution.py
+++ b/flumine/execution/betfairexecution.py
@@ -156,6 +156,7 @@ class BetfairExecution(BaseExecution):
                         replacement_order = order.trade.create_order_replacement(
                             order,
                             instruction_report.place_instruction_reports.instruction.limit_order.price,
+                            instruction_report.place_instruction_reports.instruction.limit_order.size,
                         )
                         self._order_logger(
                             replacement_order,
diff --git a/flumine/execution/simulatedexecution.py b/flumine/execution/simulatedexecution.py
index c7c3b39..8c3f75d 100644
--- a/flumine/execution/simulatedexecution.py
+++ b/flumine/execution/simulatedexecution.py
@@ -117,7 +117,9 @@ class SimulatedExecution(BaseExecution):
                 # place new order
                 self._bet_id += 1
                 replacement_order = order.trade.create_order_replacement(
-                    order, instruction.get("newPrice")
+                    order,
+                    instruction.get("newPrice"),
+                    cancel_instruction_report.size_cancelled,
                 )
                 place_instruction_report = replacement_order.simulated.place(
                     order_package.client, market.market_book, instruction, self._bet_id
diff --git a/flumine/markets/blotter.py b/flumine/markets/blotter.py
index 7accc81..4ee8587 100644
--- a/flumine/markets/blotter.py
+++ b/flumine/markets/blotter.py
@@ -3,7 +3,7 @@ from typing import Iterable, Tuple
 
 from ..order.ordertype import OrderTypes
 from ..utils import chunks, calculate_unmatched_exposure, calculate_matched_exposure
-from ..order.order import BaseOrder
+from ..order.order import BaseOrder, OrderStatus
 from ..order.orderpackage import OrderPackageType, BetfairOrderPackage
 
 logger = logging.getLogger(__name__)
@@ -108,6 +108,9 @@ class Blotter:
         moc_lose_liability = 0.0
         for order in self:
             if order.trade.strategy == strategy and order.lookup == lookup:
+                if order.status == OrderStatus.VIOLATION:
+                    continue
+
                 if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
                     if order.size_matched:
                         if order.side == "BACK":
diff --git a/flumine/order/order.py b/flumine/order/order.py
index 8521853..11e7b6b 100644
--- a/flumine/order/order.py
+++ b/flumine/order/order.py
@@ -391,7 +391,10 @@ class BetfairOrder(BaseOrder):
         try:
             return self.current_order.size_remaining or 0.0
         except AttributeError:
-            return 0.0
+            if self.size_matched > 0:  # placeResponse does not include sizeRemaining
+                return self.order_type.size - self.size_matched
+            else:
+                return self.order_type.size
 
     @property
     def size_cancelled(self) -> float:
diff --git a/flumine/order/trade.py b/flumine/order/trade.py
index 90a583c..353b37a 100644
--- a/flumine/order/trade.py
+++ b/flumine/order/trade.py
@@ -103,13 +103,13 @@ class Trade:
         return order
 
     def create_order_replacement(
-        self, order: BetfairOrder, new_price: float
+        self, order: BetfairOrder, new_price: float, size: float
     ) -> BetfairOrder:
         """Create new order due to replace
         execution"""
         order_type = LimitOrder(
             price=new_price,
-            size=order.order_type.size,
+            size=size,
             persistence_type=order.order_type.persistence_type,
         )
         order = BetfairOrder(
diff --git a/flumine/streams/datastream.py b/flumine/streams/datastream.py
index c083a4b..2a14f9f 100644
--- a/flumine/streams/datastream.py
+++ b/flumine/streams/datastream.py
@@ -42,7 +42,7 @@ class FlumineMarketStream(FlumineStream):
 
     _lookup = "mc"
 
-    def _process(self, market_books, publish_time):
+    def _process(self, market_books: list, publish_time: int) -> bool:
         for market_book in market_books:
             market_id = market_book.get("id")
             if (
@@ -66,13 +66,14 @@ class FlumineMarketStream(FlumineStream):
 
         self.on_process([self.unique_id, publish_time, market_books])
         self._updates_processed += len(market_books)
+        return False
 
 
 class FlumineRaceStream(FlumineStream):
 
     _lookup = "rc"
 
-    def _process(self, race_updates, publish_time):
+    def _process(self, race_updates: list, publish_time: int) -> bool:
         for update in race_updates:
             market_id = update["mid"]
             if self._caches.get(market_id) is None:
@@ -85,6 +86,7 @@ class FlumineRaceStream(FlumineStream):
 
         self.on_process([self.unique_id, publish_time, race_updates])
         self._updates_processed += len(race_updates)
+        return False
 
 
 class DataStream(BaseStream):


Test patch:
diff --git a/flumine/backtest/simulated.py b/flumine/backtest/simulated.py
index eff8941..41927c0 100644
--- a/flumine/backtest/simulated.py
+++ b/flumine/backtest/simulated.py
@@ -134,11 +134,14 @@ class Simulated:
         if self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
             _size_reduction = (
                 self.order.update_data.get("size_reduction") or self.size_remaining
-            )  # todo handle when size_reduction provided greater than size_remaining?
-            self.size_cancelled = _size_reduction
+            )
+            _size_cancelled = min(
+                _size_reduction, self.size_remaining
+            )  # cancelled cannot be more than remaining (does not error)
+            self.size_cancelled += _size_cancelled
             return SimulatedCancelResponse(
                 status="SUCCESS",  # todo handle errors
-                size_cancelled=self.size_cancelled,
+                size_cancelled=_size_cancelled,
                 cancelled_date=datetime.datetime.utcnow(),
             )
         else:
diff --git a/tests/test_blotter.py b/tests/test_blotter.py
index 53c59b6..74d1ad7 100644
--- a/tests/test_blotter.py
+++ b/tests/test_blotter.py
@@ -2,6 +2,7 @@ import unittest
 from unittest import mock
 
 from flumine.markets.blotter import Blotter, OrderPackageType
+from flumine.order.order import OrderStatus
 from flumine.order.ordertype import MarketOnCloseOrder, LimitOrder, LimitOnCloseOrder
 
 
@@ -268,6 +269,22 @@ class BlotterTest(unittest.TestCase):
             10.0,
         )
 
+    def test_selection_exposure_voided(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",
+            order_type=LimitOrder(price=5, size=10.0),
+            status=OrderStatus.VIOLATION,
+        )
+        self.blotter._orders = {"12345": mock_order}
+        self.assertEqual(
+            self.blotter.selection_exposure(mock_strategy, mock_order.lookup),
+            0,
+        )
+
     def test_complete_order(self):
         self.blotter._live_orders = ["test"]
         self.blotter.complete_order("test")
diff --git a/tests/test_clients.py b/tests/test_clients.py
index 059efe6..5e6cc11 100644
--- a/tests/test_clients.py
+++ b/tests/test_clients.py
@@ -29,6 +29,7 @@ class BaseClientTest(unittest.TestCase):
         self.assertEqual(self.base_client.trading_controls, [])
         self.assertTrue(self.base_client.order_stream)
         self.assertTrue(self.base_client.best_price_execution)
+        self.assertTrue(self.base_client.min_bet_validation)
         self.assertFalse(self.base_client.paper_trade)
 
     def test_login(self):
diff --git a/tests/test_order.py b/tests/test_order.py
index fc1390e..b2f96be 100644
--- a/tests/test_order.py
+++ b/tests/test_order.py
@@ -300,7 +300,12 @@ class BetfairOrderTest(unittest.TestCase):
         with self.assertRaises(OrderUpdateError):
             self.order.cancel(21)
 
-    def test_cancel_error(self):
+    @mock.patch(
+        "flumine.order.order.BetfairOrder.size_remaining",
+        new_callable=mock.PropertyMock,
+    )
+    def test_cancel_error(self, mock_size_remaining):
+        mock_size_remaining.return_value = 20
         self.mock_order_type.ORDER_TYPE = OrderTypes.LIMIT
         self.order.status = OrderStatus.PENDING
         with self.assertRaises(OrderUpdateError):
@@ -418,11 +423,26 @@ class BetfairOrderTest(unittest.TestCase):
         self.assertEqual(self.order.size_matched, mock_current_order.size_matched)
 
     def test_size_remaining(self):
+        self.mock_order_type.size = 0
         self.assertEqual(self.order.size_remaining, 0)
+        self.mock_order_type.size = 10
         mock_current_order = mock.Mock(size_remaining=10)
         self.order.responses.current_order = mock_current_order
         self.assertEqual(self.order.size_remaining, mock_current_order.size_remaining)
 
+    def test_size_remaining_missing(self):
+        self.mock_order_type.size = 2.51
+        self.assertEqual(self.order.size_remaining, 2.51)
+
+    @mock.patch(
+        "flumine.order.order.BetfairOrder.size_matched",
+        new_callable=mock.PropertyMock,
+    )
+    def test_size_remaining_missing_partial_match(self, mock_size_matched):
+        mock_size_matched.return_value = 2
+        self.mock_order_type.size = 10
+        self.assertEqual(self.order.size_remaining, 8)
+
     def test_size_cancelled(self):
         self.assertEqual(self.order.size_cancelled, 0)
         mock_current_order = mock.Mock(size_cancelled=10)
diff --git a/tests/test_simulated.py b/tests/test_simulated.py
index 20d7d47..f1efccd 100644
--- a/tests/test_simulated.py
+++ b/tests/test_simulated.py
@@ -218,9 +218,27 @@ class SimulatedTest(unittest.TestCase):
         self.simulated.order.update_data = {"size_reduction": 0.50}
         resp = self.simulated.cancel()
         self.assertEqual(self.simulated.size_cancelled, 0.50)
+        self.assertEqual(self.simulated.size_remaining, 1.50)
         self.assertEqual(resp.status, "SUCCESS")
         self.assertEqual(resp.size_cancelled, 0.50)
 
+    def test_cancel_reduction_multi(self):
+        self.simulated.size_cancelled = 0.10
+        self.simulated.order.update_data = {"size_reduction": 0.50}
+        resp = self.simulated.cancel()
+        self.assertEqual(self.simulated.size_cancelled, 0.60)
+        self.assertEqual(self.simulated.size_remaining, 1.40)
+        self.assertEqual(resp.status, "SUCCESS")
+        self.assertEqual(resp.size_cancelled, 0.50)
+
+    def test_cancel_reduction_greater_than(self):
+        self.simulated.order.update_data = {"size_reduction": 64.0}
+        resp = self.simulated.cancel()
+        self.assertEqual(self.simulated.size_cancelled, 2.00)
+        self.assertEqual(self.simulated.size_remaining, 0.00)
+        self.assertEqual(resp.status, "SUCCESS")
+        self.assertEqual(resp.size_cancelled, 2.00)
+
     def test_cancel_else(self):
         self.simulated.order.order_type.ORDER_TYPE = OrderTypes.MARKET_ON_CLOSE
         resp = self.simulated.cancel()
diff --git a/tests/test_trade.py b/tests/test_trade.py
index 8130bf0..95eb0eb 100644
--- a/tests/test_trade.py
+++ b/tests/test_trade.py
@@ -111,7 +111,7 @@ class TradeTest(unittest.TestCase):
 
     def test_create_order_replacement(self):
         mock_order = mock.Mock()
-        replacement_order = self.trade.create_order_replacement(mock_order, 12)
+        replacement_order = self.trade.create_order_replacement(mock_order, 12, 2.00)
         self.assertEqual(self.trade.orders, [replacement_order])
 
     def test_create_order_from_current_limit(self):
diff --git a/tests/test_tradingcontrols.py b/tests/test_tradingcontrols.py
index 156a744..edae773 100644
--- a/tests/test_tradingcontrols.py
+++ b/tests/test_tradingcontrols.py
@@ -175,6 +175,15 @@ class TestOrderValidation(unittest.TestCase):
         self.trading_control._validate_betfair_liability(order)
         mock_on_error.assert_called_with(order, "Order liability is less than 0")
 
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_min_size_no_validation(self, mock_on_error):
+        self.mock_flumine.client.min_bet_validation = False
+        order = mock.Mock(side="BACK")
+        order.order_type.size = 0.01
+        order.order_type.price = 2
+        self.trading_control._validate_betfair_min_size(order, OrderTypes.LIMIT)
+        mock_on_error.assert_not_called()
+
     @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
     def test__validate_betfair_min_size_limit(self, mock_on_error):
         self.mock_flumine.client.min_bet_size = 2
diff --git a/tests/test_worker.py b/tests/test_worker.py
index 31dace9..ed26e10 100644
--- a/tests/test_worker.py
+++ b/tests/test_worker.py
@@ -36,6 +36,8 @@ class BackgroundWorkerTest(unittest.TestCase):
 
     def test_shutdown(self):
         self.worker.start()
+        while not self.worker._running:
+            continue  # wait for thread to start
         self.assertTrue(self.worker.is_alive())
         self.worker.shutdown()
         self.assertFalse(self.worker._running)