Repository: liampauling/flumine
Pull request #155 (2020-04-13 09:15:46)

Dev


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 48abbaf..1a2257b 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,30 @@
 Release History
 ---------------
 
+1.3.0 (2020-04-28)
++++++++++++++++++++
+
+**Improvements**
+
+- BetfairExecution now live (place/cancel/update/replace)
+- Trading and Client controls now live
+- Trade/Order logic created and integrated
+- OrderPackage created for execution
+- Market class created
+- process.py created to handle order/trade logic and linking
+- Market catalogue worker added
+- Blotter created with some initial functions (selection_exposure)
+- Strategy runner_context added to handle selection investment
+- OrderStream created and integrated
+
+**Bug Fixes**
+
+- Error handling on keep_alive worker added
+
+**Libraries**
+
+- requests added as dependency
+
 1.2.0 (2020-04-06)
 +++++++++++++++++++
 
diff --git a/README.md b/README.md
index 71d4cba..3bbfc48 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,8 @@ Currently tested on Python 3.5, 3.6, 3.7 and 3.8.
 $ pip install flumine
 ```
 
+flumine requires Python 3.5+
+
 ## setup
 
 Get started...
@@ -45,22 +47,46 @@ Example strategy:
 
 ```python
 from flumine import BaseStrategy
+from flumine.order.trade import Trade
+from flumine.order.order import LimitOrder, OrderStatus
+from flumine.markets.market import Market
 from betfairlightweight.filters import streaming_market_filter
+from betfairlightweight.resources import MarketBook
 
 
 class ExampleStrategy(BaseStrategy):
-    def start(self):
-        # subscribe to streams
+    def start(self) -> None:
         print("starting strategy 'ExampleStrategy'")
 
-    def check_market_book(self, market, market_book):
+    def check_market_book(self, market: Market, market_book: MarketBook) -> bool:
         # process_market_book only executed if this returns True
         if market_book.status != "CLOSED":
             return True
 
-    def process_market_book(self, market, market_book):
+    def process_market_book(self, market: Market, market_book: MarketBook) -> None:
         # process marketBook object
-        print(market_book.status)
+        for runner in market_book.runners:
+            if runner.status == "ACTIVE" and runner.last_price_traded < 1.5:
+                trade = Trade(
+                    market_id=market_book.market_id, 
+                    selection_id=runner.selection_id, 
+                    strategy=self
+                )
+                order = trade.create_order(
+                    side="LAY", 
+                    order_type=LimitOrder(price=1.01, size=2.00)
+                )
+                self.place_order(market, order)
+
+    def process_orders(self, market: Market, orders: list) -> None:
+        for order in orders:
+            if order.status == OrderStatus.EXECUTABLE:
+                if order.size_remaining == 2.00:
+                    self.cancel_order(order, 0.02)  # reduce size to 1.98
+                if order.order_type.persistence_type == 'LAPSE':
+                    self.update_order(order, 'PERSIST')
+                if order.size_remaining > 0:
+                    self.replace_order(order, 1.02)  # move
 
 
 strategy = ExampleStrategy(
@@ -84,7 +110,7 @@ framework.run()
 
 - Streaming
 - Multiple strategies
-- Order execution (in development)
+- Order execution
 - Paper trading (in development)
 - Back testing (in development)
 - Analytics (in development)
@@ -94,6 +120,7 @@ framework.run()
 
 flumine relies on these libraries:
 
-* `betfairlightweight` - Betfair API support.
-* `tenacity` - Used for connection retrying (streaming).
-* `python-json-logger` - JSON logging.
+* `betfairlightweight` - Betfair API support
+* `tenacity` - Used for connection retrying (streaming)
+* `python-json-logger` - JSON logging
+* `requests` - HTTP support
diff --git a/docs/advanced.md b/docs/advanced.md
index f31b4cb..fef98ce 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -6,6 +6,8 @@ The Flumine class can be adapted by overriding the following functions:
 
 - `_process_market_books()` called on MarketBook event
 - `_process_raw_data()` called on RawData event
+- `_process_market_catalogues` called on MarketCatalogue event
+- `_process_current_orders` called on currentOrders event
 - `_process_end_flumine()` called on Flumine termination
 
 ## Base Strategy
@@ -110,8 +112,8 @@ _In development_
 
 By default flumine adds the following workers:
  
-- keep_alive: runs every 1200s to make sure the client is either logged in or kept alive
-- market
+- `keep_alive`: runs every 1200s to make sure the client is either logged in or kept alive
+- `poll_market_catalogue`: runs every 60s to poll listMarketCatalogue endpoint
 
 
 Further workers can be added as per:
@@ -133,3 +135,18 @@ framework.add_worker(
 ## Error Handling
 
 ## Logging
+
+
+## Config
+
+### hostname
+
+Used as customerStrategyRefs so that only orders created by the running instance is returned.
+
+### process_id
+
+OS process id of running application.
+
+### current_time
+
+Used for backtesting
diff --git a/docs/design.md b/docs/design.md
index c5a1a1c..28604e4 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -35,8 +35,8 @@ Backtesting is achieved by monkeypatching the datetime function utcnow(), this a
 ### Streams
 - Single stream (market)
 - As above but 'data' (flumine listener)
+- Order stream
 - Future work:
-    - Order stream
     - Custom stream
 
 ### Strategy
diff --git a/docs/index.md b/docs/index.md
index 0ad7e13..5b924c6 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -38,6 +38,8 @@ Currently tested on Python 3.5, 3.6, 3.7 and 3.8.
 $ pip install flumine
 ```
 
+flumine requires Python 3.5+
+
 ## setup
 
 Get started...
@@ -58,22 +60,46 @@ Example strategy:
 
 ```python
 from flumine import BaseStrategy
+from flumine.order.trade import Trade
+from flumine.order.order import LimitOrder, OrderStatus
+from flumine.markets.market import Market
 from betfairlightweight.filters import streaming_market_filter
+from betfairlightweight.resources import MarketBook
 
 
 class ExampleStrategy(BaseStrategy):
-    def start(self):
-        # subscribe to streams
+    def start(self) -> None:
         print("starting strategy 'ExampleStrategy'")
-        
-    def check_market_book(self, market, market_book):
+
+    def check_market_book(self, market: Market, market_book: MarketBook) -> bool:
         # process_market_book only executed if this returns True
         if market_book.status != "CLOSED":
             return True
 
-    def process_market_book(self, market, market_book):
+    def process_market_book(self, market: Market, market_book: MarketBook) -> None:
         # process marketBook object
-        print(market_book.status)
+        for runner in market_book.runners:
+            if runner.status == "ACTIVE" and runner.last_price_traded < 1.5:
+                trade = Trade(
+                    market_id=market_book.market_id, 
+                    selection_id=runner.selection_id, 
+                    strategy=self
+                )
+                order = trade.create_order(
+                    side="LAY", 
+                    order_type=LimitOrder(price=1.01, size=2.00)
+                )
+                self.place_order(market, order)
+
+    def process_orders(self, market: Market, orders: list) -> None:
+        for order in orders:
+            if order.status == OrderStatus.EXECUTABLE:
+                if order.size_remaining == 2.00:
+                    self.cancel_order(order, 0.02)  # reduce size to 1.98
+                if order.order_type.persistence_type == 'LAPSE':
+                    self.update_order(order, 'PERSIST')
+                if order.size_remaining > 0:
+                    self.replace_order(order, 1.02)  # move
 
 
 strategy = ExampleStrategy(
@@ -102,7 +128,7 @@ framework.run()
 
 - Streaming
 - Multiple strategies
-- Order execution (in development)
+- Order execution
 - Paper trading (in development)
 - Back testing (in development)
 - Analytics (in development)
@@ -112,16 +138,7 @@ framework.run()
 
 flumine relies on these libraries:
 
-* `betfairlightweight` - Betfair API support.
-* `tenacity` - Used for connection retrying (streaming).
-* `python-json-logger` - JSON logging.
-
-## Installation
-
-Install with pip:
-
-```shell
-$ pip install flumine
-```
-
-flumine requires Python 3.5+
+* `betfairlightweight` - Betfair API support
+* `tenacity` - Used for connection retrying (streaming)
+* `python-json-logger` - JSON logging
+* `requests` - HTTP support
diff --git a/examples/example.py b/examples/example.py
index 4a9422c..0c3f535 100644
--- a/examples/example.py
+++ b/examples/example.py
@@ -5,6 +5,9 @@ from betfairlightweight.filters import streaming_market_filter
 from pythonjsonlogger import jsonlogger
 
 from flumine import Flumine, clients, BaseStrategy
+from flumine.order.trade import Trade
+from flumine.order.ordertype import LimitOrder
+from flumine.order.order import OrderStatus
 
 logger = logging.getLogger()
 
@@ -29,7 +32,37 @@ class ExampleStrategy(BaseStrategy):
 
     def process_market_book(self, market, market_book):
         # process marketBook object
-        print(market_book.status, market.seconds_to_start)
+        for runner in market_book.runners:
+            if (
+                runner.status == "ACTIVE"
+                and runner.last_price_traded
+                and runner.last_price_traded < 3
+            ):
+                trade = Trade(
+                    market_id=market_book.market_id,
+                    selection_id=runner.selection_id,
+                    strategy=self,
+                )
+                order = trade.create_order(
+                    side="LAY", order_type=LimitOrder(price=1.01, size=2.00)
+                )
+                self.place_order(market, order)
+
+    def process_orders(self, market, orders):
+        for order in orders:
+            # print(order.status, order.bet_id, order.elapsed_seconds)
+            if order.status == OrderStatus.EXECUTABLE and order.elapsed_seconds > 5:
+                # print(order.bet_id, order.average_price_matched, order.size_matched)
+                if order.size_remaining == 2.00:
+                    self.cancel_order(market, order, size_reduction=1.51)
+                # self.update_order(market, order, "PERSIST")
+                if order.order_type.price == 1.01 and order.size_remaining == 0.49:
+                    self.replace_order(market, order, 1.02)
+                # if order.order_type.price == 1.02:
+                #     self.replace_order(market, order, 1.03)
+                # if order.order_type.price == 1.03:
+                #     self.replace_order(market, order, 1.05)
+                pass
 
 
 trading = betfairlightweight.APIClient("username")
@@ -38,9 +71,8 @@ client = clients.BetfairClient(trading)
 framework = Flumine(client=client)
 
 strategy = ExampleStrategy(
-    market_filter=streaming_market_filter(
-        event_type_ids=["7"], country_codes=["GB", "IE", "US"], market_types=["WIN"]
-    )
+    market_filter=streaming_market_filter(market_ids=["1.170345090"]),
+    streaming_timeout=2,
 )
 framework.add_strategy(strategy)
 
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 46e2045..f0b4f74 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.2.0"
+__version__ = "1.3.0"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/baseflumine.py b/flumine/baseflumine.py
index ecabf66..37e6ec9 100644
--- a/flumine/baseflumine.py
+++ b/flumine/baseflumine.py
@@ -1,5 +1,6 @@
 import queue
 import logging
+from typing import Type
 from betfairlightweight import resources
 
 from .strategy.strategy import Strategies, BaseStrategy
@@ -9,6 +10,12 @@ from .worker import BackgroundWorker
 from .clients.baseclient import BaseClient
 from .markets.markets import Markets
 from .markets.market import Market
+from .execution.betfairexecution import BetfairExecution
+from .execution.simulatedexecution import SimulatedExecution
+from .order.process import process_current_orders
+from .controls.clientcontrols import BaseControl, MaxOrderCount
+from .controls.tradingcontrols import OrderValidation, StrategyExposure
+
 
 logger = logging.getLogger(__name__)
 
@@ -38,18 +45,20 @@ class BaseFlumine:
         # all streams (market/order)
         self.streams = Streams(self)
 
-        # todo order execution class
-        self.local_execution = None  # backtesting / paper
-        self.betfair_execution = None
+        # order execution class
+        self.simulated_execution = SimulatedExecution(self)
+        self.betfair_execution = BetfairExecution(self)
 
         # logging controls (e.g. database logger)
         self._logging_controls = []
 
         # trading controls
-        self._trading_controls = []  # todo register default controls
-
-        # finance blotter
-        self.blotter = None  # todo
+        self._trading_controls = []
+        # add default controls (processed in order)
+        self.add_trading_control(OrderValidation)
+        self.add_trading_control(StrategyExposure)
+        # register default client controls (processed in order)
+        self.add_client_control(MaxOrderCount)
 
         # workers
         self._workers = []
@@ -65,6 +74,14 @@ class BaseFlumine:
     def add_worker(self, worker: BackgroundWorker) -> None:
         self._workers.append(worker)
 
+    def add_client_control(self, client_control: Type[BaseControl], **kwargs) -> None:
+        logger.info("Adding client control {0}".format(client_control.NAME))
+        self.client.trading_controls.append(client_control(self, self.client, **kwargs))
+
+    def add_trading_control(self, trading_control: Type[BaseControl], **kwargs) -> None:
+        logger.info("Adding trading control {0}".format(trading_control.NAME))
+        self._trading_controls.append(trading_control(self, **kwargs))
+
     def _add_default_workers(self) -> None:
         return
 
@@ -80,6 +97,27 @@ class BaseFlumine:
                 if strategy.check_market(market, market_book):
                     strategy.process_market_book(market, market_book)
 
+            self._process_market_orders(market)
+
+    def _process_market_orders(
+        self, market: Market
+    ) -> None:  # todo override for backtesting
+        for order_package in market.blotter.process_orders(self.client):
+            self.handler_queue.put(order_package)
+
+    def _process_order_package(self, order_package) -> None:
+        """Validate trading controls and
+        then execute.
+        """
+        for control in self._trading_controls:
+            control(order_package)
+        for control in order_package.client.trading_controls:
+            control(order_package)
+        if order_package.orders:
+            order_package.client.execution.handler(order_package)
+        else:
+            logger.warning("Empty package, not executing", extra=order_package.info)
+
     def _add_live_market(
         self, market_id: str, market_book: resources.MarketBook
     ) -> Market:
@@ -109,6 +147,15 @@ class BaseFlumine:
                     # todo logging control
                 market.market_catalogue = market_catalogue
 
+    def _process_current_orders(self, event: event.CurrentOrdersEvent) -> None:
+        process_current_orders(self.markets, self.strategies, event)  # update state
+        for market in self.markets:
+            if market.closed is False:
+                for strategy in self.strategies:
+                    strategy_orders = market.blotter.strategy_orders(strategy)
+                    strategy.process_orders(market, strategy_orders)
+            self._process_market_orders(market)
+
     def _process_end_flumine(self) -> None:
         for strategy in self.strategies:
             strategy.finish()
@@ -119,6 +166,8 @@ class BaseFlumine:
 
     def __enter__(self):
         logger.info("Starting flumine")
+        # add execution to clients
+        self.client.add_execution(self)
         # login
         self.client.login()
         # add default and start all workers
@@ -129,6 +178,8 @@ class BaseFlumine:
         # start strategies
         self.strategies.start()
         # start streams
+        if not self.BACKTEST:
+            self.streams.add_order_stream()  # todo move?
         self.streams.start()
 
         self._running = True
diff --git a/flumine/clients/baseclient.py b/flumine/clients/baseclient.py
index 57a2281..0b5e2f1 100644
--- a/flumine/clients/baseclient.py
+++ b/flumine/clients/baseclient.py
@@ -1,5 +1,7 @@
 from ..utils import create_short_uuid
 
+from .clients import ExchangeType
+
 DEFAULT_CAPITAL_BASE = 0
 DEFAULT_COMMISSION_BASE = 0.05
 
@@ -46,3 +48,9 @@ class BaseClient:
 
     def update_account_details(self) -> None:
         raise NotImplementedError
+
+    def add_execution(self, flumine) -> None:
+        if self.EXCHANGE == ExchangeType.SIMULATED:
+            self.execution = flumine.simulated_execution
+        elif self.EXCHANGE == ExchangeType.BETFAIR:
+            self.execution = flumine.betfair_execution
diff --git a/flumine/clients/betfairclient.py b/flumine/clients/betfairclient.py
index 6162e15..c2bf985 100644
--- a/flumine/clients/betfairclient.py
+++ b/flumine/clients/betfairclient.py
@@ -15,18 +15,18 @@ class BetfairClient(BaseClient):
 
     EXCHANGE = ExchangeType.BETFAIR
 
-    def login(self) -> None:
+    def login(self) -> resources.LoginResource:
         if self.interactive_login:
-            self.betting_client.login_interactive()
+            return self.betting_client.login_interactive()
         else:
-            self.betting_client.login()
+            return self.betting_client.login()
 
-    def keep_alive(self) -> None:
+    def keep_alive(self) -> resources.KeepAliveResource:
         if self.betting_client.session_expired:
-            self.betting_client.keep_alive()
+            return self.betting_client.keep_alive()
 
-    def logout(self) -> None:
-        self.betting_client.logout()
+    def logout(self) -> resources.LogoutResource:
+        return self.betting_client.logout()
 
     def update_account_details(self) -> None:
         # get details
diff --git a/flumine/clients/clients.py b/flumine/clients/clients.py
index b6225be..049b250 100644
--- a/flumine/clients/clients.py
+++ b/flumine/clients/clients.py
@@ -3,4 +3,4 @@ from enum import Enum
 
 class ExchangeType(Enum):
     BETFAIR = "Betfair"
-    BACKTEST = "Backtest"
+    SIMULATED = "Simulated"
diff --git a/flumine/config.py b/flumine/config.py
index 4ee873e..fa80859 100644
--- a/flumine/config.py
+++ b/flumine/config.py
@@ -1,1 +1,10 @@
+import os
+import socket
+
+hostname = socket.gethostname()[
+    :15
+]  # ie. docker container id (used as order customerStrategyRefs)
+
+process_id = os.getpid()  # process id of app
+
 current_time = None  # used for backtesting
diff --git a/flumine/controls/__init__.py b/flumine/controls/__init__.py
new file mode 100644
index 0000000..ecb8ff0
--- /dev/null
+++ b/flumine/controls/__init__.py
@@ -0,0 +1,30 @@
+import logging
+
+# from ..order.process import strategy_reset
+from ..order.orderpackage import BaseOrderPackage
+from ..order.order import BaseOrder
+
+logger = logging.getLogger(__name__)
+
+
+class BaseControl:
+
+    NAME = None
+
+    def __init__(self, flumine, *args, **kwargs):
+        self.flumine = flumine
+
+    def __call__(self, order_package: BaseOrderPackage):
+        self._validate(order_package)
+
+    def _validate(self, order_package: BaseOrderPackage) -> None:
+        raise NotImplementedError
+
+    def _on_error(self, order: BaseOrder) -> None:
+        order.violation()
+        logger.warning(
+            "Order has violated {0} and will not be placed".format(self.NAME),
+            extra=order.info,
+        )
+        # if order.flumine_order_type == "initial":
+        #     strategy_reset(order)
diff --git a/flumine/controls/clientcontrols.py b/flumine/controls/clientcontrols.py
new file mode 100644
index 0000000..6d06da4
--- /dev/null
+++ b/flumine/controls/clientcontrols.py
@@ -0,0 +1,103 @@
+import datetime
+import logging
+from betfairlightweight.metadata import transaction_limit
+
+from ..order.orderpackage import OrderPackageType
+from . import BaseControl
+from ..clients.baseclient import BaseClient
+
+logger = logging.getLogger(__name__)
+
+
+class MaxOrderCount(BaseControl):
+
+    """
+    Counts and limits orders based on max
+    order count.
+    Only prevents placeOrders if limit is
+    reached.
+    """
+
+    NAME = "MAX_ORDER_COUNT"
+
+    def __init__(self, flumine, client: BaseClient):
+        super(MaxOrderCount, self).__init__(flumine)
+        self.client = client
+        self.total = 0
+        self.place_requests = 0
+        self.cancel_requests = 0
+        self.update_requests = 0
+        self.replace_requests = 0
+        self._next_hour = None
+        self._set_next_hour()
+        self.transaction_count = 0
+        self.transaction_limit = self.client.transaction_limit
+
+    def _validate(self, order_package):
+        self._check_hour()
+        self.total += 1
+        if order_package.package_type == OrderPackageType.PLACE:
+            self._check_transaction_count(len(order_package))
+            self.place_requests += len(order_package)
+            if not self.safe:  # and order.flumine_order_type == "initial"
+                for order in order_package:
+                    self._on_error(order)
+        elif order_package.package_type == OrderPackageType.CANCEL:
+            self.cancel_requests += len(order_package)
+        elif order_package.package_type == OrderPackageType.UPDATE:
+            self.update_requests += len(order_package)
+        elif order_package.package_type == OrderPackageType.REPLACE:
+            self.replace_requests += len(order_package)
+
+    def _check_hour(self):
+        if datetime.datetime.utcnow() > self._next_hour:
+            logger.info(
+                "Execution new hour",
+                extra={
+                    "transaction_count": self.transaction_count,
+                    "place_requests": self.place_requests,
+                    "cancel_requests": self.cancel_requests,
+                    "update_requests": self.update_requests,
+                    "replace_requests": self.replace_requests,
+                    "client": self.client,
+                },
+            )
+            self._set_next_hour()
+            if self.transaction_count > transaction_limit:
+                self.client.chargeable_transaction_count += (
+                    self.transaction_count - transaction_limit
+                )
+            self.transaction_count = 0
+
+    def _check_transaction_count(self, transaction_count):
+        self.transaction_count += transaction_count
+        if self.transaction_count > self.transaction_limit:
+            logger.error(
+                "Transaction limit reached",
+                extra={
+                    "transaction_count": self.transaction_count,
+                    "transaction_limit": self.transaction_limit,
+                    "client": self.client,
+                },
+            )
+
+    def _set_next_hour(self):
+        now = datetime.datetime.utcnow()
+        self._next_hour = (now + datetime.timedelta(hours=1)).replace(
+            minute=0, second=0, microsecond=0
+        )
+
+    @property
+    def safe(self):
+        self._check_hour()
+        if self.transaction_count < self.transaction_limit:
+            return True
+        else:
+            logger.error(
+                "Transaction limit reached",
+                extra={
+                    "transaction_count": self.transaction_count,
+                    "transaction_limit": self.transaction_limit,
+                    "client": self.client,
+                },
+            )
diff --git a/flumine/controls/tradingcontrols.py b/flumine/controls/tradingcontrols.py
new file mode 100644
index 0000000..144da69
--- /dev/null
+++ b/flumine/controls/tradingcontrols.py
@@ -0,0 +1,115 @@
+import logging
+
+from ..clients.clients import ExchangeType
+from ..order.ordertype import OrderTypes
+from ..order.orderpackage import OrderPackageType
+from . import BaseControl
+from .. import utils
+
+logger = logging.getLogger(__name__)
+
+
+class OrderValidation(BaseControl):
+
+    """
+    Checks order price and size is valid for
+    exchange.
+    """
+
+    NAME = "ORDER_VALIDATION"
+
+    def _validate(self, order_package):
+        for order in order_package:
+            if order.EXCHANGE == ExchangeType.BETFAIR:
+                self._validate_betfair_order(order)
+
+    def _validate_betfair_order(self, order):
+        if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
+            self._validate_betfair_size(order)
+            self._validate_betfair_price(order)
+        elif order.order_type.ORDER_TYPE == OrderTypes.LIMIT_ON_CLOSE:
+            self._validate_betfair_price(order)
+            self._validate_betfair_liability(order)
+            self._validate_betfair_min_size(order)
+        elif order.order_type.ORDER_TYPE == OrderTypes.MARKET_ON_CLOSE:
+            self._validate_betfair_liability(order)
+            self._validate_betfair_min_size(order)
+        else:
+            self._on_error(order)  # unknown orderType
+
+    def _validate_betfair_size(self, order):
+        if order.order_type.size is None:
+            self._on_error(order)
+        elif order.order_type.size <= 0:
+            self._on_error(order)
+        elif order.order_type.size != round(order.order_type.size, 2):
+            self._on_error(order)
+
+    def _validate_betfair_price(self, order):
+        if order.order_type.price is None:
+            self._on_error(order)
+        elif utils.as_dec(order.order_type.price) not in utils.PRICES:
+            self._on_error(order)
+
+    def _validate_betfair_liability(self, order):
+        if order.order_type.liability is None:
+            self._on_error(order)
+        elif order.order_type.liability <= 0:
+            self._on_error(order)
+
+    def _validate_betfair_min_size(self, order):
+        # todo
+        return
+        if (
+            order.side == "BACK"
+            and order.order_type.liability < self.client.min_bet_size
+        ):
+            self._on_error(order)
+        elif (
+            order.side == "LAY"
+            and order.order_type.liability < self.client.min_bsp_liability
+        ):
+            self._on_error(order)
+
+
+class StrategyExposure(BaseControl):
+
+    """
+    Checks exposure does not breach strategy
+    max exposure.
+    """
+
+    NAME = "STRATEGY_EXPOSURE"
+
+    def _validate(self, order_package):
+        if order_package.package_type in (
+            OrderPackageType.PLACE,
+            OrderPackageType.REPLACE,  # todo potential bug?
+        ):
+            for order in order_package:
+                strategy = order.trade.strategy
+
+                if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
+                    if order.side == "BACK":
+                        exposure = order.order_type.size
+                    else:
+                        exposure = (order.order_type.price - 1) * order.order_type.size
+                elif order.order_type.ORDER_TYPE == OrderTypes.LIMIT_ON_CLOSE:
+                    exposure = order.order_type.liability  # todo correct?
+                elif order.order_type.ORDER_TYPE == OrderTypes.MARKET_ON_CLOSE:
+                    exposure = order.order_type.liability
+                else:
+                    continue
+
+                if exposure > strategy.max_order_exposure:
+                    self._on_error(order)
+                    continue
+
+                market = self.flumine.markets.markets[order_package.market_id]
+                current_selection_exposure = market.blotter.selection_exposure(
+                    strategy, lookup=order.lookup
+                )
+                if (
+                    current_selection_exposure - exposure
+                ) < -strategy.max_selection_exposure:
+                    self._on_error(order)
diff --git a/flumine/event/event.py b/flumine/event/event.py
index fa9d332..e154a22 100644
--- a/flumine/event/event.py
+++ b/flumine/event/event.py
@@ -2,24 +2,26 @@ from enum import Enum
 
 
 class EventType(Enum):
-    TERMINATOR = 0
+    TERMINATOR = "Terminator"
     # betfair objects
-    MARKET_CATALOGUE = 10
-    MARKET_BOOK = 20
-    RAW_DATA = 30
-    CURRENT_ORDERS = 40
-    CLEARED_MARKETS = 50
-    CLEARED_ORDERS = 60
+    MARKET_CATALOGUE = "MarketCatalogue"
+    MARKET_BOOK = "MarketBook"
+    RAW_DATA = "Raw streaming data"
+    CURRENT_ORDERS = "CurrentOrders"
+    CLEARED_MARKETS = "ClearedMarkets"
+    CLEARED_ORDERS = "ClearedOrders"
     # flumine objects
-    CLOSE_MARKET = 100
-    STRATEGY_RESET = 110
-    CUSTOM_EVENT = 120
-    NEW_DAY = 130
+    ORDER_PACKAGE = "Order package"
+    CLOSE_MARKET = "Closed market"
+    STRATEGY_RESET = "Strategy reset"
+    CUSTOM_EVENT = "Custom event"
+    NEW_DAY = "New day"
 
 
 class QueueType(Enum):
-    HANDLER = 10
-    ACCOUNT = 20
+    HANDLER = "Handler queue"
+    ACCOUNT = "Account queue"
+    LOGGING = "Logging queue"
 
 
 class BaseEvent:
diff --git a/flumine/exceptions.py b/flumine/exceptions.py
index d7d0594..af22cf2 100644
--- a/flumine/exceptions.py
+++ b/flumine/exceptions.py
@@ -9,3 +9,29 @@ class ListenerError(FlumineException):
 
     def __int__(self, message):
         super(ListenerError, self).__init__(message)
+
+
+class OrderError(FlumineException):
+    """Exception raised if incorrect
+    order/order_type requested.
+    """
+
+    def __init__(self, message):
+        super(OrderError, self).__init__(message)
+
+
+class OrderUpdateError(FlumineException):
+    """Exception raised if order update
+    incorrect.
+    """
+
+    def __init__(self, message):
+        super(OrderUpdateError, self).__init__(message)
+
+
+class OrderExecutionError(FlumineException):
+    """Exception raised error in package during
+    execution.
+    """
+
+    pass
diff --git a/flumine/execution/__init__.py b/flumine/execution/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/flumine/execution/baseexecution.py b/flumine/execution/baseexecution.py
new file mode 100644
index 0000000..4e87aa9
--- /dev/null
+++ b/flumine/execution/baseexecution.py
@@ -0,0 +1,61 @@
+import requests
+from concurrent.futures import ThreadPoolExecutor
+
+from ..order.orderpackage import BaseOrderPackage, OrderPackageType
+
+MAX_WORKERS = 32
+
+
+class BaseExecution:
+
+    EXCHANGE = None
+
+    def __init__(self, flumine, max_workers=MAX_WORKERS):
+        self.flumine = flumine
+        self._thread_pool = ThreadPoolExecutor(max_workers=max_workers)
+
+    def handler(self, order_package: BaseOrderPackage):
+        """ Handles order_package, capable of place, cancel,
+        replace and update.
+        """
+        http_session = requests.Session()  # todo keep
+        if order_package.package_type == OrderPackageType.PLACE:
+            func = self.execute_place
+        elif order_package.package_type == OrderPackageType.CANCEL:
+            func = self.execute_cancel
+        elif order_package.package_type == OrderPackageType.REPLACE:
+            func = self.execute_replace
+        elif order_package.package_type == OrderPackageType.UPDATE:
+            func = self.execute_update
+        else:
+            raise NotImplementedError()
+
+        self._thread_pool.submit(func, order_package, http_session)
+
+    def execute_place(
+        self, order_package: BaseOrderPackage, http_session: requests.Session
+    ) -> None:
+        raise NotImplementedError
+
+    def execute_cancel(
+        self, order_package: BaseOrderPackage, http_session: requests.Session
+    ) -> None:
+        raise NotImplementedError
+
+    def execute_update(
+        self, order_package: BaseOrderPackage, http_session: requests.Session
+    ) -> None:
+        raise NotImplementedError
+
+    def execute_replace(
+        self, order_package: BaseOrderPackage, http_session: requests.Session
+    ) -> None:
+        raise NotImplementedError
+
+    @property
+    def handler_queue(self):
+        return self.flumine.handler_queue
+
+    @property
+    def markets(self):
+        return self.flumine.markets
diff --git a/flumine/execution/betfairexecution.py b/flumine/execution/betfairexecution.py
new file mode 100644
index 0000000..0c90237
--- /dev/null
+++ b/flumine/execution/betfairexecution.py
@@ -0,0 +1,209 @@
+import logging
+import requests
+from typing import Callable
+from betfairlightweight import BetfairError
+
+from .baseexecution import BaseExecution
+from ..clients.clients import ExchangeType
+from ..order.orderpackage import BaseOrderPackage, OrderPackageType, BaseOrder
+from ..exceptions import OrderExecutionError
+
+logger = logging.getLogger(__name__)
+
+
+class BetfairExecution(BaseExecution):
+
+    EXCHANGE = ExchangeType.BETFAIR
+
+    def execute_place(
+        self, order_package: BaseOrderPackage, http_session: requests.Session
+    ) -> None:
+        response = self._execution_helper(self.place, order_package, http_session)
+        if response:
+            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
+
+    def place(self, order_package: OrderPackageType, session: requests.Session):
+        return order_package.client.betting_client.betting.place_orders(
+            market_id=order_package.market_id,
+            instructions=order_package.place_instructions,
+            customer_ref=order_package.id.hex,
+            market_version=order_package.market_version,
+            customer_strategy_ref=order_package.customer_strategy_ref,
+            async_=order_package.async_,
+            session=session,
+        )
+
+    def execute_cancel(
+        self, order_package: BaseOrderPackage, http_session: requests.Session
+    ) -> None:
+        response = self._execution_helper(self.cancel, order_package, http_session)
+        if response:
+            order_lookup = {o.bet_id: o for o in order_package}
+            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:
+                        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()
+
+    def cancel(self, order_package: OrderPackageType, session: requests.Session):
+        # temp copy to prevent an empty list of instructions sent
+        # this can occur if order is matched during the execution
+        # cycle, resulting in all orders being cancelled!
+        cancel_instructions = list(order_package.cancel_instructions)
+        if not cancel_instructions:
+            logger.warning("Empty cancel_instructions", extra=order_package.info)
+            raise OrderExecutionError()
+        return order_package.client.betting_client.betting.cancel_orders(
+            market_id=order_package.market_id,
+            instructions=cancel_instructions,
+            customer_ref=order_package.id.hex,
+            session=session,
+        )
+
+    def execute_update(
+        self, order_package: BaseOrderPackage, http_session: requests.Session
+    ) -> None:
+        response = self._execution_helper(self.update, order_package, http_session)
+        if response:
+            for (order, instruction_report) in zip(  # todo check this is ok
+                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()
+
+    def update(self, order_package: OrderPackageType, session: requests.Session):
+        return order_package.client.betting_client.betting.update_orders(
+            market_id=order_package.market_id,
+            instructions=order_package.update_instructions,
+            customer_ref=order_package.id.hex,
+            session=session,
+        )
+
+    def execute_replace(
+        self, order_package: BaseOrderPackage, http_session: requests.Session
+    ) -> None:
+        response = self._execution_helper(self.replace, order_package, http_session)
+        if response:
+            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()
+
+                # 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
+                    market = self.markets.markets[order.market_id]
+                    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(
+            market_id=order_package.market_id,
+            instructions=order_package.replace_instructions,
+            customer_ref=order_package.id.hex,
+            market_version=order_package.market_version,
+            async_=order_package.async_,
+            session=session,
+        )
+
+    def _execution_helper(  # todo retry!
+        self,
+        trading_function: Callable,
+        order_package: BaseOrderPackage,
+        http_session: requests.Session,
+    ):
+        if order_package.orders:
+            try:
+                response = trading_function(order_package, http_session)
+            except BetfairError as e:
+                logger.error(
+                    "Execution error",
+                    extra={
+                        "trading_function": trading_function.__name__,
+                        "response": e,
+                        "order_package": order_package.info,
+                    },
+                )
+                return
+            logger.info(
+                "execute_%s" % trading_function.__name__,
+                extra={**order_package.info, **{"response": response._data}},
+            )
+            return response
+        else:
+            logger.warning("Empty package, not executing", extra=order_package.info)
+
+    def _order_logger(
+        self, order: BaseOrder, instruction_report, package_type: OrderPackageType
+    ):
+        logger.info(
+            "Order %s: %s" % (package_type.value, instruction_report.status),
+            extra={
+                "bet_id": order.bet_id,
+                "order_id": order.id,
+                "status": instruction_report.status,
+                "error_code": instruction_report.error_code,
+            },
+        )
+        if package_type == OrderPackageType.PLACE:
+            order.responses.placed(instruction_report)
+            order.bet_id = instruction_report.bet_id
+        elif package_type == OrderPackageType.CANCEL:
+            order.responses.cancelled(instruction_report)
+        elif package_type == OrderPackageType.UPDATE:
+            order.responses.updated(instruction_report)
+        elif package_type == OrderPackageType.REPLACE:
+            order.responses.placed(instruction_report)
+            order.bet_id = instruction_report.bet_id
+        # self.flumine.log_control(order)  # todo log order
diff --git a/flumine/execution/simulatedexecution.py b/flumine/execution/simulatedexecution.py
new file mode 100644
index 0000000..9ef5367
--- /dev/null
+++ b/flumine/execution/simulatedexecution.py
@@ -0,0 +1,21 @@
+import requests
+
+from .baseexecution import BaseExecution
+from ..clients.clients import ExchangeType
+
+
+class SimulatedExecution(BaseExecution):
+
+    EXCHANGE = ExchangeType.SIMULATED
+
+    def execute_place(self, order_package, http_session: requests.Session) -> None:
+        raise NotImplementedError
+
+    def execute_cancel(self, order_package, http_session: requests.Session) -> None:
+        raise NotImplementedError
+
+    def execute_update(self, order_package, http_session: requests.Session) -> None:
+        raise NotImplementedError
+
+    def execute_replace(self, order_package, http_session: requests.Session) -> None:
+        raise NotImplementedError
diff --git a/flumine/flumine.py b/flumine/flumine.py
index d6ccaec..2be7e5c 100644
--- a/flumine/flumine.py
+++ b/flumine/flumine.py
@@ -29,7 +29,10 @@ class Flumine(BaseFlumine):
                     self._process_raw_data(event)
 
                 elif event.EVENT_TYPE == EventType.CURRENT_ORDERS:
-                    logger.info(event)
+                    self._process_current_orders(event)
+
+                elif event.EVENT_TYPE == EventType.ORDER_PACKAGE:
+                    self._process_order_package(event)
 
                 elif event.EVENT_TYPE == EventType.CLEARED_MARKETS:
                     logger.info(event)
diff --git a/flumine/markets/blotter.py b/flumine/markets/blotter.py
new file mode 100644
index 0000000..ae94c24
--- /dev/null
+++ b/flumine/markets/blotter.py
@@ -0,0 +1,112 @@
+import logging
+from typing import Iterable
+
+from ..utils import chunks, calculate_exposure
+from ..order.order import BaseOrder, OrderStatus
+from ..order.orderpackage import OrderPackageType, BetfairOrderPackage
+
+logger = logging.getLogger(__name__)
+
+# https://www.betfair.com/aboutUs/Betfair.Charges/#charges6
+IMPLIED_COMMISSION_RATE = 0.03
+
+
+class Blotter:
+    def __init__(self, market_id: str):
+        self.market_id = market_id
+        self._orders = {}  # {Order.id: Order}
+        # pending orders
+        self.pending_place = []
+        self.pending_cancel = []
+        self.pending_update = []
+        self.pending_replace = []
+
+    def strategy_orders(self, strategy) -> list:
+        """Returns all orders related to a strategy.
+        """
+        return [order for order in self if order.trade.strategy == strategy]
+
+    def process_orders(self, client) -> list:
+        packages = []
+        if self.pending_place:
+            packages += self._create_packages(
+                client, self.pending_place, OrderPackageType.PLACE
+            )
+        if self.pending_cancel:
+            packages += self._create_packages(
+                client, self.pending_cancel, OrderPackageType.CANCEL
+            )
+        if self.pending_update:
+            packages += self._create_packages(
+                client, self.pending_update, OrderPackageType.UPDATE
+            )
+        if self.pending_replace:
+            packages += self._create_packages(
+                client, self.pending_replace, OrderPackageType.REPLACE
+            )
+        if packages:
+            logger.info(
+                "%s order packages created" % len(packages),
+                extra={"order_packages": [o.info for o in packages]},
+            )
+        return packages
+
+    def _create_packages(
+        self, client, orders: list, package_type: OrderPackageType
+    ) -> list:
+        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,
+                # market_version={"version": self.market_book.version},
+            )
+            packages.append(order_package)
+        orders.clear()
+        return packages
+
+    @property
+    def live_orders(self) -> bool:
+        for order in self._orders.values():
+            if order.status == OrderStatus.EXECUTABLE:
+                return True
+        return False
+
+    """ position """
+
+    def selection_exposure(self, strategy, lookup: tuple) -> float:
+        """Returns strategy/selection exposure,
+        max value is 0
+        """
+        mb, ml = [], []  # (price, size)
+        for order in self:
+            if order.trade.strategy == strategy and order.lookup == lookup:
+                if order.side == "BACK":
+                    mb.append((order.average_price_matched, order.size_matched))
+                else:
+                    ml.append((order.average_price_matched, order.size_matched))
+        # calc exposure
+        return calculate_exposure(mb, ml)
+
+    """ getters / setters """
+
+    def has_order(self, customer_order_ref: str) -> bool:
+        return customer_order_ref in self._orders
+
+    __contains__ = has_order
+
+    def __setitem__(self, customer_order_ref: str, order) -> None:
+        self._orders[customer_order_ref] = order
+
+    def __getitem__(self, customer_order_ref: str):
+        return self._orders[customer_order_ref]
+
+    def __iter__(self) -> Iterable[BaseOrder]:
+        return iter(self._orders.values())
+
+    def __len__(self) -> int:
+        return len(self._orders)
diff --git a/flumine/markets/market.py b/flumine/markets/market.py
index fc7fc7a..ce20c7a 100644
--- a/flumine/markets/market.py
+++ b/flumine/markets/market.py
@@ -1,6 +1,11 @@
 import datetime
+import logging
 from betfairlightweight.resources.bettingresources import MarketBook, MarketCatalogue
 
+from .blotter import Blotter
+
+logger = logging.getLogger(__name__)
+
 
 class Market:
     def __init__(
@@ -13,6 +18,7 @@ class Market:
         self.closed = False
         self.market_book = market_book
         self.market_catalogue = market_catalogue
+        self.blotter = Blotter(market_id)
 
     def __call__(self, market_book: MarketBook):
         self.market_book = market_book
@@ -24,6 +30,29 @@ class Market:
     def close_market(self) -> None:
         self.closed = True
 
+    # order
+    def place_order(self, order, execute: bool = True) -> None:
+        order.place()
+        if order.id not in self.blotter:
+            self.blotter[order.id] = order
+            # todo log trade?
+        else:
+            return  # retry attempt so ignore?
+        if execute:  # handles replaceOrder
+            self.blotter.pending_place.append(order)
+
+    def cancel_order(self, order, size_reduction: float = None) -> None:
+        order.cancel(size_reduction)
+        self.blotter.pending_cancel.append(order)
+
+    def update_order(self, order, new_persistence_type: str) -> None:
+        order.update(new_persistence_type)
+        self.blotter.pending_update.append(order)
+
+    def replace_order(self, order, new_price: float) -> None:
+        order.replace(new_price)
+        self.blotter.pending_replace.append(order)
+
     @property
     def seconds_to_start(self):
         return (self.market_start_datetime - datetime.datetime.utcnow()).total_seconds()
diff --git a/flumine/markets/markets.py b/flumine/markets/markets.py
index 22b5b03..b1249cb 100644
--- a/flumine/markets/markets.py
+++ b/flumine/markets/markets.py
@@ -1,6 +1,7 @@
-from typing import Iterator
+from typing import Iterator, Optional
 
 from .market import Market
+from ..order.order import BetfairOrder
 
 
 class Markets:
@@ -18,14 +19,39 @@ class Markets:
         live_market.close_market()
         return live_market
 
+    def get_order(self, market_id: str, order_id: str) -> Optional[BetfairOrder]:
+        try:
+            return self.markets[market_id].blotter[order_id]
+        except KeyError:
+            return
+
+    def get_order_from_bet_id(
+        self, market_id: str, bet_id: str
+    ) -> Optional[BetfairOrder]:
+        blotter = self.markets[market_id].blotter
+        lookup = {order.bet_id: order for order in blotter}
+        try:
+            return lookup[bet_id]
+        except KeyError:
+            return
+
     @property
     def markets(self) -> dict:
-        return {
-            key: value for key, value in self._markets.items() if value.closed is False
-        }
+        return {key: value for key, value in self._markets.items()}
+
+    @property
+    def open_market_ids(self) -> list:
+        return [m.market_id for m in self.markets.values() if not m.closed]
+
+    @property
+    def live_orders(self) -> bool:
+        for market in self:
+            if market.closed is False and market.blotter.live_orders is True:
+                return True
+        return False
 
     def __iter__(self) -> Iterator[Market]:
-        return iter(self.markets)
+        return iter(self.markets.values())
 
     def __len__(self) -> int:
         return len(self.markets)
diff --git a/flumine/order/__init__.py b/flumine/order/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/flumine/order/order.py b/flumine/order/order.py
new file mode 100644
index 0000000..a9884d3
--- /dev/null
+++ b/flumine/order/order.py
@@ -0,0 +1,323 @@
+import uuid
+import logging
+import datetime
+from enum import Enum
+from typing import Union
+from betfairlightweight import filters
+
+from ..clients.clients import ExchangeType
+from .ordertype import LimitOrder, LimitOnCloseOrder, MarketOnCloseOrder, OrderTypes
+from .responses import Responses
+from ..exceptions import OrderUpdateError
+
+logger = logging.getLogger(__name__)
+
+
+class OrderStatus(Enum):
+    # Pending
+    PENDING = "Pending"  # pending exchange processing
+    CANCELLING = "Cancelling"  # waiting for response
+    UPDATING = "Updating"  # waiting for response
+    REPLACING = "Replacing"  # waiting for response
+    # Completed
+    EXECUTABLE = "Executable"  # an order that has a remaining unmatched portion
+    EXECUTION_COMPLETE = "Execution complete"  # an order that does not have any remaining unmatched portion
+    EXPIRED = "Expired"  # order is no longer available for execution due to its time in force constraint
+    VOIDED = "Voided"
+    LAPSED = "Lapsed"
+    VIOLATION = "Violation"  # order never placed due to failing controls
+
+
+class BaseOrder:
+
+    EXCHANGE = None
+
+    def __init__(
+        self,
+        trade,
+        side: str,
+        order_type: Union[LimitOrder, LimitOnCloseOrder, MarketOnCloseOrder],
+        handicap: float = 0,
+    ):
+        self.id = str(uuid.uuid1().time)  # 18 char str used as unique customerOrderRef
+        self.trade = trade
+        self.side = side
+        self.order_type = order_type
+        self.handicap = handicap
+
+        self.status = None
+        self.status_log = []
+        self._current_order = None  # resources.CurrentOrder
+
+        self.bet_id = None
+        self._update = {}  # stores cancel/update/replace data
+        self.responses = Responses()  # raw api responses
+
+    # status
+    def _update_status(self, status: OrderStatus) -> None:
+        self.status_log.append(status)
+        self.status = status
+        logger.info("Order status update: %s" % self.status.value, extra=self.info)
+
+    def placing(self) -> None:
+        self._update_status(OrderStatus.PENDING)
+
+    def executable(self) -> None:
+        self._update_status(OrderStatus.EXECUTABLE)
+
+    def execution_complete(self) -> None:
+        self._update_status(OrderStatus.EXECUTION_COMPLETE)
+
+    def cancelling(self) -> None:
+        self._update_status(OrderStatus.CANCELLING)
+
+    def updating(self) -> None:
+        self._update_status(OrderStatus.UPDATING)
+
+    def replacing(self) -> None:
+        self._update_status(OrderStatus.REPLACING)
+
+    def lapsed(self) -> None:
+        self._update_status(OrderStatus.LAPSED)
+
+    def voided(self) -> None:
+        self._update_status(OrderStatus.VOIDED)
+
+    def violation(self) -> None:
+        self._update_status(OrderStatus.VIOLATION)
+
+    # updates
+    def place(self) -> None:
+        raise NotImplementedError
+
+    def cancel(self, size_reduction: float = None) -> None:
+        raise NotImplementedError
+
+    def update(self, new_persistence_type: str) -> None:
+        raise NotImplementedError
+
+    def replace(self, new_price: float) -> None:
+        raise NotImplementedError
+
+    # instructions
+    def create_place_instruction(self) -> dict:
+        raise NotImplementedError
+
+    def create_cancel_instruction(self) -> dict:
+        raise NotImplementedError
+
+    def create_update_instruction(self) -> dict:
+        raise NotImplementedError
+
+    def create_replace_instruction(self) -> dict:
+        raise NotImplementedError
+
+    # currentOrder
+    def update_current_order(self, current_order):
+        self._current_order = current_order
+
+    @property
+    def current_order(self):
+        if self._current_order:
+            return self._current_order
+        elif self.responses.place_response:
+            return self.responses.place_response
+
+    @property
+    def average_price_matched(self) -> float:
+        raise NotImplementedError
+
+    @property
+    def size_matched(self) -> float:
+        raise NotImplementedError
+
+    @property
+    def size_remaining(self):
+        raise NotImplementedError
+
+    @property
+    def size_cancelled(self):
+        raise NotImplementedError
+
+    @property
+    def size_lapsed(self):
+        raise NotImplementedError
+
+    @property
+    def size_voided(self):
+        raise NotImplementedError
+
+    @property
+    def elapsed_seconds(self):
+        return (
+            datetime.datetime.utcnow() - self.responses.date_time_placed
+        ).total_seconds()
+
+    @property
+    def market_id(self) -> str:
+        return self.trade.market_id
+
+    @property
+    def selection_id(self) -> int:
+        return self.trade.selection_id
+
+    @property
+    def lookup(self) -> tuple:
+        return self.market_id, self.selection_id, self.handicap
+
+    @property
+    def customer_order_ref(self) -> str:
+        return "{0}-{1}".format(self.trade.strategy.name_hash, self.id)
+
+    @property
+    def info(self) -> dict:
+        return {
+            "market_id": self.market_id,
+            "selection_id": self.selection_id,
+            "handicap": self.handicap,
+            "id": self.id,
+            "customer_order_ref": self.customer_order_ref,
+            "bet_id": self.bet_id,
+            "trade": self.trade.info,
+            "status": self.status.value if self.status else None,
+            "status_log": ", ".join([s.value for s in self.status_log]),
+        }
+
+    def __repr__(self):
+        return "Order {0}: {1}".format(
+            self.bet_id, self.status.value if self.status else None
+        )
+
+
+class BetfairOrder(BaseOrder):
+
+    EXCHANGE = ExchangeType.BETFAIR
+
+    # updates
+    def place(self) -> None:
+        self.placing()
+
+    def cancel(self, size_reduction: float = None) -> None:
+        if self.order_type.ORDER_TYPE == OrderTypes.LIMIT:
+            if size_reduction and self.size_remaining - size_reduction < 0:
+                raise OrderUpdateError("Size reduction too large")
+            if self.status != OrderStatus.EXECUTABLE:
+                raise OrderUpdateError("Current status: %s" % self.status)
+            self._update["size_reduction"] = size_reduction
+            self.cancelling()
+        else:
+            raise OrderUpdateError(
+                "Only LIMIT orders can be cancelled or partially cancelled once placed."
+            )
+
+    def update(self, new_persistence_type: str) -> None:
+        if self.order_type.ORDER_TYPE == OrderTypes.LIMIT:
+            if self.order_type.persistence_type == new_persistence_type:
+                raise OrderUpdateError("Persistence types match")
+            elif self.status != OrderStatus.EXECUTABLE:
+                raise OrderUpdateError("Current status: %s" % self.status)
+            self.order_type.persistence_type = new_persistence_type
+            self.updating()
+        else:
+            raise OrderUpdateError("Only LIMIT orders can be updated.")
+
+    def replace(self, new_price: float) -> None:
+        if self.order_type.ORDER_TYPE in [OrderTypes.LIMIT, OrderTypes.LIMIT_ON_CLOSE]:
+            if self.order_type.price == new_price:
+                raise OrderUpdateError("Prices match")
+            elif self.status != OrderStatus.EXECUTABLE:
+                raise OrderUpdateError("Current status: %s" % self.status)
+            self._update["new_price"] = new_price
+            self.replacing()
+        else:
+            raise OrderUpdateError(
+                "Only LIMIT or LIMIT_ON_CLOSE orders can be replaced."
+            )
+
+    # instructions
+    def create_place_instruction(self) -> dict:
+        if self.order_type.ORDER_TYPE == OrderTypes.LIMIT:
+            return filters.place_instruction(
+                customer_order_ref=self.customer_order_ref,
+                selection_id=self.selection_id,
+                side=self.side,
+                order_type=self.order_type.ORDER_TYPE.name,
+                limit_order=self.order_type.place_instruction(),
+                handicap=self.handicap,
+            )
+        elif self.order_type.ORDER_TYPE == OrderTypes.LIMIT_ON_CLOSE:
+            return filters.place_instruction(
+                customer_order_ref=self.customer_order_ref,
+                selection_id=self.selection_id,
+                side=self.side,
+                order_type=self.order_type.ORDER_TYPE.name,
+                limit_on_close_order=self.order_type.place_instruction(),
+                handicap=self.handicap,
+            )
+        elif self.order_type.ORDER_TYPE == OrderTypes.MARKET_ON_CLOSE:
+            return filters.place_instruction(
+                customer_order_ref=self.customer_order_ref,
+                selection_id=self.selection_id,
+                side=self.side,
+                order_type=self.order_type.ORDER_TYPE.name,
+                market_on_close_order=self.order_type.place_instruction(),
+                handicap=self.handicap,
+            )
+
+    def create_cancel_instruction(self) -> dict:
+        return filters.cancel_instruction(
+            bet_id=self.bet_id, size_reduction=self._update.get("size_reduction")
+        )
+
+    def create_update_instruction(self) -> dict:
+        return filters.update_instruction(
+            bet_id=self.bet_id, new_persistence_type=self.order_type.persistence_type
+        )
+
+    def create_replace_instruction(self) -> dict:
+        return filters.replace_instruction(
+            bet_id=self.bet_id, new_price=self._update["new_price"]
+        )
+
+    # currentOrder
+    @property
+    def average_price_matched(self) -> float:
+        try:
+            return self.current_order.average_price_matched or 0.0
+        except AttributeError:
+            return 0.0
+
+    @property
+    def size_matched(self) -> float:
+        try:
+            return self.current_order.size_matched or 0.0
+        except AttributeError:
+            return 0.0
+
+    @property
+    def size_remaining(self):
+        try:
+            return self.current_order.size_remaining or 0.0
+        except AttributeError:
+            return 0.0
+
+    @property
+    def size_cancelled(self) -> float:
+        try:
+            return self.current_order.size_cancelled or 0.0
+        except AttributeError:
+            return 0.0
+
+    @property
+    def size_lapsed(self) -> float:
+        try:
+            return self.current_order.size_lapsed or 0.0
+        except AttributeError:
+            return 0.0
+
+    @property
+    def size_voided(self) -> float:
+        try:
+            return self.current_order.size_voided or 0.0
+        except AttributeError:
+            return 0.0
diff --git a/flumine/order/orderpackage.py b/flumine/order/orderpackage.py
new file mode 100644
index 0000000..5e597e3
--- /dev/null
+++ b/flumine/order/orderpackage.py
@@ -0,0 +1,124 @@
+import uuid
+from enum import Enum
+from typing import Iterator
+from betfairlightweight.metadata import order_limits
+
+from ..event.event import BaseEvent, EventType, QueueType
+from ..clients.clients import ExchangeType
+from .. import config
+from .order import BaseOrder, OrderStatus
+
+
+class OrderPackageType(Enum):
+    PLACE = "Place"
+    CANCEL = "Cancel"
+    REPLACE = "Replace"
+    UPDATE = "Update"
+
+
+class BaseOrderPackage(BaseEvent):
+
+    """
+    Data structure for multiple orders,
+    temporary to allow execution
+    """
+
+    # todo client/retry/._orders->orders (violation)
+
+    EVENT_TYPE = EventType.ORDER_PACKAGE
+    QUEUE_TYPE = QueueType.HANDLER
+    EXCHANGE = None
+
+    def __init__(
+        self,
+        client,
+        market_id: str,
+        orders: list,
+        package_type: OrderPackageType,
+        market_version: dict = None,
+        async_: bool = False,
+    ):
+        super(BaseOrderPackage, self).__init__(None)
+        self.id = uuid.uuid1()
+        self.client = client
+        self.market_id = market_id
+        self._orders = orders
+        self.package_type = package_type
+        self.market_version = market_version
+        self.async_ = async_
+        self.customer_strategy_ref = config.hostname
+
+    @property
+    def place_instructions(self) -> dict:
+        raise NotImplementedError
+
+    @property
+    def cancel_instructions(self) -> dict:
+        raise NotImplementedError
+
+    @property
+    def update_instructions(self) -> dict:
+        raise NotImplementedError
+
+    @property
+    def replace_instructions(self) -> dict:
+        raise NotImplementedError
+
+    @classmethod
+    def order_limit(cls, package_type: OrderPackageType) -> int:
+        raise NotImplementedError
+
+    @property
+    def orders(self) -> list:
+        return [o for o in self._orders if o.status != OrderStatus.VIOLATION]
+
+    @property
+    def info(self) -> dict:
+        return {
+            "id": self.id,
+            "client": self.client,
+            "market_id": self.market_id,
+            "orders": [o.id for o in self._orders],
+            "package_type": self.package_type.value,
+            "customer_strategy_ref": self.customer_strategy_ref,
+        }
+
+    def __iter__(self) -> Iterator[BaseOrder]:
+        return iter(self.orders)
+
+    def __len__(self) -> int:
+        return len(self.orders)
+
+
+class BetfairOrderPackage(BaseOrderPackage):
+
+    EXCHANGE = ExchangeType.BETFAIR
+
+    @property
+    def place_instructions(self):
+        return [order.create_place_instruction() for order in self]
+
+    @property
+    def cancel_instructions(self):
+        return [
+            order.create_cancel_instruction() for order in self
+        ]  # if order.size_remaining > 0
+
+    @property
+    def update_instructions(self):
+        return [order.create_update_instruction() for order in self]
+
+    @property
+    def replace_instructions(self):
+        return [order.create_replace_instruction() for order in self]
+
+    @classmethod
+    def order_limit(cls, package_type: OrderPackageType) -> int:
+        if package_type == OrderPackageType.PLACE:
+            return order_limits["placeOrders"]
+        elif package_type == OrderPackageType.CANCEL:
+            return order_limits["cancelOrders"]
+        elif package_type == OrderPackageType.UPDATE:
+            return order_limits["updateOrders"]
+        elif package_type == OrderPackageType.REPLACE:
+            return order_limits["replaceOrders"]
diff --git a/flumine/order/ordertype.py b/flumine/order/ordertype.py
new file mode 100644
index 0000000..7f1f945
--- /dev/null
+++ b/flumine/order/ordertype.py
@@ -0,0 +1,83 @@
+from enum import Enum
+from betfairlightweight.filters import (
+    limit_order,
+    limit_on_close_order,
+    market_on_close_order,
+)
+
+from ..clients.clients import ExchangeType
+
+
+class OrderTypes(Enum):
+    LIMIT = "Limit"
+    LIMIT_ON_CLOSE = "Limit on close"
+    MARKET_ON_CLOSE = "Market on close"
+
+
+class BaseOrderType:
+
+    EXCHANGE = None
+    ORDER_TYPE = None
+
+    def place_instruction(self) -> dict:
+        raise NotImplementedError
+
+
+class LimitOrder(BaseOrderType):
+
+    EXCHANGE = ExchangeType.BETFAIR
+    ORDER_TYPE = OrderTypes.LIMIT
+
+    def __init__(
+        self,
+        price: float,
+        size: float,
+        persistence_type: str = "LAPSE",
+        time_in_force: str = None,
+        min_fill_size: float = None,
+        bet_target_type: str = None,
+        bet_target_size: float = None,
+    ):
+        self.price = price
+        self.size = size
+        self.persistence_type = persistence_type
+        self.time_in_force = time_in_force
+        self.min_fill_size = min_fill_size
+        self.bet_target_type = bet_target_type
+        self.bet_target_size = bet_target_size
+
+    def place_instruction(self) -> dict:
+        return limit_order(
+            size=self.size,
+            price=self.price,
+            persistence_type=self.persistence_type,
+            time_in_force=self.time_in_force,
+            min_fill_size=self.min_fill_size,
+            bet_target_type=self.bet_target_type,
+            bet_target_size=self.bet_target_size,
+        )
+
+
+class LimitOnCloseOrder(BaseOrderType):
+
+    EXCHANGE = ExchangeType.BETFAIR
+    ORDER_TYPE = OrderTypes.LIMIT_ON_CLOSE
+
+    def __init__(self, liability: float, price: float):
+        self.liability = liability
+        self.price = price
+
+    def place_instruction(self) -> dict:
+        return limit_on_close_order(liability=self.liability, price=self.price)
+
+
+class MarketOnCloseOrder(BaseOrderType):
+
+    EXCHANGE = ExchangeType.BETFAIR
+    ORDER_TYPE = OrderTypes.MARKET_ON_CLOSE
+
+    def __init__(self, liability: float):
+        self.liability = liability
+
+    def place_instruction(self) -> dict:
+        return market_on_close_order(liability=self.liability)
diff --git a/flumine/order/process.py b/flumine/order/process.py
new file mode 100644
index 0000000..d295fe4
--- /dev/null
+++ b/flumine/order/process.py
@@ -0,0 +1,75 @@
+import logging
+
+from ..markets.markets import Markets
+from ..strategy.strategy import Strategies
+from ..order.trade import Trade
+from ..order.order import BaseOrder, OrderStatus
+
+logger = logging.getLogger(__name__)
+
+"""
+Handles trade fillkill / green etc.
+Handles orphan orders by creating empty trade and order data from CurrentOrder object/
+"""
+# todo handle fillkill/green/
+
+
+def process_current_orders(markets: Markets, strategies: Strategies, event):
+    for current_orders in event.event:
+        for current_order in current_orders.orders:
+            strategy_name_hash, order_id = current_order.customer_order_ref.split("-")
+            order = markets.get_order(
+                market_id=current_order.market_id, order_id=order_id,
+            )
+            if not order:
+                logger.warning(
+                    "Order %s not present in blotter" % current_order.bet_id,
+                    extra={
+                        "bet_id": current_order.bet_id,
+                        "market_id": current_order.market_id,
+                        "customer_strategy_ref": current_order.customer_strategy_ref,
+                        "customer_order_ref": current_order.customer_order_ref,
+                    },
+                )
+                order = create_order_from_current(markets, strategies, current_order)
+                if order:
+                    logger.info(
+                        "Order %s added to blotter" % current_order.bet_id,
+                        extra=order.info,
+                    )
+                    order.executable()  # todo correct?
+                else:
+                    continue
+            if order.bet_id != current_order.bet_id:  # replaceOrder handling (hacky)
+                order = markets.get_order_from_bet_id(
+                    market_id=current_order.market_id, bet_id=current_order.bet_id,
+                )
+
+            if order:
+                order.update_current_order(current_order)
+                process_current_order(order)
+
+
+def process_current_order(order: BaseOrder):
+    if order.status == OrderStatus.EXECUTABLE:
+        pass
+        # print(order.status, order.status_log)
+
+
+def create_order_from_current(markets: Markets, strategies: Strategies, current_order):
+    strategy_name_hash, order_id = current_order.customer_order_ref.split("-")
+    # get market
+    market = markets.markets.get(current_order.market_id)
+    if market is None:
+        # todo log
+        return
+    # get strategy
+    strategy = strategies.hashes.get(strategy_name_hash)
+    if strategy is None:
+        # todo log
+        return
+    # add trade/order
+    trade = Trade(market.market_id, current_order.selection_id, strategy)
+    order = trade.create_order_from_current(current_order, order_id)
+    market.blotter[order.id] = order
+    return order
diff --git a/flumine/order/responses.py b/flumine/order/responses.py
new file mode 100644
index 0000000..56b5e37
--- /dev/null
+++ b/flumine/order/responses.py
@@ -0,0 +1,27 @@
+import datetime
+
+
+class Responses:
+    """Order responses"""
+
+    def __init__(self):
+        self.date_time_created = datetime.datetime.utcnow()
+        self.place_response = None
+        self.cancel_responses = []
+        self.replace_responses = []
+        self.update_responses = []
+        self.date_time_placed = None
+
+    def placed(self, response):
+        self.place_response = response
+        self.date_time_placed = datetime.datetime.utcnow()
+
+    def cancelled(self, response):
+        self.cancel_responses.append(response)
+
+    def replaced(self, response):
+        self.replace_responses.append(response)
+        # self.date_time_placed = datetime.datetime.utcnow()  # todo?
+
+    def updated(self, response):
+        self.update_responses.append(response)
diff --git a/flumine/order/trade.py b/flumine/order/trade.py
new file mode 100644
index 0000000..d943834
--- /dev/null
+++ b/flumine/order/trade.py
@@ -0,0 +1,91 @@
+import uuid
+from typing import Union, Type
+from betfairlightweight.resources.bettingresources import CurrentOrder
+
+from ..strategy.strategy import BaseStrategy
+from .order import BetfairOrder
+from .ordertype import LimitOrder, LimitOnCloseOrder, MarketOnCloseOrder
+from ..exceptions import OrderError
+
+
+class Trade:
+    def __init__(
+        self,
+        market_id: str,
+        selection_id: int,
+        strategy: BaseStrategy,
+        fill_kill=None,
+        offset=None,
+        green=None,
+        stop=None,
+    ):
+        self.id = uuid.uuid1()
+        self.market_id = market_id
+        self.selection_id = selection_id
+        self.strategy = strategy
+        self.fill_kill = fill_kill
+        self.offset = offset
+        self.green = green
+        self.stop = stop
+        self.orders = []  # all orders linked to trade
+        self.status = None  # todo? OPEN/COMPLETE
+
+    def create_order(
+        self,
+        side: str,
+        order_type: Union[LimitOrder, LimitOnCloseOrder, MarketOnCloseOrder],
+        handicap: float = 0,
+        order: Type[BetfairOrder] = BetfairOrder,
+    ) -> BetfairOrder:
+        if order_type.EXCHANGE != order.EXCHANGE:
+            raise OrderError(
+                "Incorrect order/order_type exchange combination for trade.create_order"
+            )
+        order = order(trade=self, side=side, order_type=order_type, handicap=handicap)
+        self.orders.append(order)
+        return order
+
+    def create_order_replacement(self, order: BetfairOrder) -> BetfairOrder:
+        """Create new order due to replace
+        execution"""
+        order_type = LimitOrder(
+            price=order._update["new_price"],
+            size=order.order_type.size,
+            persistence_type=order.order_type.persistence_type,
+        )
+        order = BetfairOrder(
+            trade=self, side=order.side, order_type=order_type, handicap=order.handicap
+        )
+        self.orders.append(order)
+        return order
+
+    def create_order_from_current(
+        self, current_order: CurrentOrder, order_id: str
+    ) -> BetfairOrder:
+        if current_order.order_type == "LIMIT":
+            order_type = LimitOrder(
+                current_order.price_size.price,
+                current_order.price_size.size,
+                current_order.persistence_type,
+            )
+        else:
+            raise NotImplementedError
+        order = BetfairOrder(
+            trade=self,
+            side=current_order.side,
+            order_type=order_type,
+            handicap=current_order.handicap,
+        )
+        order.bet_id = current_order.bet_id
+        order.id = order_id
+        self.orders.append(order)
+        return order
+
+    @property
+    def info(self) -> dict:
+        return {
+            "id": self.id,
+            "strategy": self.strategy,
+            "status": self.status,
+            "orders": [o.id for o in self.orders],
+        }
diff --git a/flumine/strategy/runnercontext.py b/flumine/strategy/runnercontext.py
new file mode 100644
index 0000000..d230cf5
--- /dev/null
+++ b/flumine/strategy/runnercontext.py
@@ -0,0 +1,40 @@
+import datetime
+from typing import Optional
+
+
+class RunnerContext:
+    """Runner context at strategy level"""
+
+    def __init__(self, selection_id: int):
+        self.selection_id = selection_id
+        self.invested = False
+        self.datetime_last_placed = None
+        self.datetime_last_reset = None
+        self.trade_count = 0
+        self.live_trade_count = 0
+
+    def place(self) -> None:
+        self.invested = True
+        self.datetime_last_placed = datetime.datetime.utcnow()
+        self.trade_count += 1
+        self.live_trade_count += 1
+
+    def reset(self) -> None:
+        if self.invested:
+            self.invested = False
+            self.datetime_last_reset = datetime.datetime.utcnow()
+            self.live_trade_count -= 1
+
+    @property
+    def placed_elapsed_seconds(self) -> Optional[float]:
+        if self.datetime_last_placed:
+            return (
+                datetime.datetime.utcnow() - self.datetime_last_placed
+            ).total_seconds()
+
+    @property
+    def reset_elapsed_seconds(self) -> Optional[float]:
+        if self.datetime_last_reset:
+            return (
+                datetime.datetime.utcnow() - self.datetime_last_reset
+            ).total_seconds()
diff --git a/flumine/strategy/strategy.py b/flumine/strategy/strategy.py
index 3e33536..6181c63 100644
--- a/flumine/strategy/strategy.py
+++ b/flumine/strategy/strategy.py
@@ -1,9 +1,11 @@
 from typing import Type, Iterator
 from betfairlightweight import filters
-from betfairlightweight.resources import MarketBook, RaceCard, CurrentOrders
+from betfairlightweight.resources import MarketBook, RaceCard
 
 from ..streams.marketstream import BaseStream, MarketStream
 from ..markets.market import Market
+from .runnercontext import RunnerContext
+from ..utils import create_cheap_hash
 
 DEFAULT_MARKET_DATA_FILTER = filters.streaming_market_data_filter(
     fields=[
@@ -28,6 +30,8 @@ class BaseStrategy:
         stream_class: Type[BaseStream] = MarketStream,
         name: str = None,
         context: dict = None,
+        max_selection_exposure: float = 100,
+        max_order_exposure: float = 10,
     ):
         """
         Processes data from streams.
@@ -39,6 +43,8 @@ class BaseStrategy:
         :param stream_class: Can be Market or Data
         :param name: Strategy name
         :param context: Dictionary holding additional vars
+        :param max_selection_exposure: Max exposure per selection
+        :param max_order_exposure: Max exposure per order
         """
         self.market_filter = market_filter
         self.market_data_filter = market_data_filter or DEFAULT_MARKET_DATA_FILTER
@@ -47,7 +53,10 @@ class BaseStrategy:
         self.stream_class = stream_class
         self._name = name
         self.context = context
+        self.max_selection_exposure = max_selection_exposure
+        self.max_order_exposure = max_order_exposure
 
+        self._invested = {}  # {marketId: {selectionId: RunnerContext}}
         self.streams = []  # list of streams strategy is subscribed
 
     def check_market(self, market: Market, market_book: MarketBook) -> bool:
@@ -82,20 +91,51 @@ class BaseStrategy:
         # process raceCard object
         return
 
-    def process_orders(self, orders: CurrentOrders) -> None:
-        # process currentOrders object
+    def process_orders(self, market: Market, orders: list) -> None:
+        # process list of Order objects for strategy and Market
         return
 
     def finish(self) -> None:
         # called before flumine ends
         return
 
+    # order
+    def place_order(self, market: Market, order) -> None:
+        # get context
+        market_context = self._invested.get(order.market_id)
+        if market_context is None:
+            self._invested[order.market_id] = market_context = {}
+        runner_context = market_context.get(order.selection_id)
+        if runner_context is None:
+            market_context[order.selection_id] = runner_context = RunnerContext(
+                order.selection_id
+            )
+        if self.validate_order(runner_context, order):
+            runner_context.place()
+            market.place_order(order)
+
+    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 validate_order(self, runner_context: RunnerContext, order) -> bool:
+        # todo multi/count
+        if runner_context.invested:
+            return False
+        else:
+            return True
+
     @property
     def stream_ids(self) -> list:
         return [stream.stream_id for stream in self.streams]
 
     @property
-    def info(self):
+    def info(self) -> dict:
         return {
             "name": self.name,
             "market_filter": self.market_filter,
@@ -104,12 +144,17 @@ class BaseStrategy:
             "conflate_ms": self.conflate_ms,
             "stream_ids": self.stream_ids,
             "context": self.context,
+            "name_hash": self.name_hash,
         }
 
     @property
-    def name(self):
+    def name(self) -> str:
         return self._name or self.__class__.__name__
 
+    @property
+    def name_hash(self) -> str:
+        return create_cheap_hash(self.name, 13)
+
     def __str__(self):
         return "{0}".format(self.name)
 
@@ -118,7 +163,7 @@ class Strategies:
     def __init__(self):
         self._strategies = []
 
-    def __call__(self, strategy):
+    def __call__(self, strategy: BaseStrategy) -> None:
         self._strategies.append(strategy)
         strategy.add()
 
@@ -126,6 +171,10 @@ class Strategies:
         for s in self:
             s.start()
 
+    @property
+    def hashes(self) -> dict:
+        return {strategy.name_hash: strategy for strategy in self}
+
     def __iter__(self) -> Iterator[BaseStrategy]:
         return iter(self._strategies)
 
diff --git a/flumine/streams/basestream.py b/flumine/streams/basestream.py
index 0067b50..fe2b179 100644
--- a/flumine/streams/basestream.py
+++ b/flumine/streams/basestream.py
@@ -3,10 +3,6 @@ import queue
 import logging
 import betfairlightweight
 from betfairlightweight import StreamListener
-from betfairlightweight import BetfairError
-from tenacity import retry, wait_exponential
-
-from ..event.event import MarketBookEvent
 
 logger = logging.getLogger(__name__)
 
@@ -20,10 +16,10 @@ class BaseStream(threading.Thread):
         self,
         flumine,
         stream_id: int,
-        market_filter: dict,
-        market_data_filter: dict,
         streaming_timeout: float,  # snaps listener if no update
         conflate_ms: int,
+        market_filter: dict = None,
+        market_data_filter: dict = None,
     ):
         threading.Thread.__init__(self, daemon=True, name=self.__class__.__name__)
         self.flumine = flumine
diff --git a/flumine/streams/marketstream.py b/flumine/streams/marketstream.py
index e0a3bf7..056124f 100644
--- a/flumine/streams/marketstream.py
+++ b/flumine/streams/marketstream.py
@@ -1,6 +1,5 @@
 import queue
 import logging
-from betfairlightweight import StreamListener
 from betfairlightweight import BetfairError
 from tenacity import retry, wait_exponential
 
@@ -49,7 +48,7 @@ class MarketStream(BaseStream):
                 )
             except queue.Empty:
                 market_books = self._listener.snap(
-                    market_ids=self.flumine.live_markets.open_markets()
+                    market_ids=self.flumine.markets.open_market_ids
                 )
             self.flumine.handler_queue.put(MarketBookEvent(market_books))
 
diff --git a/flumine/streams/orderstream.py b/flumine/streams/orderstream.py
new file mode 100644
index 0000000..41a13b5
--- /dev/null
+++ b/flumine/streams/orderstream.py
@@ -0,0 +1,61 @@
+import queue
+import logging
+from betfairlightweight import BetfairError, filters
+from tenacity import retry, wait_exponential
+
+from .basestream import BaseStream
+from ..event.event import CurrentOrdersEvent
+from .. import config
+
+logger = logging.getLogger(__name__)
+
+
+class OrderStream(BaseStream):
+    @retry(wait=wait_exponential(multiplier=1, min=2, max=20))
+    def run(self) -> None:
+        logger.info("Starting OrderStream")
+
+        if not self._output_thread.is_alive():
+            logger.info("Starting output_thread {0}".format(self._output_thread))
+            self._output_thread.start()
+
+        self._stream = self.betting_client.streaming.create_stream(
+            unique_id=self.stream_id, listener=self._listener
+        )
+        try:
+            self.stream_id = self._stream.subscribe_to_orders(
+                order_filter=filters.streaming_order_filter(
+                    customer_strategy_refs=[config.hostname],
+                    partition_matched_by_strategy_ref=True,
+                    include_overall_position=False,
+                ),
+                conflate_ms=self.conflate_ms,
+            )
+            self._stream.start()
+        except BetfairError:
+            logger.error("MarketStream run error", exc_info=True)
+            raise
+        except Exception:
+            logger.critical("MarketStream run error", exc_info=True)
+            raise
+        logger.info("Stopped MarketStream {0}".format(self.stream_id))
+
+    def handle_output(self) -> None:
+        """Handles output from stream.
+        """
+        while self.is_alive():
+            try:
+                order_books = self._output_queue.get(
+                    block=True, timeout=self.streaming_timeout
+                )
+            except queue.Empty:
+                if self.flumine.markets.live_orders:
+                    order_books = self._listener.snap(
+                        market_ids=self.flumine.markets.open_market_ids
+                    )
+                else:
+                    continue
+
+            self.flumine.handler_queue.put(CurrentOrdersEvent(order_books))
+
+        logger.info("Stopped output_thread (OrderStream {0})".format(self.stream_id))
diff --git a/flumine/streams/streams.py b/flumine/streams/streams.py
index 146975c..6c7c16f 100644
--- a/flumine/streams/streams.py
+++ b/flumine/streams/streams.py
@@ -5,6 +5,7 @@ from ..strategy.strategy import BaseStrategy
 from .marketstream import MarketStream
 from .datastream import DataStream
 from .historicalstream import HistoricalStream
+from .orderstream import OrderStream
 
 logger = logging.getLogger(__name__)
 
@@ -28,6 +29,8 @@ class Streams:
             stream = self.add_stream(strategy)
             strategy.streams.append(stream)
 
+    """ market data """
+
     def add_stream(self, strategy: BaseStrategy) -> Union[MarketStream, DataStream]:
         for stream in self:  # check if market stream already exists
             if (
@@ -61,7 +64,7 @@ class Streams:
             self._streams.append(stream)
             return stream
 
-    def add_historical_stream(self, strategy, market):
+    def add_historical_stream(self, strategy: BaseStrategy, market) -> HistoricalStream:
         for stream in self:
             if stream.market_filter == market:
                 return stream
@@ -83,6 +86,21 @@ class Streams:
             self._streams.append(stream)
             return stream
 
+    """ order data """
+
+    def add_order_stream(
+        self, conflate_ms: int = None, streaming_timeout: float = 0.25
+    ) -> OrderStream:
+        stream_id = self._increment_stream_id()
+        stream = OrderStream(
+            flumine=self.flumine,
+            stream_id=stream_id,
+            conflate_ms=conflate_ms,
+            streaming_timeout=streaming_timeout,
+        )
+        self._streams.append(stream)
+        return stream
+
     def start(self) -> None:
         if not self.flumine.BACKTEST:
             logger.info("Starting streams..")
diff --git a/flumine/utils.py b/flumine/utils.py
index d7ec67c..5a371fc 100644
--- a/flumine/utils.py
+++ b/flumine/utils.py
@@ -1,8 +1,25 @@
 import uuid
 import logging
+import hashlib
+from decimal import Decimal
 
 logger = logging.getLogger(__name__)
 
+CUTOFFS = (
+    (2, 100),
+    (3, 50),
+    (4, 20),
+    (6, 10),
+    (10, 5),
+    (20, 2),
+    (30, 1),
+    (50, 0.5),
+    (100, 0.2),
+    (1000, 0.1),
+)
+MIN_PRICE = 1.01
+MAX_PRICE = 1000
+
 
 def create_short_uuid() -> str:
     return str(uuid.uuid4())[:8]
@@ -15,6 +32,50 @@ def file_line_count(file_path: str) -> int:
     return i + 1
 
 
-def chunks(l, n):
+def chunks(l: list, n: int) -> list:
     for i in range(0, len(l), n):
         yield l[i : i + n]
+
+
+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.
+    # https://stackoverflow.com/questions/14023350
+    hash_ = hashlib.sha1()
+    hash_.update(txt.encode())
+    return hash_.hexdigest()[:length]
+
+
+def as_dec(value):
+    return Decimal(str(value))
+
+
+def arange(start, stop, step):
+    while start < stop:
+        yield start
+        start += step
+
+
+def make_prices(min_price, cutoffs):
+    prices = []
+    cursor = as_dec(min_price)
+    for cutoff, step in cutoffs:
+        prices.extend(arange(as_dec(cursor), as_dec(cutoff), as_dec(1 / step)))
+        cursor = cutoff
+    prices.append(as_dec(MAX_PRICE))
+    return prices
+
+
+PRICES = make_prices(MIN_PRICE, CUTOFFS)
+
+
+def calculate_exposure(mb: list, ml: list) -> int:
+    """Calculates exposure based on list
+    of (price, size)
+    """
+    back_exp = sum(-i[1] for i in mb)
+    back_profit = sum(i[0] * i[1] for i in mb)
+    lay_exp = sum((i[0] - 1) * -i[1] for i in ml)
+    if lay_exp:
+        lay_exp += back_profit
+    return min(round(back_exp + lay_exp, 2), 0)  # returns negative int
diff --git a/flumine/worker.py b/flumine/worker.py
index 1a136f3..aac7842 100644
--- a/flumine/worker.py
+++ b/flumine/worker.py
@@ -30,23 +30,47 @@ class BackgroundWorker(threading.Thread):
     def run(self) -> None:
         time.sleep(self.start_delay)
         while self.is_alive():
+            logger.debug(
+                "BackgroundWorker {0} executing".format(self.name),
+                extra={"name": self.name, "function": self.function},
+            )
             try:
                 self.function(*self.func_args, **self.func_kwargs)
             except Exception as e:
-                logger.error("Error in BackgroundWorker {0}: {1}".format(self.name, e))
+                logger.error(
+                    "Error in BackgroundWorker {0}: {1}".format(self.name, e),
+                    extra={"worker_name": self.name, "function": self.function},
+                )
             time.sleep(self.interval)
 
 
 def keep_alive(client) -> None:
-    logger.info("Trading client keep_alive worker executing", extra={"client": client})
-    if client.betting_client.session_token is None:
+    """ Attempt keep alive if required or
+    login if keep alive failed
+    """
+    if client.betting_client.session_token:
+        try:
+            resp = client.keep_alive()
+            if resp is None or resp.status == "SUCCESS":
+                return
+        except BetfairError as e:
+            logger.error(
+                "keep_alive error",
+                exc_info=True,
+                extra={"trading_function": "keep_alive", "response": e},
+            )
+    # attempt login
+    try:
         client.login()
-    else:
-        client.keep_alive()
+    except BetfairError as e:
+        logger.error(
+            "login error",
+            exc_info=True,
+            extra={"trading_function": "login", "response": e},
+        )
 
 
 def poll_market_catalogue(client, markets, handler_queue: queue.Queue) -> None:
-    logger.info("Market Catalogue polling worker executing", extra={"client": client})
     live_markets = list(markets.markets.keys())
     for market_ids in chunks(live_markets, 100):
         try:
diff --git a/requirements.txt b/requirements.txt
index d059c36..0dd5d6b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
 betfairlightweight==2.3.0
 tenacity==5.0.3
 python-json-logger==0.1.11
+requests


Test patch:
diff --git a/flumine/clients/backtestclient.py b/flumine/clients/backtestclient.py
index e2add13..39ae30f 100644
--- a/flumine/clients/backtestclient.py
+++ b/flumine/clients/backtestclient.py
@@ -9,7 +9,7 @@ class BacktestClient(BaseClient):
     Backtest betting client.
     """
 
-    EXCHANGE = ExchangeType.BACKTEST
+    EXCHANGE = ExchangeType.SIMULATED
     DISCOUNT_RATE = 0
     CURRENCY_CODE = "GBP"
 
diff --git a/tests/test_baseflumine.py b/tests/test_baseflumine.py
index f806f5b..24d24af 100644
--- a/tests/test_baseflumine.py
+++ b/tests/test_baseflumine.py
@@ -14,7 +14,7 @@ class BaseFlumineTest(unittest.TestCase):
         self.assertEqual(self.base_flumine.client, self.mock_client)
         self.assertFalse(self.base_flumine._running)
         self.assertEqual(self.base_flumine._logging_controls, [])
-        self.assertEqual(self.base_flumine._trading_controls, [])
+        self.assertEqual(len(self.base_flumine._trading_controls), 2)
         self.assertEqual(self.base_flumine._workers, [])
 
     def test_run(self):
@@ -32,6 +32,20 @@ class BaseFlumineTest(unittest.TestCase):
         self.base_flumine.add_worker(mock_worker)
         self.assertEqual(len(self.base_flumine._workers), 1)
 
+    def test_add_client_control(self):
+        self.mock_client.trading_controls = []
+        mock_control = mock.Mock()
+        self.base_flumine.add_client_control(mock_control)
+        self.assertEqual(
+            self.base_flumine.client.trading_controls,
+            [mock_control(self.base_flumine, self.mock_client)],
+        )
+
+    def test_add_trading_control(self):
+        mock_control = mock.Mock()
+        self.base_flumine.add_trading_control(mock_control)
+        self.assertEqual(len(self.base_flumine._trading_controls), 3)
+
     def test__add_default_workers(self):
         self.base_flumine._add_default_workers()
         self.assertEqual(len(self.base_flumine._workers), 0)
@@ -42,6 +56,53 @@ class BaseFlumineTest(unittest.TestCase):
         mock_event.event = [mock_market_book]
         self.base_flumine._process_market_books(mock_event)
 
+    def test__process_market_orders(self):
+        mock_market = mock.Mock()
+        mock_market.blotter.process_orders.return_value = [1, 2, 3]
+        self.base_flumine._process_market_orders(mock_market)
+        mock_market.blotter.process_orders.assert_called_with(self.mock_client)
+
+    def test__process_order_package(self):
+        mock_trading_control = mock.Mock()
+        self.base_flumine._trading_controls = [mock_trading_control]
+        mock_client_trading_control = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.market_id = "1.123"
+        mock_order_package.client.trading_controls = [mock_client_trading_control]
+        self.base_flumine._process_order_package(mock_order_package)
+        mock_order_package.client.execution.handler.assert_called_with(
+            mock_order_package
+        )
+        mock_trading_control.assert_called_with(mock_order_package)
+        mock_client_trading_control.assert_called_with(mock_order_package)
+
+    def test__process_order_package_empty(self):
+        mock_client_trading_control = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.market_id = "1.123"
+        mock_order_package.client.trading_controls = [mock_client_trading_control]
+        mock_order_package.info = {}
+        mock_order_package.orders = []
+        self.base_flumine._trading_controls = []
+        self.base_flumine._process_order_package(mock_order_package)
+        mock_order_package.client.execution.handler.assert_not_called()
+
+    def test__process_order_package_controls(self):
+        mock_trading_control = mock.Mock()
+        mock_client_control = mock.Mock()
+        self.base_flumine._trading_controls = [mock_trading_control]
+        mock_client_trading_control = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.market_id = "1.123"
+        mock_order_package.client.trading_controls = [mock_client_trading_control]
+        mock_order_package.info = {}
+        mock_order_package.orders = []
+        mock_order_package.client.trading_controls = [mock_client_control]
+        self.base_flumine._process_order_package(mock_order_package)
+        mock_order_package.client.execution.handler.assert_not_called()
+        mock_trading_control.assert_called_with(mock_order_package)
+        mock_client_control.assert_called_with(mock_order_package)
+
     @mock.patch("flumine.baseflumine.Market")
     def test__add_live_market(self, mock_market):
         mock_market_book = mock.Mock()
@@ -55,6 +116,13 @@ class BaseFlumineTest(unittest.TestCase):
         mock_event.event = (12, 12345, {})
         self.base_flumine._process_raw_data(mock_event)
 
+    def test__process_current_orders(self):
+        mock_event = mock.Mock()
+        mock_current_orders = mock.Mock()
+        mock_current_orders.orders = []
+        mock_event.event = [mock_current_orders]
+        self.base_flumine._process_current_orders(mock_event)
+
     def test__process_end_flumine(self):
         self.base_flumine._process_end_flumine()
 
diff --git a/tests/test_blotter.py b/tests/test_blotter.py
new file mode 100644
index 0000000..9b38d6a
--- /dev/null
+++ b/tests/test_blotter.py
@@ -0,0 +1,112 @@
+import unittest
+from unittest import mock
+
+from flumine.markets.blotter import Blotter, OrderStatus, OrderPackageType
+
+
+class BlotterTest(unittest.TestCase):
+    def setUp(self) -> None:
+        self.blotter = Blotter("1.234")
+
+    def test_init(self):
+        self.assertEqual(self.blotter.market_id, "1.234")
+        self.assertEqual(self.blotter._orders, {})
+        self.assertEqual(self.blotter.pending_place, [])
+        self.assertEqual(self.blotter.pending_cancel, [])
+        self.assertEqual(self.blotter.pending_update, [])
+        self.assertEqual(self.blotter.pending_replace, [])
+
+    def test_strategy_orders(self):
+        mock_order = mock.Mock()
+        mock_order.trade.strategy = 69
+        self.blotter._orders = {"12345": mock_order}
+        self.assertEqual(self.blotter.strategy_orders(12), [])
+        self.assertEqual(self.blotter.strategy_orders(69), [mock_order])
+
+    @mock.patch("flumine.markets.blotter.Blotter._create_packages")
+    def test_process_orders(self, mock__create_packages):
+        mock_client = mock.Mock()
+        self.blotter.pending_place = [1]
+        self.assertEqual(
+            self.blotter.process_orders(mock_client), mock__create_packages().__radd__()
+        )
+        self.blotter.pending_place = []
+        self.blotter.pending_cancel = [2]
+        self.assertEqual(
+            self.blotter.process_orders(mock_client), mock__create_packages().__radd__()
+        )
+        self.blotter.pending_cancel = []
+        self.blotter.pending_update = [3]
+        self.assertEqual(
+            self.blotter.process_orders(mock_client), mock__create_packages().__radd__()
+        )
+        self.blotter.pending_update = []
+        self.blotter.pending_replace = [4]
+        self.assertEqual(
+            self.blotter.process_orders(mock_client), mock__create_packages().__radd__()
+        )
+
+    @mock.patch("flumine.markets.blotter.BetfairOrderPackage")
+    def test___create_packages(self, mock_cls):
+        mock_client = mock.Mock()
+        mock_order = mock.Mock()
+        mock_orders = [mock_order]
+        packages = self.blotter._create_packages(
+            mock_client, mock_orders, OrderPackageType.PLACE
+        )
+        self.assertEqual(
+            packages,
+            [
+                mock_cls(
+                    client=None,
+                    market_id=self.blotter.market_id,
+                    orders=mock_orders,
+                    package_type=OrderPackageType.PLACE,
+                )
+            ],
+        )
+        self.assertEqual(mock_orders, [])
+
+    def test_live_orders(self):
+        self.assertFalse(self.blotter.live_orders)
+        mock_order = mock.Mock()
+        mock_order.status = OrderStatus.EXECUTABLE
+        self.blotter._orders = {"12345": mock_order}
+        self.assertTrue(self.blotter.live_orders)
+
+    def test_selection_exposure(self):
+        mock_strategy = mock.Mock()
+        mock_trade = mock.Mock(strategy=mock_strategy)
+        mock_order = mock.Mock(
+            trade=mock_trade,
+            lookup=(self.blotter.market_id, 123, 0),
+            side="BACK",
+            average_price_matched=5.6,
+            size_matched=2.0,
+        )
+        self.blotter._orders = {"12345": mock_order}
+
+        self.assertEqual(
+            self.blotter.selection_exposure(
+                mock_strategy, (self.blotter.market_id, 123, 0)
+            ),
+            -2,
+        )
+
+    def test__contains(self):
+        self.blotter._orders = {"123": "test"}
+        self.assertIn("123", self.blotter)
+        self.assertNotIn("321", self.blotter)
+
+    def test__setitem(self):
+        self.blotter["123"] = "test"
+        self.assertEqual(self.blotter._orders, {"123": "test"})
+
+    def test__getitem(self):
+        self.blotter._orders = {"12345": "test", "54321": "test2"}
+        self.assertEqual(self.blotter["12345"], "test")
+        self.assertEqual(self.blotter["54321"], "test2")
+
+    def test__len(self):
+        self.blotter._orders = {"12345": "test", "54321": "test"}
+        self.assertEqual(len(self.blotter), 2)
diff --git a/tests/test_clientcontrols.py b/tests/test_clientcontrols.py
new file mode 100644
index 0000000..ad7c81d
--- /dev/null
+++ b/tests/test_clientcontrols.py
@@ -0,0 +1,157 @@
+import unittest
+import datetime
+from unittest import mock
+
+from flumine.controls.clientcontrols import (
+    BaseControl,
+    MaxOrderCount,
+    OrderPackageType,
+)
+
+
+class TestBaseControl(unittest.TestCase):
+    def setUp(self):
+        self.mock_flumine = mock.Mock()
+        self.mock_client = mock.Mock()
+        self.control = BaseControl(self.mock_flumine, self.mock_client)
+
+    def test_init(self):
+        self.assertEqual(self.control.flumine, self.mock_flumine)
+        self.assertIsNone(self.control.NAME)
+
+    @mock.patch("flumine.controls.BaseControl._validate")
+    def test_call(self, mock_validate):
+        order_package = mock.Mock()
+        self.control(order_package)
+
+        mock_validate.assert_called_with(order_package)
+
+    def test_validate(self):
+        with self.assertRaises(NotImplementedError):
+            self.control._validate(None)
+
+    # @mock.patch("flumine.controls.BaseControl.strategy_reset")
+    def test_on_error(self):
+        order = mock.Mock()
+        # order.flumine_order_type = "initial"
+        order.info = {"hello": "world"}
+        self.control._on_error(order)
+        order.violation.assert_called_with()
+        # mock_strategy_reset.assert_called_with(order)
+
+    # @mock.patch("flumine.controls.BaseControl.strategy_reset")
+    def test_on_error_offset(self):
+        order = mock.Mock()
+        # order.flumine_order_type = "offset"
+        order.info = {"hello": "world"}
+        self.control._on_error(order)
+        order.violation.assert_called_with()
+        # self.assertFalse(mock_strategy_reset.called)
+
+
+class TestMaxOrderCount(unittest.TestCase):
+    def setUp(self):
+        self.mock_client = mock.Mock()
+        self.mock_client.transaction_limit = 1000
+        self.mock_client.chargeable_transaction_count = 0
+        self.mock_flumine = mock.Mock()
+        self.trading_control = MaxOrderCount(self.mock_flumine, self.mock_client)
+
+    @mock.patch("flumine.controls.clientcontrols.MaxOrderCount._set_next_hour")
+    def test_init(self, mock_set_next_hour):
+        self.assertEqual(self.trading_control.client, self.mock_client)
+        self.assertEqual(self.trading_control.NAME, "MAX_ORDER_COUNT")
+        self.assertEqual(self.trading_control.total, 0)
+        self.assertEqual(self.trading_control.place_requests, 0)
+        self.assertEqual(self.trading_control.update_requests, 0)
+        self.assertEqual(self.trading_control.cancel_requests, 0)
+        self.assertEqual(self.trading_control.replace_requests, 0)
+
+        self.assertIsNotNone(self.trading_control._next_hour)
+        self.assertEqual(self.trading_control.transaction_count, 0)
+        self.assertEqual(self.trading_control.transaction_limit, 1000)
+
+    @mock.patch(
+        "flumine.controls.clientcontrols.MaxOrderCount._check_transaction_count"
+    )
+    @mock.patch("flumine.controls.clientcontrols.MaxOrderCount._check_hour")
+    def test_validate_place(self, mock_check_hour, mock__check_transaction_count):
+        mock_package = mock.Mock()
+        mock_package.package_type = OrderPackageType.PLACE
+        mock_package.__len__ = mock.Mock()
+        mock_package.__len__.return_value = 1
+        self.trading_control._validate(mock_package)
+
+        mock_check_hour.assert_called_with()
+        mock__check_transaction_count.assert_called_with(1)
+        self.assertEqual(self.trading_control.place_requests, 1)
+
+    @mock.patch("flumine.controls.clientcontrols.MaxOrderCount._check_hour")
+    def test_validate_cancel(self, mock_check_hour):
+        mock_package = mock.Mock()
+        mock_package.package_type = OrderPackageType.CANCEL
+        mock_package.__len__ = mock.Mock()
+        mock_package.__len__.return_value = 1
+        self.trading_control._validate(mock_package)
+
+        mock_check_hour.assert_called_with()
+        self.assertEqual(self.trading_control.cancel_requests, 1)
+
+    @mock.patch("flumine.controls.clientcontrols.MaxOrderCount._check_hour")
+    def test_validate_update(self, mock_check_hour):
+        mock_package = mock.Mock()
+        mock_package.package_type = OrderPackageType.UPDATE
+        mock_package.__len__ = mock.Mock()
+        mock_package.__len__.return_value = 1
+        self.trading_control._validate(mock_package)
+
+        mock_check_hour.assert_called_with()
+        self.assertEqual(self.trading_control.update_requests, 1)
+
+    @mock.patch("flumine.controls.clientcontrols.MaxOrderCount._check_hour")
+    def test_validate_replace(self, mock_check_hour):
+        mock_package = mock.Mock()
+        mock_package.package_type = OrderPackageType.REPLACE
+        mock_package.__len__ = mock.Mock()
+        mock_package.__len__.return_value = 1
+        self.trading_control._validate(mock_package)
+
+        mock_check_hour.assert_called_with()
+        self.assertEqual(self.trading_control.replace_requests, 1)
+
+    @mock.patch("flumine.controls.clientcontrols.MaxOrderCount._set_next_hour")
+    def test_check_hour(self, mock_set_next_hour):
+        self.trading_control._check_hour()
+
+        self.trading_control.transaction_count = 1069
+        now = datetime.datetime.now()
+        self.trading_control._next_hour = (now + datetime.timedelta(hours=-1)).replace(
+            minute=0, second=0, microsecond=0
+        )
+        self.trading_control._check_hour()
+        mock_set_next_hour.assert_called_with()
+        self.assertEqual(self.trading_control.transaction_count, 0)
+        self.assertEqual(self.mock_client.chargeable_transaction_count, 69)
+
+    def test_check_transaction_count(self):
+        self.trading_control._check_transaction_count(10)
+        self.assertEqual(self.trading_control.transaction_count, 10)
+
+    def test_set_next_hour(self):
+        self.trading_control._next_hour = None
+
+        self.trading_control._set_next_hour()
+        now = datetime.datetime.utcnow()
+        now_1 = (now + datetime.timedelta(hours=1)).replace(
+            minute=0, second=0, microsecond=0
+        )
+        self.assertEqual(self.trading_control._next_hour, now_1)
+
+    @mock.patch("flumine.controls.clientcontrols.MaxOrderCount._check_hour")
+    def test_safe(self, mock_check_hour):
+        self.assertTrue(self.trading_control.safe)
+        mock_check_hour.assert_called_with()
+
+        self.trading_control.transaction_limit = -10
+        self.assertIsNone(self.trading_control.safe)
+        mock_check_hour.assert_called_with()
diff --git a/tests/test_clients.py b/tests/test_clients.py
index a9fefe5..b17542e 100644
--- a/tests/test_clients.py
+++ b/tests/test_clients.py
@@ -44,6 +44,15 @@ class BaseClientTest(unittest.TestCase):
         with self.assertRaises(NotImplementedError):
             assert self.base_client.update_account_details()
 
+    def test_add_execution(self):
+        mock_flumine = mock.Mock()
+        self.base_client.EXCHANGE = ExchangeType.SIMULATED
+        self.base_client.add_execution(mock_flumine)
+        self.assertEqual(self.base_client.execution, mock_flumine.simulated_execution)
+        self.base_client.EXCHANGE = ExchangeType.BETFAIR
+        self.base_client.add_execution(mock_flumine)
+        self.assertEqual(self.base_client.execution, mock_flumine.betfair_execution)
+
 
 class BetfairClientTest(unittest.TestCase):
     def setUp(self):
diff --git a/tests/test_execution.py b/tests/test_execution.py
new file mode 100644
index 0000000..ad2bddc
--- /dev/null
+++ b/tests/test_execution.py
@@ -0,0 +1,582 @@
+import unittest
+from unittest import mock
+
+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):
+    def setUp(self) -> None:
+        self.mock_flumine = mock.Mock()
+        self.execution = BaseExecution(self.mock_flumine)
+
+    def test_init(self):
+        self.assertEqual(self.execution.flumine, self.mock_flumine)
+        self.assertIsNotNone(self.execution._thread_pool)
+        self.assertIsNone(self.execution.EXCHANGE)
+
+    @mock.patch("flumine.execution.baseexecution.requests")
+    @mock.patch("flumine.execution.baseexecution.BaseExecution.execute_place")
+    def test_handler_place(self, mock_execute_place, mock_requests):
+        mock_order_package = mock.Mock()
+        mock_order_package.package_type = OrderPackageType.PLACE
+        mock_thread_pool = mock.Mock()
+        self.execution._thread_pool = mock_thread_pool
+        self.execution.handler(mock_order_package)
+        mock_thread_pool.submit.assert_called_with(
+            mock_execute_place, mock_order_package, mock_requests.Session()
+        )
+
+    @mock.patch("flumine.execution.baseexecution.requests")
+    @mock.patch("flumine.execution.baseexecution.BaseExecution.execute_cancel")
+    def test_handler_cancel(self, mock_execute_cancel, mock_requests):
+        mock_order_package = mock.Mock()
+        mock_order_package.package_type = OrderPackageType.PLACE.CANCEL
+        mock_thread_pool = mock.Mock()
+        self.execution._thread_pool = mock_thread_pool
+        self.execution.handler(mock_order_package)
+        mock_thread_pool.submit.assert_called_with(
+            mock_execute_cancel, mock_order_package, mock_requests.Session()
+        )
+
+    @mock.patch("flumine.execution.baseexecution.requests")
+    @mock.patch("flumine.execution.baseexecution.BaseExecution.execute_replace")
+    def test_handler_replace(self, mock_execute_replace, mock_requests):
+        mock_order_package = mock.Mock()
+        mock_order_package.package_type = OrderPackageType.REPLACE
+        mock_thread_pool = mock.Mock()
+        self.execution._thread_pool = mock_thread_pool
+        self.execution.handler(mock_order_package)
+        mock_thread_pool.submit.assert_called_with(
+            mock_execute_replace, mock_order_package, mock_requests.Session()
+        )
+
+    @mock.patch("flumine.execution.baseexecution.requests")
+    @mock.patch("flumine.execution.baseexecution.BaseExecution.execute_update")
+    def test_handler_update(self, mock_execute_update, mock_requests):
+        mock_order_package = mock.Mock()
+        mock_order_package.package_type = OrderPackageType.UPDATE
+        mock_thread_pool = mock.Mock()
+        self.execution._thread_pool = mock_thread_pool
+        self.execution.handler(mock_order_package)
+        mock_thread_pool.submit.assert_called_with(
+            mock_execute_update, mock_order_package, mock_requests.Session()
+        )
+
+    def test_handler_unknown(self):
+        mock_order_package = mock.Mock()
+        mock_order_package.package_type = "DELETE"
+        with self.assertRaises(NotImplementedError):
+            self.execution.handler(mock_order_package)
+
+    def test_execute_place(self):
+        with self.assertRaises(NotImplementedError):
+            self.execution.execute_place(None, None)
+
+    def test_execute_cancel(self):
+        with self.assertRaises(NotImplementedError):
+            self.execution.execute_place(None, None)
+
+    def test_execute_update(self):
+        with self.assertRaises(NotImplementedError):
+            self.execution.execute_place(None, None)
+
+    def test_execute_replace(self):
+        with self.assertRaises(NotImplementedError):
+            self.execution.execute_place(None, None)
+
+    def test_handler_queue(self):
+        self.assertEqual(self.execution.handler_queue, self.mock_flumine.handler_queue)
+
+    def test_markets(self):
+        self.assertEqual(self.execution.markets, self.mock_flumine.markets)
+
+
+class BetfairExecutionTest(unittest.TestCase):
+    def setUp(self) -> None:
+        self.mock_flumine = mock.Mock()
+        self.execution = BetfairExecution(self.mock_flumine)
+
+    def test_init(self):
+        self.assertEqual(self.execution.EXCHANGE, ExchangeType.BETFAIR)
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.place")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_place_success(
+        self, mock__execution_helper, mock_place, mock__order_logger
+    ):
+        mock_session = mock.Mock()
+        mock_order = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.info = {}
+        mock_report = mock.Mock()
+        mock_instruction_report = mock.Mock()
+        mock_instruction_report.status = "SUCCESS"
+        mock_report.place_instruction_reports = [mock_instruction_report]
+        mock__execution_helper.return_value = mock_report
+        self.execution.execute_place(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_place, mock_order_package, mock_session
+        )
+        mock__order_logger.assert_called_with(
+            mock_order, mock_instruction_report, OrderPackageType.PLACE
+        )
+        mock_order.executable.assert_called_with()
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.place")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_place_failure(
+        self, mock__execution_helper, mock_place, mock__order_logger
+    ):
+        mock_session = mock.Mock()
+        mock_order = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.info = {}
+        mock_report = mock.Mock()
+        mock_instruction_report = mock.Mock()
+        mock_instruction_report.status = "FAILURE"
+        mock_report.place_instruction_reports = [mock_instruction_report]
+        mock__execution_helper.return_value = mock_report
+        self.execution.execute_place(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_place, mock_order_package, mock_session
+        )
+        mock__order_logger.assert_called_with(
+            mock_order, mock_instruction_report, OrderPackageType.PLACE
+        )
+        mock_order.lapsed.assert_called_with()
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.place")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_place_timeout(
+        self, mock__execution_helper, mock_place, mock__order_logger
+    ):
+        mock_session = mock.Mock()
+        mock_order = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.info = {}
+        mock_report = mock.Mock()
+        mock_instruction_report = mock.Mock()
+        mock_instruction_report.status = "TIMEOUT"
+        mock_report.place_instruction_reports = [mock_instruction_report]
+        mock__execution_helper.return_value = mock_report
+        self.execution.execute_place(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_place, mock_order_package, mock_session
+        )
+        mock__order_logger.assert_called_with(
+            mock_order, mock_instruction_report, OrderPackageType.PLACE
+        )
+        # mock_order.lapsed.assert_called_with()
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.place")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_place_error(self, mock__execution_helper, mock_place):
+        mock_session = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.info = {}
+        mock__execution_helper.return_value = None
+        self.execution.execute_place(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_place, mock_order_package, mock_session
+        )
+
+    def test_place(self):
+        mock_session = mock.Mock()
+        mock_order_package = mock.Mock()
+        self.assertEqual(
+            self.execution.place(mock_order_package, mock_session),
+            mock_order_package.client.betting_client.betting.place_orders(),
+        )
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.cancel")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_cancel_success(
+        self, mock__execution_helper, mock_cancel, mock__order_logger
+    ):
+        mock_session = mock.Mock()
+        mock_order = mock.Mock()
+        mock_order.bet_id = 123
+        mock_order.size_remaining = 2
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.info = {}
+        mock_report = mock.Mock()
+        mock_instruction_report = mock.Mock()
+        mock_instruction_report.status = "SUCCESS"
+        mock_instruction_report.instruction.bet_id = 123
+        mock_instruction_report.size_cancelled = 2
+        mock_report.cancel_instruction_reports = [mock_instruction_report]
+        mock__execution_helper.return_value = mock_report
+        self.execution.execute_cancel(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_cancel, mock_order_package, mock_session
+        )
+        mock__order_logger.assert_called_with(
+            mock_order, mock_instruction_report, OrderPackageType.CANCEL
+        )
+        mock_order.execution_complete.assert_called_with()
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.cancel")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_cancel_success_partial(
+        self, mock__execution_helper, mock_cancel, mock__order_logger
+    ):
+        mock_session = mock.Mock()
+        mock_order = mock.Mock()
+        mock_order.bet_id = 123
+        mock_order.size_remaining = 2
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.info = {}
+        mock_report = mock.Mock()
+        mock_instruction_report = mock.Mock()
+        mock_instruction_report.status = "SUCCESS"
+        mock_instruction_report.instruction.bet_id = 123
+        mock_instruction_report.size_cancelled = 1
+        mock_report.cancel_instruction_reports = [mock_instruction_report]
+        mock__execution_helper.return_value = mock_report
+        self.execution.execute_cancel(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_cancel, mock_order_package, mock_session
+        )
+        mock__order_logger.assert_called_with(
+            mock_order, mock_instruction_report, OrderPackageType.CANCEL
+        )
+        mock_order.executable.assert_called_with()
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.cancel")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_cancel_failure(
+        self, mock__execution_helper, mock_cancel, mock__order_logger
+    ):
+        mock_session = mock.Mock()
+        mock_order = mock.Mock()
+        mock_order.bet_id = 123
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.info = {}
+        mock_report = mock.Mock()
+        mock_instruction_report = mock.Mock()
+        mock_instruction_report.status = "FAILURE"
+        mock_instruction_report.instruction.bet_id = 123
+        mock_report.cancel_instruction_reports = [mock_instruction_report]
+        mock__execution_helper.return_value = mock_report
+        self.execution.execute_cancel(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_cancel, mock_order_package, mock_session
+        )
+        mock__order_logger.assert_called_with(
+            mock_order, mock_instruction_report, OrderPackageType.CANCEL
+        )
+        mock_order.executable.assert_called_with()
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.cancel")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_cancel_timeout(
+        self, mock__execution_helper, mock_cancel, mock__order_logger
+    ):
+        mock_session = mock.Mock()
+        mock_order = mock.Mock()
+        mock_order.bet_id = 123
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.info = {}
+        mock_report = mock.Mock()
+        mock_instruction_report = mock.Mock()
+        mock_instruction_report.status = "TIMEOUT"
+        mock_instruction_report.instruction.bet_id = 123
+        mock_report.cancel_instruction_reports = [mock_instruction_report]
+        mock__execution_helper.return_value = mock_report
+        self.execution.execute_cancel(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_cancel, mock_order_package, mock_session
+        )
+        mock__order_logger.assert_called_with(
+            mock_order, mock_instruction_report, OrderPackageType.CANCEL
+        )
+        mock_order.executable.assert_called_with()
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.cancel")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_cancel_not_returned(
+        self, mock__execution_helper, mock_cancel, mock__order_logger
+    ):
+        mock_session = mock.Mock()
+        mock_order = mock.Mock()
+        mock_order.bet_id = 123
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.info = {}
+        mock_report = mock.Mock()
+        mock_report.cancel_instruction_reports = []
+        mock__execution_helper.return_value = mock_report
+        self.execution.execute_cancel(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_cancel, mock_order_package, mock_session
+        )
+        mock_order.executable.assert_called_with()
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.cancel")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_cancel_error(self, mock__execution_helper, mock_place):
+        mock_session = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.info = {}
+        mock__execution_helper.return_value = None
+        self.execution.execute_cancel(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_place, mock_order_package, mock_session
+        )
+
+    def test_cancel(self):
+        mock_session = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.cancel_instructions = [1, 2]
+        self.assertEqual(
+            self.execution.cancel(mock_order_package, mock_session),
+            mock_order_package.client.betting_client.betting.cancel_orders(),
+        )
+
+    def test_cancel_empty(self):
+        mock_session = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.info = {}
+        mock_order_package.cancel_instructions = []
+        with self.assertRaises(OrderExecutionError):
+            self.execution.cancel(mock_order_package, mock_session)
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.update")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_update_success(
+        self, mock__execution_helper, mock_update, mock__order_logger
+    ):
+        mock_session = mock.Mock()
+        mock_order = mock.Mock()
+        mock_order.bet_id = 123
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.info = {}
+        mock_report = mock.Mock()
+        mock_instruction_report = mock.Mock()
+        mock_instruction_report.status = "SUCCESS"
+        mock_report.update_instruction_reports = [mock_instruction_report]
+        mock__execution_helper.return_value = mock_report
+        self.execution.execute_update(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_update, mock_order_package, mock_session
+        )
+        mock__order_logger.assert_called_with(
+            mock_order, mock_instruction_report, OrderPackageType.UPDATE
+        )
+        mock_order.executable.assert_called_with()
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.update")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_update_failure(
+        self, mock__execution_helper, mock_update, mock__order_logger
+    ):
+        mock_session = mock.Mock()
+        mock_order = mock.Mock()
+        mock_order.bet_id = 123
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.info = {}
+        mock_report = mock.Mock()
+        mock_instruction_report = mock.Mock()
+        mock_instruction_report.status = "FAILURE"
+        mock_report.update_instruction_reports = [mock_instruction_report]
+        mock__execution_helper.return_value = mock_report
+        self.execution.execute_update(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_update, mock_order_package, mock_session
+        )
+        mock__order_logger.assert_called_with(
+            mock_order, mock_instruction_report, OrderPackageType.UPDATE
+        )
+        mock_order.executable.assert_called_with()
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.update")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_update_timeout(
+        self, mock__execution_helper, mock_update, mock__order_logger
+    ):
+        mock_session = mock.Mock()
+        mock_order = mock.Mock()
+        mock_order.bet_id = 123
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.info = {}
+        mock_report = mock.Mock()
+        mock_instruction_report = mock.Mock()
+        mock_instruction_report.status = "TIMEOUT"
+        mock_report.update_instruction_reports = [mock_instruction_report]
+        mock__execution_helper.return_value = mock_report
+        self.execution.execute_update(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_update, mock_order_package, mock_session
+        )
+        mock__order_logger.assert_called_with(
+            mock_order, mock_instruction_report, OrderPackageType.UPDATE
+        )
+        mock_order.executable.assert_called_with()
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.update")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_update_error(self, mock__execution_helper, mock_update):
+        mock_session = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.info = {}
+        mock__execution_helper.return_value = None
+        self.execution.execute_update(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_update, mock_order_package, mock_session
+        )
+
+    def test_update(self):
+        mock_session = mock.Mock()
+        mock_order_package = mock.Mock()
+        self.assertEqual(
+            self.execution.update(mock_order_package, mock_session),
+            mock_order_package.client.betting_client.betting.update_orders(),
+        )
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._order_logger")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.replace")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_replace_success(
+        self, mock__execution_helper, mock_replace, mock__order_logger
+    ):
+        mock_market = mock.Mock()
+        self.mock_flumine.markets.markets = {"1.234": mock_market}
+        mock_session = mock.Mock()
+        mock_order = mock.Mock()
+        mock_order.market_id = "1.234"
+        mock_order.bet_id = 123
+        mock_order_package = mock.Mock()
+        mock_order_package.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        mock_order_package.info = {}
+        mock_report = mock.Mock()
+        mock_instruction_report = mock.Mock()
+        mock_instruction_report.cancel_instruction_reports.status = "SUCCESS"
+        mock_instruction_report.place_instruction_reports.status = "SUCCESS"
+        mock_report.replace_instruction_reports = [mock_instruction_report]
+        mock__execution_helper.return_value = mock_report
+        self.execution.execute_replace(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_replace, mock_order_package, mock_session
+        )
+        # mock__order_logger.assert_called_with(
+        #     mock_order, mock_instruction_report, OrderPackageType.CANCEL
+        # )
+        mock_order.execution_complete.assert_called_with()
+
+        replacement_order = mock_order.trade.create_order_replacement()
+        replacement_order.executable.assert_called_with()
+        mock_market.place_order.assert_called_with(replacement_order, execute=False)
+        mock__order_logger.assert_called_with(
+            replacement_order,
+            mock_instruction_report.place_instruction_reports,
+            OrderPackageType.REPLACE,
+        )
+
+    # def test_execute_replace_failure(self):
+    #     pass
+
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution.replace")
+    @mock.patch("flumine.execution.betfairexecution.BetfairExecution._execution_helper")
+    def test_execute_replace_error(self, mock__execution_helper, mock_replace):
+        mock_session = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.info = {}
+        mock__execution_helper.return_value = None
+        self.execution.execute_replace(mock_order_package, mock_session)
+        mock__execution_helper.assert_called_with(
+            mock_replace, mock_order_package, mock_session
+        )
+
+    def test_replace(self):
+        mock_session = mock.Mock()
+        mock_order_package = mock.Mock()
+        self.assertEqual(
+            self.execution.replace(mock_order_package, mock_session),
+            mock_order_package.client.betting_client.betting.replace_orders(),
+        )
+
+    def test__execution_helper(self):
+        mock_trading_function = mock.Mock()
+        mock_trading_function.__name__ = "test"
+        mock_session = mock.Mock()
+        mock_order_package = mock.Mock()
+        mock_order_package.info = {}
+        self.execution._execution_helper(
+            mock_trading_function, mock_order_package, mock_session
+        )
+        mock_trading_function.assert_called_with(mock_order_package, mock_session)
+
+    def test__order_logger(self):
+        mock_order = mock.Mock()
+        mock_instruction_report = mock.Mock()
+        self.execution._order_logger(
+            mock_order, mock_instruction_report, OrderPackageType.PLACE
+        )
+        self.assertEqual(mock_order.bet_id, mock_instruction_report.bet_id)
+        mock_order.responses.placed.assert_called_with(mock_instruction_report)
+
+        self.execution._order_logger(
+            mock_order, mock_instruction_report, OrderPackageType.CANCEL
+        )
+        mock_order.responses.cancelled.assert_called_with(mock_instruction_report)
+
+        self.execution._order_logger(
+            mock_order, mock_instruction_report, OrderPackageType.UPDATE
+        )
+        mock_order.responses.updated.assert_called_with(mock_instruction_report)
+
+        self.execution._order_logger(
+            mock_order, mock_instruction_report, OrderPackageType.REPLACE
+        )
+        self.assertEqual(mock_order.bet_id, mock_instruction_report.bet_id)
+        mock_order.responses.placed.assert_called_with(mock_instruction_report)
+
+
+class SimulatedExecutionTest(unittest.TestCase):
+    def setUp(self) -> None:
+        self.mock_flumine = mock.Mock()
+        self.execution = SimulatedExecution(self.mock_flumine)
+
+    def test_init(self):
+        self.assertEqual(self.execution.EXCHANGE, ExchangeType.SIMULATED)
+
+    def test_execute_place(self):
+        with self.assertRaises(NotImplementedError):
+            self.execution.execute_place(None, None)
+
+    def test_execute_cancel(self):
+        with self.assertRaises(NotImplementedError):
+            self.execution.execute_place(None, None)
+
+    def test_execute_update(self):
+        with self.assertRaises(NotImplementedError):
+            self.execution.execute_place(None, None)
+
+    def test_execute_replace(self):
+        with self.assertRaises(NotImplementedError):
+            self.execution.execute_place(None, None)
diff --git a/tests/test_flumine.py b/tests/test_flumine.py
index 6c62391..6f14cb6 100644
--- a/tests/test_flumine.py
+++ b/tests/test_flumine.py
@@ -3,6 +3,7 @@ from unittest import mock
 
 from flumine import Flumine
 from flumine.event import event
+from flumine.order.orderpackage import BaseOrderPackage
 
 
 class FlumineTest(unittest.TestCase):
@@ -10,6 +11,8 @@ class FlumineTest(unittest.TestCase):
         self.mock_trading = mock.Mock()
         self.flumine = Flumine(self.mock_trading)
 
+    @mock.patch("flumine.flumine.Flumine._process_order_package")
+    @mock.patch("flumine.flumine.Flumine._process_current_orders")
     @mock.patch("flumine.flumine.Flumine._process_end_flumine")
     @mock.patch("flumine.flumine.Flumine._process_market_catalogues")
     @mock.patch("flumine.flumine.Flumine._process_raw_data")
@@ -20,12 +23,15 @@ class FlumineTest(unittest.TestCase):
         mock__process_raw_data,
         mock__process_market_catalogues,
         mock__process_end_flumine,
+        mock__process_current_orders,
+        mock__process_order_package,
     ):
         events = [
             event.MarketCatalogueEvent(None),
             event.MarketBookEvent(None),
             event.RawDataEvent(None),
             event.CurrentOrdersEvent(None),
+            BaseOrderPackage(None, "1.123", [], "12"),
             event.ClearedMarketsEvent(None),
             event.ClearedOrdersEvent(None),
             event.CloseMarketEvent(None),
@@ -42,6 +48,8 @@ class FlumineTest(unittest.TestCase):
         mock__process_raw_data.assert_called_with(events[2])
         mock__process_market_catalogues.assert_called_with(events[0])
         mock__process_end_flumine.assert_called_with()
+        mock__process_current_orders.assert_called_with(events[3])
+        mock__process_order_package.assert_called_with(events[4])
 
     def test__add_default_workers(self):
         self.flumine._add_default_workers()
diff --git a/tests/test_markets.py b/tests/test_markets.py
index ac3c193..969f13e 100644
--- a/tests/test_markets.py
+++ b/tests/test_markets.py
@@ -31,16 +31,52 @@ class MarketsTest(unittest.TestCase):
         self.markets._markets = {"1.1": mock_market}
         self.markets.close_market("1.1")
         mock_market.close_market.assert_called_with()
-        self.assertEqual(self.markets.markets, {})
+
+    def test_get_order(self):
+        mock_market = mock.Mock()
+        mock_market.closed = False
+        mock_market.blotter = {"test": 12}
+        self.markets._markets = {"1.1": mock_market}
+
+        self.assertEqual(self.markets.get_order("1.1", "test"), 12)
+        self.assertIsNone(self.markets.get_order("1.2", "test"))
+
+    def test_get_order_from_bet_id(self):
+        mock_order = mock.Mock()
+        mock_order.bet_id = "321"
+        mock_market = mock.Mock()
+        mock_market.closed = False
+        mock_market.blotter.__iter__ = mock.Mock(return_value=iter([mock_order]))
+        self.markets._markets = {"1.1": mock_market}
+
+        self.assertEqual(self.markets.get_order_from_bet_id("1.1", "321"), mock_order)
+        self.assertIsNone(self.markets.get_order("1.2", "test"))
 
     def test_markets(self):
         self.assertEqual(self.markets.markets, {})
         mock_market = mock.Mock()
+        mock_market_two = mock.Mock()
+        self.markets._markets = {"1.1": mock_market, "2.1": mock_market_two}
+        self.assertEqual(
+            self.markets.markets, {"1.1": mock_market, "2.1": mock_market_two}
+        )
+
+    def test_open_market_ids(self):
+        self.assertEqual(self.markets.open_market_ids, [])
+        mock_market = mock.Mock()
         mock_market.closed = False
         mock_market_two = mock.Mock()
         mock_market_two.closed = True
         self.markets._markets = {"1.1": mock_market, "2.1": mock_market_two}
-        self.assertEqual(self.markets.markets, {"1.1": mock_market})
+        self.assertEqual(self.markets.open_market_ids, [mock_market.market_id])
+
+    def test_live_orders(self):
+        self.assertFalse(self.markets.live_orders)
+        mock_market = mock.Mock()
+        mock_market.closed = False
+        mock_market.blotter.live_orders = True
+        self.markets._markets = {"1.234": mock_market}
+        self.assertTrue(self.markets.live_orders)
 
     def test_iter(self):
         self.assertEqual(len([i for i in self.markets]), 0)
@@ -54,17 +90,17 @@ class MarketTest(unittest.TestCase):
         self.mock_market_book = mock.Mock()
         self.market = Market("1.234", self.mock_market_book)
 
+    def test_init(self):
+        self.assertEqual(self.market.market_id, "1.234")
+        self.assertEqual(self.market.market_book, self.mock_market_book)
+        self.assertFalse(self.market.closed)
+
     def test_call(self):
         mock_market_book = mock.Mock()
         self.market(mock_market_book)
 
         self.assertEqual(self.market.market_book, mock_market_book)
 
-    def test_init(self):
-        self.assertEqual(self.market.market_id, "1.234")
-        self.assertEqual(self.market.market_book, self.mock_market_book)
-        self.assertFalse(self.market.closed)
-
     def test_open_market(self):
         self.market.open_market()
         self.assertFalse(self.market.closed)
@@ -73,6 +109,53 @@ class MarketTest(unittest.TestCase):
         self.market.close_market()
         self.assertTrue(self.market.closed)
 
+    def test_place_order(self):
+        mock_order = mock.Mock()
+        mock_order.id = "123"
+        self.market.place_order(mock_order)
+        mock_order.place.assert_called_with()
+        self.assertEqual(self.market.blotter.pending_place, [mock_order])
+
+    def test_place_order_not_executed(self):
+        mock_order = mock.Mock()
+        mock_order.id = "123"
+        self.market.place_order(mock_order, execute=False)
+        mock_order.place.assert_called_with()
+        self.assertEqual(self.market.blotter.pending_place, [])
+
+    def test_place_order_retry(self):
+        mock_order = mock.Mock()
+        self.market.blotter._orders = {mock_order.id: mock_order}
+        self.market.place_order(mock_order)
+        self.assertEqual(self.market.blotter.pending_place, [])
+
+    def test_cancel_order(self):
+        mock_blotter = mock.Mock()
+        mock_blotter.pending_cancel = []
+        self.market.blotter = mock_blotter
+        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])
+
+    def test_update_order(self):
+        mock_blotter = mock.Mock()
+        mock_blotter.pending_update = []
+        self.market.blotter = mock_blotter
+        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])
+
+    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)
+        mock_order.replace.assert_called_with(1.01)
+        self.assertEqual(mock_blotter.pending_replace, [mock_order])
+
     def test_seconds_to_start(self):
         mock_market_catalogue = mock.Mock()
         mock_market_catalogue.market_start_time = datetime.datetime.utcfromtimestamp(1)
diff --git a/tests/test_order.py b/tests/test_order.py
new file mode 100644
index 0000000..058a7bc
--- /dev/null
+++ b/tests/test_order.py
@@ -0,0 +1,379 @@
+import unittest
+import datetime
+from unittest import mock
+
+from flumine.order.order import (
+    BaseOrder,
+    BetfairOrder,
+    ExchangeType,
+    OrderTypes,
+    OrderStatus,
+)
+from flumine.exceptions import OrderUpdateError
+
+
+class BaseOrderTest(unittest.TestCase):
+    def setUp(self) -> None:
+        self.mock_trade = mock.Mock()
+        self.mock_order_type = mock.Mock()
+        self.order = BaseOrder(self.mock_trade, "BACK", self.mock_order_type, 1)
+
+    def test_init(self):
+        self.assertIsNotNone(self.order.id)
+        self.assertEqual(self.order.trade, self.mock_trade)
+        self.assertEqual(self.order.side, "BACK")
+        self.assertEqual(self.order.order_type, self.mock_order_type)
+        self.assertEqual(self.order.handicap, 1)
+        self.assertIsNone(self.order.status)
+        self.assertEqual(self.order.status_log, [])
+        self.assertIsNone(self.order.bet_id)
+        self.assertIsNone(self.order.EXCHANGE)
+        self.assertEqual(self.order._update, {})
+        self.assertIsNone(self.order._current_order)
+
+    def test__update_status(self):
+        self.order._update_status(OrderStatus.EXECUTION_COMPLETE)
+        self.assertEqual(self.order.status_log, [OrderStatus.EXECUTION_COMPLETE])
+        self.assertEqual(self.order.status, OrderStatus.EXECUTION_COMPLETE)
+
+    @mock.patch("flumine.order.order.BaseOrder._update_status")
+    def test_placing(self, mock__update_status):
+        self.order.placing()
+        mock__update_status.assert_called_with(OrderStatus.PENDING)
+
+    @mock.patch("flumine.order.order.BaseOrder._update_status")
+    def test_executable(self, mock__update_status):
+        self.order.executable()
+        mock__update_status.assert_called_with(OrderStatus.EXECUTABLE)
+
+    @mock.patch("flumine.order.order.BaseOrder._update_status")
+    def test_execution_complete(self, mock__update_status):
+        self.order.execution_complete()
+        mock__update_status.assert_called_with(OrderStatus.EXECUTION_COMPLETE)
+
+    @mock.patch("flumine.order.order.BaseOrder._update_status")
+    def test_cancelling(self, mock__update_status):
+        self.order.cancelling()
+        mock__update_status.assert_called_with(OrderStatus.CANCELLING)
+
+    @mock.patch("flumine.order.order.BaseOrder._update_status")
+    def test_updating(self, mock__update_status):
+        self.order.updating()
+        mock__update_status.assert_called_with(OrderStatus.UPDATING)
+
+    @mock.patch("flumine.order.order.BaseOrder._update_status")
+    def test_replacing(self, mock__update_status):
+        self.order.replacing()
+        mock__update_status.assert_called_with(OrderStatus.REPLACING)
+
+    @mock.patch("flumine.order.order.BaseOrder._update_status")
+    def test_lapsed(self, mock__update_status):
+        self.order.lapsed()
+        mock__update_status.assert_called_with(OrderStatus.LAPSED)
+
+    @mock.patch("flumine.order.order.BaseOrder._update_status")
+    def test_voided(self, mock__update_status):
+        self.order.voided()
+        mock__update_status.assert_called_with(OrderStatus.VOIDED)
+
+    @mock.patch("flumine.order.order.BaseOrder._update_status")
+    def test_violation(self, mock__update_status):
+        self.order.violation()
+        mock__update_status.assert_called_with(OrderStatus.VIOLATION)
+
+    def test_place(self):
+        with self.assertRaises(NotImplementedError):
+            self.order.place()
+
+    def test_cancel(self):
+        with self.assertRaises(NotImplementedError):
+            self.order.cancel()
+
+    def test_update(self):
+        with self.assertRaises(NotImplementedError):
+            self.order.update("PERSIST")
+
+    def test_replace(self):
+        with self.assertRaises(NotImplementedError):
+            self.order.replace(20.0)
+
+    def test_create_place_instruction(self):
+        with self.assertRaises(NotImplementedError):
+            self.order.create_place_instruction()
+
+    def test_create_cancel_instruction(self):
+        with self.assertRaises(NotImplementedError):
+            self.order.create_cancel_instruction()
+
+    def test_create_update_instruction(self):
+        with self.assertRaises(NotImplementedError):
+            self.order.create_update_instruction()
+
+    def test_create_replace_instruction(self):
+        with self.assertRaises(NotImplementedError):
+            self.order.create_replace_instruction()
+
+    def test_update_current_order(self):
+        mock_current_order = mock.Mock()
+        self.order.update_current_order(mock_current_order)
+        self.assertEqual(self.order._current_order, mock_current_order)
+
+    def test_current_order(self):
+        self.assertIsNone(self.order.current_order)
+        mock_responses = mock.Mock()
+        self.order.responses = mock_responses
+        self.assertEqual(self.order.current_order, mock_responses.place_response)
+        self.order._current_order = 1
+        self.assertEqual(self.order.current_order, 1)
+
+    def test_average_price_matched(self):
+        with self.assertRaises(NotImplementedError):
+            assert self.order.average_price_matched
+
+    def test_size_matched(self):
+        with self.assertRaises(NotImplementedError):
+            assert self.order.size_matched
+
+    def test_size_remaining(self):
+        with self.assertRaises(NotImplementedError):
+            assert self.order.size_remaining
+
+    def test_size_cancelled(self):
+        with self.assertRaises(NotImplementedError):
+            assert self.order.size_cancelled
+
+    def test_size_lapsed(self):
+        with self.assertRaises(NotImplementedError):
+            assert self.order.size_lapsed
+
+    def test_size_voided(self):
+        with self.assertRaises(NotImplementedError):
+            assert self.order.size_voided
+
+    def test_elapsed_seconds(self):
+        mock_responses = mock.Mock()
+        mock_responses.date_time_placed = datetime.datetime.utcnow()
+        self.order.responses = mock_responses
+        self.assertGreaterEqual(self.order.elapsed_seconds, 0)
+
+    def test_market_id(self):
+        self.assertEqual(self.order.market_id, self.mock_trade.market_id)
+
+    def test_selection_id(self):
+        self.assertEqual(self.order.selection_id, self.mock_trade.selection_id)
+
+    def test_lookup(self):
+        self.assertEqual(
+            self.order.lookup,
+            (self.mock_trade.market_id, self.mock_trade.selection_id, 1),
+        )
+
+    def test_info(self):
+        self.order.status_log = [OrderStatus.PENDING, OrderStatus.EXECUTION_COMPLETE]
+        self.assertEqual(
+            self.order.info,
+            {
+                "bet_id": None,
+                "handicap": 1,
+                "id": self.order.id,
+                "market_id": self.mock_trade.market_id,
+                "selection_id": self.mock_trade.selection_id,
+                "status": None,
+                "status_log": "Pending, Execution complete",
+                "trade": self.mock_trade.info,
+                "customer_order_ref": self.order.customer_order_ref,
+            },
+        )
+
+    def test_repr(self):
+        self.assertEqual(repr(self.order), "Order None: None")
+
+
+class BetfairOrderTest(unittest.TestCase):
+    def setUp(self) -> None:
+        self.mock_trade = mock.Mock()
+        self.mock_status = mock.Mock()
+        self.mock_order_type = mock.Mock()
+        self.order = BetfairOrder(self.mock_trade, "BACK", self.mock_order_type)
+
+    def test_init(self):
+        self.assertEqual(self.order.EXCHANGE, ExchangeType.BETFAIR)
+
+    @mock.patch("flumine.order.order.BetfairOrder.placing")
+    def test_place(self, mock_placing):
+        self.order.place()
+        mock_placing.assert_called_with()
+
+    @mock.patch(
+        "flumine.order.order.BetfairOrder.size_remaining",
+        new_callable=mock.PropertyMock,
+    )
+    @mock.patch("flumine.order.order.BetfairOrder.cancelling")
+    def test_cancel(self, mock_cancelling, mock_size_remaining):
+        mock_size_remaining.return_value = 20
+        self.order.status = OrderStatus.EXECUTABLE
+        with self.assertRaises(OrderUpdateError):
+            self.order.cancel(12)
+
+        self.mock_order_type.ORDER_TYPE = OrderTypes.LIMIT
+        self.order.cancel(0.01)
+        self.assertEqual(self.order._update, {"size_reduction": 0.01})
+        mock_cancelling.assert_called_with()
+        self.order.cancel()
+        self.assertEqual(self.order._update, {"size_reduction": None})
+
+    @mock.patch(
+        "flumine.order.order.BetfairOrder.size_remaining",
+        new_callable=mock.PropertyMock,
+    )
+    @mock.patch("flumine.order.order.BetfairOrder.cancelling")
+    def test_cancel_error_size(self, mock_cancelling, mock_size_remaining):
+        mock_size_remaining.return_value = 20
+        self.order.status = OrderStatus.EXECUTABLE
+        with self.assertRaises(OrderUpdateError):
+            self.order.cancel(12)
+
+        self.mock_order_type.ORDER_TYPE = OrderTypes.LIMIT
+        with self.assertRaises(OrderUpdateError):
+            self.order.cancel(21)
+
+    def test_cancel_error(self):
+        self.mock_order_type.ORDER_TYPE = OrderTypes.LIMIT
+        self.order.status = OrderStatus.PENDING
+        with self.assertRaises(OrderUpdateError):
+            self.order.cancel(12)
+
+    @mock.patch("flumine.order.order.BetfairOrder.updating")
+    def test_update(self, mock_updating):
+        self.order.status = OrderStatus.EXECUTABLE
+        with self.assertRaises(OrderUpdateError):
+            self.order.update("PERSIST")
+
+        self.mock_order_type.ORDER_TYPE = OrderTypes.LIMIT
+        self.mock_order_type.persistence_type = "LAPSE"
+        self.order.update("PERSIST")
+        self.assertEqual(self.mock_order_type.persistence_type, "PERSIST")
+        mock_updating.assert_called_with()
+
+        with self.assertRaises(OrderUpdateError):
+            self.order.update("PERSIST")
+
+    def test_update_error(self):
+        self.mock_order_type.ORDER_TYPE = OrderTypes.LIMIT
+        self.mock_order_type.persistence_type = "LAPSE"
+        self.order.status = OrderStatus.PENDING
+        with self.assertRaises(OrderUpdateError):
+            self.order.update("PERSIST")
+
+    @mock.patch("flumine.order.order.BetfairOrder.replacing")
+    def test_replace(self, mock_replacing):
+        self.order.status = OrderStatus.EXECUTABLE
+        with self.assertRaises(OrderUpdateError):
+            self.order.replace(1.01)
+
+        self.mock_order_type.ORDER_TYPE = OrderTypes.LIMIT
+        self.mock_order_type.price = 2.02
+        self.order.replace(1.01)
+        self.assertEqual(self.order._update, {"new_price": 1.01})
+        mock_replacing.assert_called_with()
+
+        with self.assertRaises(OrderUpdateError):
+            self.order.replace(2.02)
+
+    def test_replace_error(self):
+        self.mock_order_type.ORDER_TYPE = OrderTypes.LIMIT
+        self.order.status = OrderStatus.PENDING
+        with self.assertRaises(OrderUpdateError):
+            self.order.replace(1.52)
+
+    def test_create_place_instruction(self):
+        self.mock_order_type.ORDER_TYPE = OrderTypes.LIMIT
+        self.assertEqual(
+            self.order.create_place_instruction(),
+            {
+                "customerOrderRef": self.order.customer_order_ref,
+                "handicap": 0,
+                "limitOrder": self.mock_order_type.place_instruction(),
+                "orderType": "LIMIT",
+                "selectionId": self.mock_trade.selection_id,
+                "side": "BACK",
+            },
+        )
+        self.mock_order_type.ORDER_TYPE = OrderTypes.LIMIT_ON_CLOSE
+        self.assertEqual(
+            self.order.create_place_instruction(),
+            {
+                "customerOrderRef": self.order.customer_order_ref,
+                "handicap": 0,
+                "limitOnCloseOrder": self.mock_order_type.place_instruction(),
+                "orderType": "LIMIT_ON_CLOSE",
+                "selectionId": self.mock_trade.selection_id,
+                "side": "BACK",
+            },
+        )
+        self.mock_order_type.ORDER_TYPE = OrderTypes.MARKET_ON_CLOSE
+        self.assertEqual(
+            self.order.create_place_instruction(),
+            {
+                "customerOrderRef": self.order.customer_order_ref,
+                "handicap": 0,
+                "marketOnCloseOrder": self.mock_order_type.place_instruction(),
+                "orderType": "MARKET_ON_CLOSE",
+                "selectionId": self.mock_trade.selection_id,
+                "side": "BACK",
+            },
+        )
+
+    def test_create_cancel_instruction(self):
+        self.order._update = {"size_reduction": 0.02}
+        self.assertEqual(
+            self.order.create_cancel_instruction(), {"sizeReduction": 0.02}
+        )
+
+    def test_create_update_instruction(self):
+        self.mock_order_type.persistence_type = "PERSIST"
+        self.assertEqual(
+            self.order.create_update_instruction(), {"newPersistenceType": "PERSIST"}
+        )
+
+    def test_create_replace_instruction(self):
+        self.order._update = {"new_price": 2.02}
+        self.assertEqual(self.order.create_replace_instruction(), {"newPrice": 2.02})
+
+    def test_average_price_matched(self):
+        self.assertEqual(self.order.average_price_matched, 0)
+        mock_current_order = mock.Mock(average_price_matched=12.3)
+        self.order._current_order = mock_current_order
+        self.assertEqual(
+            self.order.average_price_matched, mock_current_order.average_price_matched
+        )
+
+    def test_size_matched(self):
+        self.assertEqual(self.order.size_matched, 0)
+        mock_current_order = mock.Mock(size_matched=10)
+        self.order._current_order = mock_current_order
+        self.assertEqual(self.order.size_matched, mock_current_order.size_matched)
+
+    def test_size_remaining(self):
+        self.assertEqual(self.order.size_remaining, 0)
+        mock_current_order = mock.Mock(size_remaining=10)
+        self.order._current_order = mock_current_order
+        self.assertEqual(self.order.size_remaining, mock_current_order.size_remaining)
+
+    def test_size_cancelled(self):
+        self.assertEqual(self.order.size_cancelled, 0)
+        mock_current_order = mock.Mock(size_cancelled=10)
+        self.order._current_order = mock_current_order
+        self.assertEqual(self.order.size_cancelled, mock_current_order.size_cancelled)
+
+    def test_size_lapsed(self):
+        self.assertEqual(self.order.size_lapsed, 0)
+        mock_current_order = mock.Mock(size_lapsed=10)
+        self.order._current_order = mock_current_order
+        self.assertEqual(self.order.size_lapsed, mock_current_order.size_lapsed)
+
+    def test_size_voided(self):
+        self.assertEqual(self.order.size_voided, 0)
+        mock_current_order = mock.Mock(size_voided=10)
+        self.order._current_order = mock_current_order
+        self.assertEqual(self.order.size_voided, mock_current_order.size_voided)
diff --git a/tests/test_orderpackage.py b/tests/test_orderpackage.py
new file mode 100644
index 0000000..5a020ad
--- /dev/null
+++ b/tests/test_orderpackage.py
@@ -0,0 +1,131 @@
+import unittest
+from unittest import mock
+
+from flumine.order.orderpackage import (
+    OrderPackageType,
+    BaseOrderPackage,
+    EventType,
+    QueueType,
+    BetfairOrderPackage,
+    ExchangeType,
+    OrderStatus,
+)
+
+
+class OrderPackageTest(unittest.TestCase):
+    def setUp(self) -> None:
+        self.mock_package_type = mock.Mock()
+        self.mock_client = mock.Mock()
+        self.mock_order = mock.Mock()
+        self.mock_order.status = OrderStatus.PENDING
+        self.market_version = {"version": 123456}
+        self.order_package = BaseOrderPackage(
+            self.mock_client,
+            "1.234",
+            [self.mock_order],
+            self.mock_package_type,
+            self.market_version,
+        )
+
+    def test_init(self):
+        self.assertEqual(self.order_package.client, self.mock_client)
+        self.assertEqual(self.order_package.market_id, "1.234")
+        self.assertEqual(self.order_package._orders, [self.mock_order])
+        self.assertEqual(self.order_package.package_type, self.mock_package_type)
+        self.assertEqual(self.order_package.EVENT_TYPE, EventType.ORDER_PACKAGE)
+        self.assertEqual(self.order_package.QUEUE_TYPE, QueueType.HANDLER)
+        self.assertEqual(self.order_package.market_version, self.market_version)
+        self.assertIsNone(self.order_package.EXCHANGE)
+        self.assertFalse(self.order_package.async_)
+
+    def test_place_instructions(self):
+        with self.assertRaises(NotImplementedError):
+            assert self.order_package.place_instructions
+
+    def test_cancel_instructions(self):
+        with self.assertRaises(NotImplementedError):
+            assert self.order_package.cancel_instructions
+
+    def test_update_instructions(self):
+        with self.assertRaises(NotImplementedError):
+            assert self.order_package.update_instructions
+
+    def test_replace_instructions(self):
+        with self.assertRaises(NotImplementedError):
+            assert self.order_package.replace_instructions
+
+    def test_order_limit(self):
+        with self.assertRaises(NotImplementedError):
+            assert self.order_package.order_limit(OrderPackageType.PLACE)
+
+    def test_orders(self):
+        self.assertEqual(self.order_package.orders, [self.mock_order])
+        self.order_package._orders = [
+            mock.Mock(status=OrderStatus.PENDING),
+            mock.Mock(status=OrderStatus.PENDING),
+            mock.Mock(status=OrderStatus.VIOLATION),
+        ]
+        self.assertEqual(len(self.order_package.orders), 2)
+
+    def test_info(self):
+        self.assertEqual(
+            self.order_package.info,
+            {
+                "id": self.order_package.id,
+                "client": self.order_package.client,
+                "market_id": self.order_package.market_id,
+                "orders": [self.mock_order.id],
+                "package_type": self.order_package.package_type.value,
+                "customer_strategy_ref": self.order_package.customer_strategy_ref,
+            },
+        )
+
+    def test_iter(self):
+        self.assertEqual([i for i in self.order_package], self.order_package.orders)
+
+    def test_len(self):
+        self.assertEqual(len(self.order_package), 1)
+
+
+class BetfairOrderPackageTest(unittest.TestCase):
+    def setUp(self) -> None:
+        self.mock_package_type = mock.Mock()
+        self.mock_client = mock.Mock()
+        self.mock_order = mock.Mock()
+        self.mock_order.status = OrderStatus.PENDING
+        self.order_package = BetfairOrderPackage(
+            self.mock_client, "1.234", [self.mock_order], self.mock_package_type
+        )
+
+    def test_init(self):
+        self.assertEqual(self.order_package.EXCHANGE, ExchangeType.BETFAIR)
+
+    def test_place_instructions(self):
+        self.assertEqual(
+            self.order_package.place_instructions,
+            [self.mock_order.create_place_instruction()],
+        )
+
+    def test_cancel_instructions(self):
+        self.assertEqual(
+            self.order_package.cancel_instructions,
+            [self.mock_order.create_cancel_instruction()],
+        )
+
+    def test_update_instructions(self):
+        self.assertEqual(
+            self.order_package.update_instructions,
+            [self.mock_order.create_update_instruction()],
+        )
+
+    def test_replace_instructions(self):
+        self.assertEqual(
+            self.order_package.replace_instructions,
+            [self.mock_order.create_replace_instruction()],
+        )
+
+    def test_order_limit(self):
+        self.assertEqual(self.order_package.order_limit(OrderPackageType.PLACE), 200)
+        self.assertEqual(self.order_package.order_limit(OrderPackageType.CANCEL), 60)
+        self.assertEqual(self.order_package.order_limit(OrderPackageType.UPDATE), 60)
+        self.assertEqual(self.order_package.order_limit(OrderPackageType.REPLACE), 60)
diff --git a/tests/test_ordertype.py b/tests/test_ordertype.py
new file mode 100644
index 0000000..74a3136
--- /dev/null
+++ b/tests/test_ordertype.py
@@ -0,0 +1,83 @@
+import unittest
+from unittest import mock
+
+from flumine.order.ordertype import (
+    ExchangeType,
+    OrderTypes,
+    BaseOrderType,
+    LimitOrder,
+    LimitOnCloseOrder,
+    MarketOnCloseOrder,
+)
+
+
+class BaseOrderTypeTest(unittest.TestCase):
+    def setUp(self) -> None:
+        self.order_type = BaseOrderType()
+
+    def test_init(self):
+        self.assertIsNone(self.order_type.EXCHANGE)
+        self.assertIsNone(self.order_type.ORDER_TYPE)
+
+    def test_place_instruction(self):
+        with self.assertRaises(NotImplementedError):
+            self.order_type.place_instruction()
+
+
+class LimitOrderTest(unittest.TestCase):
+    def setUp(self) -> None:
+        self.order_type = LimitOrder(1.01, 2, "PERSIST", "YES", 34.5, "NO", 2)
+
+    def test_init(self):
+        self.assertEqual(self.order_type.EXCHANGE, ExchangeType.BETFAIR)
+        self.assertEqual(self.order_type.ORDER_TYPE, OrderTypes.LIMIT)
+        self.assertEqual(self.order_type.price, 1.01)
+        self.assertEqual(self.order_type.size, 2)
+        self.assertEqual(self.order_type.persistence_type, "PERSIST")
+        self.assertEqual(self.order_type.time_in_force, "YES")
+        self.assertEqual(self.order_type.min_fill_size, 34.5)
+        self.assertEqual(self.order_type.bet_target_type, "NO")
+        self.assertEqual(self.order_type.bet_target_size, 2)
+
+    def test_place_instruction(self):
+        self.assertEqual(
+            self.order_type.place_instruction(),
+            {
+                "betTargetSize": 2,
+                "betTargetType": "NO",
+                "minFillSize": 34.5,
+                "persistenceType": "PERSIST",
+                "price": 1.01,
+                "size": 2,
+                "timeInForce": "YES",
+            },
+        )
+
+
+class LimitOnCloseOrderTest(unittest.TestCase):
+    def setUp(self) -> None:
+        self.order_type = LimitOnCloseOrder(64, 1.01)
+
+    def test_init(self):
+        self.assertEqual(self.order_type.EXCHANGE, ExchangeType.BETFAIR)
+        self.assertEqual(self.order_type.ORDER_TYPE, OrderTypes.LIMIT_ON_CLOSE)
+        self.assertEqual(self.order_type.liability, 64)
+        self.assertEqual(self.order_type.price, 1.01)
+
+    def test_place_instruction(self):
+        self.assertEqual(
+            self.order_type.place_instruction(), {"price": 1.01, "liability": 64}
+        )
+
+
+class MarketOnCloseOrderTest(unittest.TestCase):
+    def setUp(self) -> None:
+        self.order_type = MarketOnCloseOrder(128)
+
+    def test_init(self):
+        self.assertEqual(self.order_type.EXCHANGE, ExchangeType.BETFAIR)
+        self.assertEqual(self.order_type.ORDER_TYPE, OrderTypes.MARKET_ON_CLOSE)
+        self.assertEqual(self.order_type.liability, 128)
+
+    def test_place_instruction(self):
+        self.assertEqual(self.order_type.place_instruction(), {"liability": 128})
diff --git a/tests/test_responses.py b/tests/test_responses.py
new file mode 100644
index 0000000..d4a37ae
--- /dev/null
+++ b/tests/test_responses.py
@@ -0,0 +1,34 @@
+import unittest
+
+from flumine.order.responses import Responses
+
+
+class ResponsesTest(unittest.TestCase):
+    def setUp(self):
+        self.responses = Responses()
+
+    def test_init(self):
+        self.assertIsNotNone(self.responses.date_time_created)
+        self.assertIsNone(self.responses.place_response)
+        self.assertEqual(self.responses.cancel_responses, [])
+        self.assertEqual(self.responses.replace_responses, [])
+        self.assertEqual(self.responses.cancel_responses, [])
+        self.assertIsNone(self.responses.date_time_placed)
+
+    def test_placed(self):
+        self.responses.placed(12)
+        self.assertIsNotNone(self.responses.date_time_placed)
+        self.assertEqual(self.responses.place_response, 12)
+
+    def test_cancelled(self):
+        self.responses.cancelled(12)
+        self.assertEqual(self.responses.cancel_responses, [12])
+
+    def test_replaced(self):
+        self.responses.replaced(12)
+        # self.assertIsNotNone(self.responses.date_time_placed)
+        self.assertEqual(self.responses.replace_responses, [12])
+
+    def test_updated(self):
+        self.responses.updated(12)
+        self.assertEqual(self.responses.update_responses, [12])
diff --git a/tests/test_runnercontext.py b/tests/test_runnercontext.py
new file mode 100644
index 0000000..bdd63e8
--- /dev/null
+++ b/tests/test_runnercontext.py
@@ -0,0 +1,43 @@
+import unittest
+import datetime
+
+from flumine.strategy.runnercontext import RunnerContext
+
+
+class RunnerContextTest(unittest.TestCase):
+    def setUp(self):
+        self.context = RunnerContext(12345)
+
+    def test_init(self):
+        self.assertEqual(self.context.selection_id, 12345)
+
+        self.assertFalse(self.context.invested)
+        self.assertIsNone(self.context.datetime_last_placed)
+        self.assertIsNone(self.context.datetime_last_reset)
+        self.assertEqual(self.context.trade_count, 0)
+        self.assertEqual(self.context.live_trade_count, 0)
+
+    def test_place(self):
+        self.context.place()
+        self.assertTrue(self.context.invested)
+        self.assertIsNotNone(self.context.datetime_last_placed)
+        self.assertEqual(self.context.trade_count, 1)
+        self.assertEqual(self.context.live_trade_count, 1)
+
+    def test_reset(self):
+        self.context.invested = True
+        self.context.live_trade_count = 1
+        self.context.reset()
+        self.assertFalse(self.context.invested)
+        self.assertIsNotNone(self.context.datetime_last_reset)
+        self.assertEqual(self.context.live_trade_count, 0)
+
+    def test_placed_elapsed_seconds(self):
+        self.assertIsNone(self.context.placed_elapsed_seconds)
+        self.context.datetime_last_placed = datetime.datetime.utcnow()
+        self.assertGreaterEqual(self.context.placed_elapsed_seconds, 0)
+
+    def test_reset_elapsed_seconds(self):
+        self.assertIsNone(self.context.reset_elapsed_seconds)
+        self.context.datetime_last_reset = datetime.datetime.utcnow()
+        self.assertGreaterEqual(self.context.reset_elapsed_seconds, 0)
diff --git a/tests/test_strategy.py b/tests/test_strategy.py
index f4a79f4..4c07290 100644
--- a/tests/test_strategy.py
+++ b/tests/test_strategy.py
@@ -45,6 +45,8 @@ class BaseStrategyTest(unittest.TestCase):
             stream_class=strategy.MarketStream,
             name="test",
             context={"trigger": 0.123},
+            max_selection_exposure=1,
+            max_order_exposure=2,
         )
 
     def test_init(self):
@@ -55,6 +57,8 @@ class BaseStrategyTest(unittest.TestCase):
         self.assertEqual(self.strategy.stream_class, strategy.MarketStream)
         self.assertEqual(self.strategy._name, "test")
         self.assertEqual(self.strategy.context, {"trigger": 0.123})
+        self.assertEqual(self.strategy.max_selection_exposure, 1)
+        self.assertEqual(self.strategy.max_order_exposure, 2)
 
     def test_check_market_no_subscribed(self):
         mock_market = mock.Mock()
@@ -116,11 +120,44 @@ class BaseStrategyTest(unittest.TestCase):
         self.strategy.process_race_card(None)
 
     def test_process_orders(self):
-        self.strategy.process_orders(None)
+        self.strategy.process_orders(None, None)
 
     def test_finish(self):
         self.strategy.finish()
 
+    def test_place_order(self):
+        mock_order = mock.Mock()
+        mock_market = mock.Mock()
+        self.strategy.place_order(mock_market, mock_order)
+        mock_market.place_order.assert_called_with(mock_order)
+        self.assertIn(mock_order.market_id, self.strategy._invested)
+
+    def test_cancel_order(self):
+        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)
+
+    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")
+
+    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)
+
+    def test_validate_order(self):
+        mock_order = mock.Mock()
+        runner_context = mock.Mock()
+        runner_context.invested = False
+        self.assertTrue(self.strategy.validate_order(runner_context, mock_order))
+        runner_context.invested = True
+        self.assertFalse(self.strategy.validate_order(runner_context, mock_order))
+
     def test_stream_ids(self):
         mock_stream = mock.Mock()
         mock_stream.stream_id = 321
@@ -135,6 +172,7 @@ class BaseStrategyTest(unittest.TestCase):
                 "market_data_filter": self.mock_market_data_filter,
                 "market_filter": self.mock_market_filter,
                 "name": "test",
+                "name_hash": "a94a8fe5ccb19",
                 "stream_ids": [],
                 "streaming_timeout": self.streaming_timeout,
                 "context": {"trigger": 0.123},
@@ -144,5 +182,8 @@ class BaseStrategyTest(unittest.TestCase):
     def test_name(self):
         self.assertEqual(self.strategy.name, "test")
 
+    def test_name_hash(self):
+        self.assertEqual(self.strategy.name_hash, "a94a8fe5ccb19")
+
     def test_str(self):
         self.assertEqual(str(self.strategy), "test")
diff --git a/tests/test_streams.py b/tests/test_streams.py
index f015d0b..fa5540f 100644
--- a/tests/test_streams.py
+++ b/tests/test_streams.py
@@ -132,6 +132,21 @@ class StreamsTest(unittest.TestCase):
         self.assertEqual(stream, mock_stream)
         self.assertEqual(len(self.streams), 1)
 
+    @mock.patch("flumine.streams.streams.OrderStream")
+    @mock.patch("flumine.streams.streams.Streams._increment_stream_id")
+    def test_add_historical_stream(self, mock_increment, mock_order_stream_class):
+        conflate_ms = 500
+        streaming_timeout = 0.5
+        self.streams.add_order_stream(conflate_ms, streaming_timeout)
+        self.assertEqual(len(self.streams), 1)
+        mock_increment.assert_called_with()
+        mock_order_stream_class.assert_called_with(
+            flumine=self.mock_flumine,
+            stream_id=mock_increment(),
+            streaming_timeout=streaming_timeout,
+            conflate_ms=conflate_ms,
+        )
+
     def test_start(self):
         mock_stream = mock.Mock()
         self.streams._streams = [mock_stream]
@@ -166,7 +181,7 @@ class TestBaseStream(unittest.TestCase):
     def setUp(self) -> None:
         self.mock_flumine = mock.Mock()
         self.stream = BaseStream(
-            self.mock_flumine, 123, {"test": "me"}, {"please": "now"}, 0.01, 100
+            self.mock_flumine, 123, 0.01, 100, {"test": "me"}, {"please": "now"}
         )
 
     def test_init(self):
@@ -203,7 +218,7 @@ class TestMarketStream(unittest.TestCase):
     def setUp(self) -> None:
         self.mock_flumine = mock.Mock()
         self.stream = streams.MarketStream(
-            self.mock_flumine, 123, {"test": "me"}, {"please": "now"}, 0.01, 100
+            self.mock_flumine, 123, 0.01, 100, {"test": "me"}, {"please": "now"}
         )
 
     def test_init(self):
@@ -226,7 +241,7 @@ class TestDataStream(unittest.TestCase):
     def setUp(self) -> None:
         self.mock_flumine = mock.Mock()
         self.stream = streams.DataStream(
-            self.mock_flumine, 123, {"test": "me"}, {"please": "now"}, 0.01, 100
+            self.mock_flumine, 123, 0.01, 100, {"test": "me"}, {"please": "now"}
         )
 
     def test_init(self):
@@ -242,9 +257,13 @@ class TestDataStream(unittest.TestCase):
             self.stream._listener.output_queue, self.mock_flumine.handler_queue
         )
 
-    # def test_run(self):
-    #     pass
-    #
+    @mock.patch("flumine.streams.marketstream.BaseStream.betting_client")
+    def test_run(self, mock_betting_client):
+        self.stream.run()
+        mock_betting_client.streaming.create_stream.assert_called_with(
+            listener=self.stream._listener, unique_id=123
+        )
+
     # def test_handle_output(self):
     #     pass
 
@@ -323,7 +342,7 @@ class TestHistoricalStream(unittest.TestCase):
     def setUp(self) -> None:
         self.mock_flumine = mock.Mock()
         self.stream = streams.HistoricalStream(
-            self.mock_flumine, 123, {"test": "me"}, {"please": "now"}, 0.01, 100
+            self.mock_flumine, 123, 0.01, 100, {"test": "me"}, {"please": "now"}
         )
 
     def test_init(self):
@@ -347,3 +366,24 @@ class TestHistoricalStream(unittest.TestCase):
         self.assertEqual(
             self.stream.create_generator(), mock_generator().get_generator()
         )
+
+
+class TestOrderStream(unittest.TestCase):
+    def setUp(self) -> None:
+        self.mock_flumine = mock.Mock()
+        self.stream = streams.OrderStream(self.mock_flumine, 123, 0.01, 100)
+
+    def test_init(self):
+        self.assertEqual(self.stream.flumine, self.mock_flumine)
+        self.assertEqual(self.stream.stream_id, 123)
+        self.assertIsNone(self.stream.market_filter)
+        self.assertIsNone(self.stream.market_data_filter)
+        self.assertEqual(self.stream.streaming_timeout, 0.01)
+        self.assertEqual(self.stream.conflate_ms, 100)
+        self.assertIsNone(self.stream._stream)
+
+    # def test_run(self):
+    #     pass
+    #
+    # def test_handle_output(self):
+    #     pass
diff --git a/tests/test_trade.py b/tests/test_trade.py
new file mode 100644
index 0000000..47017bc
--- /dev/null
+++ b/tests/test_trade.py
@@ -0,0 +1,87 @@
+import unittest
+from unittest import mock
+
+from flumine.order.trade import Trade, OrderError
+
+
+class TradeTest(unittest.TestCase):
+    def setUp(self) -> None:
+        self.mock_strategy = mock.Mock()
+        self.mock_fill_kill = mock.Mock()
+        self.mock_offset = mock.Mock()
+        self.mock_green = mock.Mock()
+        self.mock_stop = mock.Mock()
+        self.trade = Trade(
+            "1.234",
+            567,
+            self.mock_strategy,
+            self.mock_fill_kill,
+            self.mock_offset,
+            self.mock_green,
+            self.mock_stop,
+        )
+
+    def test_init(self):
+        self.assertEqual(self.trade.market_id, "1.234")
+        self.assertEqual(self.trade.selection_id, 567)
+        self.assertEqual(self.trade.strategy, self.mock_strategy)
+        self.assertEqual(self.trade.fill_kill, self.mock_fill_kill)
+        self.assertEqual(self.trade.offset, self.mock_offset)
+        self.assertEqual(self.trade.green, self.mock_green)
+        self.assertEqual(self.trade.stop, self.mock_stop)
+        self.assertEqual(self.trade.orders, [])
+
+    def test_create_order(self):
+        mock_order_type = mock.Mock()
+        mock_order_type.EXCHANGE = "SYM"
+        mock_order = mock.Mock()
+        mock_order.EXCHANGE = "SYM"
+        self.trade.create_order("BACK", mock_order_type, handicap=1, order=mock_order)
+        self.assertEqual(self.trade.orders, [mock_order()])
+
+    def test_create_order_error(self):
+        mock_order_type = mock.Mock()
+        mock_order_type.EXCHANGE = "SYM"
+        mock_order = mock.Mock()
+        mock_order.EXCHANGE = "MYS"
+        with self.assertRaises(OrderError):
+            self.trade.create_order(
+                "BACK", mock_order_type, handicap=1, order=mock_order
+            )
+
+    def test_create_order_replacement(self):
+        mock_order = mock.Mock()
+        mock_order._update = {"new_price": 12}
+        replacement_order = self.trade.create_order_replacement(mock_order)
+        self.assertEqual(self.trade.orders, [replacement_order])
+
+    def test_create_order_from_current_limit(self):
+        mock_current_order = mock.Mock()
+        mock_current_order.order_type = "LIMIT"
+        order = self.trade.create_order_from_current(mock_current_order, "12345")
+        self.assertEqual(order.bet_id, mock_current_order.bet_id)
+        self.assertEqual(order.id, "12345")
+        self.assertEqual(self.trade.orders, [order])
+
+    def test_create_order_from_current_limit_on_close(self):
+        mock_current_order = mock.Mock()
+        mock_current_order.order_type = "LIMIT_ON_CLOSE"
+        with self.assertRaises(NotImplementedError):
+            self.trade.create_order_from_current(mock_current_order, "12345")
+
+    def test_create_order_from_current_market_on_close(self):
+        mock_current_order = mock.Mock()
+        mock_current_order.order_type = "MARKET_ON_CLOSE"
+        with self.assertRaises(NotImplementedError):
+            self.trade.create_order_from_current(mock_current_order, "12345")
+
+    def test_info(self):
+        self.assertEqual(
+            self.trade.info,
+            {
+                "id": self.trade.id,
+                "orders": [],
+                "status": None,
+                "strategy": self.mock_strategy,
+            },
+        )
diff --git a/tests/test_tradingcontrols.py b/tests/test_tradingcontrols.py
new file mode 100644
index 0000000..2ec79f4
--- /dev/null
+++ b/tests/test_tradingcontrols.py
@@ -0,0 +1,269 @@
+import unittest
+from unittest import mock
+
+from flumine.controls.tradingcontrols import (
+    OrderValidation,
+    StrategyExposure,
+    OrderTypes,
+    ExchangeType,
+    OrderPackageType,
+)
+
+
+class TestOrderValidation(unittest.TestCase):
+    def setUp(self):
+        self.mock_flumine = mock.Mock()
+        self.mock_client = mock.Mock()
+        self.mock_client.min_bet_size = 2
+        self.mock_client.min_bsp_liability = 10
+        self.trading_control = OrderValidation(self.mock_flumine)
+
+    def test_init(self):
+        assert self.trading_control.NAME == "ORDER_VALIDATION"
+
+    @mock.patch(
+        "flumine.controls.tradingcontrols.OrderValidation._validate_betfair_order"
+    )
+    def test_validate_betfair(self, mock_validate_betfair_order):
+        order = mock.Mock()
+        order.order_type.ORDER_TYPE = OrderTypes.LIMIT
+        order.EXCHANGE = ExchangeType.BETFAIR
+        order.order_type.size = 12
+        order_package = [order]
+
+        self.trading_control._validate(order_package)
+        mock_validate_betfair_order.assert_called_with(order)
+
+    @mock.patch(
+        "flumine.controls.tradingcontrols.OrderValidation._validate_betfair_size"
+    )
+    @mock.patch(
+        "flumine.controls.tradingcontrols.OrderValidation._validate_betfair_price"
+    )
+    def test__validate_betfair_order_limit(
+        self, mock__validate_betfair_price, mock__validate_betfair_size
+    ):
+        order = mock.Mock()
+        order.order_type.ORDER_TYPE = OrderTypes.LIMIT
+        self.trading_control._validate_betfair_order(order)
+        mock__validate_betfair_price.assert_called_with(order)
+        mock__validate_betfair_size.assert_called_with(order)
+
+    @mock.patch(
+        "flumine.controls.tradingcontrols.OrderValidation._validate_betfair_price"
+    )
+    @mock.patch(
+        "flumine.controls.tradingcontrols.OrderValidation._validate_betfair_min_size"
+    )
+    @mock.patch(
+        "flumine.controls.tradingcontrols.OrderValidation._validate_betfair_liability"
+    )
+    def test__validate_betfair_order_limit_on_close(
+        self,
+        mock__validate_betfair_liability,
+        mock__validate_betfair_min_size,
+        mock__validate_betfair_price,
+    ):
+        order = mock.Mock()
+        order.order_type.ORDER_TYPE = OrderTypes.LIMIT_ON_CLOSE
+        self.trading_control._validate_betfair_order(order)
+        mock__validate_betfair_liability.assert_called_with(order)
+        mock__validate_betfair_min_size.assert_called_with(order)
+        mock__validate_betfair_price.assert_called_with(order)
+
+    @mock.patch(
+        "flumine.controls.tradingcontrols.OrderValidation._validate_betfair_min_size"
+    )
+    @mock.patch(
+        "flumine.controls.tradingcontrols.OrderValidation._validate_betfair_liability"
+    )
+    def test__validate_betfair_order_market_on_close(
+        self, mock__validate_betfair_liability, mock__validate_betfair_min_size
+    ):
+        order = mock.Mock()
+        order.order_type.ORDER_TYPE = OrderTypes.MARKET_ON_CLOSE
+        self.trading_control._validate_betfair_order(order)
+        mock__validate_betfair_liability.assert_called_with(order)
+        mock__validate_betfair_min_size.assert_called_with(order)
+
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_order_unknown(self, mock_on_error):
+        order = mock.Mock()
+        order.order_type.ORDER_TYPE = "RUSH"
+        self.trading_control._validate_betfair_order(order)
+        mock_on_error.assert_called_with(order)
+
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_size(self, mock_on_error):
+        order = mock.Mock()
+        order.order_type.size = 2
+        self.trading_control._validate_betfair_size(order)
+        mock_on_error.assert_not_called()
+
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_size_on_error(self, mock_on_error):
+        order = mock.Mock()
+        order.order_type.size = None
+        self.trading_control._validate_betfair_size(order)
+        mock_on_error.assert_called_with(order)
+
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_size_on_error_two(self, mock_on_error):
+        order = mock.Mock()
+        order.order_type.size = -1
+        self.trading_control._validate_betfair_size(order)
+        mock_on_error.assert_called_with(order)
+
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_size_on_error_three(self, mock_on_error):
+        order = mock.Mock()
+        order.order_type.size = 1.999
+        self.trading_control._validate_betfair_size(order)
+        mock_on_error.assert_called_with(order)
+
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_price(self, mock_on_error):
+        order = mock.Mock()
+        order.order_type.price = 2
+        self.trading_control._validate_betfair_price(order)
+        mock_on_error.assert_not_called()
+
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_price_on_error(self, mock_on_error):
+        order = mock.Mock()
+        order.order_type.price = None
+        self.trading_control._validate_betfair_price(order)
+        mock_on_error.assert_called_with(order)
+
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_price_on_error_two(self, mock_on_error):
+        order = mock.Mock()
+        order.order_type.price = -1
+        self.trading_control._validate_betfair_price(order)
+        mock_on_error.assert_called_with(order)
+
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_liability(self, mock_on_error):
+        order = mock.Mock()
+        order.order_type.liability = 2
+        self.trading_control._validate_betfair_liability(order)
+        mock_on_error.assert_not_called()
+
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_liability_on_error(self, mock_on_error):
+        order = mock.Mock()
+        order.order_type.liability = None
+        self.trading_control._validate_betfair_liability(order)
+        mock_on_error.assert_called_with(order)
+
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_liability_on_error_two(self, mock_on_error):
+        order = mock.Mock()
+        order.order_type.liability = -1
+        self.trading_control._validate_betfair_liability(order)
+        mock_on_error.assert_called_with(order)
+
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_min_size(self, mock_on_error):
+        order = mock.Mock()
+        order.client = self.mock_client
+        order.side = "BACK"
+        order.order_type.liability = 2
+        self.trading_control._validate_betfair_min_size(order)
+        mock_on_error.assert_not_called()
+
+    @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    def test__validate_betfair_min_size_two(self, mock_on_error):
+        order = mock.Mock()
+        order.client = self.mock_client
+        order.side = "LAY"
+        order.order_type.liability = 10
+        self.trading_control._validate_betfair_min_size(order)
+        mock_on_error.assert_not_called()
+
+    # @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    # def test__validate_betfair_min_size_on_error(self, mock_on_error):
+    #     order = mock.Mock()
+    #     order.client = self.mock_client
+    #     order.side = "BACK"
+    #     order.order_type.liability = 1.99
+    #     self.trading_control._validate_betfair_min_size(order)
+    #     mock_on_error.assert_called_with(order)
+    #
+    # @mock.patch("flumine.controls.tradingcontrols.OrderValidation._on_error")
+    # def test__validate_betfair_min_size_on_error_two(self, mock_on_error):
+    #     order = mock.Mock()
+    #     order.client = self.mock_client
+    #     order.side = "LAY"
+    #     order.order_type.liability = 9.99
+    #     self.trading_control._validate_betfair_min_size(order)
+    #     mock_on_error.assert_called_with(order)
+
+
+class TestStrategyExposure(unittest.TestCase):
+    def setUp(self):
+        self.mock_flumine = mock.Mock()
+        self.trading_control = StrategyExposure(self.mock_flumine)
+
+    def test_init(self):
+        self.assertEqual(self.trading_control.NAME, "STRATEGY_EXPOSURE")
+        self.assertEqual(self.trading_control.flumine, self.mock_flumine)
+
+    @mock.patch("flumine.controls.tradingcontrols.StrategyExposure._on_error")
+    def test_validate_limit(self, mock_on_error):
+        order = mock.Mock()
+        order.trade.strategy.max_order_exposure = 10
+        order.order_type.ORDER_TYPE = OrderTypes.LIMIT
+        order.side = "BACK"
+        order.order_type.size = 12
+        order_package = mock.Mock()
+        order_package.package_type = OrderPackageType.PLACE
+        order_package.__iter__ = mock.Mock(return_value=iter([order]))
+        self.trading_control._validate(order_package)
+        mock_on_error.assert_called_with(order)
+
+    @mock.patch("flumine.controls.tradingcontrols.StrategyExposure._on_error")
+    def test_validate_limit_on_close(self, mock_on_error):
+        order = mock.Mock()
+        order.trade.strategy.max_order_exposure = 10
+        order.order_type.ORDER_TYPE = OrderTypes.LIMIT_ON_CLOSE
+        order.side = "BACK"
+        order.order_type.liability = 12
+        order_package = mock.Mock()
+        order_package.package_type = OrderPackageType.PLACE
+        order_package.__iter__ = mock.Mock(return_value=iter([order]))
+        self.trading_control._validate(order_package)
+        mock_on_error.assert_called_with(order)
+
+    @mock.patch("flumine.controls.tradingcontrols.StrategyExposure._on_error")
+    def test_validate_market_on_close(self, mock_on_error):
+        order = mock.Mock()
+        order.trade.strategy.max_order_exposure = 10
+        order.order_type.ORDER_TYPE = OrderTypes.MARKET_ON_CLOSE
+        order.side = "BACK"
+        order.order_type.liability = 12
+        order_package = mock.Mock()
+        order_package.package_type = OrderPackageType.PLACE
+        order_package.__iter__ = mock.Mock(return_value=iter([order]))
+        self.trading_control._validate(order_package)
+        mock_on_error.assert_called_with(order)
+
+    @mock.patch("flumine.controls.tradingcontrols.StrategyExposure._on_error")
+    def test_validate_selection(self, mock_on_error):
+        mock_market = mock.Mock()
+        mock_market.blotter.selection_exposure.return_value = -10
+        self.mock_flumine.markets.markets = {"1.234": mock_market}
+
+        order = mock.Mock()
+        order.trade.strategy.max_order_exposure = 10
+        order.trade.strategy.max_selection_exposure = 10
+        order.order_type.ORDER_TYPE = OrderTypes.LIMIT
+        order.side = "BACK"
+        order.order_type.size = 2
+        order_package = mock.Mock()
+        order_package.market_id = "1.234"
+        order_package.package_type = OrderPackageType.PLACE
+        order_package.__iter__ = mock.Mock(return_value=iter([order]))
+
+        self.trading_control._validate(order_package)
+        mock_on_error.assert_called_with(order)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 735b092..a86780e 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -10,3 +10,36 @@ class UtilsTest(unittest.TestCase):
 
     def test_file_line_count(self):
         self.assertGreater(utils.file_line_count(__file__), 10)
+
+    def test_chunks(self):
+        self.assertEqual([i for i in utils.chunks([1, 2, 3], 1)], [[1], [2], [3]])
+
+    def test_create_cheap_hash(self):
+        self.assertEqual(
+            utils.create_cheap_hash("test"), utils.create_cheap_hash("test"),
+        )
+        self.assertEqual(len(utils.create_cheap_hash("test", 16)), 16)
+
+    def test_as_dec(self):
+        utils.as_dec(2.00)
+
+    # def test_arrange(self):
+    #     utils.arange()
+
+    def test_make_prices(self):
+        utils.make_prices(utils.MIN_PRICE, utils.CUTOFFS)
+
+    def test_calculate_exposure(self):
+        self.assertEqual(utils.calculate_exposure([], []), 0)
+        self.assertEqual(utils.calculate_exposure([(0, 0)], [(0, 0), (0, 0)]), 0)
+        self.assertEqual(utils.calculate_exposure([(5.6, 2)], []), -2)
+        self.assertEqual(utils.calculate_exposure([], [(5.6, 2)]), -9.2)
+        self.assertEqual(utils.calculate_exposure([], [(5.6, 2), (5.8, 2)]), -18.8)
+        self.assertEqual(utils.calculate_exposure([(5.6, 2)], [(5.6, 2)]), 0)
+        self.assertEqual(utils.calculate_exposure([(5.6, 2), (100, 20)], [(5.6, 2)]), 0)
+        self.assertEqual(
+            utils.calculate_exposure([(5.6, 2), (100, 20)], [(10, 1000)]), -7010.80
+        )
+        self.assertEqual(utils.calculate_exposure([(10, 2)], [(5, 2)]), 0)
+        self.assertEqual(utils.calculate_exposure([(10, 2)], [(5, 4)]), 0)
+        self.assertEqual(utils.calculate_exposure([(10, 2)], [(5, 8)]), -14)
diff --git a/tests/test_worker.py b/tests/test_worker.py
index 56b9bac..a684ca5 100644
--- a/tests/test_worker.py
+++ b/tests/test_worker.py
@@ -1,5 +1,6 @@
 import unittest
 from unittest import mock
+from betfairlightweight import BetfairError
 
 from flumine import worker
 
@@ -34,6 +35,30 @@ class WorkersTest(unittest.TestCase):
         worker.keep_alive(mock_client)
         mock_client.keep_alive.assert_called_with()
 
+    def test_keep_alive_failure(self):
+        mock_client = mock.Mock()
+        mock_client.betting_client.session_token = None
+        mock_response = mock.Mock()
+        mock_response.status = "FAILURE"
+        mock_client.betting_client.keep_alive.return_value = mock_response
+        worker.keep_alive(mock_client)
+        mock_client.login.assert_called_with()
+
+    def test_keep_alive_error(self):
+        mock_client = mock.Mock()
+        mock_client.betting_client.session_token = None
+        mock_client.betting_client.keep_alive.side_effect = BetfairError
+        worker.keep_alive(mock_client)
+        mock_client.login.assert_called_with()
+
+    def test_keep_alive_ka_error(self):
+        mock_client = mock.Mock()
+        mock_client.betting_client.session_token = 1
+        mock_client.betting_client.session_expired = True
+        mock_client.betting_client.keep_alive.side_effect = BetfairError
+        worker.keep_alive(mock_client)
+        mock_client.keep_alive.assert_called_with()
+
     @mock.patch("flumine.worker.MarketCatalogueEvent")
     def test_poll_market_catalogue(self, mock_event):
         mock_client = mock.Mock()