diff --git a/Makefile b/Makefile index a9fc1c3..b2efd31 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ doc: rm -rf docs git add --all git commit -m "Update Documentation" - git checkout master + git checkout dev # lint code lint: diff --git a/pyback/feed.py b/pyback/feed.py new file mode 100644 index 0000000..54f0bf3 --- /dev/null +++ b/pyback/feed.py @@ -0,0 +1,96 @@ +import logging + +log = logging.getLogger(__name__) + +import heapq +import itertools +from typing import Iterable, Tuple, Dict, Optional + + +class Feed(object): + """Feed is a queue structure where each element is served in order of priority. + + Elements in the feed are popped based on the priority with higher priority + elements being served before lower priority elements. If two elements have + the same priority, they will be served in the order they were added to the + queue. + + Attributes: + queue (list): Nodes added to the priority queue. + """ + + def __init__(self): + """Initialize a new Feed.""" + + self.queue = [] + self.counter = itertools.count() + + def pop(self) -> Tuple[int, Dict]: + """ + Pop top priority data from feed. + + Returns: + The data with the highest priority. + """ + + priorty, _count, data = heapq.heappop(self.queue) + return priorty, data + + def __iter__(self) -> Iterable: + """Queue iterator.""" + + return iter(sorted(self.queue)) + + def __str__(self) -> str: + """Feed to string.""" + + return f"Feed: {self.queue}" + + def append(self, data: Dict, priority: Optional[int] = 0): + """ + Append a data point to the feed. + + Args: + data: data to add to the feed. + """ + + count = next(self.counter) + heapq.heappush(self.queue, [priority, count, data]) + + def __contains__(self, key: Dict): + """ + Containment Check operator for 'in' + + Args: + key: The key to check for in the feed. + + Returns: + True if key is found in feed, False otherwise. + """ + + return key in [n[-1] for n in self.queue] + + def size(self) -> int: + """ + Get the current size of the feed. + + Returns: + Integer of number of items in queue. + """ + + return len(self.queue) + + def clear(self): + """Reset queue to empty.""" + + self.queue = [] + + def top(self) -> Tuple[int, int, Dict]: + """ + Get the top item in the queue. + + Returns: + The first item stored in the queue. + """ + + return self.queue[0] diff --git a/pyback/strategy.py b/pyback/strategy.py index 0a70dfd..20199dd 100644 --- a/pyback/strategy.py +++ b/pyback/strategy.py @@ -1,11 +1,13 @@ import logging -from typing import Union, List log = logging.getLogger(__name__) +import itertools +from typing import Union, List, Callable, Optional + class Strategy: - """ Holds the trading algorithm. A strategy consumes data feeds + """Holds the trading algorithm. A strategy consumes data feeds and returns trading orders. Args: @@ -15,11 +17,15 @@ class Strategy: def __init__(self, name: str): log.info(f"Initiating Strategy. name={name}") + self.name = name - self.feed = set() + self.feed = list() + self.algo = None + self.orders = list() + self.order_counter = itertools.count(1) def subscribe(self, feed_id: Union[str, List[str]]): - """Subscribe to data feed + """Subscribes to data feed Args: feed_id: A single, or a list of ``feed_id`` to subscribe to. @@ -30,4 +36,51 @@ def subscribe(self, feed_id: Union[str, List[str]]): feed_id = [feed_id] log.info(f"Adding feed. feed_id={feed_id}") - self.feed.update(feed_id) + self.feed += feed_id + + def set_algo(self, algo: Callable): + """Sets the strategy's trading algorithm. + + Args: + algo: An algorithm which consumes data feed and produces + trading signals + """ + + log.info(f"Setting algorithm for strategy. name={self.name}") + self.algo = algo + + def order( + self, + security_id: str, + weight: float, + limit: Optional[float] = None, + stop: Optional[float] = None, + ) -> str: + """Creates a trade order + + Args: + security: unique identifier of the security to trade + weight: amount to order, as a percentage of the portfolio value; + If ``weight`` is positive, this means the weight of the security + to buy. Otherwise it means the the weight of the security to sell. + limit: The limit price of the order. Defaults to None. + stop: The stop price of the order. Defaults to None. + + Returns: + str: a unique identifier of the order + """ + + order_id = f"{self.name}_{next(self.order_counter)}" + + order = { + "order_id": order_id, + "security_id": security_id, + "weight": weight, + "limit": limit, + "stop": stop, + } + + log.info(f"Adding order. order={order}") + self.orders.append(order) + + return order_id diff --git a/tests/unit/test_strategy.py b/tests/unit/test_strategy.py index 77cea3a..5742185 100644 --- a/tests/unit/test_strategy.py +++ b/tests/unit/test_strategy.py @@ -8,6 +8,9 @@ def test_init(): strategy = Strategy("SMA") assert strategy.name == "SMA" + assert strategy.orders == list() + assert strategy.feed == list() + assert strategy.algo is None def test_add_feed_str(): @@ -15,9 +18,10 @@ def test_add_feed_str(): """ strategy = Strategy("SMA") + strategy.subscribe("Stock-AAPL") - assert strategy.feed == set(["Stock-AAPL"]) + assert strategy.feed == ["Stock-AAPL"] def test_add_feed_list(): @@ -25,9 +29,10 @@ def test_add_feed_list(): """ strategy = Strategy("SMA") + strategy.subscribe(["Stock-AAPL", "Stock-MSFT"]) - assert strategy.feed == set(["Stock-AAPL", "Stock-MSFT"]) + assert strategy.feed == ["Stock-AAPL", "Stock-MSFT"] def test_add_feed_mixed(): @@ -36,7 +41,54 @@ def test_add_feed_mixed(): """ strategy = Strategy("SMA") + strategy.subscribe("Stock-AAPL") strategy.subscribe(["Stock-GOOGL", "Stock-MSFT"]) - assert strategy.feed == set(["Stock-AAPL", "Stock-GOOGL", "Stock-MSFT"]) + assert strategy.feed == ["Stock-AAPL", "Stock-GOOGL", "Stock-MSFT"] + + +def test_create_order(): + """Test Strategy can create an order + """ + + strategy = Strategy("SMA") + + strategy.order("AAPL", 0.5, 200) + + assert strategy.orders == [ + { + "order_id": "SMA_1", + "security_id": "AAPL", + "weight": 0.5, + "limit": 200, + "stop": None, + } + ] + + +def test_create_orders(): + """Test Strategy can create multiple orders + """ + + strategy = Strategy("SMA") + + strategy.order("AAPL", 0.5, 200) + strategy.order("GOOGL", 0.3, stop=1000) + + assert strategy.orders == [ + { + "order_id": "SMA_1", + "security_id": "AAPL", + "weight": 0.5, + "limit": 200, + "stop": None, + }, + { + "order_id": "SMA_2", + "security_id": "GOOGL", + "weight": 0.3, + "limit": None, + "stop": 1000, + }, + ]