Skip to content

Commit 432f8b4

Browse files
author
Siddharta Govindaraj
committed
Changes to work in Python 2.6+
1 parent 27abd41 commit 432f8b4

18 files changed

+300
-108
lines changed

stock_alerter/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
if __name__ == "__main__":
3+
import doctest
4+
doctest.testfile("readme.txt")

stock_alerter/__main__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import unittest
2+
3+
4+
class AttribLoader(unittest.TestLoader):
5+
def __init__(self, attrib):
6+
self.attrib = attrib
7+
8+
def loadTestsFromModule(self, module, use_load_tests=False):
9+
return super().loadTestsFromModule(module, use_load_tests=False)
10+
11+
def getTestCaseNames(self, testCaseClass):
12+
test_names = super().getTestCaseNames(testCaseClass)
13+
filtered_test_names = [test
14+
for test in test_names
15+
if hasattr(getattr(testCaseClass, test),
16+
self.attrib)]
17+
return filtered_test_names
18+
19+
20+
if __name__ == "__main__":
21+
loader = AttribLoader("slow")
22+
test_suite = loader.discover(".")
23+
runner = unittest.TextTestRunner()
24+
runner.run(test_suite)

stock_alerter/action.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from __future__ import print_function
12
import smtplib
23
from email.mime.text import MIMEText
34

@@ -17,7 +18,7 @@ def __init__(self, to):
1718
def execute(self, content):
1819
message = MIMEText(content)
1920
message["Subject"] = "New Stock Alert"
20-
message["From"] = self.from_email
21+
message["From"] = "[email protected]"
2122
message["To"] = self.to_email
2223
smtp = smtplib.SMTP("email.stocks.com")
2324
try:

stock_alerter/alert.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def connect(self, exchange):
1111
self.exchange = exchange
1212
dependent_stocks = self.rule.depends_on()
1313
for stock in dependent_stocks:
14-
self.exchange[stock].updated.connect(self.check_rule)
14+
exchange[stock].updated.connect(self.check_rule)
1515

1616
def check_rule(self, stock):
1717
if self.rule.matches(self.exchange):

stock_alerter/legacy.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from __future__ import print_function
12
from datetime import datetime
23

34
from .stock import Stock
@@ -8,14 +9,14 @@ class FileReader:
89
def __init__(self, filename):
910
self.filename = filename
1011

11-
def parse_file(self):
12+
def get_updates(self):
1213
updates = []
13-
with open(self.filename, "r") as fp:
14+
with open("updates.csv", "r") as fp:
1415
for line in fp.readlines():
1516
symbol, timestamp, price = line.split(",")
1617
updates.append((symbol,
17-
datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f"),
18-
int(price)))
18+
datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f"),
19+
int(price)))
1920
return updates
2021

2122

@@ -28,20 +29,25 @@ def __init__(self, autorun=True, reader=None, exchange=None):
2829
self.exchange = exchange
2930
rule_1 = PriceRule("GOOG", lambda stock: stock.price > 10)
3031
rule_2 = PriceRule("AAPL", lambda stock: stock.price > 5)
31-
self.exchange["GOOG"].updated.connect(lambda stock: self.print_action(stock, rule_1))
32-
self.exchange["AAPL"].updated.connect(lambda stock: self.print_action(stock, rule_2))
32+
self.exchange["GOOG"].updated.connect(
33+
lambda stock: self.print_action(stock, rule_1))
34+
self.exchange["AAPL"].updated.connect(
35+
lambda stock: self.print_action(stock, rule_2))
3336
if autorun:
3437
self.run()
3538

3639
def print_action(self, stock, rule):
3740
print(stock.symbol, stock.price) \
3841
if rule.matches(self.exchange) else None
3942

43+
def run(self):
44+
updates = self.parse_file()
45+
self.do_updates(updates)
46+
47+
def parse_file(self):
48+
return self.reader.get_updates()
49+
4050
def do_updates(self, updates):
4151
for symbol, timestamp, price in updates:
4252
stock = self.exchange[symbol]
4353
stock.update(timestamp, price)
44-
45-
def run(self):
46-
updates = self.reader.parse_file()
47-
self.do_updates(updates)

stock_alerter/readme.txt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@ are going to be processed. A simple dictionary will do.
88

99
>>> from stock_alerter.stock import Stock
1010
>>> exchange = {"GOOG": Stock("GOOG"), "AAPL": Stock("AAPL")}
11-
>>> for key in sorted(exchange.keys()):
12-
... print(key, str(exchange[key]))
13-
...
14-
AAPL Stock("AAPL")
15-
GOOG Stock("GOOG")
11+
>>> for key in sorted(exchange.keys()): #doctest: -NORMALIZE_WHITESPACE
12+
... print key, exchange[key]
13+
AAPL <stock_alerter.stock.Stock ... at 0x0...>
14+
GOOG <stock_alerter.stock.Stock ... at 0x0...>
1615

1716
Next, we configure the reader. The reader is the source from where the
1817
stock updates are coming. The module provides two readers out of the

stock_alerter/rule.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,30 @@ def matches(self, exchange):
1414
return self.condition(stock) if stock.price else False
1515

1616
def depends_on(self):
17-
return {self.symbol}
17+
return set([self.symbol])
18+
19+
20+
class TrendRule:
21+
"""TrendRule is a rule that triggers when a stock price satisfies a
22+
trend (for now the only trend supported is 3 increasing updates)"""
23+
24+
def __init__(self, symbol):
25+
self.symbol = symbol
26+
27+
def matches(self, exchange):
28+
try:
29+
stock = exchange[self.symbol]
30+
except KeyError:
31+
return False
32+
return stock.is_increasing_trend()
33+
34+
def depends_on(self):
35+
return set([self.symbol])
1836

1937

2038
class AndRule:
39+
"""AndRule triggers when all its component rules are true"""
40+
2141
def __init__(self, *args):
2242
self.rules = args
2343

stock_alerter/stock.py

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
from datetime import timedelta
21
from enum import Enum
32

3+
from .timeseries import TimeSeries
44
from .event import Event
5-
from .timeseries import TimeSeries, MovingAverage, NotEnoughDataException
65

76

87
class StockSignal(Enum):
@@ -20,10 +19,6 @@ def __init__(self, symbol):
2019
self.history = TimeSeries()
2120
self.updated = Event()
2221

23-
def __str__(self):
24-
class_name = type(self).__name__
25-
return '{}("{}")'.format(class_name, self.symbol)
26-
2722
@property
2823
def price(self):
2924
"""Returns the current price of the Stock
@@ -33,18 +28,9 @@ def price(self):
3328
10
3429
3530
The method will return the latest price by timestamp, so even if
36-
updates are out of order, it will return the latest one
37-
38-
>>> stock = Stock("GOOG")
39-
>>> stock.update(datetime(2011, 10, 3), 10)
40-
41-
Now, let us do an update with a date that is earlier than the previous
42-
one
31+
updates are out of order, it will return the latest one.
4332
4433
>>> stock.update(datetime(2011, 10, 2), 5)
45-
46-
And the method still returns the latest price
47-
4834
>>> stock.price
4935
10
5036
@@ -73,6 +59,7 @@ def update(self, timestamp, price):
7359
...
7460
ValueError: price should not be negative
7561
"""
62+
7663
if price < 0:
7764
raise ValueError("price should not be negative")
7865
self.history.update(timestamp, price)
@@ -86,26 +73,59 @@ def is_increasing_trend(self):
8673
>>> stock.is_increasing_trend()
8774
False
8875
"""
76+
8977
try:
90-
return self.history[-3].value < self.history[-2].value < self.history[-1].value
78+
return self.history[-3].value < \
79+
self.history[-2].value < self.history[-1].value
9180
except IndexError:
9281
return False
9382

94-
def _is_crossover_below_to_above(self, on_date, ma, reference_ma):
95-
prev_date = on_date - timedelta(1)
96-
return (ma.value_on(prev_date) < reference_ma.value_on(prev_date)
97-
and ma.value_on(on_date) > reference_ma.value_on(on_date))
83+
def _is_crossover_below_to_above(self, prev_ma, prev_reference_ma,
84+
current_ma, current_reference_ma):
85+
return prev_ma < prev_reference_ma \
86+
and current_ma > current_reference_ma
9887

9988
def get_crossover_signal(self, on_date):
100-
long_term_ma = MovingAverage(self.history, self.LONG_TERM_TIMESPAN)
101-
short_term_ma = MovingAverage(self.history, self.SHORT_TERM_TIMESPAN)
102-
try:
103-
if self._is_crossover_below_to_above(on_date, short_term_ma, long_term_ma):
89+
NUM_DAYS = self.LONG_TERM_TIMESPAN + 1
90+
closing_price_list = self.history.get_closing_price_list(on_date,
91+
NUM_DAYS)
92+
93+
if len(closing_price_list) < NUM_DAYS:
94+
return StockSignal.neutral
95+
96+
long_term_series = closing_price_list[-self.LONG_TERM_TIMESPAN:]
97+
prev_long_term_series = closing_price_list[-self.LONG_TERM_TIMESPAN-1:-1]
98+
short_term_series = closing_price_list[-self.SHORT_TERM_TIMESPAN:]
99+
prev_short_term_series = closing_price_list[-self.SHORT_TERM_TIMESPAN-1:-1]
100+
101+
long_term_ma = 1.0*sum([update.value
102+
for update in long_term_series])\
103+
/self.LONG_TERM_TIMESPAN
104+
prev_long_term_ma = 1.0*sum([update.value
105+
for update in prev_long_term_series])\
106+
/self.LONG_TERM_TIMESPAN
107+
short_term_ma = 1.0*sum([update.value
108+
for update in short_term_series])\
109+
/self.SHORT_TERM_TIMESPAN
110+
prev_short_term_ma = 1.0*sum([update.value
111+
for update in prev_short_term_series])\
112+
/self.SHORT_TERM_TIMESPAN
113+
114+
if self._is_crossover_below_to_above(prev_short_term_ma,
115+
prev_long_term_ma,
116+
short_term_ma,
117+
long_term_ma):
104118
return StockSignal.buy
105119

106-
if self._is_crossover_below_to_above(on_date, long_term_ma, short_term_ma):
120+
if self._is_crossover_below_to_above(prev_long_term_ma,
121+
prev_short_term_ma,
122+
long_term_ma,
123+
short_term_ma):
107124
return StockSignal.sell
108-
except NotEnoughDataException:
109-
return StockSignal.neutral
110125

111126
return StockSignal.neutral
127+
128+
129+
if __name__ == "__main__":
130+
import doctest
131+
doctest.testmod()

stock_alerter/tests/test_action.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
from __future__ import print_function
12
import smtplib
2-
import unittest
3-
from unittest import mock
3+
import unittest2 as unittest
4+
import mock
45

5-
from ..action import PrintAction, EmailAction
6+
from ..action import EmailAction, PrintAction
67

78

89
class MessageMatcher:
@@ -16,7 +17,18 @@ def __eq__(self, other):
1617
self.expected["Message"] == other._payload
1718

1819

19-
@mock.patch("builtins.print")
20+
class AlertMessageMatcher:
21+
def __init__(self, expected):
22+
self.expected = expected
23+
24+
def __eq__(self, other):
25+
return "New Stock Alert" == other["Subject"] and \
26+
self.expected["From"] == other["From"] and \
27+
self.expected["To"] == other["To"] and \
28+
self.expected["Message"] == other._payload
29+
30+
31+
@mock.patch("__builtin__.print")
2032
class PrintActionTest(unittest.TestCase):
2133
def test_executing_action_prints_message(self, mock_print):
2234
action = PrintAction()

stock_alerter/tests/test_alert.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import unittest
2-
from unittest import mock
32
from datetime import datetime
3+
import mock
44

55
from ..alert import Alert
66
from ..rule import PriceRule
7-
from ..stock import Stock
87
from ..event import Event
8+
from ..stock import Stock
99

1010

1111
class AlertTest(unittest.TestCase):
@@ -16,7 +16,7 @@ def test_action_is_executed_when_rule_matches(self):
1616
exchange = {"GOOG": goog}
1717
rule = mock.MagicMock(spec=PriceRule)
1818
rule.matches.return_value = True
19-
rule.depends_on.return_value = {"GOOG"}
19+
rule.depends_on.return_value = set(["GOOG"])
2020
action = mock.MagicMock()
2121
alert = Alert("sample alert", rule, action)
2222
alert.connect(exchange)
@@ -41,7 +41,7 @@ def test_action_fires_when_rule_matches(self):
4141
main_mock = mock.MagicMock()
4242
rule = main_mock.rule
4343
rule.matches.return_value = True
44-
rule.depends_on.return_value = {"GOOG"}
44+
rule.depends_on.return_value = set(["GOOG"])
4545
action = main_mock.action
4646
alert = Alert("sample alert", rule, action)
4747
alert.connect(exchange)

0 commit comments

Comments
 (0)