Repository: liampauling/flumine
Pull request #375 (2021-02-22 15:51:22)

strategy selection orders lookup added and integrated (much faster lo…
…okup)

minor docs improvement


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 2cdc7d3..4d03f3f 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,18 +3,6 @@
 Release History
 ---------------
 
-1.17.1 (2021-02-24)
-+++++++++++++++++++
-
-**Improvements**
-
-- Current and total transactions available from client
-- `blotter.strategy_selection_orders` func added (speed improvement on exposure calc)
-
-**Bug Fixes**
-
-- Refactor of client transaction control to correctly apply the 5000 limit
-
 1.17.0 (2021-02-22)
 +++++++++++++++++++
 
diff --git a/docs/markets.md b/docs/markets.md
index 1c8a278..41feecd 100644
--- a/docs/markets.md
+++ b/docs/markets.md
@@ -32,7 +32,17 @@ Within markets you have market objects which contains current up to date market
 - `elapsed_seconds_closed` Seconds since market was closed (543.21)
 - `market_start_datetime` Market scheduled start time
 
-### Middleware
+## Blotter
+
+The blotter is a simple and fast class to hold all orders for a particular market.
+
+### Functions
+
+- `strategy_orders(strategy)` Returns all orders related to a strategy
+- `strategy_selection_orders(strategy, selection_id, handicap)` Returns all orders related to a strategy selection
+- `selection_exposure(strategy, lookup)` Returns strategy/selection exposure
+
+## Middleware
 
 It is common that you want to carry about analysis on a market before passing through to strategies, similar to Django's middleware design flumine allows middleware to be executed.
 
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 57a4389..93071f3 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -96,11 +96,11 @@ class ExampleStrategy(BaseStrategy):
 This order will be validated through controls, stored in the blotter and sent straight to the execution thread pool for execution. It is also possible to batch orders into transactions as follows:
 
 ```python
-with market.transaction as t:
+with market.transaction() as t:
     market.place_order(order)  # executed immediately in separate transaction
     t.place_order(order)  # executed on transaction __exit__
 
-with market.transaction as t:
+with market.transaction() as t:
     t.place_order(order)
     
     t.execute()  # above order executed
diff --git a/flumine/__version__.py b/flumine/__version__.py
index eee2122..9251e59 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.17.1"
+__version__ = "1.17.0"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/execution/transaction.py b/flumine/execution/transaction.py
index f9c8a55..83a96ea 100644
--- a/flumine/execution/transaction.py
+++ b/flumine/execution/transaction.py
@@ -17,11 +17,11 @@ class Transaction:
     when it is used as a context manager requests can
     be batched, for example:
 
-        with market.transaction as t:
+        with market.transaction() as t:
             market.place_order(order)  # executed immediately in separate transaction
             t.place_order(order)  # executed on transaction __exit__
 
-        with market.transaction as t:
+        with market.transaction() as t:
             t.place_order(order)
             ..
             t.execute()  # above order executed
diff --git a/flumine/markets/blotter.py b/flumine/markets/blotter.py
index 784dc2d..7a53873 100644
--- a/flumine/markets/blotter.py
+++ b/flumine/markets/blotter.py
@@ -33,15 +33,21 @@ class Blotter:
     def __init__(self, market_id: str):
         self.market_id = market_id
         self._orders = {}  # {Order.id: Order}
-        self._live_orders = []  # cached list of live orders
-        self._strategy_orders = defaultdict(
-            list
-        )  # cache list per strategy (faster lookup)
+        # cached lists/dicts for faster lookup
+        self._live_orders = []
+        self._strategy_orders = defaultdict(list)
+        self._strategy_selection_orders = defaultdict(list)
 
     def strategy_orders(self, strategy) -> list:
         """Returns all orders related to a strategy."""
         return self._strategy_orders[strategy]
 
+    def strategy_selection_orders(
+        self, strategy, selection_id: int, handicap: float = 0
+    ) -> list:
+        """Returns all orders related to a strategy selection."""
+        return self._strategy_selection_orders[(strategy, selection_id, handicap)]
+
     @property
     def live_orders(self):
         return iter(self._live_orders)
@@ -75,37 +81,34 @@ class Blotter:
         ub, ul = [], []  # unmatched bets, (price, size)
         moc_win_liability = 0.0
         moc_lose_liability = 0.0
-        for order in self.strategy_orders(strategy):
-            if order.lookup == lookup:
-                if order.status == OrderStatus.VIOLATION:
-                    continue
-
-                if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
-                    _size_matched = order.size_matched  # cache
-                    if _size_matched:
-                        if order.side == "BACK":
-                            mb.append((order.average_price_matched, _size_matched))
-                        else:
-                            ml.append((order.average_price_matched, _size_matched))
-                    _size_remaining = order.size_remaining  # cache
-                    if order.order_type.price and _size_remaining:
-                        if order.side == "BACK":
-                            ub.append((order.order_type.price, _size_remaining))
-                        else:
-                            ul.append((order.order_type.price, _size_remaining))
-                elif order.order_type.ORDER_TYPE in (
-                    OrderTypes.LIMIT_ON_CLOSE,
-                    OrderTypes.MARKET_ON_CLOSE,
-                ):
+        for order in self.strategy_selection_orders(strategy, *lookup[1:]):
+            if order.status == OrderStatus.VIOLATION:
+                continue
+            if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
+                _size_matched = order.size_matched  # cache
+                if _size_matched:
                     if order.side == "BACK":
-                        moc_lose_liability -= order.order_type.liability
+                        mb.append((order.average_price_matched, _size_matched))
                     else:
-                        moc_win_liability -= order.order_type.liability
+                        ml.append((order.average_price_matched, _size_matched))
+                _size_remaining = order.size_remaining  # cache
+                if order.order_type.price and _size_remaining:
+                    if order.side == "BACK":
+                        ub.append((order.order_type.price, _size_remaining))
+                    else:
+                        ul.append((order.order_type.price, _size_remaining))
+            elif order.order_type.ORDER_TYPE in (
+                OrderTypes.LIMIT_ON_CLOSE,
+                OrderTypes.MARKET_ON_CLOSE,
+            ):
+                if order.side == "BACK":
+                    moc_lose_liability -= order.order_type.liability
                 else:
-                    raise ValueError(
-                        "Unexpected order type: %s" % order.order_type.ORDER_TYPE
-                    )
-
+                    moc_win_liability -= order.order_type.liability
+            else:
+                raise ValueError(
+                    "Unexpected order type: %s" % order.order_type.ORDER_TYPE
+                )
         matched_exposure = calculate_matched_exposure(mb, ml)
         unmatched_exposure = calculate_unmatched_exposure(ub, ul)
 
@@ -132,6 +135,9 @@ class Blotter:
         self._orders[customer_order_ref] = order
         self._live_orders.append(order)
         self._strategy_orders[order.trade.strategy].append(order)
+        self._strategy_selection_orders[
+            (order.trade.strategy, *order.lookup[1:])
+        ].append(order)
 
     def __getitem__(self, customer_order_ref: str):
         return self._orders[customer_order_ref]


Test patch:
diff --git a/tests/test_blotter.py b/tests/test_blotter.py
index fee889f..ab42f00 100644
--- a/tests/test_blotter.py
+++ b/tests/test_blotter.py
@@ -14,14 +14,23 @@ class BlotterTest(unittest.TestCase):
         self.assertEqual(self.blotter.market_id, "1.23")
         self.assertEqual(self.blotter._orders, {})
         self.assertEqual(self.blotter._live_orders, [])
+        self.assertEqual(self.blotter._strategy_orders, {})
+        self.assertEqual(self.blotter._strategy_selection_orders, {})
 
     def test_strategy_orders(self):
-        mock_order = mock.Mock()
+        mock_order = mock.Mock(lookup=(1, 2, 3))
         mock_order.trade.strategy = 69
         self.blotter["12345"] = mock_order
         self.assertEqual(self.blotter.strategy_orders(12), [])
         self.assertEqual(self.blotter.strategy_orders(69), [mock_order])
 
+    def test_strategy_selection_orders(self):
+        mock_order = mock.Mock(lookup=(1, 2, 3))
+        mock_order.trade.strategy = 69
+        self.blotter["12345"] = mock_order
+        self.assertEqual(self.blotter.strategy_selection_orders(12, 2, 3), [])
+        self.assertEqual(self.blotter.strategy_selection_orders(69, 2, 3), [mock_order])
+
     def test_live_orders(self):
         self.assertEqual(list(self.blotter.live_orders), [])
         mock_order = mock.Mock(complete=False)
@@ -247,13 +256,17 @@ class BlotterTest(unittest.TestCase):
         self.assertNotIn("321", self.blotter)
 
     def test__setitem(self):
-        mock_order = mock.Mock()
+        mock_order = mock.Mock(lookup=(1, 2, 3))
         self.blotter["123"] = mock_order
         self.assertEqual(self.blotter._orders, {"123": mock_order})
         self.assertEqual(self.blotter._live_orders, [mock_order])
         self.assertEqual(
             self.blotter._strategy_orders, {mock_order.trade.strategy: [mock_order]}
         )
+        self.assertEqual(
+            self.blotter._strategy_selection_orders,
+            {(mock_order.trade.strategy, 2, 3): [mock_order]},
+        )
 
     def test__getitem(self):
         self.blotter._orders = {"12345": "test", "54321": "test2"}
diff --git a/tests/test_tradingcontrols.py b/tests/test_tradingcontrols.py
index de59274..9d82705 100644
--- a/tests/test_tradingcontrols.py
+++ b/tests/test_tradingcontrols.py
@@ -331,11 +331,10 @@ class TestStrategyExposure(unittest.TestCase):
         order1.order_type.price = 2.0
         order1.order_type.size = 9.0
         order1.size_remaining = 9.0
-        order1.lookup = "lookup"
         order1.average_price_matched = 0.0
         order1.size_matched = 0
 
-        order2 = mock.Mock()
+        order2 = mock.Mock(lookup=(1, 2, 3))
         order2.trade.strategy.max_order_exposure = 10
         order2.trade.strategy.max_selection_exposure = 10
         order2.trade.strategy = strategy
@@ -344,7 +343,6 @@ class TestStrategyExposure(unittest.TestCase):
         order2.order_type.price = 3.0
         order2.order_type.size = 9.0
         order2.size_remaining = 5.0
-        order2.lookup = "lookup"
         order2.average_price_matched = 0.0
         order2.size_matched = 0
 
@@ -370,18 +368,16 @@ class TestStrategyExposure(unittest.TestCase):
         order1.order_type.price = 2.0
         order1.order_type.size = 9.0
         order1.size_remaining = 9.0
-        order1.lookup = "lookup"
         order1.average_price_matched = 0.0
         order1.size_matched = 0
 
-        order2 = mock.Mock()
+        order2 = mock.Mock(lookup=(1, 2, 3))
         order2.trade.strategy = strategy
         order2.order_type.ORDER_TYPE = OrderTypes.LIMIT
         order2.side = "BACK"
         order2.order_type.price = 3.0
         order2.order_type.size = 9.0
         order2.size_remaining = 5.0
-        order2.lookup = "lookup"
         order2.average_price_matched = 0.0
         order2.size_matched = 0