From 31f9f06c9a789c40cbeaefcb32221d1b4f0c3df3 Mon Sep 17 00:00:00 2001 From: jfkirk Date: Tue, 26 Apr 2016 16:05:08 -0400 Subject: [PATCH] MAINT: Removes static calendar from schedule_function rules --- tests/utils/test_events.py | 36 ++++++++++++++----- zipline/algorithm.py | 25 ++++++++++--- zipline/errors.py | 12 +++++++ zipline/utils/events.py | 72 ++++++++++++++++++-------------------- 4 files changed, 95 insertions(+), 50 deletions(-) diff --git a/tests/utils/test_events.py b/tests/utils/test_events.py index 2158bdd0..d7503804 100644 --- a/tests/utils/test_events.py +++ b/tests/utils/test_events.py @@ -239,6 +239,10 @@ class RuleTestCase(TestCase): cls.after_open = AfterOpen(hours=1, minutes=5) cls.class_ = None # Mark that this is the base class. + cal = get_calendar('NYSE') + cls.before_close.cal = cal + cls.after_open.cal = cal + def test_completeness(self): """ Tests that all rules are being tested. @@ -316,7 +320,10 @@ class TestStatelessRules(RuleTestCase): @subtest(minutes_for_days(), 'ms') def test_NotHalfDay(self, ms): - should_trigger = NotHalfDay().should_trigger + cal = get_calendar('NYSE') + rule = NotHalfDay() + rule.cal = cal + should_trigger = rule.should_trigger self.assertTrue(should_trigger(FULL_DAY)) self.assertFalse(should_trigger(HALF_DAY)) @@ -325,15 +332,19 @@ class TestStatelessRules(RuleTestCase): Test that we don't blow up when trying to call week_start's should_trigger on the first day of a trading environment. """ + cal = get_calendar('NYSE') + rule = NthTradingDayOfWeek(0) + rule.cal = cal self.assertTrue( - NthTradingDayOfWeek(0).should_trigger( - self.nyse_cal.all_trading_days[0] - ) + rule.should_trigger(self.nyse_cal.all_trading_days[0]) ) @subtest(param_range(MAX_WEEK_RANGE), 'n') def test_NthTradingDayOfWeek(self, n): - should_trigger = NthTradingDayOfWeek(n).should_trigger + cal = get_calendar('NYSE') + rule = NthTradingDayOfWeek(n) + rule.cal = cal + should_trigger = rule.should_trigger prev_day = self.sept_week[0].date() n_tdays = 0 for m in self.sept_week: @@ -348,7 +359,10 @@ class TestStatelessRules(RuleTestCase): @subtest(param_range(MAX_WEEK_RANGE), 'n') def test_NDaysBeforeLastTradingDayOfWeek(self, n): - should_trigger = NDaysBeforeLastTradingDayOfWeek(n).should_trigger + cal = get_calendar('NYSE') + rule = NDaysBeforeLastTradingDayOfWeek(n) + rule.cal = cal + should_trigger = rule.should_trigger for m in self.sept_week: if should_trigger(m): n_tdays = 0 @@ -470,7 +484,10 @@ class TestStatelessRules(RuleTestCase): @subtest(param_range(MAX_MONTH_RANGE), 'n') def test_NthTradingDayOfMonth(self, n): - should_trigger = NthTradingDayOfMonth(n).should_trigger + cal = get_calendar('NYSE') + rule = NthTradingDayOfMonth(n) + rule.cal = cal + should_trigger = rule.should_trigger for n_tdays, d in enumerate(self.sept_days): for m in self.nyse_cal.trading_minutes_for_day(d): if should_trigger(m): @@ -480,7 +497,10 @@ class TestStatelessRules(RuleTestCase): @subtest(param_range(MAX_MONTH_RANGE), 'n') def test_NDaysBeforeLastTradingDayOfMonth(self, n): - should_trigger = NDaysBeforeLastTradingDayOfMonth(n).should_trigger + cal = get_calendar('NYSE') + rule = NDaysBeforeLastTradingDayOfMonth(n) + rule.cal = cal + should_trigger = rule.should_trigger for n_days_before, d in enumerate(reversed(self.sept_days)): for m in self.nyse_cal.trading_minutes_for_day(d): if should_trigger(m): diff --git a/zipline/algorithm.py b/zipline/algorithm.py index b58ab083..b7c8fb24 100644 --- a/zipline/algorithm.py +++ b/zipline/algorithm.py @@ -53,8 +53,12 @@ from zipline.errors import ( UnsupportedDatetimeFormat, UnsupportedOrderParameters, UnsupportedSlippageModel, - CannotOrderDelistedAsset, UnsupportedCancelPolicy, SetCancelPolicyPostInit, - OrderInBeforeTradingStart) + CannotOrderDelistedAsset, + UnsupportedCancelPolicy, + SetCancelPolicyPostInit, + OrderInBeforeTradingStart, + ScheduleFunctionWithoutCalendar, +) from zipline.finance.trading import TradingEnvironment from zipline.finance.blotter import Blotter from zipline.finance.commission import PerShare, CommissionModel @@ -94,7 +98,10 @@ from zipline.utils.api_support import ( from zipline.utils.input_validation import ensure_upper_case, error_keywords from zipline.utils.cache import CachedObject, Expired -from zipline.utils.calendars import default_nyse_schedule +from zipline.utils.calendars import ( + default_nyse_schedule, + ExchangeTradingSchedule, +) import zipline.utils.events from zipline.utils.events import ( EventManager, @@ -981,8 +988,18 @@ class TradingAlgorithm(object): # If we are in daily mode the time_rule is ignored. time_rules.every_minute()) + # Check the type of the algorithm's schedule before pulling calendar + # Note that the ExchangeTradingSchedule is currently the only + # TradingSchedule class, so this is unlikely to be hit + # TODO The calendar should be a required arg for schedule_function + if not isinstance(self.trading_schedule, ExchangeTradingSchedule): + raise ScheduleFunctionWithoutCalendar( + schedule=self.trading_schedule + ) + cal = self.trading_schedule._exchange_calendar + self.add_event( - make_eventrule(date_rule, time_rule, half_days), + make_eventrule(date_rule, time_rule, cal, half_days), func, ) diff --git a/zipline/errors.py b/zipline/errors.py index 85440414..48c920de 100644 --- a/zipline/errors.py +++ b/zipline/errors.py @@ -654,3 +654,15 @@ class CalendarNameCollision(ZiplineError): msg = ( "A calendar with the name {calendar_name} is already registered." ) + + +class ScheduleFunctionWithoutCalendar(ZiplineError): + """ + Raised when schedule_function is called but there is not a calendar to be + used in the construction of an event rule. + """ + # TODO update message when new TradingSchedules are built + msg = ( + "To use schedule_function, the TradingAlgorithm must be running on an " + "ExchangeTradingSchedule, rather than {schedule}." + ) diff --git a/zipline/utils/events.py b/zipline/utils/events.py index 80fad7c0..6a3869a4 100644 --- a/zipline/utils/events.py +++ b/zipline/utils/events.py @@ -22,10 +22,7 @@ import pytz from .context_tricks import nop_context -from zipline.utils.calendars import ( - get_calendar, - normalize_date, -) +from zipline.utils.calendars import normalize_date __all__ = [ 'EventManager', @@ -56,9 +53,6 @@ MAX_MONTH_RANGE = 26 MAX_WEEK_RANGE = 5 -_static_nyse_cal = get_calendar('NYSE') - - def naive_to_utc(ts): """ Converts a UTC tz-naive timestamp to a tz-aware timestamp. @@ -347,10 +341,8 @@ class AfterOpen(StatelessRule): def calculate_dates(self, dt): # given a dt, find that day's open and period end (open + offset) - self._period_start, self._period_close = \ - _static_nyse_cal.open_and_close(dt) - self._period_end = \ - self._period_start + self.offset - self._one_minute + self._period_start, self._period_close = self.cal.open_and_close(dt) + self._period_end = self._period_start + self.offset - self._one_minute def should_trigger(self, dt): # There are two reasons why we might want to recalculate the dates. @@ -392,9 +384,8 @@ class BeforeClose(StatelessRule): def calculate_dates(self, dt): # given a dt, find that day's close and period start (close - offset) - self._period_end = _static_nyse_cal.open_and_close(dt)[1] - self._period_start = \ - self._period_end - self.offset + self._period_end = self.cal.open_and_close(dt)[1] + self._period_start = self._period_end - self.offset self._period_close = self._period_end def should_trigger(self, dt): @@ -421,7 +412,7 @@ class NotHalfDay(StatelessRule): A rule that only triggers when it is not a half day. """ def should_trigger(self, dt): - return normalize_date(dt) not in _static_nyse_cal.early_closes + return normalize_date(dt) not in self.cal.early_closes class TradingDayOfWeekRule(six.with_metaclass(ABCMeta, StatelessRule)): @@ -441,7 +432,7 @@ class TradingDayOfWeekRule(six.with_metaclass(ABCMeta, StatelessRule)): def calculate_start_and_end(self, dt): next_trading_day = _coerce_datetime( - _static_nyse_cal.add_trading_days( + self.cal.add_trading_days( self.td_delta, self.date_func(dt), ) @@ -452,15 +443,13 @@ class TradingDayOfWeekRule(six.with_metaclass(ABCMeta, StatelessRule)): while next_trading_day.isocalendar()[1] != dt.isocalendar()[1]: dt += datetime.timedelta(days=7) next_trading_day = _coerce_datetime( - env.add_trading_days( + self.cal.add_trading_days( self.td_delta, - self.date_func(dt, env), + self.date_func(dt), ) ) - next_open, next_close = _static_nyse_cal.open_and_close( - next_trading_day - ) + next_open, next_close = self.cal.open_and_close(next_trading_day) self.next_date_start = next_open self.next_date_end = next_close self.next_midnight_timestamp = next_trading_day @@ -491,9 +480,9 @@ class NthTradingDayOfWeek(TradingDayOfWeekRule): This is zero-indexed, n=0 is the first trading day of the week. """ @staticmethod - def get_first_trading_day_of_week(dt): + def get_first_trading_day_of_week(dt, cal): prev = dt - dt = _static_nyse_cal.previous_trading_day(dt) + dt = cal.previous_trading_day(dt) # If we're on the first trading day of the TradingEnvironment, # calling previous_trading_day on it will return None, which # will blow up when we try and call .date() on it. The first @@ -504,14 +493,14 @@ class NthTradingDayOfWeek(TradingDayOfWeekRule): return prev while dt.date().weekday() < prev.date().weekday(): prev = dt - dt = _static_nyse_cal.previous_trading_day(dt) + dt = cal.previous_trading_day(dt) if dt is None: return prev - if env.is_trading_day(prev): + if cal.is_trading_day(prev): return prev.date() else: - return env.next_trading_day(prev).date() + return cal.next_trading_day(prev).date() date_func = get_first_trading_day_of_week @@ -524,19 +513,19 @@ class NDaysBeforeLastTradingDayOfWeek(TradingDayOfWeekRule): super(NDaysBeforeLastTradingDayOfWeek, self).__init__(-n) @staticmethod - def get_last_trading_day_of_week(dt): + def get_last_trading_day_of_week(dt, cal): prev = dt - dt = _static_nyse_cal.next_trading_day(dt) + dt = cal.next_trading_day(dt) # Traverse forward until we hit a week border, then jump back to the # previous trading day. while dt.date().weekday() > prev.date().weekday(): prev = dt - dt = _static_nyse_cal.next_trading_day(dt) + dt = cal.next_trading_day(dt) - if env.is_trading_day(prev): + if cal.is_trading_day(prev): return prev.date() else: - return env.previous_trading_day(prev).date() + return cal.previous_trading_day(prev).date() date_func = get_last_trading_day_of_week @@ -564,7 +553,7 @@ class NthTradingDayOfMonth(StatelessRule): if not self.td_delta: self.day = self.get_first_trading_day_of_month(dt) else: - self.day = _static_nyse_cal.add_trading_days( + self.day = self.cal.add_trading_days( self.td_delta, self.get_first_trading_day_of_month(dt), ).date() @@ -575,8 +564,8 @@ class NthTradingDayOfMonth(StatelessRule): self.month = dt.month dt = dt.replace(day=1) - self.first_day = (dt if _static_nyse_cal.is_open_on_day(dt) - else _static_nyse_cal.next_trading_day(dt)).date() + self.first_day = (dt if self.cal.is_open_on_day(dt) + else self.cal.next_trading_day(dt)).date() return self.first_day @@ -602,7 +591,7 @@ class NDaysBeforeLastTradingDayOfMonth(StatelessRule): if not self.td_delta: self.day = self.get_last_trading_day_of_month(dt) else: - self.day = _static_nyse_cal.add_trading_days( + self.day = self.cal.add_trading_days( self.td_delta, self.get_last_trading_day_of_month(dt), ).date() @@ -621,7 +610,7 @@ class NDaysBeforeLastTradingDayOfMonth(StatelessRule): year = dt.year month = dt.month + 1 - self.last_day = _static_nyse_cal.previous_trading_day( + self.last_day = self.cal.previous_trading_day( dt.replace(year=year, month=month, day=1) ).date() return self.last_day @@ -699,13 +688,20 @@ class time_rules(object): every_minute = Always -def make_eventrule(date_rule, time_rule, half_days=True): +def make_eventrule(date_rule, time_rule, cal, half_days=True): """ Constructs an event rule from the factory api. """ + + # Insert the calendar in to the individual rules + date_rule.cal = cal + time_rule.cal = cal + if half_days: inner_rule = date_rule & time_rule else: - inner_rule = date_rule & time_rule & NotHalfDay() + nhd_rule = NotHalfDay() + nhd_rule.cal = cal + inner_rule = date_rule & time_rule & nhd_rule return OncePerDay(rule=inner_rule)