Repository: liampauling/flumine
Pull request #229 (2020-07-06 09:29:51)

Client control 'transaction_limit' None bug fix
Simulated bug fix
- On initial start incorrectly calculates all historic traded data available for matching during cycle
- Fix removes this by setting the current traded volume on init
- Bug present when restricting data to inplay or data was recorded after initial opening


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 2f8362c..8dd1db1 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,18 @@
 Release History
 ---------------
 
+1.8.2 (2020-07-06)
++++++++++++++++++++
+
+**Improvements**
+
+- Previous 'middle' and 'matched' added to simulated
+
+**Bug Fixes**
+
+- Simulated bug fix on when data is not recorded from the beginning
+- Client control 'None' bug fix
+
 1.8.1 (2020-06-30)
 +++++++++++++++++++
 
@@ -329,7 +341,7 @@ Release History
 **Improvements**
 
 - black fmt
-- _async renamed to async_ to match bflw
+- _async renamed to `async_` to match bflw
 - py3.7 added to travis
 - #28 readme update
 
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 60cfd93..9e8f1fc 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.8.1"
+__version__ = "1.8.2"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/controls/clientcontrols.py b/flumine/controls/clientcontrols.py
index bf1d31b..8c4cad5 100644
--- a/flumine/controls/clientcontrols.py
+++ b/flumine/controls/clientcontrols.py
@@ -76,7 +76,7 @@ class MaxOrderCount(BaseControl):
 
     def _check_transaction_count(self, transaction_count: int) -> None:
         self.transaction_count += transaction_count
-        if self.transaction_count > self.transaction_limit:
+        if self.transaction_limit and self.transaction_count > self.transaction_limit:
             logger.error(
                 "Transaction limit reached",
                 extra={
diff --git a/flumine/markets/middleware.py b/flumine/markets/middleware.py
index 1330b1c..0714b5b 100644
--- a/flumine/markets/middleware.py
+++ b/flumine/markets/middleware.py
@@ -3,6 +3,7 @@ from collections import defaultdict
 from betfairlightweight.resources.bettingresources import RunnerBook
 
 from ..order.order import OrderStatus
+from ..utils import get_price
 
 logger = logging.getLogger(__name__)
 
@@ -56,26 +57,28 @@ class SimulatedMiddleware(Middleware):
 class RunnerAnalytics:
     def __init__(self, runner: RunnerBook):
         self._runner = runner
-        self.traded = {}
-        self._traded_volume = []  # runner.ex.traded_volume
+        self.traded = {}  # price: size traded since last event
+        self.middle = None  # middle of odds at last event
+        self.matched = 0  # amount matched since last event
+        self._traded_volume = runner.ex.traded_volume
 
     def __call__(self, runner: RunnerBook):
+        self.middle = self._calculate_middle(self._runner)  # use last event
+        self.matched = self._calculate_matched(runner)
         self.traded = self._calculate_traded(runner)
         self._traded_volume = runner.ex.traded_volume
         self._runner = runner
 
     def _calculate_traded(self, runner: RunnerBook) -> dict:
-        if self._traded_volume == {}:
-            return {}
-        elif self._traded_volume == runner.ex.traded_volume:
+        if self._traded_volume == runner.ex.traded_volume:
             return {}
         else:
-            c_v, p_v, traded_dictionary = {}, {}, {}
+            c_v, p_v, traded = {}, {}, {}
             # create dictionaries
             for i in runner.ex.traded_volume:
                 c_v[i["price"]] = i["size"]
             for i in self._traded_volume:
-                p_v[i["price"]] = i["size"]
+                p_v[i["price"]] = i["size"]  # todo cache from previous run?
             # calculate difference
             for key in c_v.keys():
                 if key in p_v:
@@ -84,5 +87,18 @@ class RunnerAnalytics:
                     new_value = float(c_v[key])
                 if new_value > 0:
                     new_value = round(new_value, 2)
-                    traded_dictionary[key] = new_value
-            return traded_dictionary
+                    traded[key] = new_value
+            return traded
+
+    @staticmethod
+    def _calculate_middle(runner: RunnerBook) -> float:
+        back = get_price(runner.ex.available_to_back, 0) or 0
+        lay = get_price(runner.ex.available_to_lay, 0) or 1001
+        return (float(back) + float(lay)) / 2
+
+    def _calculate_matched(self, runner: RunnerBook) -> float:
+        prev_total_matched = self._runner.total_matched or 0
+        total_matched = (
+            runner.total_matched or prev_total_matched
+        )  # handles non-runner -> 0
+        return round(total_matched - prev_total_matched, 2)


Test patch:
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
index 5c7c52d..3d511ce 100644
--- a/tests/test_middleware.py
+++ b/tests/test_middleware.py
@@ -80,21 +80,34 @@ class RunnerAnalyticsTest(unittest.TestCase):
 
     def test_init(self):
         self.assertEqual(self.runner_analytics._runner, self.mock_runner)
-        self.assertEqual(self.runner_analytics._traded_volume, [])
+        self.assertEqual(
+            self.runner_analytics._traded_volume, self.mock_runner.ex.traded_volume
+        )
         self.assertEqual(self.runner_analytics.traded, {})
+        self.assertEqual(self.runner_analytics.matched, 0)
+        self.assertIsNone(self.runner_analytics.middle)
 
+    @mock.patch("flumine.markets.middleware.RunnerAnalytics._calculate_matched")
+    @mock.patch("flumine.markets.middleware.RunnerAnalytics._calculate_middle")
     @mock.patch("flumine.markets.middleware.RunnerAnalytics._calculate_traded")
-    def test_call(self, mock__calculate_traded):
+    def test_call(
+        self, mock__calculate_traded, mock__calculate_middle, mock__calculate_matched
+    ):
         mock_runner = mock.Mock()
         self.runner_analytics(mock_runner)
         mock__calculate_traded.assert_called_with(mock_runner)
+        mock__calculate_middle.assert_called_with(self.mock_runner)
+        mock__calculate_matched.assert_called_with(mock_runner)
         self.assertEqual(
             self.runner_analytics._traded_volume, mock_runner.ex.traded_volume
         )
+        self.assertEqual(self.runner_analytics.middle, mock__calculate_middle())
+        self.assertEqual(self.runner_analytics.matched, mock__calculate_matched())
         self.assertEqual(self.runner_analytics.traded, mock__calculate_traded())
         self.assertEqual(self.runner_analytics._runner, mock_runner)
 
     def test__calculate_traded_dict_empty(self):
+        self.runner_analytics._traded_volume = []
         mock_runner = mock.Mock()
         mock_runner.ex.traded_volume = []
         self.assertEqual(self.runner_analytics._calculate_traded(mock_runner), {})
@@ -123,3 +136,29 @@ class RunnerAnalyticsTest(unittest.TestCase):
         self.assertEqual(
             self.runner_analytics._calculate_traded(mock_runner), {1.01: 39.0, 10: 32},
         )
+
+    def test__calculate_middle(self):
+        mock_runner = mock.Mock()
+        mock_runner.ex.available_to_back = []
+        mock_runner.ex.available_to_lay = []
+        self.assertEqual(self.runner_analytics._calculate_middle(mock_runner), 500.5)
+        mock_runner.ex.available_to_back = [{"price": 2.00}]
+        mock_runner.ex.available_to_lay = [{"price": 2.02}]
+        self.assertEqual(self.runner_analytics._calculate_middle(mock_runner), 2.01)
+        mock_runner.ex.available_to_back = [{"price": 10.00}]
+        mock_runner.ex.available_to_lay = [{"price": 15.5}]
+        self.assertEqual(self.runner_analytics._calculate_middle(mock_runner), 12.75)
+
+    def test__calculate_matched(self):
+        self.runner_analytics._runner.total_matched = 12344
+        mock_runner = mock.Mock(total_matched=12345)
+        self.assertEqual(self.runner_analytics._calculate_matched(mock_runner), 1)
+        self.runner_analytics._runner = mock_runner
+        self.assertEqual(self.runner_analytics._calculate_matched(mock_runner), 0)
+
+    def test__calculate_matched_runner_removal(self):
+        self.runner_analytics._runner.total_matched = 12344
+        mock_runner = mock.Mock(total_matched=0)
+        self.assertEqual(self.runner_analytics._calculate_matched(mock_runner), 0)
+        self.runner_analytics._runner = mock_runner
+        self.assertEqual(self.runner_analytics._calculate_matched(mock_runner), 0)