Repository: liampauling/flumine
Pull request #181 (2020-05-29 10:04:47)

Task/sim update replace fixes


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 10a7e2d..b1b61a0 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,18 +3,6 @@
 Release History
 ---------------
 
-1.6.0 (2020-06-01)
-+++++++++++++++++++
-
-**Improvements**
-
-- #175 Update/Replace simulated handling
-- Trade context manager added
-
-**Bug Fixes**
-
-- BetfairExecution replace bugfix
-
 1.5.7 (2020-06-01)
 +++++++++++++++++++
 
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 8698451..b9101f6 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.6.0"
+__version__ = "1.5.7"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/execution/betfairexecution.py b/flumine/execution/betfairexecution.py
index 566e68a..c1335f9 100644
--- a/flumine/execution/betfairexecution.py
+++ b/flumine/execution/betfairexecution.py
@@ -23,14 +23,17 @@ class BetfairExecution(BaseExecution):
             for (order, instruction_report) in zip(
                 order_package, response.place_instruction_reports
             ):
-                self._order_logger(order, instruction_report, OrderPackageType.PLACE)
-                if instruction_report.status == "SUCCESS":
-                    order.executable()
-                elif instruction_report.status == "FAILURE":
-                    order.lapsed()  # todo correct?
-                elif instruction_report.status == "TIMEOUT":
-                    # https://docs.developer.betfair.com/display/1smk3cen4v3lu3yomq5qye0ni/Betting+Enums#BettingEnums-ExecutionReportStatus
-                    pass
+                with order.trade:
+                    self._order_logger(
+                        order, instruction_report, OrderPackageType.PLACE
+                    )
+                    if instruction_report.status == "SUCCESS":
+                        order.executable()
+                    elif instruction_report.status == "FAILURE":
+                        order.lapsed()  # todo correct?
+                    elif instruction_report.status == "TIMEOUT":
+                        # https://docs.developer.betfair.com/display/1smk3cen4v3lu3yomq5qye0ni/Betting+Enums#BettingEnums-ExecutionReportStatus
+                        pass
 
     def place(self, order_package: OrderPackageType, session: requests.Session):
         return order_package.client.betting_client.betting.place_orders(
@@ -52,22 +55,26 @@ class BetfairExecution(BaseExecution):
             for instruction_report in response.cancel_instruction_reports:
                 # get order (can't rely on the order they are returned)
                 order = order_lookup.pop(instruction_report.instruction.bet_id)
-                self._order_logger(order, instruction_report, OrderPackageType.CANCEL)
-                if instruction_report.status == "SUCCESS":
-                    if (
-                        instruction_report.size_cancelled == order.size_remaining
-                    ):  # todo what if?
-                        order.execution_complete()
-                    else:
+                with order.trade:
+                    self._order_logger(
+                        order, instruction_report, OrderPackageType.CANCEL
+                    )
+                    if instruction_report.status == "SUCCESS":
+                        if (
+                            instruction_report.size_cancelled == order.size_remaining
+                        ):  # todo what if?
+                            order.execution_complete()
+                        else:
+                            order.executable()
+                    elif instruction_report.status == "FAILURE":
+                        order.executable()
+                    elif instruction_report.status == "TIMEOUT":
                         order.executable()
-                elif instruction_report.status == "FAILURE":
-                    order.executable()
-                elif instruction_report.status == "TIMEOUT":
-                    order.executable()
 
             # reset any not returned so that they can be picked back up
             for order in order_lookup.values():
-                order.executable()
+                with order.trade:
+                    order.executable()
 
     def cancel(self, order_package: OrderPackageType, session: requests.Session):
         # temp copy to prevent an empty list of instructions sent
@@ -92,13 +99,16 @@ class BetfairExecution(BaseExecution):
             for (order, instruction_report) in zip(
                 order_package, response.update_instruction_reports
             ):
-                self._order_logger(order, instruction_report, OrderPackageType.UPDATE)
-                if instruction_report.status == "SUCCESS":
-                    order.executable()
-                elif instruction_report.status == "FAILURE":
-                    order.executable()
-                elif instruction_report.status == "TIMEOUT":
-                    order.executable()
+                with order.trade:
+                    self._order_logger(
+                        order, instruction_report, OrderPackageType.UPDATE
+                    )
+                    if instruction_report.status == "SUCCESS":
+                        order.executable()
+                    elif instruction_report.status == "FAILURE":
+                        order.executable()
+                    elif instruction_report.status == "TIMEOUT":
+                        order.executable()
 
     def update(self, order_package: OrderPackageType, session: requests.Session):
         return order_package.client.betting_client.betting.update_orders(
@@ -116,35 +126,54 @@ class BetfairExecution(BaseExecution):
             for (order, instruction_report) in zip(
                 order_package, response.replace_instruction_reports
             ):
-                # process cancel response
-                if instruction_report.cancel_instruction_reports.status == "SUCCESS":
-                    self._order_logger(
-                        order,
-                        instruction_report.cancel_instruction_reports,
-                        OrderPackageType.CANCEL,
-                    )
-                    order.execution_complete()
-                elif instruction_report.cancel_instruction_reports.status == "FAILURE":
-                    order.executable()
-                elif instruction_report.cancel_instruction_reports.status == "TIMEOUT":
-                    order.executable()
+                with order.trade:
+                    # process cancel response
+                    if (
+                        instruction_report.cancel_instruction_reports.status
+                        == "SUCCESS"
+                    ):
+                        self._order_logger(
+                            order,
+                            instruction_report.cancel_instruction_reports,
+                            OrderPackageType.CANCEL,
+                        )
+                        order.execution_complete()
+                    elif (
+                        instruction_report.cancel_instruction_reports.status
+                        == "FAILURE"
+                    ):
+                        order.executable()
+                    elif (
+                        instruction_report.cancel_instruction_reports.status
+                        == "TIMEOUT"
+                    ):
+                        order.executable()
 
-                # process place response
-                if instruction_report.place_instruction_reports.status == "SUCCESS":
-                    # create new order
-                    replacement_order = order.trade.create_order_replacement(order)
-                    self._order_logger(
-                        replacement_order,
-                        instruction_report.place_instruction_reports,
-                        OrderPackageType.REPLACE,
-                    )
-                    # add to blotter
-                    order_package.market.place_order(replacement_order, execute=False)
-                    replacement_order.executable()
-                elif instruction_report.place_instruction_reports.status == "FAILURE":
-                    pass  # todo
-                elif instruction_report.place_instruction_reports.status == "TIMEOUT":
-                    pass  # todo
+                    # process place response
+                    if instruction_report.place_instruction_reports.status == "SUCCESS":
+                        # create new order
+                        replacement_order = order.trade.create_order_replacement(
+                            order,
+                            instruction_report.place_instruction_reports.instruction.limit_order.price,
+                        )
+                        self._order_logger(
+                            replacement_order,
+                            instruction_report.place_instruction_reports,
+                            OrderPackageType.REPLACE,
+                        )
+                        # add to blotter
+                        order_package.market.place_order(
+                            replacement_order, execute=False
+                        )
+                        replacement_order.executable()
+                    elif (
+                        instruction_report.place_instruction_reports.status == "FAILURE"
+                    ):
+                        pass  # todo
+                    elif (
+                        instruction_report.place_instruction_reports.status == "TIMEOUT"
+                    ):
+                        pass  # todo
 
     def replace(self, order_package: OrderPackageType, session: requests.Session):
         return order_package.client.betting_client.betting.replace_orders(
diff --git a/flumine/execution/simulatedexecution.py b/flumine/execution/simulatedexecution.py
index 387002e..dd997c3 100644
--- a/flumine/execution/simulatedexecution.py
+++ b/flumine/execution/simulatedexecution.py
@@ -40,15 +40,18 @@ class SimulatedExecution(BaseExecution):
         #     time.sleep(order_package.bet_delay + self.PLACE_LATENCY)
         market_book = order_package.market.market_book
         for order, instruction in zip(order_package, order_package.place_instructions):
-            self._bet_id += 1
-            simulated_response = order.simulated.place(
-                market_book, instruction, self._bet_id
-            )
-            self._order_logger(order, simulated_response, order_package.package_type)
-            if simulated_response.status == "SUCCESS":
-                order.executable()
-            elif simulated_response.status == "FAILURE":
-                order.lapsed()
+            with order.trade:
+                self._bet_id += 1
+                simulated_response = order.simulated.place(
+                    market_book, instruction, self._bet_id
+                )
+                self._order_logger(
+                    order, simulated_response, order_package.package_type
+                )
+                if simulated_response.status == "SUCCESS":
+                    order.executable()
+                elif simulated_response.status == "FAILURE":
+                    order.lapsed()
 
     def execute_cancel(
         self, order_package, http_session: Optional[requests.Session]
@@ -56,19 +59,71 @@ class SimulatedExecution(BaseExecution):
         # todo if order_package.client.paper_trade:
         #     time.sleep(self.CANCEL_LATENCY)
         for order in order_package:
-            simulated_response = order.simulated.cancel()
-            self._order_logger(order, simulated_response, order_package.package_type)
-            if simulated_response.status == "SUCCESS":
-                order.execution_complete()
-            elif simulated_response.status == "FAILURE":
-                order.executable()
+            with order.trade:
+                simulated_response = order.simulated.cancel()
+                self._order_logger(
+                    order, simulated_response, order_package.package_type
+                )
+                if simulated_response.status == "SUCCESS":
+                    order.execution_complete()
+                elif simulated_response.status == "FAILURE":
+                    order.executable()
 
     def execute_update(
         self, order_package, http_session: Optional[requests.Session]
     ) -> None:
-        raise NotImplementedError
+        # todo if order_package.client.paper_trade:
+        #     time.sleep(self.UPDATE_LATENCY)
+        for order, instruction in zip(order_package, order_package.update_instructions):
+            with order.trade:
+                simulated_response = order.simulated.update(instruction)
+                self._order_logger(
+                    order, simulated_response, order_package.package_type
+                )
+                if simulated_response.status == "SUCCESS":
+                    order.executable()
+                elif simulated_response.status == "FAILURE":
+                    order.executable()
 
     def execute_replace(
         self, order_package, http_session: Optional[requests.Session]
     ) -> None:
-        raise NotImplementedError
+        # todo if order_package.client.paper_trade:
+        #     time.sleep(self.REPLACE_LATENCY)
+        market_book = order_package.market.market_book
+        for order, instruction in zip(
+            order_package, order_package.replace_instructions
+        ):
+            with order.trade:
+                # cancel current order
+                cancel_instruction_report = order.simulated.cancel()
+                if cancel_instruction_report.status == "SUCCESS":
+                    order.execution_complete()
+                elif cancel_instruction_report.status == "FAILURE":
+                    order.executable()  # todo do not carry out replace
+                else:
+                    order.lapsed()  # todo do not carry out replace
+                self._order_logger(
+                    order, cancel_instruction_report, OrderPackageType.CANCEL,
+                )
+
+                # place new order
+                self._bet_id += 1
+                replacement_order = order.trade.create_order_replacement(
+                    order, instruction.get("newPrice")
+                )
+                place_instruction_report = replacement_order.simulated.place(
+                    market_book, instruction, self._bet_id
+                )
+                if place_instruction_report.status == "SUCCESS":
+                    self._order_logger(
+                        replacement_order,
+                        place_instruction_report,
+                        order_package.package_type,
+                    )
+                    # add to blotter
+                    market = order_package.market
+                    market.place_order(replacement_order, execute=False)
+                    replacement_order.executable()
+                elif place_instruction_report.status == "FAILURE":
+                    order.executable()
diff --git a/flumine/markets/blotter.py b/flumine/markets/blotter.py
index 99d9de0..9bdd639 100644
--- a/flumine/markets/blotter.py
+++ b/flumine/markets/blotter.py
@@ -72,7 +72,10 @@ class Blotter:
     @property
     def live_orders(self) -> bool:
         for order in self._orders.values():
-            if order.status == OrderStatus.EXECUTABLE:
+            if (
+                order.status == OrderStatus.EXECUTABLE
+                or order.trade.trade_complete is False
+            ):
                 return True
         return False
 
diff --git a/flumine/order/process.py b/flumine/order/process.py
index d524cb6..404ec77 100644
--- a/flumine/order/process.py
+++ b/flumine/order/process.py
@@ -59,10 +59,8 @@ def process_current_order(order: BaseOrder):
         elif order.size_remaining == 0:
             order.execution_complete()
 
-    if order.status == OrderStatus.EXECUTION_COMPLETE:
-        if order.trade.status == TradeStatus.LIVE:
-            if not order.trade.offset_orders:
-                order.trade.complete()
+    if order.trade.trade_complete:
+        order.trade.complete()
 
 
 def create_order_from_current(markets: Markets, strategies: Strategies, current_order):
diff --git a/flumine/order/trade.py b/flumine/order/trade.py
index 79e2000..29a4bb1 100644
--- a/flumine/order/trade.py
+++ b/flumine/order/trade.py
@@ -6,7 +6,7 @@ from typing import Union, Type
 from betfairlightweight.resources.bettingresources import CurrentOrder
 
 from ..strategy.strategy import BaseStrategy
-from .order import BetfairOrder
+from .order import BetfairOrder, OrderStatus
 from .ordertype import LimitOrder, LimitOnCloseOrder, MarketOnCloseOrder
 from ..exceptions import OrderError
 
@@ -14,6 +14,7 @@ logger = logging.getLogger(__name__)
 
 
 class TradeStatus(Enum):
+    PENDING = "Pending"  # pending exchange processing
     LIVE = "Live"
     COMPLETE = "Complete"
 
@@ -62,6 +63,17 @@ class Trade:
         )
         runner_context.reset()  # todo race condition?
 
+    @property
+    def trade_complete(self) -> bool:
+        if self.status != TradeStatus.LIVE:
+            return False
+        if self.offset_orders:
+            return False
+        for order in self.orders:
+            if order.status != OrderStatus.EXECUTION_COMPLETE:
+                return False
+        return True
+
     def create_order(
         self,
         side: str,
@@ -77,11 +89,13 @@ class Trade:
         self.orders.append(order)
         return order
 
-    def create_order_replacement(self, order: BetfairOrder) -> BetfairOrder:
+    def create_order_replacement(
+        self, order: BetfairOrder, new_price: float
+    ) -> BetfairOrder:
         """Create new order due to replace
         execution"""
         order_type = LimitOrder(
-            price=order.update_data["new_price"],
+            price=new_price,
             size=order.order_type.size,
             persistence_type=order.order_type.persistence_type,
         )
@@ -126,3 +140,13 @@ class Trade:
             "orders": [o.id for o in self.orders],
             "notes": self.notes_str,
         }
+
+    def __enter__(self):
+        # todo raise error if already pending?
+        self._update_status(TradeStatus.PENDING)
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        if exc_tb is None:
+            self._update_status(TradeStatus.LIVE)
+        else:
+            logger.critical("Trade error in %s" % self.id, exc_info=True)


Test patch:
diff --git a/flumine/backtest/simulated.py b/flumine/backtest/simulated.py
index 6e9c877..05e296d 100644
--- a/flumine/backtest/simulated.py
+++ b/flumine/backtest/simulated.py
@@ -1,7 +1,11 @@
 import datetime
 from betfairlightweight.resources.bettingresources import MarketBook, RunnerBook
 
-from .utils import SimulatedPlaceResponse, SimulatedCancelResponse
+from .utils import (
+    SimulatedPlaceResponse,
+    SimulatedCancelResponse,
+    SimulatedUpdateResponse,
+)
 from ..utils import get_price, wap
 from ..order.ordertype import OrderTypes
 from .. import config
@@ -98,13 +102,20 @@ class Simulated:
                 status="FAILURE", error_code="BET_ACTION_ERROR",  # todo ?
             )
 
-    def update(self):
+    def update(self, instruction: dict):
         # simulates updateOrder request->update->response
-        pass
-
-    def replace(self):
-        # simulates replaceOrder request->cancel/matching->response
-        pass
+        if (
+            self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT
+            and self.size_remaining > 0
+        ):
+            self.order.order_type.persistence_type = instruction.get(
+                "newPersistenceType"
+            )
+            return SimulatedUpdateResponse(status="SUCCESS")
+        else:
+            return SimulatedCancelResponse(
+                status="FAILURE", error_code="BET_ACTION_ERROR",
+            )
 
     def _get_runner(self, market_book: MarketBook) -> RunnerBook:
         runner_dict = {
diff --git a/flumine/backtest/utils.py b/flumine/backtest/utils.py
index 9aa475c..bc5befe 100644
--- a/flumine/backtest/utils.py
+++ b/flumine/backtest/utils.py
@@ -42,3 +42,11 @@ class SimulatedCancelResponse:
         self.size_cancelled = size_cancelled
         self.cancelled_date = cancelled_date
         self.error_code = error_code
+
+
+class SimulatedUpdateResponse:
+    def __init__(
+        self, status: str = None, error_code: str = None,
+    ):
+        self.status = status
+        self.error_code = error_code
diff --git a/tests/test_blotter.py b/tests/test_blotter.py
index e639885..33a1659 100644
--- a/tests/test_blotter.py
+++ b/tests/test_blotter.py
@@ -75,6 +75,14 @@ class BlotterTest(unittest.TestCase):
         self.blotter._orders = {"12345": mock_order}
         self.assertTrue(self.blotter.live_orders)
 
+    def test_live_orders_trade(self):
+        self.assertFalse(self.blotter.live_orders)
+        mock_order = mock.Mock()
+        mock_order.trade.trade_complete = False
+        mock_order.status = OrderStatus.EXECUTION_COMPLETE
+        self.blotter._orders = {"12345": mock_order}
+        self.assertTrue(self.blotter.live_orders)
+
     def test_process_closed_market(self):
         mock_market_book = mock.Mock()
         mock_runner = mock.Mock(selection_id=123, handicap=0.0)
diff --git a/tests/test_execution.py b/tests/test_execution.py
index cb3feba..83842e0 100644
--- a/tests/test_execution.py
+++ b/tests/test_execution.py
@@ -1,11 +1,12 @@
 import unittest
 from unittest import mock
+from unittest.mock import call
 
+from flumine.clients.clients import ExchangeType
+from flumine.exceptions import OrderExecutionError
 from flumine.execution.baseexecution import BaseExecution, OrderPackageType
 from flumine.execution.betfairexecution import BetfairExecution
 from flumine.execution.simulatedexecution import SimulatedExecution
-from flumine.clients.clients import ExchangeType
-from flumine.exceptions import OrderExecutionError
 
 
 class BaseExecutionTest(unittest.TestCase):
@@ -141,6 +142,8 @@ class BetfairExecutionTest(unittest.TestCase):
     ):
         mock_session = mock.Mock()
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order_package = mock.Mock()
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_order_package.info = {}
@@ -157,6 +160,8 @@ class BetfairExecutionTest(unittest.TestCase):
             mock_order, mock_instruction_report, OrderPackageType.PLACE
         )
         mock_order.executable.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.place")
@@ -166,6 +171,8 @@ class BetfairExecutionTest(unittest.TestCase):
     ):
         mock_session = mock.Mock()
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order_package = mock.Mock()
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_order_package.info = {}
@@ -182,6 +189,8 @@ class BetfairExecutionTest(unittest.TestCase):
             mock_order, mock_instruction_report, OrderPackageType.PLACE
         )
         mock_order.lapsed.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.place")
@@ -191,6 +200,8 @@ class BetfairExecutionTest(unittest.TestCase):
     ):
         mock_session = mock.Mock()
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order_package = mock.Mock()
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_order_package.info = {}
@@ -207,6 +218,8 @@ class BetfairExecutionTest(unittest.TestCase):
             mock_order, mock_instruction_report, OrderPackageType.PLACE
         )
         # mock_order.lapsed.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.place")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
@@ -236,6 +249,8 @@ class BetfairExecutionTest(unittest.TestCase):
     ):
         mock_session = mock.Mock()
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order.bet_id = 123
         mock_order.size_remaining = 2
         mock_order_package = mock.Mock()
@@ -256,6 +271,8 @@ class BetfairExecutionTest(unittest.TestCase):
             mock_order, mock_instruction_report, OrderPackageType.CANCEL
         )
         mock_order.execution_complete.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.cancel")
@@ -265,6 +282,8 @@ class BetfairExecutionTest(unittest.TestCase):
     ):
         mock_session = mock.Mock()
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order.bet_id = 123
         mock_order.size_remaining = 2
         mock_order_package = mock.Mock()
@@ -285,6 +304,8 @@ class BetfairExecutionTest(unittest.TestCase):
             mock_order, mock_instruction_report, OrderPackageType.CANCEL
         )
         mock_order.executable.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.cancel")
@@ -294,6 +315,8 @@ class BetfairExecutionTest(unittest.TestCase):
     ):
         mock_session = mock.Mock()
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order.bet_id = 123
         mock_order_package = mock.Mock()
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
@@ -312,6 +335,8 @@ class BetfairExecutionTest(unittest.TestCase):
             mock_order, mock_instruction_report, OrderPackageType.CANCEL
         )
         mock_order.executable.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.cancel")
@@ -321,6 +346,8 @@ class BetfairExecutionTest(unittest.TestCase):
     ):
         mock_session = mock.Mock()
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order.bet_id = 123
         mock_order_package = mock.Mock()
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
@@ -339,6 +366,8 @@ class BetfairExecutionTest(unittest.TestCase):
             mock_order, mock_instruction_report, OrderPackageType.CANCEL
         )
         mock_order.executable.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.cancel")
@@ -348,6 +377,8 @@ class BetfairExecutionTest(unittest.TestCase):
     ):
         mock_session = mock.Mock()
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order.bet_id = 123
         mock_order_package = mock.Mock()
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
@@ -360,6 +391,8 @@ class BetfairExecutionTest(unittest.TestCase):
             mock_cancel, mock_order_package, mock_session
         )
         mock_order.executable.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.cancel")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
@@ -398,6 +431,8 @@ class BetfairExecutionTest(unittest.TestCase):
     ):
         mock_session = mock.Mock()
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order.bet_id = 123
         mock_order_package = mock.Mock()
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
@@ -415,6 +450,8 @@ class BetfairExecutionTest(unittest.TestCase):
             mock_order, mock_instruction_report, OrderPackageType.UPDATE
         )
         mock_order.executable.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.update")
@@ -424,6 +461,8 @@ class BetfairExecutionTest(unittest.TestCase):
     ):
         mock_session = mock.Mock()
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order.bet_id = 123
         mock_order_package = mock.Mock()
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
@@ -441,6 +480,8 @@ class BetfairExecutionTest(unittest.TestCase):
             mock_order, mock_instruction_report, OrderPackageType.UPDATE
         )
         mock_order.executable.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.update")
@@ -450,6 +491,8 @@ class BetfairExecutionTest(unittest.TestCase):
     ):
         mock_session = mock.Mock()
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order.bet_id = 123
         mock_order_package = mock.Mock()
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
@@ -467,6 +510,8 @@ class BetfairExecutionTest(unittest.TestCase):
             mock_order, mock_instruction_report, OrderPackageType.UPDATE
         )
         mock_order.executable.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.update")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
@@ -496,6 +541,8 @@ class BetfairExecutionTest(unittest.TestCase):
     ):
         mock_session = mock.Mock()
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order.market_id = "1.234"
         mock_order.bet_id = 123
         mock_order_package = mock.Mock()
@@ -526,9 +573,8 @@ class BetfairExecutionTest(unittest.TestCase):
             mock_instruction_report.place_instruction_reports,
             OrderPackageType.REPLACE,
         )
-
-    # def test_execute_replace_failure(self):
-    #     pass
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution.replace")
     @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
@@ -607,6 +653,8 @@ class SimulatedExecutionTest(unittest.TestCase):
     @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger")
     def test_execute_place_success(self, mock__order_logger):
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order_package = mock.Mock()
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_order_package.place_instructions = [1]
@@ -622,10 +670,14 @@ class SimulatedExecutionTest(unittest.TestCase):
             mock_order, mock_sim_resp, mock_order_package.package_type
         )
         mock_order.executable.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
     @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger")
     def test_execute_place_failure(self, mock__order_logger):
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order_package = mock.Mock()
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_order_package.place_instructions = [1]
@@ -641,10 +693,14 @@ class SimulatedExecutionTest(unittest.TestCase):
             mock_order, mock_sim_resp, mock_order_package.package_type
         )
         mock_order.lapsed.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
     @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger")
     def test_execute_cancel(self, mock__order_logger):
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order_package = mock.Mock()
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_sim_resp = mock.Mock()
@@ -656,10 +712,14 @@ class SimulatedExecutionTest(unittest.TestCase):
             mock_order, mock_sim_resp, mock_order_package.package_type
         )
         mock_order.execution_complete.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
     @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger")
     def test_execute_cancel_failure(self, mock__order_logger):
         mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
         mock_order_package = mock.Mock()
         mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
         mock_sim_resp = mock.Mock()
@@ -671,11 +731,123 @@ class SimulatedExecutionTest(unittest.TestCase):
             mock_order, mock_sim_resp, mock_order_package.package_type
         )
         mock_order.executable.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
-    def test_execute_update(self):
-        with self.assertRaises(NotImplementedError):
-            self.execution.execute_update(None, None)
+    @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger")
+    def test_execute_update(self, mock__order_logger):
+        mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.update_instructions = ["PERSIST"]
+        mock_order_package.info = {}
+        mock_sim_resp = mock.Mock()
+        mock_sim_resp.status = "SUCCESS"
+        mock_order.simulated.update.return_value = mock_sim_resp
+        self.execution.execute_update(mock_order_package, None)
+        mock_order.simulated.update.assert_called_with("PERSIST")
+        mock__order_logger.assert_called_with(
+            mock_order, mock_sim_resp, mock_order_package.package_type
+        )
+        mock_order.executable.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
 
-    def test_execute_replace(self):
-        with self.assertRaises(NotImplementedError):
-            self.execution.execute_replace(None, None)
+    @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger")
+    def test_execute_update_failure(self, mock__order_logger):
+        mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.update_instructions = ["PERSIST"]
+        mock_order_package.info = {}
+        mock_sim_resp = mock.Mock()
+        mock_sim_resp.status = "FAILURE"
+        mock_order.simulated.update.return_value = mock_sim_resp
+        self.execution.execute_update(mock_order_package, None)
+        mock_order.simulated.update.assert_called_with("PERSIST")
+        mock__order_logger.assert_called_with(
+            mock_order, mock_sim_resp, mock_order_package.package_type
+        )
+        mock_order.executable.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
+
+    @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger")
+    def test_execute_replace(self, mock__order_logger):
+        mock_market = mock.Mock()
+        self.mock_flumine.markets.markets = {"1.234": mock_market}
+        mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
+        mock_order.market_id = "1.234"
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.replace_instructions = [{"newPrice": 2.03}]
+        mock_replacement_order = mock.Mock()
+        mock_replacement_order_package = mock.Mock()
+        mock_replacement_order_package.__iter__ = mock.Mock(
+            return_value=iter([mock_order])
+        )
+        mock_sim_resp = mock.Mock()
+        mock_sim_resp.status = "SUCCESS"
+        mock_order.simulated.cancel.return_value = mock_sim_resp
+        mock_replacement_order.simulated.place.return_value = mock_sim_resp
+        mock_order.trade.create_order_replacement.return_value = mock_replacement_order
+        self.execution.execute_replace(mock_order_package, None)
+        mock_order.simulated.cancel.assert_called_with()
+        mock_replacement_order.simulated.place.assert_called_with(
+            mock_order_package.market.market_book,
+            {"newPrice": 2.03},
+            self.execution._bet_id,
+        )
+        mock__order_logger.assert_has_calls(
+            [
+                call(mock_order, mock_sim_resp, OrderPackageType.CANCEL),
+                call(
+                    mock_replacement_order,
+                    mock_sim_resp,
+                    mock_order_package.package_type,
+                ),
+            ]
+        )
+        mock_order.execution_complete.assert_called_with()
+        mock_replacement_order.executable.assert_called_with()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
+
+    @mock.patch("flumine.execution.simulatedexecution.SimulatedExecution._order_logger")
+    def test_execute_replace_failure(self, mock__order_logger):
+        mock_order = mock.Mock()
+        mock_order.trade.__enter__ = mock.Mock()
+        mock_order.trade.__exit__ = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.replace_instructions = [{"newPrice": 2.54}]
+        mock_replacement_order = mock.Mock()
+        mock_replacement_order_package = mock.Mock()
+        mock_replacement_order_package.__iter__ = mock.Mock(
+            return_value=iter([mock_order])
+        )
+        mock_sim_resp = mock.Mock()
+        mock_sim_resp.status = "FAILURE"
+        mock_order.simulated.cancel.return_value = mock_sim_resp
+        mock_replacement_order.simulated.place.return_value = mock_sim_resp
+        mock_order.trade.create_order_replacement.return_value = mock_replacement_order
+        self.execution.execute_replace(mock_order_package, None)
+        mock_order.simulated.cancel.assert_called_with()
+        mock_replacement_order.simulated.place.assert_called_with(
+            mock_order_package.market.market_book,
+            {"newPrice": 2.54},
+            self.execution._bet_id,
+        )
+        mock__order_logger.assert_called_with(
+            mock_order, mock_sim_resp, OrderPackageType.CANCEL
+        )
+        mock_order.executable.assert_called_with()
+        mock_replacement_order.executable.assert_not_called()
+        mock_order.trade.__enter__.assert_called_with()
+        mock_order.trade.__exit__.assert_called_with(None, None, None)
diff --git a/tests/test_simulated.py b/tests/test_simulated.py
index 04f5eab..c1a5019 100644
--- a/tests/test_simulated.py
+++ b/tests/test_simulated.py
@@ -158,11 +158,15 @@ class SimulatedTest(unittest.TestCase):
         self.assertEqual(resp.status, "FAILURE")
         self.assertEqual(resp.error_code, "BET_ACTION_ERROR")
 
-    # def test_update(self):
-    #     pass
+    def test_update(self):
+        resp = self.simulated.update({"newPersistenceType": "PERSIST"})
+        self.assertEqual(resp.status, "SUCCESS")
 
-    # def test_replace(self):
-    #     pass
+    def test_update_else(self):
+        self.simulated.order.order_type.ORDER_TYPE = OrderTypes.MARKET_ON_CLOSE
+        resp = self.simulated.update({"newPersistenceType": "PERSIST"})
+        self.assertEqual(resp.status, "FAILURE")
+        self.assertEqual(resp.error_code, "BET_ACTION_ERROR")
 
     def test__get_runner(self):
         mock_market_book = mock.Mock()
diff --git a/tests/test_trade.py b/tests/test_trade.py
index 9226763..66c72cc 100644
--- a/tests/test_trade.py
+++ b/tests/test_trade.py
@@ -1,12 +1,14 @@
+import logging
 import collections
 import unittest
 from unittest import mock
 
-from flumine.order.trade import Trade, OrderError, TradeStatus
+from flumine.order.trade import Trade, OrderError, OrderStatus, TradeStatus
 
 
 class TradeTest(unittest.TestCase):
     def setUp(self) -> None:
+        logging.disable(logging.CRITICAL)
         self.mock_strategy = mock.Mock()
         self.mock_fill_kill = mock.Mock()
         self.mock_offset = mock.Mock()
@@ -53,6 +55,23 @@ class TradeTest(unittest.TestCase):
         )
         runner_context.reset.assert_called_with()
 
+    def test_trade_complete(self):
+        self.assertTrue(self.trade.trade_complete)
+        mock_order = mock.Mock(status=OrderStatus.PENDING)
+        self.trade.orders.append(mock_order)
+        self.assertFalse(self.trade.trade_complete)
+
+    def test_trade_complete_offset(self):
+        self.trade.offset_orders = [1]
+        self.assertFalse(self.trade.trade_complete)
+
+    def test_trade_complete_replace_order(self):
+        self.assertTrue(self.trade.trade_complete)
+        mock_order = mock.Mock(status=OrderStatus.EXECUTION_COMPLETE)
+        self.trade.status = TradeStatus.COMPLETE
+        self.trade.orders.append(mock_order)
+        self.assertFalse(self.trade.trade_complete)
+
     def test_create_order(self):
         mock_order_type = mock.Mock()
         mock_order_type.EXCHANGE = "SYM"
@@ -73,8 +92,7 @@ class TradeTest(unittest.TestCase):
 
     def test_create_order_replacement(self):
         mock_order = mock.Mock()
-        mock_order.update_data = {"new_price": 12}
-        replacement_order = self.trade.create_order_replacement(mock_order)
+        replacement_order = self.trade.create_order_replacement(mock_order, 12)
         self.assertEqual(self.trade.orders, [replacement_order])
 
     def test_create_order_from_current_limit(self):
@@ -114,3 +132,16 @@ class TradeTest(unittest.TestCase):
                 "notes": "123",
             },
         )
+
+    @mock.patch("flumine.order.trade.Trade._update_status")
+    def test_enter_exit(self, mock__update_status):
+        with self.trade:
+            mock__update_status.assert_called_with(TradeStatus.PENDING)
+        mock__update_status.assert_called_with(TradeStatus.LIVE)
+
+    @mock.patch("flumine.order.trade.Trade._update_status")
+    def test_enter_error(self, mock__update_status):
+        with self.assertRaises(ValueError):
+            with self.trade:
+                raise ValueError()
+        mock__update_status.assert_called_with(TradeStatus.PENDING)