diff --git a/tests/test_tradingcalendar.py b/tests/test_tradingcalendar.py index 4a62fb01..e2331f4b 100644 --- a/tests/test_tradingcalendar.py +++ b/tests/test_tradingcalendar.py @@ -182,3 +182,63 @@ If Nov has 5 Thursdays, {0} Thanksgiving is not the last week. If NYE falls on a weekend, {0} the Tuesday after is the first trading day. """.strip().format(first_trading_day_after_new_years_sunday) ) + + def test_day_after_thanksgiving(self): + early_closes = tradingcalendar.get_early_closes( + tradingcalendar.start, + tradingcalendar.end.replace(year=tradingcalendar.end.year + 1) + ) + + # November 2012 + # Su Mo Tu We Th Fr Sa + # 1 2 3 + # 4 5 6 7 8 9 10 + # 11 12 13 14 15 16 17 + # 18 19 20 21 22 23 24 + # 25 26 27 28 29 30 + fourth_friday = datetime.datetime(2012, 11, 23, tzinfo=pytz.utc) + self.assertIn(fourth_friday, early_closes) + + # November 2013 + # Su Mo Tu We Th Fr Sa + # 1 2 + # 3 4 5 6 7 8 9 + # 10 11 12 13 14 15 16 + # 17 18 19 20 21 22 23 + # 24 25 26 27 28 29 30 + fifth_friday = datetime.datetime(2013, 11, 29, tzinfo=pytz.utc) + self.assertIn(fifth_friday, early_closes) + + def test_early_close_independence_day_thursday(self): + """ + Until 2013, the market closed early the Friday after an + Independence Day on Thursday. Since then, the early close is on + Wednesday. + """ + early_closes = tradingcalendar.get_early_closes( + tradingcalendar.start, + tradingcalendar.end.replace(year=tradingcalendar.end.year + 1) + ) + # July 2002 + # Su Mo Tu We Th Fr Sa + # 1 2 3 4 5 6 + # 7 8 9 10 11 12 13 + # 14 15 16 17 18 19 20 + # 21 22 23 24 25 26 27 + # 28 29 30 31 + wednesday_before = datetime.datetime(2002, 7, 3, tzinfo=pytz.utc) + friday_after = datetime.datetime(2002, 7, 5, tzinfo=pytz.utc) + self.assertNotIn(wednesday_before, early_closes) + self.assertIn(friday_after, early_closes) + + # July 2013 + # Su Mo Tu We Th Fr Sa + # 1 2 3 4 5 6 + # 7 8 9 10 11 12 13 + # 14 15 16 17 18 19 20 + # 21 22 23 24 25 26 27 + # 28 29 30 31 + wednesday_before = datetime.datetime(2013, 7, 3, tzinfo=pytz.utc) + friday_after = datetime.datetime(2013, 7, 5, tzinfo=pytz.utc) + self.assertIn(wednesday_before, early_closes) + self.assertNotIn(friday_after, early_closes) diff --git a/zipline/finance/trading.py b/zipline/finance/trading.py index 879260d2..264d9a5f 100644 --- a/zipline/finance/trading.py +++ b/zipline/finance/trading.py @@ -22,6 +22,7 @@ from delorean import Delorean import pandas as pd from zipline.data.loader import load_market_data +from zipline.utils.tradingcalendar import get_early_closes log = logbook.Logger('Trading') @@ -89,6 +90,7 @@ class TradingEnvironment(object): self.treasury_curves = self.treasury_curves[:max_date] self.full_trading_day = datetime.timedelta(hours=6, minutes=30) + self.early_close_trading_day = datetime.timedelta(hours=3, minutes=30) self.exchange_tz = exchange_tz bm = None @@ -112,6 +114,9 @@ class TradingEnvironment(object): self.first_trading_day = self.trading_days[0] self.last_trading_day = self.trading_days[-1] + self.early_closes = get_early_closes(self.first_trading_day, + self.last_trading_day) + def __enter__(self, *args, **kwargs): global environment self.prev_environment = environment @@ -203,8 +208,10 @@ Last successful date: %s" % self.last_trading_day) return market_open, market_close def get_trading_day_duration(self, trading_day): - # TODO: make a list of half-days and modify the - # calculation of market close to reflect them. + trading_day = self.normalize_date(trading_day) + if trading_day in self.early_closes: + return self.early_close_trading_day + return self.full_trading_day def trading_day_distance(self, first_date, second_date): diff --git a/zipline/utils/tradingcalendar.py b/zipline/utils/tradingcalendar.py index 5a53baec..75abc3d8 100644 --- a/zipline/utils/tradingcalendar.py +++ b/zipline/utils/tradingcalendar.py @@ -247,4 +247,105 @@ def get_trading_days(start, end): return business_days - non_trading_days + trading_days = get_trading_days(start, end) + + +def get_early_closes(start, end): + # 1:00 PM close rules based on + # http://quant.stackexchange.com/questions/4083/nyse-early-close-rules-july-4th-and-dec-25th # noqa + # and verified against http://www.nyse.com/pdfs/closings.pdf + + # These rules are valid starting in 1993 + start = max(start, datetime(1993, 1, 1, tzinfo=pytz.utc)) + end = max(end, datetime(1993, 1, 1, tzinfo=pytz.utc)) + + # Not included here are early closes prior to 1993 + # or unplanned early closes + + early_close_rules = [] + + day_after_thanksgiving = rrule.rrule( + rrule.MONTHLY, + bymonth=11, + # 4th Friday isn't correct if month starts on Friday, so restrict to + # day range: + byweekday=(rrule.FR), + bymonthday=range(23, 30), + cache=True, + dtstart=start, + until=end + ) + early_close_rules.append(day_after_thanksgiving) + + christmas_eve = rrule.rrule( + rrule.MONTHLY, + bymonth=12, + bymonthday=24, + byweekday=(rrule.MO, rrule.TU, rrule.WE, rrule.TH), + cache=True, + dtstart=start, + until=end + ) + early_close_rules.append(christmas_eve) + + friday_after_christmas = rrule.rrule( + rrule.MONTHLY, + bymonth=12, + bymonthday=26, + byweekday=rrule.FR, + cache=True, + dtstart=start, + # valid 1993-2007 + until=min(end, datetime(2007, 12, 31, tzinfo=pytz.utc)) + ) + early_close_rules.append(friday_after_christmas) + + day_before_independence_day = rrule.rrule( + rrule.MONTHLY, + bymonth=7, + bymonthday=3, + byweekday=(rrule.MO, rrule.TU, rrule.TH), + cache=True, + dtstart=start, + until=end + ) + early_close_rules.append(day_before_independence_day) + + day_after_independence_day = rrule.rrule( + rrule.MONTHLY, + bymonth=7, + bymonthday=5, + byweekday=rrule.FR, + cache=True, + dtstart=start, + # starting in 2013: wednesday before independence day + until=min(end, datetime(2012, 12, 31, tzinfo=pytz.utc)) + ) + early_close_rules.append(day_after_independence_day) + + wednesday_before_independence_day = rrule.rrule( + rrule.MONTHLY, + bymonth=7, + bymonthday=3, + byweekday=rrule.WE, + cache=True, + # starting in 2013 + dtstart=max(start, datetime(2013, 1, 1, tzinfo=pytz.utc)), + until=max(end, datetime(2013, 1, 1, tzinfo=pytz.utc)) + ) + early_close_rules.append(wednesday_before_independence_day) + + early_close_ruleset = rrule.rruleset() + + for rule in early_close_rules: + early_close_ruleset.rrule(rule) + early_closes = early_close_ruleset.between(start, end, inc=True) + + # Misc early closings from NYSE listing. + # http://www.nyse.com/pdfs/closings.pdf + # + # New Year's Eve + early_closes.append(datetime(1999, 12, 31, tzinfo=pytz.utc)) + + return pd.DatetimeIndex(sorted(early_closes))