Repository: liampauling/flumine
Pull request #363 (2021-01-11 11:10:09)

Task/order execution args


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 0b1d16c..333a472 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,23 +3,6 @@
 Release History
 ---------------
 
-1.15.2 (2021-01-11)
-+++++++++++++++++++
-
-**Improvements**
-
-- Order execution args added on place/cancel/update/replace
-- License update
-- Example update (@lunswor)
-
-**Bug Fixes**
-
-- #358 dynamic keep alive (based on trading client)
-
-**Libraries**
-
-- py3.5 removed from setup.py
-
 1.15.1 (2020-12-28)
 +++++++++++++++++++
 
diff --git a/LICENSE b/LICENSE
index f32fff9..75348bf 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2021, Liam Pauling
+Copyright (c) 2020, Liam Pauling
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/examples/controls/jupyterloggingcontrol.py b/examples/controls/jupyterloggingcontrol.py
index c7cd02b..99aaf24 100644
--- a/examples/controls/jupyterloggingcontrol.py
+++ b/examples/controls/jupyterloggingcontrol.py
@@ -23,11 +23,9 @@ use:
 class JupyterLoggingControl(LoggingControl):
     NAME = "JUPYTER_LOGGING_CONTROL"
 
-    def __init__(
-        self, file_name: str = "orders.json", directory: str = "/tmp", *args, **kwargs
-    ):
+    def __init__(self, *args, **kwargs):
         super(JupyterLoggingControl, self).__init__(*args, **kwargs)
-        self.file_name = os.path.join(directory, file_name)
+        self.filename = os.path.join("/tmp", "orders.json")
 
     def _process_end_flumine(self, event):
         framework = event.event
@@ -68,10 +66,9 @@ class JupyterLoggingControl(LoggingControl):
 
     def _create_json(self, data):
         # save json to file
-        with open(self.file_name, "w") as f:
+        with open(self.filename, "w") as f:
             json.dump(data, f)
 
-    @staticmethod
     def launch(self, argv=None, **kwargs):
         from jupyterlab.labapp import LabApp
 
diff --git a/examples/middlewares/marketcatalogue.py b/examples/middlewares/marketcatalogue.py
index 4e5d5c3..4b3b6f4 100644
--- a/examples/middlewares/marketcatalogue.py
+++ b/examples/middlewares/marketcatalogue.py
@@ -11,9 +11,6 @@ MARKET_CATALOGUE_PATH = ""  # update to correct path
 # Usage framework.add_middleware(MarketCatalogueMiddleware())
 class MarketCatalogueMiddleware(Middleware):
     def __call__(self, market) -> None:
-        pass
-
-    def add_market(self, market) -> None:
         catalogue_file_path = os.path.join(MARKET_CATALOGUE_PATH, market.market_id)
         if os.path.exists(catalogue_file_path):
             with open(catalogue_file_path, "r") as r:
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 3a2f232..40ca538 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.15.2"
+__version__ = "1.15.1"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/flumine.py b/flumine/flumine.py
index 99a2229..d9c6ff5 100644
--- a/flumine/flumine.py
+++ b/flumine/flumine.py
@@ -55,11 +55,8 @@ class Flumine(BaseFlumine):
                 del event
 
     def _add_default_workers(self):
-        ka_interval = min((self.client.betting_client.session_timeout / 2), 1200)
         self.add_worker(
-            worker.BackgroundWorker(
-                self, function=worker.keep_alive, interval=ka_interval
-            )
+            worker.BackgroundWorker(self, function=worker.keep_alive, interval=1200)
         )
         self.add_worker(
             worker.BackgroundWorker(
diff --git a/flumine/markets/blotter.py b/flumine/markets/blotter.py
index f605e46..f1d15a0 100644
--- a/flumine/markets/blotter.py
+++ b/flumine/markets/blotter.py
@@ -2,7 +2,12 @@ import logging
 from typing import Iterable
 
 from ..order.ordertype import OrderTypes
-from ..utils import chunks, calculate_unmatched_exposure, calculate_matched_exposure
+from ..utils import (
+    batch_orders,
+    chunks,
+    calculate_unmatched_exposure,
+    calculate_matched_exposure,
+)
 from ..order.order import BaseOrder, OrderStatus
 from ..order.orderpackage import OrderPackageType, BetfairOrderPackage
 
@@ -17,7 +22,7 @@ class Blotter:
         self.market_id = market_id
         self._orders = {}  # {Order.id: Order}
         self._live_orders = []  # cached list of live orders
-        # pending orders
+        # pending orders, list of (<Order>, {..})
         self.pending_place = []
         self.pending_cancel = []
         self.pending_update = []
@@ -61,15 +66,21 @@ class Blotter:
         packages = []
         _package_cls = BetfairOrderPackage
         limit = _package_cls.order_limit(package_type)
-        for chunked_orders in chunks(orders, limit):
-            order_package = _package_cls(
-                client=client,
-                market_id=self.market_id,
-                orders=chunked_orders,
-                package_type=package_type,
-                bet_delay=bet_delay,
-            )
-            packages.append(order_package)
+        # batch based on request data dict
+        batched_orders = batch_orders(orders)
+        # create packages
+        for market_version, mv_batched_orders in batched_orders.items():
+            for _orders in mv_batched_orders:
+                for chunked_orders in chunks(_orders, limit):
+                    order_package = _package_cls(
+                        client=client,
+                        market_id=self.market_id,
+                        orders=chunked_orders,
+                        package_type=package_type,
+                        bet_delay=bet_delay,
+                        market_version=market_version,
+                    )
+                    packages.append(order_package)
         orders.clear()
         return packages
 
diff --git a/flumine/markets/market.py b/flumine/markets/market.py
index a8d5eb0..5675019 100644
--- a/flumine/markets/market.py
+++ b/flumine/markets/market.py
@@ -46,7 +46,13 @@ class Market:
         )
 
     # order
-    def place_order(self, order, execute: bool = True) -> None:
+    def place_order(
+        self,
+        order,
+        batch: bool = True,
+        market_version: int = None,
+        execute: bool = True,
+    ) -> None:
         order.place(self.market_book.publish_time)
         if order.id not in self.blotter:
             self.blotter[order.id] = order
@@ -56,19 +62,40 @@ class Market:
         else:
             return  # retry attempt so ignore?
         if execute:  # handles replaceOrder
-            self.blotter.pending_place.append(order)
+            self.blotter.pending_place.append(
+                (order, {"batch": batch, "market_version": market_version})
+            )
 
-    def cancel_order(self, order, size_reduction: float = None) -> None:
+    def cancel_order(
+        self, order, size_reduction: float = None, batch: bool = True
+    ) -> None:
         order.cancel(size_reduction)
-        self.blotter.pending_cancel.append(order)
+        self.blotter.pending_cancel.append(
+            (order, {"size_reduction": size_reduction, "batch": batch})
+        )
 
-    def update_order(self, order, new_persistence_type: str) -> None:
+    def update_order(
+        self, order, new_persistence_type: str, batch: bool = True
+    ) -> None:
         order.update(new_persistence_type)
-        self.blotter.pending_update.append(order)
+        self.blotter.pending_update.append(
+            (order, {"new_persistence_type": new_persistence_type, "batch": batch})
+        )
 
-    def replace_order(self, order, new_price: float) -> None:
+    def replace_order(
+        self, order, new_price: float, batch: bool = True, market_version: int = None
+    ) -> None:
         order.replace(new_price)
-        self.blotter.pending_replace.append(order)
+        self.blotter.pending_replace.append(
+            (
+                order,
+                {
+                    "new_price": new_price,
+                    "batch": batch,
+                    "market_version": market_version,
+                },
+            )
+        )
 
     @property
     def event(self) -> dict:
diff --git a/flumine/order/orderpackage.py b/flumine/order/orderpackage.py
index 95fd8b7..f7aa7a8 100644
--- a/flumine/order/orderpackage.py
+++ b/flumine/order/orderpackage.py
@@ -35,6 +35,7 @@ class BaseOrderPackage(BaseEvent):
         package_type: OrderPackageType,
         bet_delay: int,
         async_: bool = False,
+        market_version: int = None,
     ):
         super(BaseOrderPackage, self).__init__(None)
         self.id = uuid.uuid1()
@@ -44,6 +45,7 @@ class BaseOrderPackage(BaseEvent):
         self.package_type = package_type
         self.bet_delay = bet_delay  # used for simulated execution
         self.async_ = async_
+        self._market_version = market_version
         self.customer_strategy_ref = config.hostname
         self.processed = False  # used for simulated execution
         self._retry = True
@@ -91,15 +93,15 @@ class BaseOrderPackage(BaseEvent):
             "package_type": self.package_type.value,
             "customer_strategy_ref": self.customer_strategy_ref,
             "bet_delay": self.bet_delay,
-            "market_version": self.market_version,
+            "market_version": self._market_version,
             "retry": self._retry,
             "retry_count": self._retry_count,
         }
 
     @property
     def market_version(self) -> Optional[dict]:
-        return None
-        # todo return {"version": self.market.market_book.version}
+        if self._market_version:
+            return {"version": self._market_version}
 
     def __iter__(self) -> Iterator[BaseOrder]:
         return iter(self.orders)
diff --git a/flumine/strategy/strategy.py b/flumine/strategy/strategy.py
index b958ccf..dabf2af 100644
--- a/flumine/strategy/strategy.py
+++ b/flumine/strategy/strategy.py
@@ -124,26 +124,51 @@ class BaseStrategy:
             del self._invested[i]
 
     # order
-    def place_order(self, market: Market, order) -> bool:
+    def place_order(
+        self,
+        market: Market,
+        order,
+        batch: bool = True,  # batch placeRequest with other orders
+        market_version: int = None,
+    ) -> bool:
         """Returns True if passes
         validate_order and placed.
         """
         runner_context = self.get_runner_context(*order.lookup)
         if self.validate_order(runner_context, order):
             runner_context.place()
-            market.place_order(order)
+            market.place_order(order, batch, market_version)
             return True
         else:
             return False
 
-    def cancel_order(self, market: Market, order, size_reduction: float = None) -> None:
-        market.cancel_order(order, size_reduction)
-
-    def update_order(self, market: Market, order, new_persistence_type: str) -> None:
-        market.update_order(order, new_persistence_type)
-
-    def replace_order(self, market: Market, order, new_price: float) -> None:
-        market.replace_order(order, new_price)
+    def cancel_order(
+        self,
+        market: Market,
+        order,
+        size_reduction: float = None,
+        batch: bool = True,  # batch cancelRequest with other orders
+    ) -> None:
+        market.cancel_order(order, size_reduction, batch)
+
+    def update_order(
+        self,
+        market: Market,
+        order,
+        new_persistence_type: str,
+        batch: bool = True,  # batch updateRequest with other orders
+    ) -> None:
+        market.update_order(order, new_persistence_type, batch)
+
+    def replace_order(
+        self,
+        market: Market,
+        order,
+        new_price: float,
+        batch: bool = True,  # batch replaceRequest with other orders
+        market_version: int = None,
+    ) -> None:
+        market.replace_order(order, new_price, batch, market_version)
 
     def validate_order(self, runner_context: RunnerContext, order) -> bool:
         # validate context
diff --git a/flumine/utils.py b/flumine/utils.py
index e5b0c42..c0b396c 100644
--- a/flumine/utils.py
+++ b/flumine/utils.py
@@ -1,6 +1,7 @@
 import uuid
 import logging
 import hashlib
+from collections import defaultdict
 from typing import Optional, Tuple, Callable
 from decimal import Decimal, ROUND_HALF_UP
 from betfairlightweight.resources.bettingresources import MarketBook, RunnerBook
@@ -42,6 +43,31 @@ def chunks(l: list, n: int) -> list:
         yield l[i : i + n]
 
 
+def batch_orders(orders: list) -> dict:
+    # batch by market_version -> batch
+    _return_orders = defaultdict(list)  # market_version: [[<Order>, ], .. ]
+    a = _split_orders_by(orders, "market_version")
+    for market_version, a_orders in a.items():
+        b = _split_orders_by(a_orders, "batch", True)
+        for batch, b_orders in b.items():
+            _temp = []
+            for order, _ in b_orders:
+                if batch is False:
+                    _return_orders[market_version].append([order])
+                else:
+                    _temp.append(order)
+            if _temp:
+                _return_orders[market_version].append(_temp)
+    return _return_orders
+
+
+def _split_orders_by(orders: list, by: str, default=None) -> dict:
+    res = defaultdict(list)
+    for order, request_data in orders:
+        res[request_data.get(by, default)].append((order, request_data))
+    return res
+
+
 def create_cheap_hash(txt: str, length: int = 15) -> str:
     # This is just a hash for debugging purposes.
     #    It does not need to be unique, just fast and short.
diff --git a/setup.py b/setup.py
index 7c83895..d066f0d 100644
--- a/setup.py
+++ b/setup.py
@@ -32,10 +32,10 @@ setup(
     classifiers=[
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
+        "Programming Language :: Python :: 3.5",
         "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
     ],
     test_suite="tests",
 )


Test patch:
diff --git a/flumine/backtest/simulated.py b/flumine/backtest/simulated.py
index 372aadb..5843f06 100644
--- a/flumine/backtest/simulated.py
+++ b/flumine/backtest/simulated.py
@@ -31,10 +31,10 @@ class Simulated:
         self.size_voided = 0.0
         self._piq = 0.0
         self._bsp_reconciled = False
-        # todo handle limit lapsing
 
     def __call__(self, market_book: MarketBook, runner_analytics):
         # simulates order matching
+        # todo handle limit lapsing
         runner = self._get_runner(market_book)
         if (
             self._bsp_reconciled is False
@@ -46,7 +46,7 @@ class Simulated:
         elif (
             self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT and self.size_remaining
         ):
-            # todo piq cancellations
+            # todo estimated piq cancellations
             self._process_traded(
                 market_book.publish_time_epoch, runner_analytics.traded
             )
@@ -56,6 +56,7 @@ class Simulated:
     ) -> SimulatedPlaceResponse:
         # simulates placeOrder request->matching->response
         # todo instruction/fillkill/timeInForce etc
+        # todo check marketVersion
         if self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
             runner = self._get_runner(market_book)
 
diff --git a/tests/test_blotter.py b/tests/test_blotter.py
index dd2fea5..b450473 100644
--- a/tests/test_blotter.py
+++ b/tests/test_blotter.py
@@ -51,22 +51,60 @@ class BlotterTest(unittest.TestCase):
 
     @mock.patch("flumine.markets.blotter.BetfairOrderPackage")
     def test___create_packages(self, mock_cls):
+        mock_cls.order_limit.return_value = 10
         mock_client = mock.Mock()
-        mock_order = mock.Mock()
-        mock_orders = [mock_order]
+        mock_order_one = mock.Mock()
+        mock_order_two = mock.Mock()
+        mock_order_three = mock.Mock()
+        mock_order_four = mock.Mock()
+        mock_order_five = mock.Mock()
+        mock_orders = [
+            (mock_order_one, {}),
+            (mock_order_two, {"batch": False}),
+            (mock_order_three, {"market_version": 123}),
+            (mock_order_four, {"market_version": 123}),
+            (mock_order_five, {"market_version": 123, "batch": False}),
+        ]
         packages = self.blotter._create_packages(
             mock_client, mock_orders, OrderPackageType.PLACE, 0
         )
+        self.assertEqual(len(packages), 4)
+        mock_cls.order_limit.assert_called_with(OrderPackageType.PLACE)
         self.assertEqual(
-            packages,
+            mock_cls.call_args_list,
             [
-                mock_cls(
-                    client=None,
+                mock.call(
+                    client=mock_client,
+                    market_id=self.blotter.market_id,
+                    orders=[mock_order_one],
+                    package_type=OrderPackageType.PLACE,
+                    bet_delay=0,
+                    market_version=None,
+                ),
+                mock.call(
+                    client=mock_client,
+                    market_id=self.blotter.market_id,
+                    orders=[mock_order_two],
+                    package_type=OrderPackageType.PLACE,
+                    bet_delay=0,
+                    market_version=None,
+                ),
+                mock.call(
+                    client=mock_client,
+                    market_id=self.blotter.market_id,
+                    orders=[mock_order_three, mock_order_four],
+                    package_type=OrderPackageType.PLACE,
+                    bet_delay=0,
+                    market_version=123,
+                ),
+                mock.call(
+                    client=mock_client,
                     market_id=self.blotter.market_id,
-                    orders=mock_orders,
+                    orders=[mock_order_five],
                     package_type=OrderPackageType.PLACE,
                     bet_delay=0,
-                )
+                    market_version=123,
+                ),
             ],
         )
         self.assertEqual(mock_orders, [])
diff --git a/tests/test_flumine.py b/tests/test_flumine.py
index 7bc2773..05d1571 100644
--- a/tests/test_flumine.py
+++ b/tests/test_flumine.py
@@ -66,7 +66,6 @@ class FlumineTest(unittest.TestCase):
         mock__add_default_workers.assert_called()
 
     def test__add_default_workers(self):
-        self.mock_trading.betting_client.session_timeout = 1200
         self.flumine._add_default_workers()
         self.assertEqual(len(self.flumine._workers), 4)
 
diff --git a/tests/test_markets.py b/tests/test_markets.py
index 148eacd..a55f85e 100644
--- a/tests/test_markets.py
+++ b/tests/test_markets.py
@@ -135,7 +135,10 @@ class MarketTest(unittest.TestCase):
         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.assertEqual(
+            self.market.blotter.pending_place,
+            [(mock_order, {"batch": True, "market_version": None})],
+        )
         self.mock_flumine.log_control.assert_called_with(mock_events.TradeEvent())
         mock_order.trade.update_market_notes.assert_called_with(self.market)
 
@@ -161,7 +164,10 @@ class MarketTest(unittest.TestCase):
         mock_order = mock.Mock()
         self.market.cancel_order(mock_order, 0.01)
         mock_order.cancel.assert_called_with(0.01)
-        self.assertEqual(mock_blotter.pending_cancel, [mock_order])
+        self.assertEqual(
+            mock_blotter.pending_cancel,
+            [(mock_order, {"size_reduction": 0.01, "batch": True})],
+        )
 
     def test_update_order(self):
         mock_blotter = mock.Mock()
@@ -170,16 +176,31 @@ class MarketTest(unittest.TestCase):
         mock_order = mock.Mock()
         self.market.update_order(mock_order, "PERSIST")
         mock_order.update.assert_called_with("PERSIST")
-        self.assertEqual(mock_blotter.pending_update, [mock_order])
+        self.assertEqual(
+            mock_blotter.pending_update,
+            [(mock_order, {"new_persistence_type": "PERSIST", "batch": True})],
+        )
 
     def test_replace_order(self):
         mock_blotter = mock.Mock()
         mock_blotter.pending_replace = []
         self.market.blotter = mock_blotter
         mock_order = mock.Mock()
-        self.market.replace_order(mock_order, 1.01)
+        self.market.replace_order(mock_order, 1.01, True, 321)
         mock_order.replace.assert_called_with(1.01)
-        self.assertEqual(mock_blotter.pending_replace, [mock_order])
+        self.assertEqual(
+            mock_blotter.pending_replace,
+            [
+                (
+                    mock_order,
+                    {
+                        "new_price": 1.01,
+                        "batch": True,
+                        "market_version": 321,
+                    },
+                )
+            ],
+        )
 
     def test_event(self):
         mock_market_catalogue = mock.Mock()
diff --git a/tests/test_orderpackage.py b/tests/test_orderpackage.py
index 7647537..d180bfe 100644
--- a/tests/test_orderpackage.py
+++ b/tests/test_orderpackage.py
@@ -24,6 +24,7 @@ class OrderPackageTest(unittest.TestCase):
             [self.mock_order],
             self.mock_package_type,
             1,
+            market_version=123,
         )
 
     def test_init(self):
@@ -36,6 +37,7 @@ class OrderPackageTest(unittest.TestCase):
         self.assertEqual(self.order_package.bet_delay, 1)
         self.assertIsNone(self.order_package.EXCHANGE)
         self.assertFalse(self.order_package.async_)
+        self.assertEqual(self.order_package._market_version, 123)
         self.assertFalse(self.order_package.processed)
         self.assertTrue(self.order_package._retry)
         self.assertEqual(self.order_package._max_retries, 3)
@@ -97,14 +99,17 @@ class OrderPackageTest(unittest.TestCase):
                 "package_type": self.order_package.package_type.value,
                 "customer_strategy_ref": self.order_package.customer_strategy_ref,
                 "bet_delay": self.order_package.bet_delay,
-                "market_version": self.order_package.market_version,
+                "market_version": self.order_package._market_version,
                 "retry": self.order_package._retry,
                 "retry_count": self.order_package._retry_count,
             },
         )
 
     def test_market_version(self):
-        self.assertIsNone(self.order_package.market_version)
+        self.assertEqual(
+            self.order_package.market_version,
+            {"version": self.order_package._market_version},
+        )
 
     def test_iter(self):
         self.assertEqual([i for i in self.order_package], self.order_package.orders)
diff --git a/tests/test_strategy.py b/tests/test_strategy.py
index aaa321a..d93f835 100644
--- a/tests/test_strategy.py
+++ b/tests/test_strategy.py
@@ -150,8 +150,8 @@ class BaseStrategyTest(unittest.TestCase):
         mock_order = mock.Mock()
         mock_order.lookup = ("1", 2, 3)
         mock_market = mock.Mock()
-        self.assertTrue(self.strategy.place_order(mock_market, mock_order))
-        mock_market.place_order.assert_called_with(mock_order)
+        self.assertTrue(self.strategy.place_order(mock_market, mock_order, False, 123))
+        mock_market.place_order.assert_called_with(mock_order, False, 123)
         self.assertIn(mock_order.lookup, self.strategy._invested)
 
     @mock.patch(
@@ -169,19 +169,19 @@ class BaseStrategyTest(unittest.TestCase):
         mock_order = mock.Mock()
         mock_market = mock.Mock()
         self.strategy.cancel_order(mock_market, mock_order, 0.01)
-        mock_market.cancel_order.assert_called_with(mock_order, 0.01)
+        mock_market.cancel_order.assert_called_with(mock_order, 0.01, True)
 
     def test_update_order(self):
         mock_order = mock.Mock()
         mock_market = mock.Mock()
         self.strategy.update_order(mock_market, mock_order, "PERSIST")
-        mock_market.update_order.assert_called_with(mock_order, "PERSIST")
+        mock_market.update_order.assert_called_with(mock_order, "PERSIST", True)
 
     def test_replace_order(self):
         mock_order = mock.Mock()
         mock_market = mock.Mock()
-        self.strategy.replace_order(mock_market, mock_order, 1.01)
-        mock_market.replace_order.assert_called_with(mock_order, 1.01)
+        self.strategy.replace_order(mock_market, mock_order, 1.01, True, 123)
+        mock_market.replace_order.assert_called_with(mock_order, 1.01, True, 123)
 
     def test_validate_order(self):
         mock_order = mock.Mock()
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 610a30a..6804594 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -18,6 +18,31 @@ class UtilsTest(unittest.TestCase):
     def test_chunks(self):
         self.assertEqual([i for i in utils.chunks([1, 2, 3], 1)], [[1], [2], [3]])
 
+    def test_batch_orders(self):
+        order_one = (mock.Mock(), {})
+        order_two = (mock.Mock(), {"market_version": 123})
+        order_three = (mock.Mock(), {"batch": False})
+        order_four = (mock.Mock(), {"market_version": 123, "batch": True})
+        self.assertEqual(
+            utils.batch_orders([order_one, order_two, order_three, order_four]),
+            {
+                None: [[order_one[0]], [order_three[0]]],
+                123: [[order_two[0], order_four[0]]],
+            },
+        )
+
+    def test__split_orders_by(self):
+        order_one = (mock.Mock(), {})
+        order_four = (mock.Mock(), {"market_version": 123, "batch": True})
+        order_eight = (mock.Mock(), {"market_version": 456, "batch": True})
+        order_ten = (mock.Mock(), {"market_version": 456, "batch": True})
+        self.assertEqual(
+            utils._split_orders_by(
+                [order_one, order_four, order_eight, order_ten], "market_version", 321
+            ),
+            {321: [order_one], 123: [order_four], 456: [order_eight, order_ten]},
+        )
+
     def test_create_cheap_hash(self):
         self.assertEqual(
             utils.create_cheap_hash("test"),