diff --git a/stock_alerter/__init__.py b/stock_alerter/__init__.py index e69de29..e8e9e96 100644 --- a/stock_alerter/__init__.py +++ b/stock_alerter/__init__.py @@ -0,0 +1,4 @@ + +if __name__ == "__main__": + import doctest + doctest.testfile("readme.txt") diff --git a/stock_alerter/__main__.py b/stock_alerter/__main__.py new file mode 100644 index 0000000..5595d1a --- /dev/null +++ b/stock_alerter/__main__.py @@ -0,0 +1,24 @@ +import unittest + + +class AttribLoader(unittest.TestLoader): + def __init__(self, attrib): + self.attrib = attrib + + def loadTestsFromModule(self, module, use_load_tests=False): + return super().loadTestsFromModule(module, use_load_tests=False) + + def getTestCaseNames(self, testCaseClass): + test_names = super().getTestCaseNames(testCaseClass) + filtered_test_names = [test + for test in test_names + if hasattr(getattr(testCaseClass, test), + self.attrib)] + return filtered_test_names + + +if __name__ == "__main__": + loader = AttribLoader("slow") + test_suite = loader.discover(".") + runner = unittest.TextTestRunner() + runner.run(test_suite) diff --git a/stock_alerter/action.py b/stock_alerter/action.py index bb436ce..c309ce5 100644 --- a/stock_alerter/action.py +++ b/stock_alerter/action.py @@ -1,3 +1,4 @@ +from __future__ import print_function import smtplib from email.mime.text import MIMEText diff --git a/stock_alerter/legacy.py b/stock_alerter/legacy.py index bf857ed..0748768 100644 --- a/stock_alerter/legacy.py +++ b/stock_alerter/legacy.py @@ -1,3 +1,4 @@ +from __future__ import print_function from datetime import datetime from .stock import Stock @@ -8,14 +9,14 @@ class FileReader: def __init__(self, filename): self.filename = filename - def parse_file(self): + def get_updates(self): updates = [] - with open(self.filename, "r") as fp: + with open("updates.csv", "r") as fp: for line in fp.readlines(): symbol, timestamp, price = line.split(",") updates.append((symbol, - datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f"), - int(price))) + datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f"), + int(price))) return updates @@ -28,8 +29,10 @@ def __init__(self, autorun=True, reader=None, exchange=None): self.exchange = exchange rule_1 = PriceRule("GOOG", lambda stock: stock.price > 10) rule_2 = PriceRule("AAPL", lambda stock: stock.price > 5) - self.exchange["GOOG"].updated.connect(lambda stock: self.print_action(stock, rule_1)) - self.exchange["AAPL"].updated.connect(lambda stock: self.print_action(stock, rule_2)) + self.exchange["GOOG"].updated.connect( + lambda stock: self.print_action(stock, rule_1)) + self.exchange["AAPL"].updated.connect( + lambda stock: self.print_action(stock, rule_2)) if autorun: self.run() @@ -37,11 +40,14 @@ def print_action(self, stock, rule): print(stock.symbol, stock.price) \ if rule.matches(self.exchange) else None + def run(self): + updates = self.parse_file() + self.do_updates(updates) + + def parse_file(self): + return self.reader.get_updates() + def do_updates(self, updates): for symbol, timestamp, price in updates: stock = self.exchange[symbol] stock.update(timestamp, price) - - def run(self): - updates = self.reader.parse_file() - self.do_updates(updates) diff --git a/stock_alerter/readme.txt b/stock_alerter/readme.txt index a6cd892..3a739d4 100644 --- a/stock_alerter/readme.txt +++ b/stock_alerter/readme.txt @@ -8,11 +8,10 @@ are going to be processed. A simple dictionary will do. >>> from stock_alerter.stock import Stock >>> exchange = {"GOOG": Stock("GOOG"), "AAPL": Stock("AAPL")} ->>> for key in sorted(exchange.keys()): -... print(key, exchange[key]) -... -AAPL -GOOG +>>> for key in sorted(exchange.keys()): #doctest: -NORMALIZE_WHITESPACE +... print key, exchange[key] +AAPL +GOOG Next, we configure the reader. The reader is the source from where the stock updates are coming. The module provides two readers out of the diff --git a/stock_alerter/rule.py b/stock_alerter/rule.py index fef9787..ca34f21 100644 --- a/stock_alerter/rule.py +++ b/stock_alerter/rule.py @@ -14,10 +14,30 @@ def matches(self, exchange): return self.condition(stock) if stock.price else False def depends_on(self): - return {self.symbol} + return set([self.symbol]) + + +class TrendRule: + """TrendRule is a rule that triggers when a stock price satisfies a + trend (for now the only trend supported is 3 increasing updates)""" + + def __init__(self, symbol): + self.symbol = symbol + + def matches(self, exchange): + try: + stock = exchange[self.symbol] + except KeyError: + return False + return stock.is_increasing_trend() + + def depends_on(self): + return set([self.symbol]) class AndRule: + """AndRule triggers when all its component rules are true""" + def __init__(self, *args): self.rules = args diff --git a/stock_alerter/stock.py b/stock_alerter/stock.py index 49893af..4dd0d50 100644 --- a/stock_alerter/stock.py +++ b/stock_alerter/stock.py @@ -1,8 +1,7 @@ -from datetime import timedelta from enum import Enum +from .timeseries import TimeSeries from .event import Event -from .timeseries import TimeSeries, MovingAverage, NotEnoughDataException class StockSignal(Enum): @@ -29,18 +28,9 @@ def price(self): 10 The method will return the latest price by timestamp, so even if - updates are out of order, it will return the latest one - - >>> stock = Stock("GOOG") - >>> stock.update(datetime(2011, 10, 3), 10) - - Now, let us do an update with a date that is earlier than the previous - one + updates are out of order, it will return the latest one. >>> stock.update(datetime(2011, 10, 2), 5) - - And the method still returns the latest price - >>> stock.price 10 @@ -69,6 +59,7 @@ def update(self, timestamp, price): ... ValueError: price should not be negative """ + if price < 0: raise ValueError("price should not be negative") self.history.update(timestamp, price) @@ -82,26 +73,59 @@ def is_increasing_trend(self): >>> stock.is_increasing_trend() False """ + try: - return self.history[-3].value < self.history[-2].value < self.history[-1].value + return self.history[-3].value < \ + self.history[-2].value < self.history[-1].value except IndexError: return False - def _is_crossover_below_to_above(self, on_date, ma, reference_ma): - prev_date = on_date - timedelta(1) - return (ma.value_on(prev_date) < reference_ma.value_on(prev_date) - and ma.value_on(on_date) > reference_ma.value_on(on_date)) + def _is_crossover_below_to_above(self, prev_ma, prev_reference_ma, + current_ma, current_reference_ma): + return prev_ma < prev_reference_ma \ + and current_ma > current_reference_ma def get_crossover_signal(self, on_date): - long_term_ma = MovingAverage(self.history, self.LONG_TERM_TIMESPAN) - short_term_ma = MovingAverage(self.history, self.SHORT_TERM_TIMESPAN) - try: - if self._is_crossover_below_to_above(on_date, short_term_ma, long_term_ma): + NUM_DAYS = self.LONG_TERM_TIMESPAN + 1 + closing_price_list = self.history.get_closing_price_list(on_date, + NUM_DAYS) + + if len(closing_price_list) < NUM_DAYS: + return StockSignal.neutral + + long_term_series = closing_price_list[-self.LONG_TERM_TIMESPAN:] + prev_long_term_series = closing_price_list[-self.LONG_TERM_TIMESPAN-1:-1] + short_term_series = closing_price_list[-self.SHORT_TERM_TIMESPAN:] + prev_short_term_series = closing_price_list[-self.SHORT_TERM_TIMESPAN-1:-1] + + long_term_ma = 1.0*sum([update.value + for update in long_term_series])\ + /self.LONG_TERM_TIMESPAN + prev_long_term_ma = 1.0*sum([update.value + for update in prev_long_term_series])\ + /self.LONG_TERM_TIMESPAN + short_term_ma = 1.0*sum([update.value + for update in short_term_series])\ + /self.SHORT_TERM_TIMESPAN + prev_short_term_ma = 1.0*sum([update.value + for update in prev_short_term_series])\ + /self.SHORT_TERM_TIMESPAN + + if self._is_crossover_below_to_above(prev_short_term_ma, + prev_long_term_ma, + short_term_ma, + long_term_ma): return StockSignal.buy - if self._is_crossover_below_to_above(on_date, long_term_ma, short_term_ma): + if self._is_crossover_below_to_above(prev_long_term_ma, + prev_short_term_ma, + long_term_ma, + short_term_ma): return StockSignal.sell - except NotEnoughDataException: - return StockSignal.neutral return StockSignal.neutral + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/stock_alerter/tests/test_action.py b/stock_alerter/tests/test_action.py index fb0ff57..0135a03 100644 --- a/stock_alerter/tests/test_action.py +++ b/stock_alerter/tests/test_action.py @@ -1,8 +1,9 @@ +from __future__ import print_function import smtplib -import unittest -from unittest import mock +import unittest2 as unittest +import mock -from ..action import PrintAction, EmailAction +from ..action import EmailAction, PrintAction class MessageMatcher: @@ -16,7 +17,18 @@ def __eq__(self, other): self.expected["Message"] == other._payload -@mock.patch("builtins.print") +class AlertMessageMatcher: + def __init__(self, expected): + self.expected = expected + + def __eq__(self, other): + return "New Stock Alert" == other["Subject"] and \ + self.expected["From"] == other["From"] and \ + self.expected["To"] == other["To"] and \ + self.expected["Message"] == other._payload + + +@mock.patch("__builtin__.print") class PrintActionTest(unittest.TestCase): def test_executing_action_prints_message(self, mock_print): action = PrintAction() diff --git a/stock_alerter/tests/test_alert.py b/stock_alerter/tests/test_alert.py index dc83efe..972483d 100644 --- a/stock_alerter/tests/test_alert.py +++ b/stock_alerter/tests/test_alert.py @@ -1,11 +1,11 @@ import unittest -from unittest import mock from datetime import datetime +import mock from ..alert import Alert from ..rule import PriceRule -from ..stock import Stock from ..event import Event +from ..stock import Stock class AlertTest(unittest.TestCase): @@ -16,7 +16,7 @@ def test_action_is_executed_when_rule_matches(self): exchange = {"GOOG": goog} rule = mock.MagicMock(spec=PriceRule) rule.matches.return_value = True - rule.depends_on.return_value = {"GOOG"} + rule.depends_on.return_value = set(["GOOG"]) action = mock.MagicMock() alert = Alert("sample alert", rule, action) alert.connect(exchange) @@ -41,7 +41,7 @@ def test_action_fires_when_rule_matches(self): main_mock = mock.MagicMock() rule = main_mock.rule rule.matches.return_value = True - rule.depends_on.return_value = {"GOOG"} + rule.depends_on.return_value = set(["GOOG"]) action = main_mock.action alert = Alert("sample alert", rule, action) alert.connect(exchange) diff --git a/stock_alerter/tests/test_doctest.py b/stock_alerter/tests/test_doctest.py index e5da804..92baee8 100644 --- a/stock_alerter/tests/test_doctest.py +++ b/stock_alerter/tests/test_doctest.py @@ -1,6 +1,5 @@ import doctest from datetime import datetime - from stock_alerter import stock @@ -15,5 +14,7 @@ def load_tests(loader, tests, pattern): "Stock": stock.Stock }, setUp=setup_stock_doctest)) options = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE - tests.addTests(doctest.DocFileSuite("readme.txt", package="stock_alerter", optionflags=options)) + tests.addTests(doctest.DocFileSuite("readme.txt", + package="stock_alerter", + optionflags=options)) return tests diff --git a/stock_alerter/tests/test_event.py b/stock_alerter/tests/test_event.py index 454b380..91064ff 100644 --- a/stock_alerter/tests/test_event.py +++ b/stock_alerter/tests/test_event.py @@ -1,5 +1,5 @@ import unittest -from unittest import mock +import mock from ..event import Event diff --git a/stock_alerter/tests/test_legacy.py b/stock_alerter/tests/test_legacy.py index e60eb51..95b14c0 100644 --- a/stock_alerter/tests/test_legacy.py +++ b/stock_alerter/tests/test_legacy.py @@ -1,5 +1,5 @@ import unittest -from unittest import mock +import mock from datetime import datetime from ..legacy import AlertProcessor @@ -12,7 +12,7 @@ def __init__(self, exchange): class AlertProcessorTest(unittest.TestCase): - @mock.patch("builtins.print") + @mock.patch("__builtin__.print") def test_processor_characterization_1(self, mock_print): AlertProcessor() mock_print.assert_has_calls([mock.call("AAPL", 8), @@ -22,7 +22,7 @@ def test_processor_characterization_1(self, mock_print): def test_processor_characterization_2(self): processor = AlertProcessor(autorun=False) - with mock.patch("builtins.print") as mock_print: + with mock.patch("__builtin__.print") as mock_print: processor.run() mock_print.assert_has_calls([mock.call("AAPL", 8), mock.call("GOOG", 15), @@ -68,20 +68,20 @@ def test_processor_characterization_6(self): ('GOOG', datetime(2014, 2, 11, 14, 15, 22, 130000), 21)]) def test_processor_characterization_7(self): - mock_reader = mock.MagicMock() - mock_reader.parse_file.return_value = [ + processor = AlertProcessor(autorun=False) + processor.parse_file = mock.Mock() + processor.parse_file.return_value = [ ('GOOG', datetime(2014, 2, 11, 14, 12, 22, 130000), 15)] - processor = AlertProcessor(autorun=False, reader=mock_reader) - with mock.patch("builtins.print") as mock_print: + with mock.patch("__builtin__.print") as mock_print: processor.run() mock_print.assert_called_with("GOOG", 15) def test_processor_characterization_8(self): - mock_reader = mock.MagicMock() - mock_reader.parse_file.return_value = [ + processor = AlertProcessor(autorun=False) + processor.parse_file = mock.Mock() + processor.parse_file.return_value = [ ('GOOG', datetime(2014, 2, 11, 14, 10, 22, 130000), 5)] - processor = AlertProcessor(autorun=False, reader=mock_reader) - with mock.patch("builtins.print") as mock_print: + with mock.patch("__builtin__.print") as mock_print: processor.run() self.assertFalse(mock_print.called) @@ -94,7 +94,7 @@ def test_processor_characterization_9(self): def test_processor_gets_values_from_reader(self): mock_reader = mock.MagicMock() - mock_reader.parse_file.return_value = \ + mock_reader.get_updates.return_value = \ [('GOOG', datetime(2014, 2, 11, 14, 12, 22, 130000), 15)] processor = AlertProcessor(autorun=False, reader=mock_reader) processor.print_action = mock.Mock() diff --git a/stock_alerter/tests/test_reader.py b/stock_alerter/tests/test_reader.py index d6f76c5..170464a 100644 --- a/stock_alerter/tests/test_reader.py +++ b/stock_alerter/tests/test_reader.py @@ -1,18 +1,65 @@ import unittest -from unittest import mock from datetime import datetime +import mock -from ..reader import FileReader +from ..reader import FileReader, ListReader + + +class ReaderTest(unittest.TestCase): + @staticmethod + def get_update(data): + try: + return next(data) + except StopIteration: + pass class FileReaderTest(unittest.TestCase): - @mock.patch("builtins.open", + def test_FileReader_opens_the_given_file(self): + mock_open = mock.mock_open(read_data="") + with mock.patch("__builtin__.open", mock_open, create=True): + reader = FileReader("stocks.txt") + updater = reader.get_updates() + self.get_update(updater) + mock_open.assert_called_with("stocks.txt", "r") + + @staticmethod + def get_update(data): + try: + return next(data) + except StopIteration: + pass + + @mock.patch("__builtin__.open", mock.mock_open(read_data="""\ GOOG,2014-02-11T14:10:22.13,10""")) def test_FileReader_returns_the_file_contents(self): reader = FileReader("stocks.txt") updater = reader.get_updates() - update = next(updater) + update = self.get_update(updater) self.assertEqual(("GOOG", datetime(2014, 2, 11, 14, 10, 22, 130000), 10), update) + + @mock.patch("__builtin__.open", + mock.mock_open(read_data="""\ +GOOG,2014-02-11T14:10:22.13,10 +MSFT,2014-02-11T00:00:00.0,8""")) + def test_get_updates_returns_next_line_for_each_call(self): + reader = FileReader("stocks.txt") + updater = reader.get_updates() + self.get_update(updater) + update = self.get_update(updater) + self.assertEqual(("MSFT", datetime(2014, 2, 11), 8), update) + + +class ListReaderTest(ReaderTest): + def test_list_reader_returns_list_data_every_update(self): + data = [("GOOG", datetime(2014, 2, 11), 3), + ("AAPL", datetime(2014, 2, 11), 13)] + reader = ListReader(data) + updater = reader.get_updates() + update = self.get_update(updater) + self.assertEqual(("GOOG", datetime(2014, 2, 11), 3), update) + update = self.get_update(updater) + self.assertEqual(("AAPL", datetime(2014, 2, 11), 13), update) diff --git a/stock_alerter/tests/test_rule.py b/stock_alerter/tests/test_rule.py index edaef28..1c3fd67 100644 --- a/stock_alerter/tests/test_rule.py +++ b/stock_alerter/tests/test_rule.py @@ -2,7 +2,7 @@ from datetime import datetime from ..stock import Stock -from ..rule import PriceRule, AndRule +from ..rule import PriceRule, TrendRule, AndRule class PriceRuleTest(unittest.TestCase): @@ -31,7 +31,42 @@ def test_a_PriceRule_is_False_if_the_stock_hasnt_got_an_update_yet(self): def test_a_PriceRule_only_depends_on_its_stock(self): rule = PriceRule("MSFT", lambda stock: stock.price > 10) - self.assertEqual({"MSFT"}, rule.depends_on()) + self.assertEqual(set(["MSFT"]), rule.depends_on()) + + +class TrendRuleTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + goog = Stock("GOOG") + goog.update(datetime(2014, 2, 10), 8) + goog.update(datetime(2014, 2, 11), 10) + goog.update(datetime(2014, 2, 12), 12) + msft = Stock("MSFT") + msft.update(datetime(2014, 2, 10), 10) + msft.update(datetime(2014, 2, 11), 10) + msft.update(datetime(2014, 2, 12), 12) + cls.exchange = {"GOOG": goog, "MSFT": msft} + + def test_a_TrendRule_matches_if_the_stock_increased_last_3_updates(self): + rule = TrendRule("GOOG") + self.assertTrue(rule.matches(self.exchange)) + + def test_a_TrendRule_is_False_if_stock_doesnt_increasing_trend(self): + rule = TrendRule("MSFT") + self.assertFalse(rule.matches(self.exchange)) + + def test_a_TrendRule_is_False_if_stock_is_not_in_the_exchange(self): + rule = TrendRule("APPL") + self.assertFalse(rule.matches(self.exchange)) + + def test_a_TrendRule_is_False_if_the_stock_hasnt_got_an_update_yet(self): + self.exchange["AAPL"] = Stock("AAPL") + rule = PriceRule("AAPL", lambda price: price > 10) + self.assertFalse(rule.matches(self.exchange)) + + def test_a_TrendRule_only_depends_on_its_stock(self): + rule = TrendRule("AAPL") + self.assertEqual(set(["AAPL"]), rule.depends_on()) class AndRuleTest(unittest.TestCase): @@ -53,3 +88,34 @@ def test_an_AndRule_matches_if_all_component_rules_are_true(self): rule = AndRule(PriceRule("GOOG", lambda stock: stock.price > 8), PriceRule("MSFT", lambda stock: stock.price > 10)) self.assertTrue(rule.matches(self.exchange)) + + def test_an_AndRule_is_False_if_any_component_is_false(self): + rule = AndRule(PriceRule("GOOG", lambda stock: stock.price > 15), + PriceRule("MSFT", lambda stock: stock.price > 10)) + self.assertFalse(rule.matches(self.exchange)) + + def test_an_AndRule_should_support_any_number_of_subrules(self): + rule = AndRule(PriceRule("RHT", lambda stock: stock.price < 10), + PriceRule("GOOG", lambda stock: stock.price > 8), + PriceRule("MSFT", lambda stock: stock.price > 10)) + self.assertTrue(rule.matches(self.exchange)) + + def test_an_empty_AndRule_is_true(self): + rule = AndRule() + self.assertTrue(rule.matches(self.exchange)) + + def test_an_AndRule_can_be_nested(self): + rule = AndRule(PriceRule("RHT", lambda stock: stock.price < 10), + AndRule(PriceRule("GOOG", lambda stock: stock.price > 8), + PriceRule("MSFT", lambda stock: stock.price > 10))) + self.assertTrue(rule.matches(self.exchange)) + + def test_an_AndRule_depends_on_what_the_component_rules_depend_on(self): + rule = AndRule(PriceRule("AAPL", lambda stock: stock.price < 10), + PriceRule("GOOG", lambda stock: stock.price > 8)) + self.assertEqual(set(["AAPL", "GOOG"]), rule.depends_on()) + + def test_depends_on_should_not_have_duplicates(self): + rule = AndRule(PriceRule("AAPL", lambda stock: stock.price < 10), + PriceRule("AAPL", lambda stock: stock.price > 5)) + self.assertEqual(set(["AAPL"]), rule.depends_on()) diff --git a/stock_alerter/tests/test_stock.py b/stock_alerter/tests/test_stock.py index cfb644b..7ccf73e 100644 --- a/stock_alerter/tests/test_stock.py +++ b/stock_alerter/tests/test_stock.py @@ -1,4 +1,4 @@ -import unittest +import unittest2 as unittest import collections from datetime import datetime, timedelta @@ -9,25 +9,18 @@ class StockTest(unittest.TestCase): def setUp(self): self.goog = Stock("GOOG") - def test_price_of_a_new_stock_class_should_be_None(self): - self.assertIsNone(self.goog.price) - def test_stock_update(self): - """An update should set the price on the stock object - - We will be using the `datetime` module for the timestamp - """ self.goog.update(datetime(2014, 2, 12), price=10) self.assertEqual(10, self.goog.price) def test_negative_price_should_throw_ValueError(self): - with self.assertRaises(ValueError): - self.goog.update(datetime(2014, 2, 13), -1) + self.assertRaises(ValueError, self.goog.update, + datetime(2014, 2, 13), -1) def test_stock_price_should_give_the_latest_price(self): self.goog.update(datetime(2014, 2, 12), price=10) self.goog.update(datetime(2014, 2, 13), price=8.4) - self.assertAlmostEqual(8.4, self.goog.price, delta=0.0001) + self.assertAlmostEqual(8.4, self.goog.price, places=4) def test_price_is_the_latest_even_if_updates_are_made_out_of_order(self): self.goog.update(datetime(2014, 2, 13), price=8) @@ -36,11 +29,11 @@ def test_price_is_the_latest_even_if_updates_are_made_out_of_order(self): class StockTrendTest(unittest.TestCase): - def given_a_series_of_prices(self, goog, prices): + def given_a_series_of_prices(self, stock, prices): timestamps = [datetime(2014, 2, 10), datetime(2014, 2, 11), datetime(2014, 2, 12), datetime(2014, 2, 13)] for timestamp, price in zip(timestamps, prices): - goog.update(timestamp, price) + stock.update(timestamp, price) def test_stock_trends(self): dataset = [ diff --git a/stock_alerter/tests/test_timeseries.py b/stock_alerter/tests/test_timeseries.py index abbf537..c8cbfee 100644 --- a/stock_alerter/tests/test_timeseries.py +++ b/stock_alerter/tests/test_timeseries.py @@ -4,18 +4,34 @@ from ..timeseries import TimeSeries +class TimeSeriesTest(unittest.TestCase): + def test_closing_price_list_before_series_start_date(self): + """Empty list is returned if on_date is before the start of the series + + The moving average calculation might be done before any data has been + added to the stock. We return an empty list so that the calculation can + still proceed as usual. + """ + series = TimeSeries() + series.update(datetime(2014, 3, 10), 5) + on_date = datetime(2014, 3, 9) + self.assertEqual([], + series.get_closing_price_list(on_date, 1)) + + class TimeSeriesTestCase(unittest.TestCase): def assert_has_price_history(self, price_list, series): for index, expected_price in enumerate(price_list): actual_price = series[index].value if actual_price != expected_price: - raise self.failureException("Price index {0}: {1} != {2}".format( + raise self.failureException("Price index %d: %d != %d".format( index, expected_price, actual_price)) -class TimeSeriesEqualityTest(TimeSeriesTestCase): +class TimeSeriesEqualityTest(unittest.TestCase): def test_timeseries_price_history(self): series = TimeSeries() series.update(datetime(2014, 3, 10), 5) series.update(datetime(2014, 3, 11), 15) - self.assert_has_price_history([5, 15], series) + self.assertEqual(5, series[0].value) + self.assertEqual(15, series[1].value) diff --git a/stock_alerter/timeseries.py b/stock_alerter/timeseries.py index f16615d..d17e3cd 100644 --- a/stock_alerter/timeseries.py +++ b/stock_alerter/timeseries.py @@ -5,10 +5,6 @@ Update = collections.namedtuple("Update", ["timestamp", "value"]) -class NotEnoughDataException(Exception): - pass - - class TimeSeries: def __init__(self): self.series = [] @@ -33,16 +29,3 @@ def get_closing_price_list(self, on_date, num_days): closing_price_list.insert(0, price_event) break return closing_price_list - - -class MovingAverage: - def __init__(self, series, timespan): - self.series = series - self.timespan = timespan - - def value_on(self, end_date): - moving_avg_series = self.series.get_closing_price_list(end_date, self.timespan) - if len(moving_avg_series) < self.timespan: - raise NotEnoughDataException("Not enough data to calculate moving average") - price_list = [update.value for update in moving_avg_series] - return sum(price_list)/self.timespan