From 6d6cd58c3b00f1e9c8e4283a3d9c27eb20cd9587 Mon Sep 17 00:00:00 2001 From: Andrew Liang Date: Fri, 15 Apr 2016 13:49:24 -0400 Subject: [PATCH] BUG: Recalculate trigger for week rule if we miss the first one If we start the simulation on a day so that we miss the trigger (the first for the sim) for that week, recalculate the trigger for next week --- tests/utils/test_events.py | 41 ++++++++++++++++++++++++++++---------- zipline/utils/events.py | 17 ++++++++++------ 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/tests/utils/test_events.py b/tests/utils/test_events.py index afe3261b..134df3da 100644 --- a/tests/utils/test_events.py +++ b/tests/utils/test_events.py @@ -369,15 +369,29 @@ class TestStatelessRules(RuleTestCase): self.assertEqual(m, first_of_week + timedelta(days=4-n)) self.assertEqual(n_triggered, 1) - @parameter_space(n=(0, 1, 2, 3, 4), type=('week_start', 'week_end')) - def test_edge_cases_for_TradingDayOfWeek(self, n, type): + @parameter_space( + rule_offset=(0, 1, 2, 3, 4), + start_offset=(0, 1, 2, 3, 4), + type=('week_start', 'week_end') + ) + def test_edge_cases_for_TradingDayOfWeek(self, + rule_offset, + start_offset, + type): """ Test that we account for midweek holidays. Monday 01/20 is a holiday. - Ensure that the trigger date for that week is adjustmented - appropriately, or thrown out if not enough trading days. + Ensure that the trigger date for that week is adjusted + appropriately, or thrown out if not enough trading days. Also, test + that if we start the simulation on a day where we miss the trigger + for that week, that the trigger is recalculated for next week. """ + + sim_start = pd.Timestamp('01-06-2014 14:31:00', tz='UTC') + \ + timedelta(days=start_offset) + jan_minutes = self.env.minutes_for_days_in_range( - datetime.date(year=2014, month=1, day=6), + datetime.date(year=2014, month=1, day=6) + + timedelta(days=start_offset), datetime.date(year=2014, month=1, day=31) ) @@ -391,7 +405,8 @@ class TestStatelessRules(RuleTestCase): pd.Timestamp('2014-01-21 14:31:00', tz='UTC'), pd.Timestamp('2014-01-27 14:31:00', tz='UTC'), ] - trigger_dates = [x + timedelta(days=n) for x in trigger_dates] + trigger_dates = \ + [x + timedelta(days=rule_offset) for x in trigger_dates] else: rule = NDaysBeforeLastTradingDayOfWeek # Expect to trigger on the last trading day of the week, minus the @@ -402,16 +417,22 @@ class TestStatelessRules(RuleTestCase): pd.Timestamp('2014-01-24 14:31:00', tz='UTC'), pd.Timestamp('2014-01-31 14:31:00', tz='UTC'), ] - trigger_dates = [x - timedelta(days=n) for x in trigger_dates] + trigger_dates = \ + [x - timedelta(days=rule_offset) for x in trigger_dates] should_trigger = partial( - rule(n).should_trigger, env=self.env + rule(rule_offset).should_trigger, env=self.env ) # If offset is 4, there is not enough trading days in the short week, # and so it should not trigger - if n == 4: + if rule_offset == 4: del trigger_dates[2] + + # Filter out trigger dates that happen before the simulation starts + trigger_dates = [x for x in trigger_dates if x >= sim_start] + + expected_n_triggered = len(trigger_dates) trigger_dates = iter(trigger_dates) n_triggered = 0 @@ -420,7 +441,7 @@ class TestStatelessRules(RuleTestCase): self.assertEqual(m, next(trigger_dates)) n_triggered += 1 - self.assertEqual(n_triggered, 4 if n != 4 else 3) + self.assertEqual(n_triggered, expected_n_triggered) @subtest(param_range(MAX_MONTH_RANGE), 'n') def test_NthTradingDayOfMonth(self, n): diff --git a/zipline/utils/events.py b/zipline/utils/events.py index ae36aacc..1a031430 100644 --- a/zipline/utils/events.py +++ b/zipline/utils/events.py @@ -445,8 +445,7 @@ class TradingDayOfWeekRule(six.with_metaclass(ABCMeta, StatelessRule)): # If after applying the offset to the start/end day of the week, we get # day in a different week, skip this week and go on to the next - while next_trading_day.date().isocalendar()[1] != \ - dt.date().isocalendar()[1]: + while next_trading_day.isocalendar()[1] != dt.isocalendar()[1]: dt += datetime.timedelta(days=7) next_trading_day = _coerce_datetime( env.add_trading_days( @@ -458,15 +457,21 @@ class TradingDayOfWeekRule(six.with_metaclass(ABCMeta, StatelessRule)): next_open, next_close = env.get_open_and_close(next_trading_day) self.next_date_start = next_open self.next_date_end = next_close - self.next_midnight_timestamp = \ - pd.Timestamp(next_trading_day.date(), tz='UTC') + self.next_midnight_timestamp = next_trading_day def should_trigger(self, dt, env): if self.next_date_start is None: - # first time this method has been called. calculate the midnight, - # open, and close of the next matching day. + # First time this method has been called. Calculate the midnight, + # open, and close for the first trigger, which occurs on the week + # of the simulation start self.calculate_start_and_end(dt, env) + # If we've missed the first trigger because it occurs before the + # simulation starts, recalculate for the next week + if dt > self.next_date_end: + self.calculate_start_and_end(dt + datetime.timedelta(days=7), + env) + # if the given dt is within the next matching day, return true. Also # calculate the start and end dates for the next trigger if self.next_date_start <= dt <= self.next_date_end or \