Repository: liampauling/flumine
Pull request #383 (2021-03-01 09:27:33)

prevent replace orders from being validated as though they are 'new'
limit replace/place integration tests added


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index bb3230a..48a96b8 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,13 @@
 Release History
 ---------------
 
+1.17.5 (2021-03-01)
++++++++++++++++++++
+
+**Bug Fixes**
+
+- #382 replace order failure fix (no execute)
+
 1.17.4 (2021-02-26)
 +++++++++++++++++++
 
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 16d2113..5ef6e16 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.4"
+__version__ = "1.17.5"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/execution/transaction.py b/flumine/execution/transaction.py
index 1ba5715..a3c09ab 100644
--- a/flumine/execution/transaction.py
+++ b/flumine/execution/transaction.py
@@ -42,7 +42,7 @@ class Transaction:
     def place_order(
         self, order, market_version: int = None, execute: bool = True
     ) -> bool:
-        if self._validate_controls(order, OrderPackageType.PLACE) is False:
+        if execute and self._validate_controls(order, OrderPackageType.PLACE) is False:
             return False
         # place
         order.place(self.market.market_book.publish_time)


Test patch:
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 8c04bc8..6f7feb9 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -28,6 +28,64 @@ class IntegrationTest(unittest.TestCase):
 
     def test_backtest_pro(self):
         class LimitOrders(BaseStrategy):
+            def check_market_book(self, market, market_book):
+                if not market_book.inplay and market.seconds_to_start < 100:
+                    return True
+
+            def process_market_book(self, market, market_book):
+                with market.transaction() as t:
+                    for runner in market_book.runners:
+                        if runner.status == "ACTIVE":
+                            back = get_price(runner.ex.available_to_back, 0)
+                            runner_context = self.get_runner_context(
+                                market.market_id, runner.selection_id
+                            )
+                            if runner_context.trade_count == 0:
+                                trade = Trade(
+                                    market_book.market_id,
+                                    runner.selection_id,
+                                    runner.handicap,
+                                    self,
+                                )
+                                order = trade.create_order(
+                                    side="BACK",
+                                    order_type=LimitOrder(back, 2.00),
+                                )
+                                t.place_order(order)
+
+        class LimitReplaceOrders(BaseStrategy):
+            def check_market_book(self, market, market_book):
+                if not market_book.inplay and market.seconds_to_start < 100:
+                    return True
+
+            def process_market_book(self, market, market_book):
+                with market.transaction() as t:
+                    for runner in market_book.runners:
+                        if runner.status == "ACTIVE":
+                            runner_context = self.get_runner_context(
+                                market.market_id, runner.selection_id
+                            )
+                            if runner_context.trade_count == 0:
+                                trade = Trade(
+                                    market_book.market_id,
+                                    runner.selection_id,
+                                    runner.handicap,
+                                    self,
+                                )
+                                order = trade.create_order(
+                                    side="BACK",
+                                    order_type=LimitOrder(1000, 2.00),
+                                )
+                                t.place_order(order)
+
+            def process_orders(self, market, orders: list) -> None:
+                with market.transaction() as t:
+                    for order in orders:
+                        if order.status == OrderStatus.EXECUTABLE:
+                            if order.size_matched == 0:
+                                t.replace_order(order, new_price=1.01)
+
+        class LimitOrdersInplay(BaseStrategy):
             def check_market_book(self, market, market_book):
                 if market_book.inplay:
                     return True
@@ -84,8 +142,22 @@ class IntegrationTest(unittest.TestCase):
             market_filter={"markets": ["tests/resources/PRO-1.170258213"]},
             max_order_exposure=1000,
             max_selection_exposure=105,
+            max_trade_count=1,
         )
         framework.add_strategy(limit_strategy)
+        limit_replace_strategy = LimitReplaceOrders(
+            market_filter={"markets": ["tests/resources/PRO-1.170258213"]},
+            max_order_exposure=1000,
+            max_selection_exposure=105,
+            max_trade_count=1,
+        )
+        framework.add_strategy(limit_replace_strategy)
+        limit_inplay_strategy = LimitOrdersInplay(
+            market_filter={"markets": ["tests/resources/PRO-1.170258213"]},
+            max_order_exposure=1000,
+            max_selection_exposure=105,
+        )
+        framework.add_strategy(limit_inplay_strategy)
         market_strategy = MarketOnCloseOrders(
             market_filter={"markets": ["tests/resources/PRO-1.170258213"]},
             max_order_exposure=1000,
@@ -99,16 +171,28 @@ class IntegrationTest(unittest.TestCase):
         for market in framework.markets:
             limit_orders = market.blotter.strategy_orders(limit_strategy)
             self.assertEqual(
-                round(sum([o.simulated.profit for o in limit_orders]), 2), 18.96
+                round(sum([o.simulated.profit for o in limit_orders]), 2), -16.8
+            )
+            self.assertEqual(len(limit_orders), 14)
+            limit_replace_orders = market.blotter.strategy_orders(
+                limit_replace_strategy
+            )
+            self.assertEqual(
+                round(sum([o.simulated.profit for o in limit_replace_orders]), 2), -16.8
+            )
+            self.assertEqual(len(limit_replace_orders), 28)
+            limit_inplay_orders = market.blotter.strategy_orders(limit_inplay_strategy)
+            self.assertEqual(
+                round(sum([o.simulated.profit for o in limit_inplay_orders]), 2), 18.96
             )
-            self.assertEqual(len(limit_orders), 15)
+            self.assertEqual(len(limit_inplay_orders), 15)
             market_orders = market.blotter.strategy_orders(market_strategy)
             self.assertEqual(
                 round(sum([o.simulated.profit for o in market_orders]), 2), -6.68
             )
             self.assertEqual(len(market_orders), 14)
             # check transaction count
-            self.assertEqual(market._transaction_id, 180)
+            self.assertEqual(market._transaction_id, 25428)
 
     def tearDown(self) -> None:
         config.simulated = False
diff --git a/tests/test_transaction.py b/tests/test_transaction.py
index b3a8c5d..533c771 100644
--- a/tests/test_transaction.py
+++ b/tests/test_transaction.py
@@ -57,7 +57,7 @@ class TransactionTest(unittest.TestCase):
         self.transaction.market.flumine.log_control.assert_called_with(
             mock_events.TradeEvent()
         )
-        mock__validate_controls.assert_called_with(mock_order, OrderPackageType.PLACE)
+        mock__validate_controls.assert_not_called()
         self.transaction._pending_place = []
         self.assertFalse(self.transaction._pending_orders)