Repository: liampauling/flumine
Pull request #216 (2020-06-15 09:47:30)

bpe handling added


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index a260f31..c7e3762 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,15 @@
 Release History
 ---------------
 
+1.7.0 (2020-06-15)
++++++++++++++++++++
+
+**Improvements**
+
+- market_notes added to Trade
+- market removed after closed for 3600 seconds
+- client.best_price_execution handling added
+
 1.6.8 (2020-06-10)
 +++++++++++++++++++
 
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 6f2c51e..0af11c9 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.8"
+__version__ = "1.7.0"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/baseflumine.py b/flumine/baseflumine.py
index 6e37962..6a03d42 100644
--- a/flumine/baseflumine.py
+++ b/flumine/baseflumine.py
@@ -214,9 +214,7 @@ class BaseFlumine:
 
         self.cleared_market_queue.put(market.market_id)
         self.log_control(event)
-        logger.info(
-            "Market removed", extra={"market_id": market.market_id, **self.info}
-        )
+        logger.info("Market closed", extra={"market_id": market.market_id, **self.info})
 
     def _process_cleared_orders(self, event):
         market_id = event.event.market_id
@@ -231,7 +229,7 @@ class BaseFlumine:
         meta_orders = market.blotter.process_cleared_orders(event.event)
         self.log_control(events.ClearedOrdersMetaEvent(meta_orders))
         logger.info(
-            "Market closed and cleared",
+            "Market cleared",
             extra={
                 "market_id": market_id,
                 "order_count": len(meta_orders),
@@ -251,6 +249,16 @@ class BaseFlumine:
                 },
             )
 
+        # check for markets that have been closed for x seconds
+        closed_markets = [
+            m.market_id
+            for m in self.markets
+            if m.elapsed_seconds_closed and m.elapsed_seconds_closed > 3600
+        ]
+        for market_id in closed_markets:
+            logger.info("Removing market %s" % market_id)
+            self.markets.remove_market(market_id)
+
     def _process_end_flumine(self) -> None:
         for strategy in self.strategies:
             strategy.finish()
diff --git a/flumine/clients/baseclient.py b/flumine/clients/baseclient.py
index e5f2865..00acd31 100644
--- a/flumine/clients/baseclient.py
+++ b/flumine/clients/baseclient.py
@@ -23,6 +23,7 @@ class BaseClient:
         interactive_login: bool = False,
         id_: str = None,
         order_stream: bool = True,
+        best_price_execution: bool = True,
     ):
         self.id = id_ or create_short_uuid()
         self.betting_client = betting_client
@@ -31,6 +32,7 @@ class BaseClient:
         self.commission_base = commission_base
         self.interactive_login = interactive_login
         self.order_stream = order_stream
+        self.best_price_execution = best_price_execution
 
         self.account_details = None
         self.account_funds = None
@@ -78,4 +80,6 @@ class BaseClient:
             "betting_client": self.betting_client,
             "chargeable_transaction_count": self.chargeable_transaction_count,
             "trading_controls": self.trading_controls,
+            "order_stream": self.order_stream,
+            "best_price_execution": self.best_price_execution,
         }
diff --git a/flumine/execution/simulatedexecution.py b/flumine/execution/simulatedexecution.py
index dd997c3..b4fb519 100644
--- a/flumine/execution/simulatedexecution.py
+++ b/flumine/execution/simulatedexecution.py
@@ -43,7 +43,7 @@ class SimulatedExecution(BaseExecution):
             with order.trade:
                 self._bet_id += 1
                 simulated_response = order.simulated.place(
-                    market_book, instruction, self._bet_id
+                    order_package.client, market_book, instruction, self._bet_id
                 )
                 self._order_logger(
                     order, simulated_response, order_package.package_type
@@ -113,7 +113,7 @@ class SimulatedExecution(BaseExecution):
                     order, instruction.get("newPrice")
                 )
                 place_instruction_report = replacement_order.simulated.place(
-                    market_book, instruction, self._bet_id
+                    order_package.client, market_book, instruction, self._bet_id
                 )
                 if place_instruction_report.status == "SUCCESS":
                     self._order_logger(
diff --git a/flumine/markets/market.py b/flumine/markets/market.py
index a5d9b6d..90af463 100644
--- a/flumine/markets/market.py
+++ b/flumine/markets/market.py
@@ -1,5 +1,6 @@
 import datetime
 import logging
+from typing import Optional
 from betfairlightweight.resources.bettingresources import MarketBook, MarketCatalogue
 
 from .blotter import Blotter
@@ -19,6 +20,7 @@ class Market:
         self.flumine = flumine
         self.market_id = market_id
         self.closed = False
+        self.date_time_closed = None
         self.market_book = market_book
         self.market_catalogue = market_catalogue
         self.context = {"simulated": {}}  # data store (raceCard / scores etc)
@@ -32,12 +34,15 @@ class Market:
 
     def close_market(self) -> None:
         self.closed = True
+        self.date_time_closed = datetime.datetime.utcnow()
 
     # order
     def place_order(self, order, execute: bool = True) -> None:
         order.place(self.market_book.publish_time)
         if order.id not in self.blotter:
             self.blotter[order.id] = order
+            if order.trade.market_notes is None:
+                order.trade.update_market_notes(self.market_book)
             self.flumine.log_control(events.TradeEvent(order.trade))  # todo dupes?
         else:
             return  # retry attempt so ignore?
@@ -70,6 +75,11 @@ class Market:
     def seconds_to_start(self):
         return (self.market_start_datetime - datetime.datetime.utcnow()).total_seconds()
 
+    @property
+    def elapsed_seconds_closed(self) -> Optional[float]:
+        if self.closed and self.date_time_closed:
+            return (datetime.datetime.utcnow() - self.date_time_closed).total_seconds()
+
     @property
     def market_start_datetime(self):
         if self.market_catalogue:
diff --git a/flumine/markets/markets.py b/flumine/markets/markets.py
index 2669002..714bcbc 100644
--- a/flumine/markets/markets.py
+++ b/flumine/markets/markets.py
@@ -19,6 +19,9 @@ class Markets:
         live_market.close_market()
         return live_market
 
+    def remove_market(self, market_id: str) -> None:
+        del self._markets[market_id]
+
     def get_order(self, market_id: str, order_id: str) -> Optional[BetfairOrder]:
         try:
             return self.markets[market_id].blotter[order_id]
diff --git a/flumine/order/trade.py b/flumine/order/trade.py
index 0a74f53..ecc3a5f 100644
--- a/flumine/order/trade.py
+++ b/flumine/order/trade.py
@@ -4,12 +4,13 @@ import datetime
 import collections
 from enum import Enum
 from typing import Union, Type
-from betfairlightweight.resources.bettingresources import CurrentOrder
+from betfairlightweight.resources.bettingresources import MarketBook, CurrentOrder
 
 from ..strategy.strategy import BaseStrategy
-from .order import BetfairOrder, OrderStatus
+from .order import BetfairOrder
 from .ordertype import LimitOrder, LimitOnCloseOrder, MarketOnCloseOrder
 from ..exceptions import OrderError
+from ..utils import get_price, get_runner_book
 
 logger = logging.getLogger(__name__)
 
@@ -41,6 +42,7 @@ class Trade:
         self.notes = (
             notes if notes else collections.OrderedDict()
         )  # trade notes (e.g. triggers/market state)
+        self.market_notes = None  # back,lay,lpt (initial order only)
         self.fill_kill = fill_kill
         self.offset = offset
         self.green = green
@@ -52,6 +54,15 @@ class Trade:
         self.date_time_created = datetime.datetime.utcnow()
         self.date_time_complete = None
 
+    def update_market_notes(self, market_book: MarketBook) -> None:
+        runner = get_runner_book(market_book, self.selection_id)
+        if runner:
+            self.market_notes = "{0},{1},{2}".format(
+                get_price(runner.ex.available_to_back, 0),
+                get_price(runner.ex.available_to_lay, 0),
+                runner.last_price_traded,
+            )
+
     # status
     def _update_status(self, status: TradeStatus) -> None:
         self.status_log.append(status)
@@ -143,6 +154,7 @@ class Trade:
             "status": self.status,
             "orders": [o.id for o in self.orders],
             "notes": self.notes_str,
+            "market_notes": self.market_notes,
         }
 
     def __enter__(self):
diff --git a/flumine/utils.py b/flumine/utils.py
index 754e036..99dbb33 100644
--- a/flumine/utils.py
+++ b/flumine/utils.py
@@ -3,7 +3,7 @@ import logging
 import hashlib
 from typing import Optional, Tuple, Callable
 from decimal import Decimal
-from betfairlightweight.resources.bettingresources import RunnerBook
+from betfairlightweight.resources.bettingresources import MarketBook, RunnerBook
 
 from . import config
 from .exceptions import FlumineException
@@ -145,7 +145,9 @@ def wap(matched: list) -> Tuple[float, float]:
         return round(b, 2), round(a / b, 2)
 
 
-def call_check_market(strategy_check_market: Callable, market, market_book) -> bool:
+def call_check_market(
+    strategy_check_market: Callable, market, market_book: MarketBook
+) -> bool:
     try:
         return strategy_check_market(market, market_book)
     except FlumineException as e:
@@ -166,7 +168,7 @@ def call_check_market(strategy_check_market: Callable, market, market_book) -> b
 
 
 def call_process_market_book(
-    strategy_process_market_book: Callable, market, market_book
+    strategy_process_market_book: Callable, market, market_book: MarketBook
 ) -> None:
     try:
         strategy_process_market_book(market, market_book)
@@ -184,3 +186,14 @@ def call_process_market_book(
         )
         if config.raise_errors:
             raise
+
+
+def get_runner_book(market_book: MarketBook, selection_id: int, handicap=0):
+    """Returns runner book based on selection id.
+    """
+    for runner_book in market_book.runners:
+        if (
+            runner_book.selection_id == selection_id
+            and runner_book.handicap == handicap
+        ):
+            return runner_book


Test patch:
diff --git a/flumine/backtest/simulated.py b/flumine/backtest/simulated.py
index 488bf0d..f5e84c5 100644
--- a/flumine/backtest/simulated.py
+++ b/flumine/backtest/simulated.py
@@ -47,10 +47,10 @@ class Simulated:
             )
 
     def place(
-        self, market_book: MarketBook, instruction: dict, bet_id: int
+        self, client, market_book: MarketBook, instruction: dict, bet_id: int
     ) -> SimulatedPlaceResponse:
         # simulates placeOrder request->matching->response
-        # todo instruction/fillkill/timeInForce/BPE etc
+        # todo instruction/fillkill/timeInForce etc
         if self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
             runner = self._get_runner(market_book)
             available_to_back = get_price(runner.ex.available_to_back, 0) or 1.01
@@ -58,7 +58,13 @@ class Simulated:
             price = self.order.order_type.price
             size = self.order.order_type.size
             if self.order.side == "BACK":
-                if available_to_back >= price:
+                if not client.best_price_execution and available_to_back > price:
+                    return self._create_place_response(
+                        bet_id,
+                        status="FAILURE",
+                        error_code="BET_LAPSED_PRICE_IMPROVEMENT_TOO_LARGE",
+                    )
+                elif available_to_back >= price:
                     self._process_price_matched(
                         market_book.publish_time_epoch,
                         price,
@@ -68,7 +74,13 @@ class Simulated:
                     return self._create_place_response(bet_id)
                 available = runner.ex.available_to_lay
             else:
-                if available_to_lay <= price:
+                if not client.best_price_execution and available_to_lay < price:
+                    return self._create_place_response(
+                        bet_id,
+                        status="FAILURE",
+                        error_code="BET_LAPSED_PRICE_IMPROVEMENT_TOO_LARGE",
+                    )
+                elif available_to_lay <= price:
                     self._process_price_matched(
                         market_book.publish_time_epoch,
                         price,
diff --git a/tests/test_baseflumine.py b/tests/test_baseflumine.py
index 6fb780b..84b1c7c 100644
--- a/tests/test_baseflumine.py
+++ b/tests/test_baseflumine.py
@@ -241,6 +241,16 @@ class BaseFlumineTest(unittest.TestCase):
         mock_event.event.orders = []
         self.base_flumine._process_cleared_markets(mock_event)
 
+    def test__process_cleared_markets_closed(self):
+        self.base_flumine.markets._markets = {
+            "123": mock.Mock(market_id="123", elapsed_seconds_closed=25),
+            "456": mock.Mock(market_id="456", elapsed_seconds_closed=3601),
+        }
+        mock_event = mock.Mock()
+        mock_event.event.orders = []
+        self.base_flumine._process_cleared_markets(mock_event)
+        self.assertEqual(len(self.base_flumine.markets._markets), 1)
+
     def test__process_end_flumine(self):
         self.base_flumine._process_end_flumine()
 
diff --git a/tests/test_clients.py b/tests/test_clients.py
index c7a1814..c0adbfd 100644
--- a/tests/test_clients.py
+++ b/tests/test_clients.py
@@ -28,6 +28,7 @@ class BaseClientTest(unittest.TestCase):
         self.assertIsNone(self.base_client.execution)
         self.assertEqual(self.base_client.trading_controls, [])
         self.assertTrue(self.base_client.order_stream)
+        self.assertTrue(self.base_client.best_price_execution)
 
     def test_login(self):
         with self.assertRaises(NotImplementedError):
diff --git a/tests/test_execution.py b/tests/test_execution.py
index 7b08c63..96a3176 100644
--- a/tests/test_execution.py
+++ b/tests/test_execution.py
@@ -674,7 +674,10 @@ class SimulatedExecutionTest(unittest.TestCase):
         mock_order.simulated.place.return_value = mock_sim_resp
         self.execution.execute_place(mock_order_package, None)
         mock_order.simulated.place.assert_called_with(
-            mock_order_package.market.market_book, 1, self.execution._bet_id
+            mock_order_package.client,
+            mock_order_package.market.market_book,
+            1,
+            self.execution._bet_id,
         )
         mock__order_logger.assert_called_with(
             mock_order, mock_sim_resp, mock_order_package.package_type
@@ -697,7 +700,10 @@ class SimulatedExecutionTest(unittest.TestCase):
         mock_order.simulated.place.return_value = mock_sim_resp
         self.execution.execute_place(mock_order_package, None)
         mock_order.simulated.place.assert_called_with(
-            mock_order_package.market.market_book, 1, self.execution._bet_id
+            mock_order_package.client,
+            mock_order_package.market.market_book,
+            1,
+            self.execution._bet_id,
         )
         mock__order_logger.assert_called_with(
             mock_order, mock_sim_resp, mock_order_package.package_type
@@ -810,6 +816,7 @@ class SimulatedExecutionTest(unittest.TestCase):
         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.client,
             mock_order_package.market.market_book,
             {"newPrice": 2.03},
             self.execution._bet_id,
@@ -850,6 +857,7 @@ class SimulatedExecutionTest(unittest.TestCase):
         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.client,
             mock_order_package.market.market_book,
             {"newPrice": 2.54},
             self.execution._bet_id,
diff --git a/tests/test_markets.py b/tests/test_markets.py
index b0a40bb..c12d411 100644
--- a/tests/test_markets.py
+++ b/tests/test_markets.py
@@ -32,6 +32,12 @@ class MarketsTest(unittest.TestCase):
         self.markets.close_market("1.1")
         mock_market.close_market.assert_called_with()
 
+    def test_remove_market(self):
+        mock_market = mock.Mock()
+        self.markets._markets = {"1.1": mock_market}
+        self.markets.remove_market("1.1")
+        self.assertEqual(self.markets._markets, {})
+
     def test_get_order(self):
         mock_market = mock.Mock()
         mock_market.closed = False
@@ -101,6 +107,7 @@ class MarketTest(unittest.TestCase):
         self.assertEqual(self.market.flumine, self.mock_flumine)
         self.assertEqual(self.market.market_id, "1.234")
         self.assertFalse(self.market.closed)
+        self.assertIsNone(self.market.date_time_closed)
         self.assertEqual(self.market.market_book, self.mock_market_book)
         self.assertEqual(self.market.market_catalogue, self.mock_market_catalogue)
         self.assertEqual(self.market.context, {"simulated": {}})
@@ -117,15 +124,18 @@ class MarketTest(unittest.TestCase):
     def test_close_market(self):
         self.market.close_market()
         self.assertTrue(self.market.closed)
+        self.assertIsNotNone(self.market.date_time_closed)
 
     @mock.patch("flumine.markets.market.events")
     def test_place_order(self, mock_events):
         mock_order = mock.Mock()
         mock_order.id = "123"
+        mock_order.trade.market_notes = None
         self.market.place_order(mock_order)
         mock_order.place.assert_called_with(self.market.market_book.publish_time)
         self.assertEqual(self.market.blotter.pending_place, [mock_order])
         self.mock_flumine.log_control.assert_called_with(mock_events.TradeEvent())
+        mock_order.trade.update_market_notes.assert_called_with(self.market.market_book)
 
     @mock.patch("flumine.markets.market.events")
     def test_place_order_not_executed(self, mock_events):
@@ -208,3 +218,9 @@ class MarketTest(unittest.TestCase):
         self.market.market_book = None
         self.market.market_catalogue = None
         self.assertLess(self.market.seconds_to_start, 0)
+
+    def test_elapsed_seconds_closed(self):
+        self.assertIsNone(self.market.elapsed_seconds_closed)
+        self.market.closed = True
+        self.market.date_time_closed = datetime.datetime.utcnow()
+        self.assertGreaterEqual(self.market.elapsed_seconds_closed, 0)
diff --git a/tests/test_simulated.py b/tests/test_simulated.py
index 09e46df..464ac6b 100644
--- a/tests/test_simulated.py
+++ b/tests/test_simulated.py
@@ -78,12 +78,13 @@ class SimulatedTest(unittest.TestCase):
 
     @mock.patch("flumine.backtest.simulated.Simulated._get_runner")
     def test_place_limit_back(self, mock__get_runner):
+        mock_client = mock.Mock(best_price_execution=True)
         mock_market_book = mock.Mock()
         mock_runner = mock.Mock()
         mock_runner.ex.available_to_back = [{"price": 12, "size": 120}]
         mock_runner.ex.available_to_lay = [{"price": 13, "size": 120}]
         mock__get_runner.return_value = mock_runner
-        resp = self.simulated.place(mock_market_book, {}, 1)
+        resp = self.simulated.place(mock_client, mock_market_book, {}, 1)
         self.assertEqual(resp.average_price_matched, 12)
         self.assertEqual(resp.size_matched, 2)
         self.assertEqual(
@@ -92,6 +93,7 @@ class SimulatedTest(unittest.TestCase):
 
     @mock.patch("flumine.backtest.simulated.Simulated._get_runner")
     def test_place_limit_back_unmatched(self, mock__get_runner):
+        mock_client = mock.Mock(best_price_execution=True)
         mock_market_book = mock.Mock()
         mock_runner = mock.Mock()
         mock_runner.ex.available_to_back = [{"price": 10, "size": 120}]
@@ -102,21 +104,35 @@ class SimulatedTest(unittest.TestCase):
             {"price": 15, "size": 32},
         ]
         mock__get_runner.return_value = mock_runner
-        resp = self.simulated.place(mock_market_book, {}, 1)
+        resp = self.simulated.place(mock_client, mock_market_book, {}, 1)
         self.assertEqual(resp.average_price_matched, 0)
         self.assertEqual(resp.size_matched, 0)
         self.assertEqual(self.simulated.matched, [])
         self.assertEqual(self.simulated._piq, 22)
 
+    @mock.patch("flumine.backtest.simulated.Simulated._get_runner")
+    def test_place_limit_back_bpe(self, mock__get_runner):
+        mock_client = mock.Mock(best_price_execution=False)
+        mock_market_book = mock.Mock()
+        mock_runner = mock.Mock()
+        mock_runner.ex.available_to_back = [{"price": 15, "size": 120}]
+        mock_runner.ex.available_to_lay = [{"price": 16, "size": 120}]
+        mock__get_runner.return_value = mock_runner
+        resp = self.simulated.place(mock_client, mock_market_book, {}, 1)
+        self.assertEqual(resp.status, "FAILURE")
+        self.assertEqual(resp.error_code, "BET_LAPSED_PRICE_IMPROVEMENT_TOO_LARGE")
+        self.assertEqual(self.simulated.matched, [])
+
     @mock.patch("flumine.backtest.simulated.Simulated._get_runner")
     def test_place_limit_lay(self, mock__get_runner):
+        mock_client = mock.Mock(best_price_execution=True)
         self.simulated.order.side = "LAY"
         mock_market_book = mock.Mock()
         mock_runner = mock.Mock()
         mock_runner.ex.available_to_back = [{"price": 11, "size": 120}]
         mock_runner.ex.available_to_lay = [{"price": 12, "size": 120}]
         mock__get_runner.return_value = mock_runner
-        resp = self.simulated.place(mock_market_book, {}, 1)
+        resp = self.simulated.place(mock_client, mock_market_book, {}, 1)
         self.assertEqual(resp.average_price_matched, 12)
         self.assertEqual(resp.size_matched, 2)
         self.assertEqual(
@@ -125,6 +141,7 @@ class SimulatedTest(unittest.TestCase):
 
     @mock.patch("flumine.backtest.simulated.Simulated._get_runner")
     def test_place_limit_lay_unmatched(self, mock__get_runner):
+        mock_client = mock.Mock(best_price_execution=True)
         self.simulated.order.side = "LAY"
         mock_market_book = mock.Mock()
         mock_runner = mock.Mock()
@@ -136,16 +153,31 @@ class SimulatedTest(unittest.TestCase):
         ]
         mock_runner.ex.available_to_lay = [{"price": 15, "size": 32}]
         mock__get_runner.return_value = mock_runner
-        resp = self.simulated.place(mock_market_book, {}, 1)
+        resp = self.simulated.place(mock_client, mock_market_book, {}, 1)
         self.assertEqual(resp.average_price_matched, 0)
         self.assertEqual(resp.size_matched, 0)
         self.assertEqual(self.simulated.matched, [])
         self.assertEqual(self.simulated._piq, 22)
 
+    @mock.patch("flumine.backtest.simulated.Simulated._get_runner")
+    def test_place_limit_lay_bpe(self, mock__get_runner):
+        mock_client = mock.Mock(best_price_execution=False)
+        self.simulated.order.side = "LAY"
+        mock_market_book = mock.Mock()
+        mock_runner = mock.Mock()
+        mock_runner.ex.available_to_back = [{"price": 10, "size": 120}]
+        mock_runner.ex.available_to_lay = [{"price": 10.5, "size": 120}]
+        mock__get_runner.return_value = mock_runner
+        resp = self.simulated.place(mock_client, mock_market_book, {}, 1)
+        self.assertEqual(resp.status, "FAILURE")
+        self.assertEqual(resp.error_code, "BET_LAPSED_PRICE_IMPROVEMENT_TOO_LARGE")
+        self.assertEqual(self.simulated.matched, [])
+
     def test_place_else(self):
+        mock_client = mock.Mock(best_price_execution=True)
         self.simulated.order.order_type.ORDER_TYPE = OrderTypes.MARKET_ON_CLOSE
         mock_market_book = mock.Mock()
-        self.simulated.place(mock_market_book, {}, 1)
+        self.simulated.place(mock_client, mock_market_book, {}, 1)
         self.assertEqual(self.simulated.matched, [])
 
     def test__create_place_response(self):
diff --git a/tests/test_trade.py b/tests/test_trade.py
index e8cc3b3..c1fff6e 100644
--- a/tests/test_trade.py
+++ b/tests/test_trade.py
@@ -3,7 +3,8 @@ import collections
 import unittest
 from unittest import mock
 
-from flumine.order.trade import Trade, OrderError, OrderStatus, TradeStatus
+from flumine.order.order import OrderStatus
+from flumine.order.trade import Trade, OrderError, TradeStatus
 
 
 class TradeTest(unittest.TestCase):
@@ -42,6 +43,17 @@ class TradeTest(unittest.TestCase):
         self.assertEqual(self.trade.offset_orders, [])
         self.assertIsNotNone(self.trade.date_time_created)
         self.assertIsNone(self.trade.date_time_complete)
+        self.assertIsNone(self.trade.market_notes)
+
+    @mock.patch("flumine.order.trade.get_price", return_value=1.01)
+    def test_update_market_notes(self, mock_get_price):
+        mock_market_book = mock.Mock()
+        mock_runner = mock.Mock(
+            selection_id=self.trade.selection_id, handicap=0, last_price_traded=5
+        )
+        mock_market_book.runners = [mock_runner]
+        self.trade.update_market_notes(mock_market_book)
+        self.assertEqual(self.trade.market_notes, "1.01,1.01,5")
 
     def test__update_status(self):
         self.trade._update_status(TradeStatus.COMPLETE)
@@ -133,6 +145,7 @@ class TradeTest(unittest.TestCase):
                 "status": TradeStatus.LIVE,
                 "strategy": self.mock_strategy,
                 "notes": "123",
+                "market_notes": None,
             },
         )
 
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 85892bb..20d6a79 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -177,3 +177,9 @@ class UtilsTest(unittest.TestCase):
         mock_market_book = mock.Mock()
         with self.assertRaises(ZeroDivisionError):
             utils.call_process_market_book(mock_strategy, mock_market, mock_market_book)
+
+    def test_get_runner_book(self):
+        mock_market_book = mock.Mock()
+        mock_runner = mock.Mock(selection_id=123, handicap=0)
+        mock_market_book.runners = [mock_runner]
+        self.assertEqual(utils.get_runner_book(mock_market_book, 123), mock_runner)