Repository: liampauling/flumine
Pull request #237 (2020-07-20 07:43:38)

paper trade #180
simulated order stream added
client.paper_trade added
integrated into streams and client


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index c18535c..6edb55b 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,18 @@
 Release History
 ---------------
 
+1.10.0 (2020-07-20)
++++++++++++++++++++
+
+**Improvements**
+
+- #180 client paper trade now implemented
+- #193 initial work on multi client implementation
+
+**Bug Fixes**
+
+-
+
 1.9.3 (2020-07-17)
 +++++++++++++++++++
 
diff --git a/README.md b/README.md
index 5e13f78..c29a21a 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ Currently tested on Python 3.6, 3.7 and 3.8.
 $ pip install flumine
 ```
 
-flumine requires Python 3.5+
+flumine requires Python 3.6+
 
 ## setup
 
@@ -112,7 +112,7 @@ framework.run()
 - Streaming
 - Multiple strategies
 - Order execution
-- Paper trading (in development)
+- Paper trading
 - Back testing
 - Analytics (in development)
 - Middleware and background workers to enable Scores / RaceCard / InPlayService
diff --git a/docs/advanced.md b/docs/advanced.md
index a5241ef..622523c 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -37,6 +37,7 @@ The Flumine class can be adapted by overriding the following functions:
 - `context` Dictionary object where any extra data can be stored here such as triggers
 - `max_selection_exposure` Max exposure per selection (including new order), note this does __not__ handle reduction in exposure due to laying another runner
 - `max_order_exposure` Max exposure per order
+- `client` Strategy client, half implemented when flumine will be migrated to multi clients
 
 ### Functions
 
diff --git a/docs/index.md b/docs/index.md
index 18b66e6..eb98fba 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -38,7 +38,7 @@ Currently tested on Python 3.6, 3.7 and 3.8.
 $ pip install flumine
 ```
 
-flumine requires Python 3.5+
+flumine requires Python 3.6+
 
 ## setup
 
@@ -129,7 +129,7 @@ framework.run()
 - Streaming
 - Multiple strategies
 - Order execution
-- Paper trading (in development)
+- Paper trading
 - Back testing
 - Analytics (in development)
 - Middleware and background workers to enable Scores / RaceCard / InPlayService
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 1a1155f..03ec30e 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -14,9 +14,7 @@ from flumine import Flumine, clients
 trading = betfairlightweight.APIClient("username")
 client = clients.BetfairClient(trading)
 
-framework = Flumine(
-    client=client,
-)
+framework = Flumine(client=client)
 ```
 
 !!! note
@@ -95,6 +93,31 @@ strategy = ExampleDataStrategy(
 flumine.add_strategy(strategy)
 ```
 
+## Paper Trading
+
+Flumine can be used to paper trade strategies live using the following code:
+
+```python
+import betfairlightweight
+from flumine import Flumine, clients
+
+trading = betfairlightweight.APIClient("username")
+client = clients.BetfairClient(trading, paper_trade=True)
+framework = Flumine(client=client)
+
+strategy = ExampleStrategy(
+    market_filter={"markets": ["/tmp/marketdata/1.170212754"]}
+)
+framework.add_strategy(strategy)
+
+framework.run()
+```
+
+Market data will be recieved as per live but any orders will use Simulated execution and Simulated order polling to replicate live trading.
+
+!!! tip
+    This can be handy when testing strategies as the betfair website can be used to validate the market.
+
 ## Backtesting
 
 Flumine can be used to backtest strategies using the following code:
diff --git a/flumine/__version__.py b/flumine/__version__.py
index 8e7caac..66bae86 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.9.3"
+__version__ = "1.10.0"
 __author__ = "Liam Pauling"
 __license__ = "MIT"
diff --git a/flumine/baseflumine.py b/flumine/baseflumine.py
index f9be4b2..22f8f1d 100644
--- a/flumine/baseflumine.py
+++ b/flumine/baseflumine.py
@@ -73,10 +73,11 @@ class BaseFlumine:
     def run(self) -> None:
         raise NotImplementedError
 
-    def add_strategy(self, strategy: BaseStrategy) -> None:
+    def add_strategy(self, strategy: BaseStrategy, client: BaseClient = None) -> None:
         logger.info("Adding strategy {0}".format(strategy))
+        _client = client or self.client
         self.streams(strategy)  # create required streams
-        self.strategies(strategy)  # store in strategies
+        self.strategies(strategy, _client)  # store in strategies
         self.log_control(events.StrategyEvent(strategy))
 
     def add_worker(self, worker: BackgroundWorker) -> None:
diff --git a/flumine/clients/baseclient.py b/flumine/clients/baseclient.py
index 713c176..21fa30b 100644
--- a/flumine/clients/baseclient.py
+++ b/flumine/clients/baseclient.py
@@ -25,6 +25,7 @@ class BaseClient:
         id_: str = None,
         order_stream: bool = True,
         best_price_execution: bool = True,
+        paper_trade: bool = False,
     ):
         self.id = id_ or create_short_uuid()
         self.betting_client = betting_client
@@ -34,6 +35,7 @@ class BaseClient:
         self.interactive_login = interactive_login
         self.order_stream = order_stream
         self.best_price_execution = best_price_execution
+        self.paper_trade = paper_trade
 
         self.account_details = None
         self.account_funds = None
@@ -56,7 +58,7 @@ class BaseClient:
         raise NotImplementedError
 
     def add_execution(self, flumine) -> None:
-        if self.EXCHANGE == ExchangeType.SIMULATED:
+        if self.EXCHANGE == ExchangeType.SIMULATED or self.paper_trade:
             self.execution = flumine.simulated_execution
         elif self.EXCHANGE == ExchangeType.BETFAIR:
             self.execution = flumine.betfair_execution
@@ -83,4 +85,5 @@ class BaseClient:
             "trading_controls": self.trading_controls,
             "order_stream": self.order_stream,
             "best_price_execution": self.best_price_execution,
+            "paper_trade": self.paper_trade,
         }
diff --git a/flumine/order/trade.py b/flumine/order/trade.py
index 4fcb151..b86f025 100644
--- a/flumine/order/trade.py
+++ b/flumine/order/trade.py
@@ -134,6 +134,11 @@ class Trade:
         self.orders.append(order)
         return order
 
+    @property
+    def client(self):
+        # 193 todo trade.client
+        return self.strategy.client
+
     @property
     def notes_str(self) -> str:
         return ",".join(str(x) for x in self.notes.values())
diff --git a/flumine/strategy/strategy.py b/flumine/strategy/strategy.py
index d38166d..199bbeb 100644
--- a/flumine/strategy/strategy.py
+++ b/flumine/strategy/strategy.py
@@ -6,6 +6,7 @@ from ..streams.marketstream import BaseStream, MarketStream
 from ..markets.market import Market
 from .runnercontext import RunnerContext
 from ..utils import create_cheap_hash
+from ..clients import BaseClient
 
 DEFAULT_MARKET_DATA_FILTER = filters.streaming_market_data_filter(
     fields=[
@@ -32,6 +33,7 @@ class BaseStrategy:
         context: dict = None,
         max_selection_exposure: float = 100,
         max_order_exposure: float = 10,
+        client: BaseClient = None,
     ):
         """
         Processes data from streams.
@@ -55,6 +57,7 @@ class BaseStrategy:
         self.context = context or {}
         self.max_selection_exposure = max_selection_exposure
         self.max_order_exposure = max_order_exposure
+        self.client = client
 
         self._invested = {}  # {(marketId, selectionId, handicap): RunnerContext}
         self.streams = []  # list of streams strategy is subscribed
@@ -172,7 +175,8 @@ class Strategies:
     def __init__(self):
         self._strategies = []
 
-    def __call__(self, strategy: BaseStrategy) -> None:
+    def __call__(self, strategy: BaseStrategy, client: BaseClient) -> None:
+        strategy.client = client
         self._strategies.append(strategy)
         strategy.add()
 
diff --git a/flumine/streams/simulatedorderstream.py b/flumine/streams/simulatedorderstream.py
new file mode 100644
index 0000000..3ed6798
--- /dev/null
+++ b/flumine/streams/simulatedorderstream.py
@@ -0,0 +1,37 @@
+import time
+import logging
+
+from .basestream import BaseStream
+from ..events.events import CurrentOrdersEvent
+
+logger = logging.getLogger(__name__)
+
+
+class CurrentOrders:
+    def __init__(self, orders):
+        self.orders = orders
+        self.more_available = False
+
+
+class SimulatedOrderStream(BaseStream):
+    def run(self) -> None:
+        logger.info("Starting SimulatedOrderStream")
+
+        while self.is_alive():
+            if self.flumine.markets.live_orders:
+                current_orders = self._get_current_orders()
+                if current_orders:
+                    self.flumine.handler_queue.put(
+                        CurrentOrdersEvent([CurrentOrders(current_orders)])
+                    )
+            time.sleep(self.streaming_timeout)
+        logger.info("Stopped SimulatedOrderStream {0}".format(self.stream_id))
+
+    def _get_current_orders(self) -> list:
+        current_orders = []
+        for market in self.flumine.markets:
+            if market.closed is False:
+                for order in market.blotter:
+                    if order.simulated and order.trade.client == self.client:
+                        current_orders.append(order)
+        return current_orders
diff --git a/flumine/streams/streams.py b/flumine/streams/streams.py
index c538c63..5f0f20c 100644
--- a/flumine/streams/streams.py
+++ b/flumine/streams/streams.py
@@ -6,6 +6,7 @@ from .marketstream import MarketStream
 from .datastream import DataStream
 from .historicalstream import HistoricalStream
 from .orderstream import OrderStream
+from .simulatedorderstream import SimulatedOrderStream
 from ..clients import ExchangeType, BaseClient
 
 logger = logging.getLogger(__name__)
@@ -35,8 +36,9 @@ class Streams:
 
     def add_client(self, client: BaseClient) -> None:
         if client.order_stream:
-            # todo if paper_trade: add_simulated_order_stream()
-            if client.EXCHANGE == ExchangeType.BETFAIR:
+            if client.paper_trade:
+                self.add_simulated_order_stream(client)
+            elif client.EXCHANGE == ExchangeType.BETFAIR:
                 self.add_order_stream(client)
 
     """ market data """
@@ -119,8 +121,25 @@ class Streams:
         self._streams.append(stream)
         return stream
 
-    def add_simulated_order_stream(self):  # todo
-        return
+    def add_simulated_order_stream(
+        self,
+        client: BaseClient,
+        conflate_ms: int = None,
+        streaming_timeout: float = 0.25,
+    ) -> SimulatedOrderStream:
+        logger.warning(
+            "Client {0} now paper trading".format(client.betting_client.username)
+        )
+        stream_id = self._increment_stream_id()
+        stream = SimulatedOrderStream(
+            flumine=self.flumine,
+            stream_id=stream_id,
+            conflate_ms=conflate_ms,
+            streaming_timeout=streaming_timeout,
+            client=client,
+        )
+        self._streams.append(stream)
+        return stream
 
     def start(self) -> None:
         if not self.flumine.BACKTEST:


Test patch:
diff --git a/flumine/backtest/simulated.py b/flumine/backtest/simulated.py
index f5e84c5..668e4dd 100644
--- a/flumine/backtest/simulated.py
+++ b/flumine/backtest/simulated.py
@@ -270,4 +270,4 @@ class Simulated:
             return 0.0
 
     def __bool__(self):
-        return config.simulated
+        return config.simulated or self.order.trade.client.paper_trade
diff --git a/tests/test_baseflumine.py b/tests/test_baseflumine.py
index 0bc16e2..c875fe5 100644
--- a/tests/test_baseflumine.py
+++ b/tests/test_baseflumine.py
@@ -6,7 +6,7 @@ from flumine.baseflumine import BaseFlumine
 
 class BaseFlumineTest(unittest.TestCase):
     def setUp(self):
-        self.mock_client = mock.Mock()
+        self.mock_client = mock.Mock(paper_trade=False)
         self.base_flumine = BaseFlumine(self.mock_client)
 
     def test_init(self):
@@ -26,7 +26,8 @@ class BaseFlumineTest(unittest.TestCase):
     @mock.patch("flumine.baseflumine.BaseFlumine.log_control")
     def test_add_strategy(self, mock_log_control, mock_events):
         mock_strategy = mock.Mock()
-        self.base_flumine.add_strategy(mock_strategy)
+        mock_client = mock.Mock()
+        self.base_flumine.add_strategy(mock_strategy, mock_client)
         self.assertEqual(len(self.base_flumine.strategies), 1)
         self.assertEqual(len(self.base_flumine.streams), 1)
         mock_log_control.assert_called_with(mock_events.StrategyEvent(mock_strategy))
diff --git a/tests/test_clients.py b/tests/test_clients.py
index c0adbfd..059efe6 100644
--- a/tests/test_clients.py
+++ b/tests/test_clients.py
@@ -29,6 +29,7 @@ class BaseClientTest(unittest.TestCase):
         self.assertEqual(self.base_client.trading_controls, [])
         self.assertTrue(self.base_client.order_stream)
         self.assertTrue(self.base_client.best_price_execution)
+        self.assertFalse(self.base_client.paper_trade)
 
     def test_login(self):
         with self.assertRaises(NotImplementedError):
@@ -55,6 +56,13 @@ class BaseClientTest(unittest.TestCase):
         self.base_client.add_execution(mock_flumine)
         self.assertEqual(self.base_client.execution, mock_flumine.betfair_execution)
 
+    def test_add_execution_paper(self):
+        self.base_client.paper_trade = True
+        self.base_client.EXCHANGE = ExchangeType.BETFAIR
+        mock_flumine = mock.Mock()
+        self.base_client.add_execution(mock_flumine)
+        self.assertEqual(self.base_client.execution, mock_flumine.simulated_execution)
+
     def test_min_bet_size(self):
         with self.assertRaises(NotImplementedError):
             assert self.base_client.min_bet_size
diff --git a/tests/test_order.py b/tests/test_order.py
index 0b3e181..17b2977 100644
--- a/tests/test_order.py
+++ b/tests/test_order.py
@@ -14,7 +14,8 @@ from flumine.exceptions import OrderUpdateError
 
 class BaseOrderTest(unittest.TestCase):
     def setUp(self) -> None:
-        self.mock_trade = mock.Mock()
+        mock_client = mock.Mock(paper_trade=False)
+        self.mock_trade = mock.Mock(client=mock_client)
         self.mock_order_type = mock.Mock()
         self.order = BaseOrder(self.mock_trade, "BACK", self.mock_order_type, 1)
 
@@ -230,7 +231,8 @@ class BaseOrderTest(unittest.TestCase):
 
 class BetfairOrderTest(unittest.TestCase):
     def setUp(self) -> None:
-        self.mock_trade = mock.Mock()
+        mock_client = mock.Mock(paper_trade=False)
+        self.mock_trade = mock.Mock(client=mock_client)
         self.mock_status = mock.Mock()
         self.mock_order_type = mock.Mock()
         self.order = BetfairOrder(self.mock_trade, "BACK", self.mock_order_type)
diff --git a/tests/test_simulated.py b/tests/test_simulated.py
index 464ac6b..40fabc7 100644
--- a/tests/test_simulated.py
+++ b/tests/test_simulated.py
@@ -10,8 +10,14 @@ class SimulatedTest(unittest.TestCase):
         self.mock_order_type = mock.Mock(
             price=12, size=2.00, ORDER_TYPE=OrderTypes.LIMIT
         )
+        mock_client = mock.Mock(paper_trade=False)
+        mock_trade = mock.Mock(client=mock_client)
         self.mock_order = mock.Mock(
-            selection_id=1234, handicap=1, side="BACK", order_type=self.mock_order_type
+            selection_id=1234,
+            handicap=1,
+            side="BACK",
+            order_type=self.mock_order_type,
+            trade=mock_trade,
         )
         self.simulated = simulated.Simulated(self.mock_order)
 
@@ -476,3 +482,9 @@ class SimulatedTest(unittest.TestCase):
 
         config.simulated = True
         self.assertTrue(self.simulated)
+        config.simulated = False
+
+    def test_bool_paper_trade(self):
+        self.assertFalse(self.simulated)
+        self.simulated.order.trade.client.paper_trade = True
+        self.assertTrue(self.simulated)
diff --git a/tests/test_strategy.py b/tests/test_strategy.py
index 906315f..f271bf1 100644
--- a/tests/test_strategy.py
+++ b/tests/test_strategy.py
@@ -13,9 +13,11 @@ class StrategiesTest(unittest.TestCase):
 
     def test_call(self):
         mock_strategy = mock.Mock()
-        self.strategies(mock_strategy)
+        mock_client = mock.Mock()
+        self.strategies(mock_strategy, mock_client)
         self.assertEqual(self.strategies._strategies, [mock_strategy])
         mock_strategy.add.assert_called_with()
+        mock_strategy.client = mock_client
 
     def test_start(self):
         mock_strategy = mock.Mock()
@@ -37,6 +39,7 @@ class BaseStrategyTest(unittest.TestCase):
         self.mock_market_data_filter = mock.Mock()
         self.streaming_timeout = 2
         self.conflate_ms = 100
+        self.mock_client = mock.Mock()
         self.strategy = strategy.BaseStrategy(
             market_filter=self.mock_market_filter,
             market_data_filter=self.mock_market_data_filter,
@@ -47,6 +50,7 @@ class BaseStrategyTest(unittest.TestCase):
             context={"trigger": 0.123},
             max_selection_exposure=1,
             max_order_exposure=2,
+            client=self.mock_client,
         )
 
     def test_init(self):
@@ -59,6 +63,7 @@ class BaseStrategyTest(unittest.TestCase):
         self.assertEqual(self.strategy.context, {"trigger": 0.123})
         self.assertEqual(self.strategy.max_selection_exposure, 1)
         self.assertEqual(self.strategy.max_order_exposure, 2)
+        self.assertEqual(self.strategy.client, self.mock_client)
 
     def test_check_market_no_subscribed(self):
         mock_market = mock.Mock()
diff --git a/tests/test_streams.py b/tests/test_streams.py
index 04f48b7..84ef9d4 100644
--- a/tests/test_streams.py
+++ b/tests/test_streams.py
@@ -1,10 +1,10 @@
 import unittest
 from unittest import mock
 
-from flumine.streams import streams, datastream
+from flumine.streams import streams, datastream, historicalstream
 from flumine.streams.basestream import BaseStream
+from flumine.streams.simulatedorderstream import CurrentOrders
 from flumine.exceptions import ListenerError
-from flumine.streams import historicalstream
 
 
 class StreamsTest(unittest.TestCase):
@@ -64,11 +64,18 @@ class StreamsTest(unittest.TestCase):
 
     @mock.patch("flumine.streams.streams.Streams.add_order_stream")
     def test_add_client_betfair(self, mock_add_order_stream):
-        mock_client = mock.Mock(order_stream=True)
+        mock_client = mock.Mock(order_stream=True, paper_trade=False)
         mock_client.EXCHANGE = streams.ExchangeType.BETFAIR
         self.streams.add_client(mock_client)
         mock_add_order_stream.assert_called_with(mock_client)
 
+    @mock.patch("flumine.streams.streams.Streams.add_simulated_order_stream")
+    def test_add_client_paper_trade(self, mock_add_simulated_order_stream):
+        mock_client = mock.Mock(order_stream=True, paper_trade=True)
+        mock_client.EXCHANGE = streams.ExchangeType.BETFAIR
+        self.streams.add_client(mock_client)
+        mock_add_simulated_order_stream.assert_called_with(mock_client)
+
     @mock.patch("flumine.streams.streams.Streams.add_order_stream")
     def test_add_client_no_order_stream(self, mock_add_order_stream):
         mock_client = mock.Mock(order_stream=False)
@@ -171,6 +178,25 @@ class StreamsTest(unittest.TestCase):
             client=mock_client,
         )
 
+    @mock.patch("flumine.streams.streams.SimulatedOrderStream")
+    @mock.patch("flumine.streams.streams.Streams._increment_stream_id")
+    def test_add_simulated_order_stream(self, mock_increment, mock_order_stream_class):
+        conflate_ms = 500
+        streaming_timeout = 0.5
+        mock_client = mock.Mock()
+        self.streams.add_simulated_order_stream(
+            mock_client, 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,
+            client=mock_client,
+        )
+
     def test_start(self):
         mock_stream = mock.Mock()
         self.streams._streams = [mock_stream]
@@ -527,3 +553,36 @@ class TestOrderStream(unittest.TestCase):
     #
     # def test_handle_output(self):
     #     pass
+
+
+class TestSimulatedOrderStream(unittest.TestCase):
+    def setUp(self) -> None:
+        self.mock_flumine = mock.Mock()
+        self.stream = streams.SimulatedOrderStream(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_current_orders(self):
+        current_orders = CurrentOrders([1])
+        self.assertEqual(current_orders.orders, [1])
+        self.assertFalse(current_orders.more_available)
+
+    # def test_run(self):
+    #     pass
+
+    def test__get_current_orders(self):
+        mock_market = mock.Mock(closed=False)
+        order_one = mock.Mock(simulated=True)
+        order_one.trade.client = self.stream.client
+        order_two = mock.Mock(simulated=True)
+        order_three = mock.Mock(simulated=True)
+        mock_market.blotter = [order_one, order_two, order_three]
+        self.stream.flumine.markets = [mock_market, mock.Mock(closed=True)]
+        self.assertEqual(self.stream._get_current_orders(), [order_one])
diff --git a/tests/test_trade.py b/tests/test_trade.py
index 284b881..074f90e 100644
--- a/tests/test_trade.py
+++ b/tests/test_trade.py
@@ -9,7 +9,8 @@ from flumine.order.trade import Trade, OrderError, TradeStatus
 class TradeTest(unittest.TestCase):
     def setUp(self) -> None:
         logging.disable(logging.CRITICAL)
-        self.mock_strategy = mock.Mock()
+        mock_client = mock.Mock(paper_trade=False)
+        self.mock_strategy = mock.Mock(client=mock_client)
         self.mock_fill_kill = mock.Mock()
         self.mock_offset = mock.Mock()
         self.mock_green = mock.Mock()
@@ -122,6 +123,9 @@ class TradeTest(unittest.TestCase):
         with self.assertRaises(NotImplementedError):
             self.trade.create_order_from_current(mock_current_order, "12345")
 
+    def test_client(self):
+        self.assertEqual(self.trade.client, self.mock_strategy.client)
+
     def test_notes_str(self):
         self.trade.notes = collections.OrderedDict({"1": 1, 2: "2", 3: 3, 4: "four"})
         # self.assertEqual(self.trade.notes_str, "1,2,3,four")