Repository: liampauling/flumine
Pull request #435 (2021-05-17 07:39:15)

history/version


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 6ffa5f6..d3f490a 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,17 @@
 Release History
 ---------------
 
+1.18.9 (2021-05-17)
++++++++++++++++++++
+
+**Improvements**
+
+- Notes and market notes added to order (potential race condition fix on transaction)
+
+**Bug Fixes**
+
+- #433 Liability persistence types not checked for <= 2 decimal places (@petercoles)
+
 1.18.8 (2021-05-14)
 +++++++++++++++++++
 
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 2fa773b..1eb1d8c 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.8"
+__version__ = "1.18.9"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/controls/tradingcontrols.py b/flumine/controls/tradingcontrols.py
index 49f5be7..aef96b4 100644
--- a/flumine/controls/tradingcontrols.py
+++ b/flumine/controls/tradingcontrols.py
@@ -56,6 +56,8 @@ class OrderValidation(BaseControl):
             self._on_error(order, "Order liability is None")
         elif order.order_type.liability <= 0:
             self._on_error(order, "Order liability is less than 0")
+        elif order.order_type.liability != round(order.order_type.liability, 2):
+            self._on_error(order, "Order liability has more than 2dp")
 
     def _validate_betfair_min_size(self, order, order_type):
         client = self.flumine.client
diff --git a/flumine/execution/transaction.py b/flumine/execution/transaction.py
index 57f66ff..35e3439 100644
--- a/flumine/execution/transaction.py
+++ b/flumine/execution/transaction.py
@@ -4,7 +4,7 @@ from collections import defaultdict
 from ..order.orderpackage import OrderPackageType, BetfairOrderPackage
 from ..events import events
 from ..exceptions import ControlError, OrderError
-from ..utils import chunks
+from ..utils import chunks, get_market_notes
 
 logger = logging.getLogger(__name__)
 
@@ -47,18 +47,20 @@ class Transaction:
             return False
         # place
         order.place(self.market.market_book.publish_time)
-        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
+            # update market_notes
+            market_notes = get_market_notes(self.market, order.selection_id)
+            order.market_notes = market_notes
             if order.trade.market_notes is None:
-                order.trade.update_market_notes(self.market)
+                order.trade.market_notes = market_notes
         else:
             raise OrderError("Order %s has already been placed" % order.id)
+        if self.market.blotter.has_trade(order.trade.id) is False:
+            self.market.flumine.log_control(events.TradeEvent(order.trade))
         if execute:  # handles replaceOrder
+            runner_context = order.trade.strategy.get_runner_context(*order.lookup)
+            runner_context.place(order.trade.id)
             self._pending_place.append((order, market_version))
             self._pending_orders = True
         return True
diff --git a/flumine/order/order.py b/flumine/order/order.py
index 6725aa1..438d977 100644
--- a/flumine/order/order.py
+++ b/flumine/order/order.py
@@ -3,6 +3,7 @@ import uuid
 import logging
 import datetime
 import string
+import collections
 from enum import Enum
 from typing import Union, Optional
 from betfairlightweight import filters
@@ -51,6 +52,7 @@ class BaseOrder:
         handicap: float = 0,
         sep: str = "-",
         context: dict = None,
+        notes: collections.OrderedDict = None,  # order notes (e.g. triggers/market state)
     ):
         self.id = str(uuid.uuid1().time)  # 18 char str used as unique customerOrderRef
         self.trade = trade
@@ -65,6 +67,8 @@ class BaseOrder:
         self.status_log = []
         self.violation_msg = None
         self.context = context or {}  # store order specific notes/triggers
+        self.notes = notes or collections.OrderedDict()
+        self.market_notes = None  # back,lay,lpt
 
         self.bet_id = None
         self.update_data = {}  # stores cancel/update/replace data
@@ -252,6 +256,10 @@ class BaseOrder:
     def customer_order_ref(self) -> str:
         return "{0}{1}{2}".format(self.trade.strategy.name_hash, self.sep, self.id)
 
+    @property
+    def notes_str(self) -> str:
+        return ",".join(str(x) for x in self.notes.values())
+
     @property
     def info(self) -> dict:
         return {
@@ -285,6 +293,8 @@ class BaseOrder:
             "status_log": ", ".join([s.value for s in self.status_log]),
             "violation_msg": self.violation_msg,
             "simulated": self.simulated.info,
+            "notes": self.notes_str,
+            "market_notes": self.market_notes,
         }
 
     def json(self) -> str:
diff --git a/flumine/order/trade.py b/flumine/order/trade.py
index 80e45cd..452ad22 100644
--- a/flumine/order/trade.py
+++ b/flumine/order/trade.py
@@ -10,7 +10,6 @@ from ..strategy.strategy import BaseStrategy
 from .order import BetfairOrder
 from .ordertype import LimitOrder, LimitOnCloseOrder, MarketOnCloseOrder
 from ..exceptions import OrderError
-from ..utils import get_market_notes
 
 logger = logging.getLogger(__name__)
 
@@ -28,7 +27,7 @@ class Trade:
         selection_id: int,
         handicap: float,
         strategy: BaseStrategy,
-        notes: collections.OrderedDict = None,
+        notes: collections.OrderedDict = None,  # trade notes (e.g. triggers/market state)
         place_reset_seconds: float = 0.0,  # seconds to wait since `runner_context.reset` before allowing another order
         reset_seconds: float = 0.0,  # seconds to wait since `runner_context.place` before allowing another order
     ):
@@ -37,9 +36,7 @@ class Trade:
         self.selection_id = selection_id
         self.handicap = handicap
         self.strategy = strategy
-        self.notes = (
-            notes if notes else collections.OrderedDict()
-        )  # trade notes (e.g. triggers/market state)
+        self.notes = notes or collections.OrderedDict()
         self.market_notes = None  # back,lay,lpt
         self.place_reset_seconds = place_reset_seconds
         self.reset_seconds = reset_seconds
@@ -50,9 +47,6 @@ class Trade:
         self.date_time_created = datetime.datetime.utcnow()
         self.date_time_complete = None
 
-    def update_market_notes(self, market) -> None:
-        self.market_notes = get_market_notes(market, self.selection_id)
-
     # status
     def _update_status(self, status: TradeStatus) -> None:
         self.status_log.append(status)


Test patch:
diff --git a/tests/test_order.py b/tests/test_order.py
index dfcf732..85ff619 100644
--- a/tests/test_order.py
+++ b/tests/test_order.py
@@ -1,6 +1,7 @@
 import string
 import unittest
 import datetime
+import collections
 from unittest import mock
 
 from flumine.order.order import (
@@ -40,6 +41,8 @@ class BaseOrderTest(unittest.TestCase):
         self.assertEqual(self.order.status_log, [])
         self.assertIsNone(self.order.violation_msg)
         self.assertEqual(self.order.context, {1: 2})
+        self.assertEqual(self.order.notes, {})
+        self.assertIsNone(self.order.market_notes)
         self.assertIsNone(self.order.bet_id)
         self.assertIsNone(self.order.EXCHANGE)
         self.assertEqual(self.order.update_data, {})
@@ -258,6 +261,12 @@ class BaseOrderTest(unittest.TestCase):
         self.order.sep = "O"
         self.assertEqual("my_name_hashO1234", self.order.customer_order_ref)
 
+    def test_notes_str(self):
+        self.order.notes = collections.OrderedDict({"1": 1, 2: "2", 3: 3, 4: "four"})
+        self.assertEqual(self.order.notes_str, "1,2,3,four")
+        self.order.notes = collections.OrderedDict()
+        self.assertEqual(self.order.notes_str, "")
+
 
 class BetfairOrderTest(unittest.TestCase):
     def setUp(self) -> None:
@@ -536,6 +545,8 @@ class BetfairOrderTest(unittest.TestCase):
                     "elapsed_seconds_executable": None,
                 },
                 "runner_status": self.order.runner_status,
+                "market_notes": None,
+                "notes": "",
             },
         )
 
diff --git a/tests/test_trade.py b/tests/test_trade.py
index 0df5d02..e3f1cf8 100644
--- a/tests/test_trade.py
+++ b/tests/test_trade.py
@@ -37,12 +37,6 @@ class TradeTest(unittest.TestCase):
         self.assertEqual(self.trade.place_reset_seconds, 12)
         self.assertEqual(self.trade.reset_seconds, 34)
 
-    @mock.patch("flumine.order.trade.get_market_notes")
-    def test_update_market_notes(self, mock_get_market_notes):
-        mock_market = mock.Mock()
-        self.trade.update_market_notes(mock_market)
-        self.assertEqual(self.trade.market_notes, mock_get_market_notes())
-
     @mock.patch("flumine.order.trade.Trade.complete_trade")
     @mock.patch(
         "flumine.order.trade.Trade.complete",
@@ -151,7 +145,7 @@ class TradeTest(unittest.TestCase):
 
     def test_notes_str(self):
         self.trade.notes = collections.OrderedDict({"1": 1, 2: "2", 3: 3, 4: "four"})
-        # self.assertEqual(self.trade.notes_str, "1,2,3,four")
+        self.assertEqual(self.trade.notes_str, "1,2,3,four")
         self.trade.notes = collections.OrderedDict()
         self.assertEqual(self.trade.notes_str, "")
 
diff --git a/tests/test_tradingcontrols.py b/tests/test_tradingcontrols.py
index ec7be0d..a84120d 100644
--- a/tests/test_tradingcontrols.py
+++ b/tests/test_tradingcontrols.py
@@ -173,6 +173,13 @@ class TestOrderValidation(unittest.TestCase):
         self.trading_control._validate_betfair_liability(order)
         mock_on_error.assert_called_with(order, "Order liability is less than 0")
 
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_liability_on_error_three(self, mock_on_error):
+        order = mock.Mock()
+        order.order_type.liability = 2.999
+        self.trading_control._validate_betfair_liability(order)
+        mock_on_error.assert_called_with(order, "Order liability has more than 2dp")
+
     @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
     def test__validate_betfair_min_size_no_validation(self, mock_on_error):
         self.mock_flumine.client.min_bet_validation = False
diff --git a/tests/test_transaction.py b/tests/test_transaction.py
index fce9153..f9181fd 100644
--- a/tests/test_transaction.py
+++ b/tests/test_transaction.py
@@ -22,12 +22,15 @@ class TransactionTest(unittest.TestCase):
         self.assertEqual(self.transaction._pending_update, [])
         self.assertEqual(self.transaction._pending_replace, [])
 
+    @mock.patch("flumine.execution.transaction.get_market_notes")
     @mock.patch(
         "flumine.execution.transaction.Transaction._validate_controls",
         return_value=True,
     )
     @mock.patch("flumine.execution.transaction.events")
-    def test_place_order(self, mock_events, mock__validate_controls):
+    def test_place_order(
+        self, mock_events, mock__validate_controls, mock_get_market_notes
+    ):
         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))
@@ -39,7 +42,9 @@ class TransactionTest(unittest.TestCase):
         self.transaction.market.flumine.log_control.assert_called_with(
             mock_events.TradeEvent()
         )
-        mock_order.trade.update_market_notes.assert_called_with(self.mock_market)
+        mock_get_market_notes.assert_called_with(
+            self.mock_market, mock_order.selection_id
+        )
         mock__validate_controls.assert_called_with(mock_order, OrderPackageType.PLACE)
         mock__validate_controls.assert_called_with(mock_order, OrderPackageType.PLACE)
         self.transaction._pending_place = [(mock_order, None)]
@@ -51,12 +56,15 @@ class TransactionTest(unittest.TestCase):
             mock_order.trade.id
         )
 
+    @mock.patch("flumine.execution.transaction.get_market_notes")
     @mock.patch(
         "flumine.execution.transaction.Transaction._validate_controls",
         return_value=True,
     )
     @mock.patch("flumine.execution.transaction.events")
-    def test_place_order_not_executed(self, mock_events, mock__validate_controls):
+    def test_place_order_not_executed(
+        self, mock_events, mock__validate_controls, mock_get_market_notes
+    ):
         self.transaction.market.blotter = mock.MagicMock()
         self.transaction.market.blotter.has_trade.return_value = False
         mock_order = mock.Mock(id="123")
@@ -74,11 +82,12 @@ class TransactionTest(unittest.TestCase):
             mock_order.trade.id
         )
 
+    @mock.patch("flumine.execution.transaction.get_market_notes")
     @mock.patch(
         "flumine.execution.transaction.Transaction._validate_controls",
         return_value=True,
     )
-    def test_place_order_retry(self, mock__validate_controls):
+    def test_place_order_retry(self, mock__validate_controls, mock_get_market_notes):
         self.transaction.market.blotter = mock.MagicMock()
         self.transaction.market.blotter.has_trade.return_value = False
         self.transaction.market.blotter.__contains__.return_value = True
@@ -88,6 +97,7 @@ class TransactionTest(unittest.TestCase):
         mock__validate_controls.assert_called_with(mock_order, OrderPackageType.PLACE)
         self.transaction._pending_place = []
         self.assertFalse(self.transaction._pending_orders)
+        mock_get_market_notes.assert_not_called()
 
     @mock.patch(
         "flumine.execution.transaction.Transaction._validate_controls",