Repository: liampauling/flumine
Pull request #431 (2021-05-14 08:13:46)

black upgrade


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 3d9b43d..6ffa5f6 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,22 @@
 Release History
 ---------------
 
+1.18.8 (2021-05-14)
++++++++++++++++++++
+
+**Improvements**
+
+- Simulation optimisations
+
+**Bug Fixes**
+
+- #173 dead heat profit calculation (@lunswor)
+- listenerKwargs inplay / MoC / SP orders fix (@jsphon)
+
+**Libraries**
+
+- black upgraded to 21.5b1
+
 1.18.7 (2021-05-10)
 +++++++++++++++++++
 
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 21cea79..2fa773b 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.8"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/events/events.py b/flumine/events/events.py
index d5ad22a..69a37c6 100644
--- a/flumine/events/events.py
+++ b/flumine/events/events.py
@@ -29,10 +29,12 @@ class QueueType(Enum):
     LOGGING = "Logging queue"
 
 
-class BaseEvent:  # todo __slots__?
+class BaseEvent:
     EVENT_TYPE = None
     QUEUE_TYPE = None
 
+    __slots__ = ["_time_created", "event", "callback"]
+
     def __init__(self, event):
         self._time_created = datetime.datetime.utcnow()
         self.event = event
diff --git a/flumine/markets/blotter.py b/flumine/markets/blotter.py
index 86647df..12406a1 100644
--- a/flumine/markets/blotter.py
+++ b/flumine/markets/blotter.py
@@ -59,6 +59,9 @@ class Blotter:
         return bool(self._live_orders)
 
     def process_closed_market(self, market_book) -> None:
+        number_of_winners = len(
+            [runner for runner in market_book.runners if runner.status == "WINNER"]
+        )
         for order in self:
             for runner in market_book.runners:
                 if (order.selection_id, order.handicap) == (
@@ -66,6 +69,8 @@ class Blotter:
                     runner.handicap,
                 ):
                     order.runner_status = runner.status
+                    if number_of_winners > market_book.number_of_winners:
+                        order.number_of_dead_heat_winners = number_of_winners
 
     def process_cleared_orders(self, cleared_orders) -> list:
         for cleared_order in cleared_orders.orders:
diff --git a/flumine/markets/middleware.py b/flumine/markets/middleware.py
index c8eea79..4a14ad1 100644
--- a/flumine/markets/middleware.py
+++ b/flumine/markets/middleware.py
@@ -3,7 +3,7 @@ from collections import defaultdict
 from betfairlightweight.resources.bettingresources import RunnerBook
 
 from ..order.order import OrderStatus, OrderTypes
-from ..utils import get_price, wap
+from ..utils import wap
 
 logger = logging.getLogger(__name__)
 
@@ -134,11 +134,11 @@ class SimulatedMiddleware(Middleware):
 
     @staticmethod
     def _process_simulated_orders(market, market_analytics: dict) -> None:
-        for order in market.blotter.live_orders:
+        for order in market.blotter._live_orders:
             if order.simulated and order.status == OrderStatus.EXECUTABLE:
-                runner_analytics = market_analytics.get(
+                runner_analytics = market_analytics[
                     (order.selection_id, order.handicap)
-                )
+                ]
                 order.simulated(market.market_book, runner_analytics)
 
     @staticmethod
@@ -169,36 +169,45 @@ class RunnerAnalytics:
         if update:
             self.middle = self._calculate_middle(self._runner)  # use last update
             self.matched = self._calculate_matched(runner)
-            self.traded = self._calculate_traded(runner.ex.traded_volume)
-            self._traded_volume = runner.ex.traded_volume
+            _tv = runner.ex.traded_volume
+            if self._traded_volume == _tv:
+                self.traded = {}
+            else:
+                self.traded = self._calculate_traded(_tv)
+            self._traded_volume = _tv
             self._runner = runner
         else:
             self.matched = 0
             self.traded = {}
 
     def _calculate_traded(self, traded_volume: list) -> dict:
-        if self._traded_volume == traded_volume:
-            return {}
-        else:
-            p_v, traded = self._p_v, {}
-            # create dictionary
-            c_v = {i["price"]: i["size"] for i in traded_volume}
-            # calculate difference
-            for key, value in c_v.items():
-                if key in p_v:
-                    new_value = float(value) - float(p_v[key])
-                    if new_value > 0:
-                        traded[key] = round(new_value, 2)
-                else:
-                    traded[key] = value
-            # cache for next update
-            self._p_v = c_v
-            return traded
+        p_v, traded = self._p_v, {}
+        # create dictionary
+        c_v = {i["price"]: i["size"] for i in traded_volume}
+        # calculate difference
+        for key, value in c_v.items():
+            if key in p_v:
+                new_value = float(value) - float(p_v[key])
+                if new_value > 0:
+                    traded[key] = round(new_value, 2)
+            else:
+                traded[key] = value
+        # cache for next update
+        self._p_v = c_v
+        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
+        _back = runner.ex.available_to_back
+        if _back:
+            back = _back[0]["price"]
+        else:
+            back = 0
+        _lay = runner.ex.available_to_lay
+        if _lay:
+            lay = _lay[0]["price"]
+        else:
+            lay = 1001
         return (float(back) + float(lay)) / 2
 
     def _calculate_matched(self, runner: RunnerBook) -> float:
@@ -206,4 +215,7 @@ class RunnerAnalytics:
         total_matched = (
             runner.total_matched or prev_total_matched
         )  # handles non-runner -> 0
-        return round(total_matched - prev_total_matched, 2)
+        if total_matched != prev_total_matched:
+            return round(total_matched - prev_total_matched, 2)
+        else:
+            return 0.0
diff --git a/flumine/order/order.py b/flumine/order/order.py
index 043a3ed..6725aa1 100644
--- a/flumine/order/order.py
+++ b/flumine/order/order.py
@@ -60,6 +60,7 @@ class BaseOrder:
         self.lookup = self.market_id, self.selection_id, self.handicap
 
         self.runner_status = None  # RunnerBook.status
+        self.number_of_dead_heat_winners = None
         self.status = None
         self.status_log = []
         self.violation_msg = None
diff --git a/flumine/streams/historicalstream.py b/flumine/streams/historicalstream.py
index 0cff5ba..04fdaed 100644
--- a/flumine/streams/historicalstream.py
+++ b/flumine/streams/historicalstream.py
@@ -56,8 +56,8 @@ class FlumineMarketStream(MarketStream):
         for cache in list(self._caches.values()):
             if market_ids and cache.market_id not in market_ids:
                 continue
-            # if market has closed send regardless
-            if cache._definition_status != "CLOSED":
+            # if market is not open (closed/suspended) send regardless
+            if cache._definition_status == "OPEN":
                 if self._listener.inplay:
                     if not cache._definition_in_play:
                         continue


Test patch:
diff --git a/flumine/backtest/simulated.py b/flumine/backtest/simulated.py
index b8a80bf..2caf4a7 100644
--- a/flumine/backtest/simulated.py
+++ b/flumine/backtest/simulated.py
@@ -383,10 +383,24 @@ class Simulated:
     @property
     def profit(self) -> float:
         if self.order.runner_status == "WINNER":
-            if self.side == "BACK":
-                return round((self.average_price_matched - 1) * self.size_matched, 2)
-            else:
-                return round((self.average_price_matched - 1) * -self.size_matched, 2)
+            number_of_dead_heat_winners = self.order.number_of_dead_heat_winners or 1
+            profit = (self.size_matched / number_of_dead_heat_winners) * (
+                self.average_price_matched - 1
+            )
+
+            if number_of_dead_heat_winners == 2:
+                profit = profit - (self.size_matched / number_of_dead_heat_winners)
+            elif number_of_dead_heat_winners > 2:
+                profit = profit - (
+                    self.size_matched
+                    * (number_of_dead_heat_winners - 1)
+                    / number_of_dead_heat_winners
+                )
+
+            if self.side == "LAY":
+                profit = -profit
+
+            return round(profit, 2)
         elif self.order.runner_status == "LOSER":
             if self.side == "BACK":
                 return -self.size_matched
diff --git a/requirements-test.txt b/requirements-test.txt
index 7e1a600..ef0c0d2 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,5 +1,5 @@
 # Tests & Linting
-black==21.5b0
+black==21.5b1
 coverage
 
 # Documentation
diff --git a/tests/test_blotter.py b/tests/test_blotter.py
index 5f5b362..46426a2 100644
--- a/tests/test_blotter.py
+++ b/tests/test_blotter.py
@@ -44,7 +44,7 @@ class BlotterTest(unittest.TestCase):
         self.assertTrue(self.blotter.has_live_orders)
 
     def test_process_closed_market(self):
-        mock_market_book = mock.Mock()
+        mock_market_book = mock.Mock(number_of_winners=1)
         mock_runner = mock.Mock(selection_id=123, handicap=0.0)
         mock_market_book.runners = [mock_runner]
         mock_order = mock.Mock(selection_id=123, handicap=0.0)
diff --git a/tests/test_events.py b/tests/test_events.py
index 2e1cf39..86efbd4 100644
--- a/tests/test_events.py
+++ b/tests/test_events.py
@@ -21,6 +21,5 @@ class BaseEventTest(unittest.TestCase):
         self.assertGreaterEqual(self.base_event.elapsed_seconds, 0)
 
     def test_str(self):
-        self.base_event.EVENT_TYPE = events.EventType.MARKET_BOOK
-        self.base_event.QUEUE_TYPE = events.QueueType.HANDLER
+        self.base_event = events.MarketBookEvent(None)
         self.assertEqual(str(self.base_event), "<MARKET_BOOK [HANDLER]>")
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
index 2ae65b2..73806e3 100644
--- a/tests/test_middleware.py
+++ b/tests/test_middleware.py
@@ -170,15 +170,17 @@ class SimulatedMiddlewareTest(unittest.TestCase):
 
     def test__process_simulated_orders(self):
         mock_market_book = mock.Mock()
-        mock_order = mock.Mock()
-        mock_order.status = OrderStatus.EXECUTABLE
         mock_market = mock.Mock()
-        mock_order_two = mock.Mock()
-        mock_order_two.status = OrderStatus.PENDING
-        mock_order_three = mock.Mock()
-        mock_order_three.status = OrderStatus.EXECUTABLE
-        mock_order_three._simulated = False
-        mock_market.blotter.live_orders = [
+        mock_order = mock.Mock(
+            selection_id=123, handicap=1, status=OrderStatus.EXECUTABLE
+        )
+        mock_order_two = mock.Mock(
+            selection_id=123, handicap=1, status=OrderStatus.PENDING
+        )
+        mock_order_three = mock.Mock(
+            selection_id=123, handicap=1, status=OrderStatus.EXECUTABLE, simulated=False
+        )
+        mock_market.blotter._live_orders = [
             mock_order,
             mock_order_two,
             mock_order_three,
diff --git a/tests/test_simulated.py b/tests/test_simulated.py
index 2ac9b5d..43c8554 100644
--- a/tests/test_simulated.py
+++ b/tests/test_simulated.py
@@ -18,6 +18,7 @@ class SimulatedTest(unittest.TestCase):
             side="BACK",
             order_type=self.mock_order_type,
             trade=mock_trade,
+            number_of_dead_heat_winners=None,
         )
         self.simulated = simulated.Simulated(self.mock_order)
 
@@ -678,6 +679,33 @@ class SimulatedTest(unittest.TestCase):
         self.simulated.order.runner_status = "LOSER"
         self.assertEqual(self.simulated.profit, 2.0)
 
+    def test_profit_dead_heat_back(self):
+        self.simulated.order.number_of_dead_heat_winners = 2
+
+        self.simulated.order.runner_status = "WINNER"
+        self.simulated._update_matched([1234, 4.0, 10.0])
+        self.assertEqual(self.simulated.profit, 10)
+
+        self.simulated.order.runner_status = "LOSER"
+        self.assertEqual(self.simulated.profit, -10)
+
+    def test_profit_dead_heat_lay(self):
+        self.simulated.order.side = "LAY"
+        self.simulated.order.number_of_dead_heat_winners = 2
+
+        self.simulated.order.runner_status = "WINNER"
+        self.simulated._update_matched([1234, 5.0, 20.0])
+        self.assertEqual(self.simulated.profit, -30)
+
+        self.simulated.order.runner_status = "LOSER"
+        self.assertEqual(self.simulated.profit, 20)
+
+    def test_profit_dead_heat_four(self):
+        self.simulated.order.number_of_dead_heat_winners = 4
+        self.simulated.order.runner_status = "WINNER"
+        self.simulated._update_matched([1234, 11.0, 50.0])
+        self.assertEqual(self.simulated.profit, 87.50)
+
     def test_status(self):
         self.mock_order.status.value = "EXECUTION_COMPLETE"
         self.assertEqual(self.simulated.status, "EXECUTION_COMPLETE")
diff --git a/tests/test_streams.py b/tests/test_streams.py
index aff68d8..b84e3af 100644
--- a/tests/test_streams.py
+++ b/tests/test_streams.py
@@ -609,10 +609,10 @@ class TestFlumineMarketStream(unittest.TestCase):
             "1.123": mock.Mock(
                 publish_time=123,
                 market_definition={
-                    "status": "OPEN",
-                    "inPlay": False,
                     "marketTime": 456,
                 },
+                _definition_status="OPEN",
+                _definition_in_play=False,
             )
         }
         self.assertEqual(len(self.stream.snap()), 1)
@@ -620,10 +620,10 @@ class TestFlumineMarketStream(unittest.TestCase):
             "1.123": mock.Mock(
                 publish_time=123,
                 market_definition={
-                    "status": "OPEN",
-                    "inPlay": False,
                     "marketTime": 1234567,
                 },
+                _definition_status="OPEN",
+                _definition_in_play=False,
             )
         }
         self.assertEqual(len(self.stream.snap()), 0)