Repository: betcode-org/flumine
Issue: Strategy error handling

Currently an error in a user created strategy will break everything as it runs in the main thread, error handling needs to be added to:

```python
def call_check_market(strategy_check_market: Callable, market...)
    try:
        strategy_check_market(market...
```

```python
def call_process_market_book(strategy_process_market_book: Callable, market...)
    try:
        strategy_process_market_book(market...
```

And then add to baseflumine.

Disable strategy if errors?

Changed files:
flumine/baseflumine.py, tests/test_utils.py, flumine/utils.py, flumine/config.py, flumine/backtest/backtest.py

diff --git a/flumine/backtest/backtest.py b/flumine/backtest/backtest.py
index 223304d..2c7fdd3 100644
--- a/flumine/backtest/backtest.py
+++ b/flumine/backtest/backtest.py
@@ -3,7 +3,7 @@ import datetime
 
 from ..baseflumine import BaseFlumine
 from ..events import events
-from .. import config
+from .. import config, utils
 from ..clients import ExchangeType
 from ..exceptions import RunError
 from .utils import PendingPackages
@@ -87,8 +87,10 @@ class FlumineBacktest(BaseFlumine):
                 middleware(market)  # todo err handling?
 
             for strategy in self.strategies:
-                if strategy.check_market(market, market_book):
-                    strategy.process_market_book(market, market_book)
+                if utils.call_check_market(strategy.check_market, market, market_book):
+                    utils.call_process_market_book(
+                        strategy.process_market_book, market, market_book
+                    )
 
             # process current orders
             blotter = market.blotter
diff --git a/flumine/baseflumine.py b/flumine/baseflumine.py
index 9332acd..fa526df 100644
--- a/flumine/baseflumine.py
+++ b/flumine/baseflumine.py
@@ -17,7 +17,7 @@ from .order.process import process_current_orders
 from .controls.clientcontrols import BaseControl, MaxOrderCount
 from .controls.tradingcontrols import OrderValidation, StrategyExposure
 from .controls.loggingcontrols import LoggingControl
-from . import config
+from . import config, utils
 
 
 logger = logging.getLogger(__name__)
@@ -122,8 +122,10 @@ class BaseFlumine:
                 middleware(market)  # todo err handling?
 
             for strategy in self.strategies:
-                if strategy.check_market(market, market_book):
-                    strategy.process_market_book(market, market_book)
+                if utils.call_check_market(strategy.check_market, market, market_book):
+                    utils.call_process_market_book(
+                        strategy.process_market_book, market, market_book
+                    )
 
             self._process_market_orders(market)
 
diff --git a/flumine/config.py b/flumine/config.py
index b202c36..122ba06 100644
--- a/flumine/config.py
+++ b/flumine/config.py
@@ -10,3 +10,5 @@ hostname = socket.gethostname()[
 process_id = os.getpid()  # process id of app
 
 current_time = None  # used for backtesting
+
+raise_errors = False  # used for call_check_market / call_process_market_book
diff --git a/flumine/utils.py b/flumine/utils.py
index f920b3b..869cd6b 100644
--- a/flumine/utils.py
+++ b/flumine/utils.py
@@ -1,10 +1,13 @@
 import uuid
 import logging
 import hashlib
-from typing import Optional, Tuple
+from typing import Optional, Tuple, Callable
 from decimal import Decimal
 from betfairlightweight.resources.bettingresources import RunnerBook
 
+from . import config
+from .exceptions import FlumineException
+
 logger = logging.getLogger(__name__)
 
 CUTOFFS = (
@@ -139,3 +142,44 @@ def wap(matched: list) -> Tuple[float, float]:
         return 0, 0
     else:
         return round(b, 2), round(a / b, 2)
+
+
+def call_check_market(strategy_check_market: Callable, market, market_book) -> bool:
+    try:
+        return strategy_check_market(market, market_book)
+    except FlumineException as e:
+        logger.error(
+            "FlumineException %s in strategy_check_market %s %s"
+            % (e, strategy_check_market, market.market_id),
+            exc_info=True,
+        )
+    except Exception as e:
+        logger.critical(
+            "Unknown error %s in strategy_check_market %s %s"
+            % (e, strategy_check_market, market.market_id),
+            exc_info=True,
+        )
+        if config.raise_errors:
+            raise
+    return False
+
+
+def call_process_market_book(
+    strategy_process_market_book: Callable, market, market_book
+) -> None:
+    try:
+        strategy_process_market_book(market, market_book)
+    except FlumineException as e:
+        logger.error(
+            "FlumineException %s in strategy_process_market_book %s %s"
+            % (e, strategy_process_market_book, market.market_id),
+            exc_info=True,
+        )
+    except Exception as e:
+        logger.critical(
+            "Unknown error %s in strategy_process_market_book %s %s"
+            % (e, strategy_process_market_book, market.market_id),
+            exc_info=True,
+        )
+        if config.raise_errors:
+            raise
diff --git a/tests/test_utils.py b/tests/test_utils.py
index ef4f2d7..0fae954 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,10 +1,14 @@
+import logging
 import unittest
 from unittest import mock
 
-from flumine import utils
+from flumine import utils, FlumineException
 
 
 class UtilsTest(unittest.TestCase):
+    def setUp(self) -> None:
+        logging.disable(logging.CRITICAL)
+
     def test_create_short_uuid(self):
         self.assertTrue(utils.create_short_uuid())
 
@@ -103,3 +107,67 @@ class UtilsTest(unittest.TestCase):
         self.assertEqual(utils.wap([(1.5, 100), (1.6, 100)]), (200, 1.55))
         self.assertEqual(utils.wap([]), (0, 0))
         self.assertEqual(utils.wap([(1.5, 0)]), (0, 0))
+
+    def test_call_check_market(self):
+        mock_strategy_check = mock.Mock()
+        mock_market = mock.Mock()
+        mock_market_book = mock.Mock()
+        utils.call_check_market(mock_strategy_check, mock_market, mock_market_book)
+        mock_strategy_check.assert_called_with(mock_market, mock_market_book)
+
+    def test_call_check_market_flumine_error(self):
+        mock_strategy_check = mock.Mock(side_effect=FlumineException)
+        mock_market = mock.Mock()
+        mock_market_book = mock.Mock()
+        self.assertFalse(
+            utils.call_check_market(mock_strategy_check, mock_market, mock_market_book)
+        )
+        mock_strategy_check.assert_called_with(mock_market, mock_market_book)
+
+    def test_call_check_market_error(self):
+        mock_strategy_check = mock.Mock(side_effect=ValueError)
+        mock_market = mock.Mock()
+        mock_market_book = mock.Mock()
+        self.assertFalse(
+            utils.call_check_market(mock_strategy_check, mock_market, mock_market_book)
+        )
+        mock_strategy_check.assert_called_with(mock_market, mock_market_book)
+
+    @mock.patch("flumine.utils.config")
+    def test_call_check_market_raise(self, mock_config):
+        mock_config.raise_errors = True
+        mock_strategy_check = mock.Mock(side_effect=ValueError)
+        mock_market = mock.Mock()
+        mock_market_book = mock.Mock()
+        with self.assertRaises(ValueError):
+            utils.call_check_market(mock_strategy_check, mock_market, mock_market_book)
+
+    def test_call_process_market_book(self):
+        mock_strategy = mock.Mock()
+        mock_market = mock.Mock()
+        mock_market_book = mock.Mock()
+        utils.call_process_market_book(mock_strategy, mock_market, mock_market_book)
+        mock_strategy.assert_called_with(mock_market, mock_market_book)
+
+    def test_call_process_market_book_flumine_error(self):
+        mock_strategy = mock.Mock(side_effect=FlumineException)
+        mock_market = mock.Mock()
+        mock_market_book = mock.Mock()
+        utils.call_process_market_book(mock_strategy, mock_market, mock_market_book)
+        mock_strategy.assert_called_with(mock_market, mock_market_book)
+
+    def test_call_process_market_book_error(self):
+        mock_strategy = mock.Mock(side_effect=ZeroDivisionError)
+        mock_market = mock.Mock()
+        mock_market_book = mock.Mock()
+        utils.call_process_market_book(mock_strategy, mock_market, mock_market_book)
+        mock_strategy.assert_called_with(mock_market, mock_market_book)
+
+    @mock.patch("flumine.utils.config")
+    def test_call_process_market_book_raise(self, mock_config):
+        mock_config.raise_errors = True
+        mock_strategy = mock.Mock(side_effect=ZeroDivisionError)
+        mock_market = mock.Mock()
+        mock_market_book = mock.Mock()
+        with self.assertRaises(ZeroDivisionError):
+            utils.call_process_market_book(mock_strategy, mock_market, mock_market_book)