From 6cebf054178b6c2d1a571fd643daac2dbdd01c84 Mon Sep 17 00:00:00 2001 From: Andrew Daniels Date: Tue, 19 Jul 2016 13:19:40 -0400 Subject: [PATCH] BUG: Further corrections for days_at_time (#1334) * BUG: Further corrections for days_at_time - Revert to using DateOffset, as Timedelta doesn't handle offsetting by one day over a tz change properly: In [12]: pd.Timestamp('2004-04-05', tz='America/Chicago') + pd.Timedelta(days=-1) Out[12]: Timestamp('2004-04-03 23:00:00-0600', tz='America/Chicago') In [13]: pd.Timestamp('2004-04-05', tz='America/Chicago') + pd.DateOffset(days=-1) Out[13]: Timestamp('2004-04-04 00:00:00-0600', tz='America/Chicago') By creating a DateOffset using the `days` kwarg, the issue previously fixed in bcc867b is addressed. - To preempt any other pandas issues around day offsets, changes to performing these with no timezone, then localizing to the local timezone when shifting the time. - Adds unit test for days_at_time * STY: Remove unused import --- tests/test_trading_calendar.py | 34 +++++++++++++++++++++ zipline/utils/calendars/trading_calendar.py | 15 ++++++--- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/tests/test_trading_calendar.py b/tests/test_trading_calendar.py index f00feefd..d0f784cb 100644 --- a/tests/test_trading_calendar.py +++ b/tests/test_trading_calendar.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from datetime import time from os.path import ( abspath, dirname, @@ -23,16 +24,19 @@ from collections import namedtuple import numpy as np import pandas as pd +from nose_parameterized import parameterized from pandas import ( read_csv, Timestamp, ) from pandas.util.testing import assert_index_equal +from pytz import timezone from zipline.errors import ( CalendarNameCollision, InvalidCalendarName, ) from zipline.utils.calendars.exchange_calendar_nyse import NYSEExchangeCalendar +from zipline.utils.calendars.trading_calendar import days_at_time from zipline.utils.calendars import( register_calendar, deregister_calendar, @@ -81,6 +85,36 @@ class CalendarRegistrationTestCase(TestCase): self.assertNotEqual(real_nyse, retr_cal) +class DaysAtTimeTestCase(TestCase): + @parameterized.expand([ + # NYSE standard day + ( + '2016-07-19', 0, time(9, 31), timezone('US/Eastern'), + '2016-07-19 9:31', + ), + # CME standard day + ( + '2016-07-19', -1, time(17, 1), timezone('America/Chicago'), + '2016-07-18 17:01', + ), + # CME day after DST start + ( + '2004-04-05', -1, time(17, 1), timezone('America/Chicago'), + '2004-04-04 17:01' + ), + # ICE day after DST start + ( + '1990-04-02', -1, time(19, 1), timezone('America/Chicago'), + '1990-04-01 19:01', + ), + ]) + def test_days_at_time(self, day, day_offset, time_offset, tz, expected): + days = pd.DatetimeIndex([pd.Timestamp(day, tz=tz)]) + result = days_at_time(days, time_offset, tz, day_offset)[0] + expected = pd.Timestamp(expected, tz=tz).tz_convert('UTC') + self.assertEqual(result, expected) + + class ExchangeCalendarTestBase(object): # Override in subclasses. diff --git a/zipline/utils/calendars/trading_calendar.py b/zipline/utils/calendars/trading_calendar.py index 56b2fba8..bcd5ebdd 100644 --- a/zipline/utils/calendars/trading_calendar.py +++ b/zipline/utils/calendars/trading_calendar.py @@ -21,7 +21,7 @@ from pandas import ( DataFrame, date_range, DatetimeIndex, - Timedelta + DateOffset ) from pandas.tseries.offsets import CustomBusinessDay @@ -677,11 +677,16 @@ def days_at_time(days, t, tz, day_offset=0): day_offset : int The number of days we want to offset @days by """ - days = DatetimeIndex(days).tz_localize(None).tz_localize(tz) - days_offset = days + Timedelta(days=day_offset) + + # Offset days without tz to avoid timezone issues. + days = DatetimeIndex(days).tz_localize(None) + days_offset = days + DateOffset(days=day_offset) + + # Shift all days to the target time in the local timezone, then + # convert to UTC. return days_offset.shift( - 1, freq=Timedelta(hours=t.hour, minutes=t.minute, seconds=t.second) - ).tz_convert('UTC') + 1, freq=DateOffset(hour=t.hour, minute=t.minute, second=t.second) + ).tz_localize(tz).tz_convert('UTC') def holidays_at_time(calendar, start, end, time, tz):