Repository: liampauling/flumine
Pull request #412 (2021-04-20 07:56:15)

Duplicate Trade logging handled and has_trade added to Blotter


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 7229187..438c6e3 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,13 @@
 Release History
 ---------------
 
+1.18.4 (2021-04-20)
++++++++++++++++++++
+
+**Bug Fixes**
+
+- Duplicate Trade logging handled and has_trade added to Blotter
+
 1.18.3 (2021-04-16)
 +++++++++++++++++++
 
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 3d52322..8ff1b53 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.3"
+__version__ = "1.18.4"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/execution/transaction.py b/flumine/execution/transaction.py
index 469b414..57f66ff 100644
--- a/flumine/execution/transaction.py
+++ b/flumine/execution/transaction.py
@@ -50,11 +50,12 @@ class Transaction:
         if execute:
             runner_context = order.trade.strategy.get_runner_context(*order.lookup)
             runner_context.place(order.trade.id)
+        if self.market.blotter.has_trade(order.trade.id) is False:
+            self.market.flumine.log_control(events.TradeEvent(order.trade))
         if order.id not in self.market.blotter:
             self.market.blotter[order.id] = order
             if order.trade.market_notes is None:
                 order.trade.update_market_notes(self.market)
-            self.market.flumine.log_control(events.TradeEvent(order.trade))
         else:
             raise OrderError("Order %s has already been placed" % order.id)
         if execute:  # handles replaceOrder
diff --git a/flumine/markets/blotter.py b/flumine/markets/blotter.py
index 43838ee..1a3b0ce 100644
--- a/flumine/markets/blotter.py
+++ b/flumine/markets/blotter.py
@@ -35,6 +35,7 @@ class Blotter:
         self.market_id = market_id
         self._orders = {}  # {Order.id: Order}
         # cached lists/dicts for faster lookup
+        self._trades = defaultdict(list)  # {Trade.id: [Order,]}
         self._live_orders = []
         self._strategy_orders = defaultdict(list)
         self._strategy_selection_orders = defaultdict(list)
@@ -150,11 +151,15 @@ class Blotter:
     def has_order(self, customer_order_ref: str) -> bool:
         return customer_order_ref in self._orders
 
+    def has_trade(self, trade_id: str) -> bool:
+        return trade_id in self._trades
+
     __contains__ = has_order
 
     def __setitem__(self, customer_order_ref: str, order) -> None:
         self._orders[customer_order_ref] = order
         self._live_orders.append(order)
+        self._trades[order.trade.id].append(order)
         self._strategy_orders[order.trade.strategy].append(order)
         self._strategy_selection_orders[
             (order.trade.strategy, *order.lookup[1:])


Test patch:
diff --git a/tests/test_blotter.py b/tests/test_blotter.py
index e681b3f..833850c 100644
--- a/tests/test_blotter.py
+++ b/tests/test_blotter.py
@@ -14,6 +14,7 @@ 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._trades, {})
         self.assertEqual(self.blotter._strategy_orders, {})
         self.assertEqual(self.blotter._strategy_selection_orders, {})
 
@@ -349,6 +350,11 @@ class BlotterTest(unittest.TestCase):
         self.blotter._live_orders = ["test"]
         self.blotter.complete_order("test")
 
+    def test_has_trade(self):
+        self.assertFalse(self.blotter.has_trade("123"))
+        self.blotter._trades["123"].append(1)
+        self.assertTrue(self.blotter.has_trade("123"))
+
     def test__contains(self):
         self.blotter._orders = {"123": "test"}
         self.assertIn("123", self.blotter)
@@ -359,6 +365,7 @@ class BlotterTest(unittest.TestCase):
         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._trades, {mock_order.trade.id: [mock_order]})
         self.assertEqual(
             self.blotter._strategy_orders, {mock_order.trade.strategy: [mock_order]}
         )
diff --git a/tests/test_transaction.py b/tests/test_transaction.py
index cbc24d2..fce9153 100644
--- a/tests/test_transaction.py
+++ b/tests/test_transaction.py
@@ -28,7 +28,8 @@ class TransactionTest(unittest.TestCase):
     )
     @mock.patch("flumine.execution.transaction.events")
     def test_place_order(self, mock_events, mock__validate_controls):
-        self.transaction.market.blotter = {}
+        self.transaction.market.blotter = mock.MagicMock()
+        self.transaction.market.blotter.has_trade.return_value = False
         mock_order = mock.Mock(id="123", lookup=(1, 2, 3))
         mock_order.trade.market_notes = None
         self.assertTrue(self.transaction.place_order(mock_order))
@@ -46,6 +47,9 @@ class TransactionTest(unittest.TestCase):
         mock_order.trade.strategy.get_runner_context.assert_called_with(
             *mock_order.lookup
         )
+        self.transaction.market.blotter.has_trade.assert_called_with(
+            mock_order.trade.id
+        )
 
     @mock.patch(
         "flumine.execution.transaction.Transaction._validate_controls",
@@ -53,6 +57,8 @@ class TransactionTest(unittest.TestCase):
     )
     @mock.patch("flumine.execution.transaction.events")
     def test_place_order_not_executed(self, mock_events, mock__validate_controls):
+        self.transaction.market.blotter = mock.MagicMock()
+        self.transaction.market.blotter.has_trade.return_value = False
         mock_order = mock.Mock(id="123")
         self.assertTrue(self.transaction.place_order(mock_order, execute=False))
         mock_order.place.assert_called_with(
@@ -64,14 +70,19 @@ class TransactionTest(unittest.TestCase):
         mock__validate_controls.assert_not_called()
         self.transaction._pending_place = []
         self.assertFalse(self.transaction._pending_orders)
+        self.transaction.market.blotter.has_trade.assert_called_with(
+            mock_order.trade.id
+        )
 
     @mock.patch(
         "flumine.execution.transaction.Transaction._validate_controls",
         return_value=True,
     )
     def test_place_order_retry(self, mock__validate_controls):
+        self.transaction.market.blotter = mock.MagicMock()
+        self.transaction.market.blotter.has_trade.return_value = False
+        self.transaction.market.blotter.__contains__.return_value = True
         mock_order = mock.Mock(lookup=(1, 2, 3))
-        self.transaction.market.blotter = {mock_order.id: mock_order}
         with self.assertRaises(OrderError):
             self.transaction.place_order(mock_order)
         mock__validate_controls.assert_called_with(mock_order, OrderPackageType.PLACE)