Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
96 changes: 96 additions & 0 deletions pyback/feed.py
Original file line number Diff line number Diff line change
@@ -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]
63 changes: 58 additions & 5 deletions pyback/strategy.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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.
Expand All @@ -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
58 changes: 55 additions & 3 deletions tests/unit/test_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,31 @@ 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():
"""Test Strategy instances can subscribe to feeds with single str
"""

strategy = Strategy("SMA")

strategy.subscribe("Stock-AAPL")

assert strategy.feed == set(["Stock-AAPL"])
assert strategy.feed == ["Stock-AAPL"]


def test_add_feed_list():
"""Test Strategy instances can subscribe to a list of feeds
"""

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():
Expand All @@ -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,
},
]